[ipcamera] FIX: TAPO branded cameras require xAddr port to be different (#15073)

* FIX TAPO branded cameras require xAddr port to be different from the
main ONVIF PORT
* Fix for old API instar cameras.

---------

Signed-off-by: Matthew Skinner <matt@pcmus.com>
This commit is contained in:
Matthew Skinner 2023-07-16 21:32:40 +10:00 committed by GitHub
parent 6e85021f6b
commit 8701b86a37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 111 additions and 98 deletions

View File

@ -176,6 +176,8 @@ public class Ffmpeg {
case SNAPSHOT: case SNAPSHOT:
notFrozen = true;// RTSP_ALARMS, MJPEG and SNAPSHOT all set this to true, no break. notFrozen = true;// RTSP_ALARMS, MJPEG and SNAPSHOT all set this to true, no break.
break; break;
default:
break;
} }
} }
} }

View File

@ -384,7 +384,9 @@ public class InstarHandler extends ChannelDuplexHandler {
ArrayList<String> lowPriorityRequests = new ArrayList<String>(7); ArrayList<String> lowPriorityRequests = new ArrayList<String>(7);
lowPriorityRequests.add("/param.cgi?cmd=getaudioalarmattr"); lowPriorityRequests.add("/param.cgi?cmd=getaudioalarmattr");
lowPriorityRequests.add("/cgi-bin/hi3510/param.cgi?cmd=getmdattr"); lowPriorityRequests.add("/cgi-bin/hi3510/param.cgi?cmd=getmdattr");
lowPriorityRequests.add("/param.cgi?cmd=getalarmattr"); if (ipCameraHandler.newInstarApi) {// old API cameras get a error 404 response to this
lowPriorityRequests.add("/param.cgi?cmd=getalarmattr");
}
lowPriorityRequests.add("/param.cgi?cmd=getinfrared"); lowPriorityRequests.add("/param.cgi?cmd=getinfrared");
lowPriorityRequests.add("/param.cgi?cmd=getoverlayattr&-region=1"); lowPriorityRequests.add("/param.cgi?cmd=getoverlayattr&-region=1");
lowPriorityRequests.add("/param.cgi?cmd=getpirattr"); lowPriorityRequests.add("/param.cgi?cmd=getpirattr");

View File

@ -364,7 +364,6 @@ public class IpCameraHandler extends BaseThingHandler {
} }
@Override @Override
@SuppressWarnings("PMD.CompareObjectsWithEquals")
public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception { public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
if (ctx == null) { if (ctx == null) {
return; return;
@ -384,7 +383,7 @@ public class IpCameraHandler extends BaseThingHandler {
} }
ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen); ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
if (channelTracking != null) { if (channelTracking != null) {
if (channelTracking.getChannel() == ctx.channel()) { if (channelTracking.getChannel().equals(ctx.channel())) {
return; // don't auto close this as it is for the alarms. return; // don't auto close this as it is for the alarms.
} }
} }
@ -745,7 +744,6 @@ public class IpCameraHandler extends BaseThingHandler {
* open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
* still occurring. * still occurring.
*/ */
@SuppressWarnings("PMD.CompareObjectsWithEquals")
private void cleanChannels() { private void cleanChannels() {
for (Channel channel : openChannels) { for (Channel channel : openChannels) {
boolean oldChannel = true; boolean oldChannel = true;
@ -753,7 +751,7 @@ public class IpCameraHandler extends BaseThingHandler {
if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) { if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
channelTrackingMap.remove(channelTracking.getRequestUrl()); channelTrackingMap.remove(channelTracking.getRequestUrl());
} }
if (channelTracking.getChannel() == channel) { if (channelTracking.getChannel().equals(channel)) {
logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl()); logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
oldChannel = false; oldChannel = false;
} }

View File

@ -58,7 +58,6 @@ import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.timeout.IdleStateHandler; import io.netty.handler.timeout.IdleStateHandler;
@ -308,7 +307,7 @@ public class OnvifConnection {
if (message.contains("PullMessagesResponse")) { if (message.contains("PullMessagesResponse")) {
eventRecieved(message); eventRecieved(message);
} else if (message.contains("RenewResponse")) { } else if (message.contains("RenewResponse")) {
sendOnvifRequest(requestBuilder(RequestType.PullMessages, subscriptionXAddr)); sendOnvifRequest(RequestType.PullMessages, subscriptionXAddr);
} else if (message.contains("GetSystemDateAndTimeResponse")) {// 1st to be sent. } else if (message.contains("GetSystemDateAndTimeResponse")) {// 1st to be sent.
connecting.lock(); connecting.lock();
try { try {
@ -320,7 +319,7 @@ public class OnvifConnection {
logger.debug("Openhabs UTC dateTime is:{}", getUTCdateTime()); logger.debug("Openhabs UTC dateTime is:{}", getUTCdateTime());
} else if (message.contains("GetCapabilitiesResponse")) {// 2nd to be sent. } else if (message.contains("GetCapabilitiesResponse")) {// 2nd to be sent.
parseXAddr(message); parseXAddr(message);
sendOnvifRequest(requestBuilder(RequestType.GetProfiles, mediaXAddr)); sendOnvifRequest(RequestType.GetProfiles, mediaXAddr);
} else if (message.contains("GetProfilesResponse")) {// 3rd to be sent. } else if (message.contains("GetProfilesResponse")) {// 3rd to be sent.
connecting.lock(); connecting.lock();
try { try {
@ -329,25 +328,25 @@ public class OnvifConnection {
connecting.unlock(); connecting.unlock();
} }
parseProfiles(message); parseProfiles(message);
sendOnvifRequest(requestBuilder(RequestType.GetSnapshotUri, mediaXAddr)); sendOnvifRequest(RequestType.GetSnapshotUri, mediaXAddr);
sendOnvifRequest(requestBuilder(RequestType.GetStreamUri, mediaXAddr)); sendOnvifRequest(RequestType.GetStreamUri, mediaXAddr);
if (ptzDevice) { if (ptzDevice) {
sendPTZRequest(RequestType.GetNodes); sendPTZRequest(RequestType.GetNodes);
} }
if (usingEvents) {// stops API cameras from getting sent ONVIF events. if (usingEvents) {// stops API cameras from getting sent ONVIF events.
sendOnvifRequest(requestBuilder(RequestType.GetEventProperties, eventXAddr)); sendOnvifRequest(RequestType.GetEventProperties, eventXAddr);
sendOnvifRequest(requestBuilder(RequestType.GetServiceCapabilities, eventXAddr)); sendOnvifRequest(RequestType.GetServiceCapabilities, eventXAddr);
} }
} else if (message.contains("GetServiceCapabilitiesResponse")) { } else if (message.contains("GetServiceCapabilitiesResponse")) {
if (message.contains("WSSubscriptionPolicySupport=\"true\"")) { if (message.contains("WSSubscriptionPolicySupport=\"true\"")) {
sendOnvifRequest(requestBuilder(RequestType.Subscribe, eventXAddr)); sendOnvifRequest(RequestType.Subscribe, eventXAddr);
} }
} else if (message.contains("GetEventPropertiesResponse")) { } else if (message.contains("GetEventPropertiesResponse")) {
sendOnvifRequest(requestBuilder(RequestType.CreatePullPointSubscription, eventXAddr)); sendOnvifRequest(RequestType.CreatePullPointSubscription, eventXAddr);
} else if (message.contains("CreatePullPointSubscriptionResponse")) { } else if (message.contains("CreatePullPointSubscriptionResponse")) {
subscriptionXAddr = Helper.fetchXML(message, "SubscriptionReference>", "Address>"); subscriptionXAddr = Helper.fetchXML(message, "SubscriptionReference>", "Address>");
logger.debug("subscriptionXAddr={}", subscriptionXAddr); logger.debug("subscriptionXAddr={}", subscriptionXAddr);
sendOnvifRequest(requestBuilder(RequestType.PullMessages, subscriptionXAddr)); sendOnvifRequest(RequestType.PullMessages, subscriptionXAddr);
} else if (message.contains("GetStatusResponse")) { } else if (message.contains("GetStatusResponse")) {
processPTZLocation(message); processPTZLocation(message);
} else if (message.contains("GetPresetsResponse")) { } else if (message.contains("GetPresetsResponse")) {
@ -380,54 +379,6 @@ public class OnvifConnection {
} }
} }
HttpRequest requestBuilder(RequestType requestType, String xAddr) {
logger.trace("Sending ONVIF request:{}", requestType);
String security = "";
String extraEnvelope = "";
String headerTo = "";
String getXmlCache = getXml(requestType);
if (requestType.equals(RequestType.CreatePullPointSubscription) || requestType.equals(RequestType.PullMessages)
|| requestType.equals(RequestType.Renew) || requestType.equals(RequestType.Unsubscribe)) {
headerTo = "<a:To s:mustUnderstand=\"1\">" + xAddr + "</a:To>";
extraEnvelope = " xmlns:a=\"http://www.w3.org/2005/08/addressing\"";
}
String headers;
if (!password.isEmpty() && !requestType.equals(RequestType.GetSystemDateAndTime)) {
String nonce = createNonce();
String dateTime = getUTCdateTime();
String digest = createDigest(nonce, dateTime);
security = "<Security s:mustUnderstand=\"1\" xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\"><UsernameToken><Username>"
+ user
+ "</Username><Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"
+ digest
+ "</Password><Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">"
+ encodeBase64(nonce)
+ "</Nonce><Created xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">"
+ dateTime + "</Created></UsernameToken></Security>";
headers = "<s:Header>" + security + headerTo + "</s:Header>";
} else {// GetSystemDateAndTime must not be password protected as per spec.
headers = "";
}
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("POST"),
removeIPfromUrl(xAddr));
String actionString = Helper.fetchXML(getXmlCache, requestType.toString(), "xmlns=\"");
request.headers().add("Content-Type",
"application/soap+xml; charset=utf-8; action=\"" + actionString + "/" + requestType + "\"");
request.headers().add("Charset", "utf-8");
request.headers().set("Host", ipAddress + ":" + onvifPort);
request.headers().set("Connection", HttpHeaderValues.CLOSE);
request.headers().set("Accept-Encoding", "gzip, deflate");
String fullXml = "<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\"" + extraEnvelope + ">"
+ headers
+ "<s:Body xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
+ getXmlCache + "</s:Body></s:Envelope>";
request.headers().add("SOAPAction", "\"" + actionString + "/" + requestType + "\"");
ByteBuf bbuf = Unpooled.copiedBuffer(fullXml, StandardCharsets.UTF_8);
request.headers().set("Content-Length", bbuf.readableBytes());
request.content().clear().writeBytes(bbuf);
return request;
}
/** /**
* The {@link removeIPfromUrl} Will throw away all text before the cameras IP, also removes the IP and the PORT * The {@link removeIPfromUrl} Will throw away all text before the cameras IP, also removes the IP and the PORT
* leaving just the URL. * leaving just the URL.
@ -456,6 +407,19 @@ public class OnvifConnection {
return ""; return "";
} }
int extractPortFromUrl(String url) {
int startIndex = url.indexOf("//") + 2;// skip past http://
startIndex = url.indexOf(":", startIndex);
if (startIndex == -1) {// no port defined so use port 80
return 80;
}
int endIndex = url.indexOf("/", startIndex);// skip past any :port to the slash /
if (endIndex == -1) {
return 80;
}
return Integer.parseInt(url.substring(startIndex + 1, endIndex));
}
void parseXAddr(String message) { void parseXAddr(String message) {
// Normally I would search '<tt:XAddr>' instead but Foscam needed this work around. // Normally I would search '<tt:XAddr>' instead but Foscam needed this work around.
String temp = Helper.fetchXML(message, "<tt:Device", "tt:XAddr"); String temp = Helper.fetchXML(message, "<tt:Device", "tt:XAddr");
@ -536,19 +500,64 @@ public class OnvifConnection {
return Base64.getEncoder().encodeToString(encryptedRaw); return Base64.getEncoder().encodeToString(encryptedRaw);
} }
@SuppressWarnings("null") public void sendOnvifRequest(RequestType requestType, String xAddr) {
public void sendOnvifRequest(HttpRequest request) { logger.trace("Sending ONVIF request:{}", requestType);
if (bootstrap == null) { String security = "";
String extraEnvelope = "";
String headerTo = "";
String getXmlCache = getXml(requestType);
if (requestType.equals(RequestType.CreatePullPointSubscription) || requestType.equals(RequestType.PullMessages)
|| requestType.equals(RequestType.Renew) || requestType.equals(RequestType.Unsubscribe)) {
headerTo = "<a:To s:mustUnderstand=\"1\">" + xAddr + "</a:To>";
extraEnvelope = " xmlns:a=\"http://www.w3.org/2005/08/addressing\"";
}
String headers;
if (!password.isEmpty() && !requestType.equals(RequestType.GetSystemDateAndTime)) {
String nonce = createNonce();
String dateTime = getUTCdateTime();
String digest = createDigest(nonce, dateTime);
security = "<Security s:mustUnderstand=\"1\" xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\"><UsernameToken><Username>"
+ user
+ "</Username><Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"
+ digest
+ "</Password><Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">"
+ encodeBase64(nonce)
+ "</Nonce><Created xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">"
+ dateTime + "</Created></UsernameToken></Security>";
headers = "<s:Header>" + security + headerTo + "</s:Header>";
} else {// GetSystemDateAndTime must not be password protected as per spec.
headers = "";
}
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("POST"),
removeIPfromUrl(xAddr));
String actionString = Helper.fetchXML(getXmlCache, requestType.toString(), "xmlns=\"");
request.headers().add("Content-Type",
"application/soap+xml; charset=utf-8; action=\"" + actionString + "/" + requestType + "\"");
request.headers().add("Charset", "utf-8");
request.headers().set("Host", ipAddress + ":" + onvifPort);
request.headers().set("Connection", HttpHeaderValues.CLOSE);
request.headers().set("Accept-Encoding", "gzip, deflate");
String fullXml = "<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\"" + extraEnvelope + ">"
+ headers
+ "<s:Body xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
+ getXmlCache + "</s:Body></s:Envelope>";
request.headers().add("SOAPAction", "\"" + actionString + "/" + requestType + "\"");
ByteBuf bbuf = Unpooled.copiedBuffer(fullXml, StandardCharsets.UTF_8);
request.headers().set("Content-Length", bbuf.readableBytes());
request.content().clear().writeBytes(bbuf);
Bootstrap localBootstap = bootstrap;
if (localBootstap == null) {
mainEventLoopGroup = new NioEventLoopGroup(2); mainEventLoopGroup = new NioEventLoopGroup(2);
bootstrap = new Bootstrap(); localBootstap = new Bootstrap();
bootstrap.group(mainEventLoopGroup); localBootstap.group(mainEventLoopGroup);
bootstrap.channel(NioSocketChannel.class); localBootstap.channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.SO_KEEPALIVE, true); localBootstap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000); localBootstap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
bootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8); localBootstap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
bootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024); localBootstap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
bootstrap.option(ChannelOption.TCP_NODELAY, true); localBootstap.option(ChannelOption.TCP_NODELAY, true);
bootstrap.handler(new ChannelInitializer<SocketChannel>() { localBootstap.handler(new ChannelInitializer<SocketChannel>() {
@Override @Override
public void initChannel(SocketChannel socketChannel) throws Exception { public void initChannel(SocketChannel socketChannel) throws Exception {
@ -557,26 +566,28 @@ public class OnvifConnection {
socketChannel.pipeline().addLast("OnvifCodec", new OnvifCodec(getHandle())); socketChannel.pipeline().addLast("OnvifCodec", new OnvifCodec(getHandle()));
} }
}); });
bootstrap = localBootstap;
} }
if (!mainEventLoopGroup.isShuttingDown()) { if (!mainEventLoopGroup.isShuttingDown()) {
bootstrap.connect(new InetSocketAddress(ipAddress, onvifPort)).addListener(new ChannelFutureListener() { localBootstap.connect(new InetSocketAddress(ipAddress, extractPortFromUrl(xAddr)))
.addListener(new ChannelFutureListener() {
@Override @Override
public void operationComplete(@Nullable ChannelFuture future) { public void operationComplete(@Nullable ChannelFuture future) {
if (future == null) { if (future == null) {
return; return;
} }
if (future.isSuccess()) { if (future.isSuccess()) {
Channel ch = future.channel(); Channel ch = future.channel();
ch.writeAndFlush(request); ch.writeAndFlush(request);
} else { // an error occured } else { // an error occured
logger.debug("Camera is not reachable on ONVIF port:{} or the port may be wrong.", onvifPort); logger.debug("Camera is not reachable when using xAddr:{}.", xAddr);
if (isConnected) { if (isConnected) {
disconnect(); disconnect();
}
}
} }
} });
}
});
} else { } else {
logger.debug("ONVIF message not sent as connection is shutting down"); logger.debug("ONVIF message not sent as connection is shutting down");
} }
@ -621,7 +632,7 @@ public class OnvifConnection {
public void eventRecieved(String eventMessage) { public void eventRecieved(String eventMessage) {
String topic = Helper.fetchXML(eventMessage, "Topic", "tns1:"); String topic = Helper.fetchXML(eventMessage, "Topic", "tns1:");
if (topic.isEmpty()) { if (topic.isEmpty()) {
sendOnvifRequest(requestBuilder(RequestType.Renew, subscriptionXAddr)); sendOnvifRequest(RequestType.Renew, subscriptionXAddr);
return; return;
} }
String dataName = Helper.fetchXML(eventMessage, "tt:Data", "Name=\""); String dataName = Helper.fetchXML(eventMessage, "tt:Data", "Name=\"");
@ -751,7 +762,7 @@ public class OnvifConnection {
default: default:
logger.debug("Please report this camera has an un-implemented ONVIF event. Topic:{}", topic); logger.debug("Please report this camera has an un-implemented ONVIF event. Topic:{}", topic);
} }
sendOnvifRequest(requestBuilder(RequestType.Renew, subscriptionXAddr)); sendOnvifRequest(RequestType.Renew, subscriptionXAddr);
} }
public boolean supportsPTZ() { public boolean supportsPTZ() {
@ -898,11 +909,11 @@ public class OnvifConnection {
logger.debug("ONVIF was not connected when a PTZ request was made, connecting now"); logger.debug("ONVIF was not connected when a PTZ request was made, connecting now");
connect(usingEvents); connect(usingEvents);
} }
sendOnvifRequest(requestBuilder(requestType, ptzXAddr)); sendOnvifRequest(requestType, ptzXAddr);
} }
public void sendEventRequest(RequestType requestType) { public void sendEventRequest(RequestType requestType) {
sendOnvifRequest(requestBuilder(requestType, eventXAddr)); sendOnvifRequest(requestType, eventXAddr);
} }
public void connect(boolean useEvents) { public void connect(boolean useEvents) {
@ -911,9 +922,9 @@ public class OnvifConnection {
if (!isConnected) { if (!isConnected) {
logger.debug("Connecting {} to ONVIF", ipAddress); logger.debug("Connecting {} to ONVIF", ipAddress);
threadPool = Executors.newScheduledThreadPool(2); threadPool = Executors.newScheduledThreadPool(2);
sendOnvifRequest(requestBuilder(RequestType.GetSystemDateAndTime, deviceXAddr)); sendOnvifRequest(RequestType.GetSystemDateAndTime, deviceXAddr);
usingEvents = useEvents; usingEvents = useEvents;
sendOnvifRequest(requestBuilder(RequestType.GetCapabilities, deviceXAddr)); sendOnvifRequest(RequestType.GetCapabilities, deviceXAddr);
} }
} finally { } finally {
connecting.unlock(); connecting.unlock();
@ -951,7 +962,7 @@ public class OnvifConnection {
if (bootstrap != null) { if (bootstrap != null) {
if (usingEvents && !mainEventLoopGroup.isShuttingDown()) { if (usingEvents && !mainEventLoopGroup.isShuttingDown()) {
// Some cameras may continue to send events even when they can't reach a server. // Some cameras may continue to send events even when they can't reach a server.
sendOnvifRequest(requestBuilder(RequestType.Unsubscribe, subscriptionXAddr)); sendOnvifRequest(RequestType.Unsubscribe, subscriptionXAddr);
} }
// give time for the Unsubscribe request to be sent, shutdownGracefully will try to send it first. // give time for the Unsubscribe request to be sent, shutdownGracefully will try to send it first.
threadPool.schedule(this::cleanup, 50, TimeUnit.MILLISECONDS); threadPool.schedule(this::cleanup, 50, TimeUnit.MILLISECONDS);