]> git.basschouten.com Git - openhab-addons.git/blob
ca2596030c5b9ae762c7006aacdf4a7aeb382fe5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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
14 package org.openhab.binding.ipcamera.internal.onvif;
15
16 import java.io.BufferedReader;
17 import java.io.IOException;
18 import java.io.InputStreamReader;
19 import java.math.BigDecimal;
20 import java.net.HttpURLConnection;
21 import java.net.InetAddress;
22 import java.net.InetSocketAddress;
23 import java.net.MalformedURLException;
24 import java.net.NetworkInterface;
25 import java.net.SocketException;
26 import java.net.URL;
27 import java.net.UnknownHostException;
28 import java.nio.charset.StandardCharsets;
29 import java.util.ArrayList;
30 import java.util.Enumeration;
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.ChannelFuture;
46 import io.netty.channel.ChannelHandlerContext;
47 import io.netty.channel.ChannelOption;
48 import io.netty.channel.SimpleChannelInboundHandler;
49 import io.netty.channel.nio.NioEventLoopGroup;
50 import io.netty.channel.socket.DatagramChannel;
51 import io.netty.channel.socket.DatagramPacket;
52 import io.netty.channel.socket.InternetProtocolFamily;
53 import io.netty.channel.socket.nio.NioDatagramChannel;
54 import io.netty.util.CharsetUtil;
55
56 /**
57  * The {@link OnvifDiscovery} is responsible for finding cameras that are Onvif using UDP multicast.
58  *
59  * @author Matthew Skinner - Initial contribution
60  */
61
62 @NonNullByDefault
63 public class OnvifDiscovery {
64     private IpCameraDiscoveryService ipCameraDiscoveryService;
65     private final Logger logger = LoggerFactory.getLogger(OnvifDiscovery.class);
66     public ArrayList<DatagramPacket> listOfReplys = new ArrayList<DatagramPacket>(10);
67
68     public OnvifDiscovery(IpCameraDiscoveryService ipCameraDiscoveryService) {
69         this.ipCameraDiscoveryService = ipCameraDiscoveryService;
70     }
71
72     public @Nullable NetworkInterface getLocalNIF() {
73         try {
74             for (Enumeration<NetworkInterface> enumNetworks = NetworkInterface.getNetworkInterfaces(); enumNetworks
75                     .hasMoreElements();) {
76                 NetworkInterface networkInterface = enumNetworks.nextElement();
77                 for (Enumeration<InetAddress> enumIpAddr = networkInterface.getInetAddresses(); enumIpAddr
78                         .hasMoreElements();) {
79                     InetAddress inetAddress = enumIpAddr.nextElement();
80                     if (!inetAddress.isLoopbackAddress() && inetAddress.getHostAddress().toString().length() < 18
81                             && inetAddress.isSiteLocalAddress()) {
82                         return networkInterface;
83                     }
84                 }
85             }
86         } catch (SocketException ex) {
87         }
88         return null;
89     }
90
91     void searchReply(String url, String xml) {
92         String ipAddress = "";
93         String temp = url;
94         BigDecimal onvifPort = new BigDecimal(80);
95
96         logger.info("Camera found at xAddr:{}", url);
97         int endIndex = temp.indexOf(" ");// Some xAddr have two urls with a space in between.
98         if (endIndex > 0) {
99             temp = temp.substring(0, endIndex);// Use only the first url from now on.
100         }
101
102         int beginIndex = temp.indexOf(":") + 3;// add 3 to ignore the :// after http.
103         int secondIndex = temp.indexOf(":", beginIndex); // find second :
104         endIndex = temp.indexOf("/", beginIndex);
105         if (secondIndex > beginIndex && endIndex > secondIndex) {// http://192.168.0.1:8080/onvif/device_service
106             ipAddress = temp.substring(beginIndex, secondIndex);
107             onvifPort = new BigDecimal(temp.substring(secondIndex + 1, endIndex));
108         } else {// // http://192.168.0.1/onvif/device_service
109             ipAddress = temp.substring(beginIndex, endIndex);
110         }
111         String brand = checkForBrand(xml);
112         if (brand.equals("onvif")) {
113             try {
114                 brand = getBrandFromLoginPage(ipAddress);
115             } catch (IOException e) {
116                 brand = "onvif";
117             }
118         }
119         ipCameraDiscoveryService.newCameraFound(brand, ipAddress, onvifPort.intValue());
120     }
121
122     void processCameraReplys() {
123         for (DatagramPacket packet : listOfReplys) {
124             logger.trace("Device replied to discovery with:{}", packet.toString());
125             String xml = packet.content().toString(CharsetUtil.UTF_8);
126             String xAddr = Helper.fetchXML(xml, "", "<d:XAddrs>");
127             if (!xAddr.equals("")) {
128                 searchReply(xAddr, xml);
129             } else if (xml.contains("onvif")) {
130                 logger.info("Possible ONVIF camera found at:{}", packet.sender().getHostString());
131                 ipCameraDiscoveryService.newCameraFound("onvif", packet.sender().getHostString(), 80);
132             }
133         }
134     }
135
136     String checkForBrand(String response) {
137         if (response.toLowerCase().contains("amcrest")) {
138             return "dahua";
139         } else if (response.toLowerCase().contains("dahua")) {
140             return "dahua";
141         } else if (response.toLowerCase().contains("foscam")) {
142             return "foscam";
143         } else if (response.toLowerCase().contains("hikvision")) {
144             return "hikvision";
145         } else if (response.toLowerCase().contains("instar")) {
146             return "instar";
147         } else if (response.toLowerCase().contains("doorbird")) {
148             return "doorbird";
149         } else if (response.toLowerCase().contains("ipc-")) {
150             return "dahua";
151         } else if (response.toLowerCase().contains("dh-sd")) {
152             return "dahua";
153         }
154         return "onvif";
155     }
156
157     public String getBrandFromLoginPage(String hostname) throws IOException {
158         URL url = new URL("http://" + hostname);
159         String brand = "onvif";
160         HttpURLConnection connection = (HttpURLConnection) url.openConnection();
161         connection.setConnectTimeout(1000);
162         connection.setReadTimeout(2000);
163         connection.setInstanceFollowRedirects(true);
164         connection.setRequestMethod("GET");
165         try {
166             connection.connect();
167             BufferedReader reply = new BufferedReader(new InputStreamReader(connection.getInputStream()));
168             String response = "";
169             String temp;
170             while ((temp = reply.readLine()) != null) {
171                 response += temp;
172             }
173             reply.close();
174             logger.trace("Cameras Login page is:{}", response);
175             brand = checkForBrand(response);
176         } catch (MalformedURLException e) {
177         } finally {
178             connection.disconnect();
179         }
180         return brand;
181     }
182
183     public void discoverCameras(int port) throws UnknownHostException, InterruptedException {
184         String uuid = UUID.randomUUID().toString();
185         String xml = "";
186
187         if (port == 3702) {
188             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:"
189                     + uuid
190                     + "</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:dp0=\"http://www.onvif.org/ver10/network/wsdl\">dp0:NetworkVideoTransmitter</d:Types></d:Probe></e:Body></e:Envelope>";
191         }
192         ByteBuf discoveryProbeMessage = Unpooled.copiedBuffer(xml, 0, xml.length(), StandardCharsets.UTF_8);
193         InetSocketAddress localNetworkAddress = new InetSocketAddress(0);// Listen for replies on all connections.
194         InetSocketAddress multiCastAddress = new InetSocketAddress(InetAddress.getByName("239.255.255.250"), port);
195         DatagramPacket datagramPacket = new DatagramPacket(discoveryProbeMessage, multiCastAddress,
196                 localNetworkAddress);
197         NetworkInterface networkInterface = getLocalNIF();
198         DatagramChannel datagramChannel;
199
200         Bootstrap bootstrap = new Bootstrap().group(new NioEventLoopGroup())
201                 .channelFactory(new ChannelFactory<NioDatagramChannel>() {
202                     @Override
203                     public NioDatagramChannel newChannel() {
204                         return new NioDatagramChannel(InternetProtocolFamily.IPv4);
205                     }
206                 }).handler(new SimpleChannelInboundHandler<DatagramPacket>() {
207                     @Override
208                     protected void channelRead0(@Nullable ChannelHandlerContext ctx, DatagramPacket msg)
209                             throws Exception {
210                         msg.retain(1);
211                         listOfReplys.add(msg);
212                     }
213                 }).option(ChannelOption.SO_BROADCAST, true).option(ChannelOption.SO_REUSEADDR, true)
214                 .option(ChannelOption.IP_MULTICAST_LOOP_DISABLED, false).option(ChannelOption.SO_RCVBUF, 2048)
215                 .option(ChannelOption.IP_MULTICAST_TTL, 255).option(ChannelOption.IP_MULTICAST_IF, networkInterface);
216
217         datagramChannel = (DatagramChannel) bootstrap.bind(localNetworkAddress).sync().channel();
218         datagramChannel.joinGroup(multiCastAddress, networkInterface).sync();
219         ChannelFuture chFuture;
220         if (port == 1900) {
221             String ssdp = "M-SEARCH * HTTP/1.1\n" + "HOST: 239.255.255.250:1900\n" + "MAN: \"ssdp:discover\"\n"
222                     + "MX: 1\n" + "ST: urn:dial-multiscreen-org:service:dial:1\n"
223                     + "USER-AGENT: Microsoft Edge/83.0.478.61 Windows\n" + "\n" + "";
224             ByteBuf ssdpProbeMessage = Unpooled.copiedBuffer(ssdp, 0, ssdp.length(), StandardCharsets.UTF_8);
225             datagramPacket = new DatagramPacket(ssdpProbeMessage, multiCastAddress, localNetworkAddress);
226             chFuture = datagramChannel.writeAndFlush(datagramPacket);
227         } else {
228             chFuture = datagramChannel.writeAndFlush(datagramPacket);
229         }
230         chFuture.awaitUninterruptibly(2000);
231         chFuture = datagramChannel.closeFuture();
232         TimeUnit.SECONDS.sleep(5);
233         datagramChannel.close();
234         chFuture.awaitUninterruptibly(6000);
235         processCameraReplys();
236         bootstrap.config().group().shutdownGracefully();
237     }
238 }