]> git.basschouten.com Git - openhab-addons.git/blob
b8e70461a18ac2de8df8c58de22d777eb60c2236
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.ipcamera.internal.onvif;
14
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStreamReader;
18 import java.math.BigDecimal;
19 import java.net.HttpURLConnection;
20 import java.net.InetAddress;
21 import java.net.InetSocketAddress;
22 import java.net.MalformedURLException;
23 import java.net.NetworkInterface;
24 import java.net.SocketException;
25 import java.net.URL;
26 import java.net.UnknownHostException;
27 import java.nio.charset.StandardCharsets;
28 import java.util.ArrayList;
29 import java.util.Enumeration;
30 import java.util.List;
31 import java.util.UUID;
32 import java.util.concurrent.TimeUnit;
33
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.openhab.binding.ipcamera.internal.Helper;
37 import org.openhab.binding.ipcamera.internal.IpCameraDiscoveryService;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 import io.netty.bootstrap.Bootstrap;
42 import io.netty.buffer.ByteBuf;
43 import io.netty.buffer.Unpooled;
44 import io.netty.channel.ChannelFactory;
45 import io.netty.channel.ChannelHandlerContext;
46 import io.netty.channel.ChannelOption;
47 import io.netty.channel.SimpleChannelInboundHandler;
48 import io.netty.channel.group.ChannelGroup;
49 import io.netty.channel.group.DefaultChannelGroup;
50 import io.netty.channel.nio.NioEventLoopGroup;
51 import io.netty.channel.socket.DatagramChannel;
52 import io.netty.channel.socket.DatagramPacket;
53 import io.netty.channel.socket.InternetProtocolFamily;
54 import io.netty.channel.socket.nio.NioDatagramChannel;
55 import io.netty.util.CharsetUtil;
56 import io.netty.util.concurrent.GlobalEventExecutor;
57
58 /**
59  * The {@link OnvifDiscovery} is responsible for finding cameras that are ONVIF using UDP multicast.
60  *
61  * @author Matthew Skinner - Initial contribution
62  */
63
64 @NonNullByDefault
65 public class OnvifDiscovery {
66     private IpCameraDiscoveryService ipCameraDiscoveryService;
67     private final Logger logger = LoggerFactory.getLogger(OnvifDiscovery.class);
68     public ArrayList<DatagramPacket> listOfReplys = new ArrayList<DatagramPacket>(10);
69
70     public OnvifDiscovery(IpCameraDiscoveryService ipCameraDiscoveryService) {
71         this.ipCameraDiscoveryService = ipCameraDiscoveryService;
72     }
73
74     public @Nullable List<NetworkInterface> getLocalNICs() {
75         List<NetworkInterface> results = new ArrayList<>(2);
76         try {
77             for (Enumeration<NetworkInterface> enumNetworks = NetworkInterface.getNetworkInterfaces(); enumNetworks
78                     .hasMoreElements();) {
79                 NetworkInterface networkInterface = enumNetworks.nextElement();
80                 for (Enumeration<InetAddress> enumIpAddr = networkInterface.getInetAddresses(); enumIpAddr
81                         .hasMoreElements();) {
82                     InetAddress inetAddress = enumIpAddr.nextElement();
83                     if (!inetAddress.isLoopbackAddress() && inetAddress.getHostAddress().toString().length() < 18
84                             && inetAddress.isSiteLocalAddress()) {
85                         results.add(networkInterface);
86                     }
87                 }
88             }
89         } catch (SocketException ex) {
90         }
91         return results;
92     }
93
94     void searchReply(String url, String xml) {
95         String ipAddress = "";
96         String temp = url;
97         BigDecimal onvifPort = new BigDecimal(80);
98
99         logger.info("Camera found at xAddr:{}", url);
100         int endIndex = temp.indexOf(" ");// Some xAddr have two urls with a space in between.
101         if (endIndex > 0) {
102             temp = temp.substring(0, endIndex);// Use only the first url from now on.
103         }
104
105         int beginIndex = temp.indexOf(":") + 3;// add 3 to ignore the :// after http.
106         int secondIndex = temp.indexOf(":", beginIndex); // find second :
107         endIndex = temp.indexOf("/", beginIndex);
108         if (secondIndex > beginIndex && endIndex > secondIndex) {// http://192.168.0.1:8080/onvif/device_service
109             ipAddress = temp.substring(beginIndex, secondIndex);
110             onvifPort = new BigDecimal(temp.substring(secondIndex + 1, endIndex));
111         } else {// // http://192.168.0.1/onvif/device_service
112             ipAddress = temp.substring(beginIndex, endIndex);
113         }
114         String brand = checkForBrand(xml);
115         if ("onvif".equals(brand)) {
116             try {
117                 brand = getBrandFromLoginPage(ipAddress);
118             } catch (IOException e) {
119                 brand = "onvif";
120             }
121         }
122         ipCameraDiscoveryService.newCameraFound(brand, ipAddress, onvifPort.intValue());
123     }
124
125     void processCameraReplys() {
126         for (DatagramPacket packet : listOfReplys) {
127             String xml = packet.content().toString(CharsetUtil.UTF_8);
128             logger.trace("Device replied to discovery with:{}", xml);
129             String xAddr = Helper.fetchXML(xml, "", "d:XAddrs>");// Foscam <wsdd:XAddrs> and all other brands <d:XAddrs>
130             if (!xAddr.isEmpty()) {
131                 searchReply(xAddr, xml);
132             } else if (xml.contains("onvif")) {
133                 logger.info("Possible ONVIF camera found at:{}", packet.sender().getHostString());
134                 ipCameraDiscoveryService.newCameraFound("onvif", packet.sender().getHostString(), 80);
135             }
136         }
137     }
138
139     String checkForBrand(String response) {
140         if (response.toLowerCase().contains("amcrest")) {
141             return "dahua";
142         } else if (response.toLowerCase().contains("dahua")) {
143             return "dahua";
144         } else if (response.toLowerCase().contains("foscam")) {
145             return "foscam";
146         } else if (response.toLowerCase().contains("hikvision")) {
147             return "hikvision";
148         } else if (response.toLowerCase().contains("instar")) {
149             return "instar";
150         } else if (response.toLowerCase().contains("doorbird")) {
151             return "doorbird";
152         } else if (response.toLowerCase().contains("ipc-")) {
153             return "dahua";
154         } else if (response.toLowerCase().contains("dh-sd")) {
155             return "dahua";
156         } else if (response.toLowerCase().contains("reolink")) {
157             return "reolink";
158         }
159         return "onvif";
160     }
161
162     public String getBrandFromLoginPage(String hostname) throws IOException {
163         URL url = new URL("http://" + hostname);
164         String brand = "onvif";
165         HttpURLConnection connection = (HttpURLConnection) url.openConnection();
166         connection.setConnectTimeout(1000);
167         connection.setReadTimeout(2000);
168         connection.setInstanceFollowRedirects(true);
169         connection.setRequestMethod("GET");
170         try {
171             connection.connect();
172             BufferedReader reply = new BufferedReader(new InputStreamReader(connection.getInputStream()));
173             String response = "";
174             String temp;
175             while ((temp = reply.readLine()) != null) {
176                 response += temp;
177             }
178             reply.close();
179             logger.trace("Cameras Login page is:{}", response);
180             brand = checkForBrand(response);
181         } catch (MalformedURLException e) {
182         } finally {
183             connection.disconnect();
184         }
185         return brand;
186     }
187
188     private DatagramPacket wsDiscovery() throws UnknownHostException {
189         String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><e:Envelope xmlns:e=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:w=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:dn=\"http://www.onvif.org/ver10/network/wsdl\"><e:Header><w:MessageID>uuid:"
190                 + UUID.randomUUID()
191                 + "</w:MessageID><w:To e:mustUnderstand=\"true\">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To><w:Action a:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action></e:Header><e:Body><d:Probe><d:Types xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:dp0=\"http://www.onvif.org/ver10/network/wsdl\">dp0:NetworkVideoTransmitter</d:Types></d:Probe></e:Body></e:Envelope>";
192         ByteBuf discoveryProbeMessage = Unpooled.copiedBuffer(xml, 0, xml.length(), StandardCharsets.UTF_8);
193         return new DatagramPacket(discoveryProbeMessage,
194                 new InetSocketAddress(InetAddress.getByName("239.255.255.250"), 3702), new InetSocketAddress(0));
195     }
196
197     public void discoverCameras() throws UnknownHostException, InterruptedException {
198         List<NetworkInterface> nics = getLocalNICs();
199         if (nics == null || nics.isEmpty()) {
200             return;
201         }
202         NetworkInterface networkInterface = nics.get(0);
203         Bootstrap bootstrap = new Bootstrap().group(new NioEventLoopGroup())
204                 .channelFactory(new ChannelFactory<NioDatagramChannel>() {
205                     @Override
206                     public NioDatagramChannel newChannel() {
207                         return new NioDatagramChannel(InternetProtocolFamily.IPv4);
208                     }
209                 }).handler(new SimpleChannelInboundHandler<DatagramPacket>() {
210                     @Override
211                     protected void channelRead0(@Nullable ChannelHandlerContext ctx, DatagramPacket msg)
212                             throws Exception {
213                         msg.retain(1);
214                         listOfReplys.add(msg);
215                     }
216                 }).option(ChannelOption.SO_BROADCAST, true).option(ChannelOption.SO_REUSEADDR, true)
217                 .option(ChannelOption.IP_MULTICAST_LOOP_DISABLED, false).option(ChannelOption.SO_RCVBUF, 2048)
218                 .option(ChannelOption.IP_MULTICAST_TTL, 255).option(ChannelOption.IP_MULTICAST_IF, networkInterface);
219         ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
220         for (NetworkInterface nic : nics) {
221             DatagramChannel datagramChannel = (DatagramChannel) bootstrap.option(ChannelOption.IP_MULTICAST_IF, nic)
222                     .bind(new InetSocketAddress(0)).sync().channel();
223             datagramChannel
224                     .joinGroup(new InetSocketAddress(InetAddress.getByName("239.255.255.250"), 3702), networkInterface)
225                     .sync();
226             openChannels.add(datagramChannel);
227         }
228         if (!openChannels.isEmpty()) {
229             openChannels.writeAndFlush(wsDiscovery());
230             TimeUnit.SECONDS.sleep(6);
231             openChannels.close();
232             processCameraReplys();
233             bootstrap.config().group().shutdownGracefully();
234         }
235     }
236 }