]> git.basschouten.com Git - openhab-addons.git/blob
b93bfad8f477d5c861d8447bd30a3c3a5aaeb795
[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 package org.openhab.binding.velux.internal.bridge.slip.io;
14
15 import java.io.DataOutputStream;
16 import java.io.IOException;
17 import java.net.ConnectException;
18 import java.net.UnknownHostException;
19 import java.security.KeyManagementException;
20 import java.security.NoSuchAlgorithmException;
21 import java.security.cert.CertificateException;
22 import java.security.cert.X509Certificate;
23
24 import javax.net.ssl.SSLContext;
25 import javax.net.ssl.SSLSocket;
26 import javax.net.ssl.TrustManager;
27 import javax.net.ssl.X509TrustManager;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.velux.internal.VeluxBindingConstants;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * Transport layer supported by the Velux bridge.
37  * <P>
38  * SLIP-based 2nd Level I/O interface towards the <B>Velux</B> bridge.
39  * <P>
40  * It provides methods for pre- and post-communication
41  * as well as a common method for the real communication.
42  * <UL>
43  * <LI>{@link SSLconnection#SSLconnection} for establishing the connection,</LI>
44  * <LI>{@link SSLconnection#send} for sending a message to the bridge,</LI>
45  * <LI>{@link SSLconnection#available} for observation whether there are bytes available,</LI>
46  * <LI>{@link SSLconnection#receive} for receiving a message from the bridge,</LI>
47  * <LI>{@link SSLconnection#close} for tearing down the connection.</LI>
48  * <LI>{@link SSLconnection#setTimeout} for adapting communication parameters.</LI>
49  * </UL>
50  *
51  * @author Guenther Schreiner - Initial contribution.
52  */
53 @NonNullByDefault
54 class SSLconnection {
55     private final Logger logger = LoggerFactory.getLogger(SSLconnection.class);
56
57     // Public definition
58     public static final SSLconnection UNKNOWN = new SSLconnection();
59
60     /*
61      * ***************************
62      * ***** Private Objects *****
63      */
64
65     private static final int CONNECTION_BUFFER_SIZE = 4096;
66
67     private boolean ready = false;
68     private @Nullable SSLSocket socket;
69     private @Nullable DataOutputStream dOut;
70     private @Nullable DataInputStreamWithTimeout dIn;
71     private int ioTimeoutMSecs = 60000;
72
73     /**
74      * Fake trust manager to suppress any certificate errors,
75      * used within {@link #SSLconnection} for seamless operation
76      * even on self-signed certificates like provided by <B>Velux</B>.
77      */
78     private final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
79         @Override
80         public X509Certificate @Nullable [] getAcceptedIssuers() {
81             return null;
82         }
83
84         @Override
85         public void checkClientTrusted(X509Certificate @Nullable [] arg0, @Nullable String arg1)
86                 throws CertificateException {
87         }
88
89         @Override
90         public void checkServerTrusted(X509Certificate @Nullable [] arg0, @Nullable String arg1)
91                 throws CertificateException {
92         }
93     } };
94
95     /*
96      * ************************
97      * ***** Constructors *****
98      */
99
100     /**
101      * Constructor for initialization of an unfinished connectivity.
102      */
103     SSLconnection() {
104         logger.debug("SSLconnection() called.");
105         ready = false;
106         logger.trace("SSLconnection() finished.");
107     }
108
109     /**
110      * Constructor to setup and establish a connection.
111      *
112      * @param host as String describing the Service Access Point location i.e. hostname.
113      * @param port as String describing the Service Access Point location i.e. TCP port.
114      * @throws java.net.ConnectException in case of unrecoverable communication failures.
115      * @throws java.io.IOException in case of continuous communication I/O failures.
116      * @throws java.net.UnknownHostException in case of continuous communication I/O failures.
117      */
118     SSLconnection(String host, int port) throws ConnectException, IOException, UnknownHostException {
119         logger.debug("SSLconnection({},{}) called.", host, port);
120         logger.info("Starting {} bridge connection.", VeluxBindingConstants.BINDING_ID);
121         SSLContext ctx = null;
122         try {
123             ctx = SSLContext.getInstance("SSL");
124             ctx.init(null, trustAllCerts, null);
125         } catch (NoSuchAlgorithmException | KeyManagementException e) {
126             throw new IOException(String.format("create of an empty trust store failed: %s.", e.getMessage()));
127         }
128         logger.trace("SSLconnection(): creating socket...");
129         // Just for avoidance of Potential null pointer access
130         SSLSocket socketX = (SSLSocket) ctx.getSocketFactory().createSocket(host, port);
131         logger.trace("SSLconnection(): starting SSL handshake...");
132         if (socketX != null) {
133             socketX.startHandshake();
134             dOut = new DataOutputStream(socketX.getOutputStream());
135             dIn = new DataInputStreamWithTimeout(socketX.getInputStream());
136             ready = true;
137             socket = socketX;
138         }
139         logger.trace("SSLconnection() finished.");
140     }
141
142     /*
143      * **************************
144      * ***** Public Methods *****
145      */
146
147     /**
148      * Method to query the readiness of the connection.
149      *
150      * @return <b>ready</b> as boolean for an established connection.
151      */
152     synchronized boolean isReady() {
153         return ready;
154     }
155
156     /**
157      * Method to pass a message towards the bridge.
158      * This method gets called when we are initiating a new SLIP transaction.
159      * <p>
160      * Note that DataOutputStream and DataInputStream are buffered I/O's. The SLIP protocol requires that prior requests
161      * should have been fully sent over the socket, and their responses should have been fully read from the buffer
162      * before the next request is initiated. i.e. Both read and write buffers should already be empty. Nevertheless,
163      * just in case, we do the following..
164      * <p>
165      * 1) Flush from the read buffer any orphan response data that may have been left over from prior transactions, and
166      * 2) Flush the write buffer directly to the socket to ensure that any exceptions are raised immediately, and the
167      * KLF starts work immediately
168      *
169      * @param packet as Array of bytes to be transmitted towards the bridge via the established connection.
170      * @throws java.io.IOException in case of a communication I/O failure, and sets 'ready' = false
171      */
172     @SuppressWarnings("null")
173     synchronized void send(byte[] packet) throws IOException {
174         logger.trace("send() called, writing {} bytes.", packet.length);
175         try {
176             if (!ready || (dOut == null) || (dIn == null)) {
177                 throw new IOException();
178             }
179             // flush the read buffer if (exceptionally) there is orphan response data in it
180             flushReadBufffer();
181             // copy packet data to the write buffer
182             dOut.write(packet, 0, packet.length);
183             // force the write buffer data to be written to the socket
184             dOut.flush();
185             if (logger.isTraceEnabled()) {
186                 StringBuilder sb = new StringBuilder();
187                 for (byte b : packet) {
188                     sb.append(String.format("%02X ", b));
189                 }
190                 logger.trace("send() finished after having send {} bytes: {}", packet.length, sb.toString());
191             }
192         } catch (IOException e) {
193             ready = false;
194             throw e;
195         }
196     }
197
198     /**
199      * Method to verify that there is message from the bridge.
200      *
201      * @return <b>true</b> if there are any bytes ready to be queried using {@link SSLconnection#receive}.
202      * @throws java.io.IOException in case of a communication I/O failure.
203      */
204     synchronized boolean available() throws IOException {
205         logger.trace("available() called.");
206         if (!ready || (dIn == null)) {
207             throw new IOException();
208         }
209         @SuppressWarnings("null")
210         int availableBytes = dIn.available();
211         logger.trace("available(): found {} bytes ready to be read (> 0 means true).", availableBytes);
212         return availableBytes > 0;
213     }
214
215     /**
216      * Method to get a message from the bridge.
217      *
218      * @return <b>packet</b> as Array of bytes as received from the bridge via the established connection.
219      * @throws java.io.IOException in case of a communication I/O failure, and sets 'ready' = false
220      */
221     synchronized byte[] receive() throws IOException {
222         logger.trace("receive() called.");
223         try {
224             if (!ready || (dIn == null)) {
225                 throw new IOException();
226             }
227             byte[] message = new byte[CONNECTION_BUFFER_SIZE];
228             @SuppressWarnings("null")
229             int messageLength = dIn.read(message, 0, message.length, ioTimeoutMSecs);
230             byte[] packet = new byte[messageLength];
231             System.arraycopy(message, 0, packet, 0, messageLength);
232             if (logger.isTraceEnabled()) {
233                 StringBuilder sb = new StringBuilder();
234                 for (byte b : packet) {
235                     sb.append(String.format("%02X ", b));
236                 }
237                 logger.trace("receive() finished after having read {} bytes: {}", messageLength, sb.toString());
238             }
239             return packet;
240         } catch (IOException e) {
241             ready = false;
242             throw e;
243         }
244     }
245
246     /**
247      * Destructor to tear down a connection.
248      *
249      * @throws java.io.IOException in case of a communication I/O failure.
250      */
251     synchronized void close() throws IOException {
252         logger.debug("close() called.");
253         ready = false;
254         logger.info("Shutting down Velux bridge connection.");
255         // Just for avoidance of Potential null pointer access
256         DataInputStreamWithTimeout dInX = dIn;
257         if (dInX != null) {
258             dInX.close();
259             dIn = null;
260         }
261         // Just for avoidance of Potential null pointer access
262         DataOutputStream dOutX = dOut;
263         if (dOutX != null) {
264             dOutX.close();
265             dOut = null;
266         }
267         // Just for avoidance of Potential null pointer access
268         SSLSocket socketX = socket;
269         if (socketX != null) {
270             socketX.close();
271             socket = null;
272         }
273         logger.trace("close() finished.");
274     }
275
276     /**
277      * Parameter modification.
278      *
279      * @param timeoutMSecs the maximum duration in milliseconds for read operations.
280      */
281     void setTimeout(int timeoutMSecs) {
282         logger.debug("setTimeout() set timeout to {} milliseconds.", timeoutMSecs);
283         ioTimeoutMSecs = timeoutMSecs;
284     }
285
286     /**
287      * Method to flush the input buffer.
288      *
289      * @throws java.io.IOException in case of a communication I/O failure.
290      */
291     private void flushReadBufffer() throws IOException {
292         logger.trace("flushReadBuffer() called.");
293         DataInputStreamWithTimeout dInX = dIn;
294         if (!ready || (dInX == null)) {
295             throw new IOException();
296         }
297         int byteCount = dInX.available();
298         if (byteCount > 0) {
299             byte[] byteArray = new byte[byteCount];
300             dInX.readFully(byteArray);
301             if (logger.isTraceEnabled()) {
302                 StringBuilder stringBuilder = new StringBuilder();
303                 for (byte currByte : byteArray) {
304                     stringBuilder.append(String.format("%02X ", currByte));
305                 }
306                 logger.trace("flushReadBuffer(): discarded {} unexpected bytes in the input buffer: {}", byteCount,
307                         stringBuilder.toString());
308             } else {
309                 logger.warn("flushReadBuffer(): discarded {} unexpected bytes in the input buffer", byteCount);
310             }
311         }
312     }
313 }