]> git.basschouten.com Git - openhab-addons.git/blob
47ce5cb5480343ab6926a912e7ea03f35548a523
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.io.openhabcloud.internal;
14
15 import java.io.IOException;
16 import java.net.MalformedURLException;
17 import java.net.URI;
18 import java.net.URISyntaxException;
19 import java.net.URL;
20 import java.net.URLEncoder;
21 import java.nio.charset.StandardCharsets;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.concurrent.ConcurrentHashMap;
27
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.client.api.Request;
31 import org.eclipse.jetty.client.util.BytesContentProvider;
32 import org.eclipse.jetty.http.HttpField;
33 import org.eclipse.jetty.http.HttpFields;
34 import org.eclipse.jetty.http.HttpMethod;
35 import org.eclipse.jetty.http.HttpStatus;
36 import org.eclipse.jetty.util.BufferUtil;
37 import org.eclipse.jetty.util.URIUtil;
38 import org.json.JSONException;
39 import org.json.JSONObject;
40 import org.openhab.core.OpenHAB;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 import io.socket.client.IO;
45 import io.socket.client.Manager;
46 import io.socket.client.Socket;
47 import io.socket.emitter.Emitter;
48 import io.socket.engineio.client.Transport;
49
50 /**
51  * This class provides communication between openHAB and the openHAB Cloud service.
52  * It also implements async http proxy for serving requests from user to
53  * openHAB through the openHAB Cloud. It uses Socket.IO connection to connect to
54  * the openHAB Cloud service and Jetty Http client to send local http requests to
55  * openHAB.
56  *
57  * @author Victor Belov - Initial contribution
58  * @author Kai Kreuzer - migrated code to new Jetty client and ESH APIs
59  */
60 public class CloudClient {
61     /*
62      * Logger for this class
63      */
64     private final Logger logger = LoggerFactory.getLogger(CloudClient.class);
65
66     /*
67      * This variable holds base URL for the openHAB Cloud connections
68      */
69     private final String baseURL;
70
71     /*
72      * This variable holds openHAB's UUID for authenticating and connecting to the openHAB Cloud
73      */
74     private final String uuid;
75
76     /*
77      * This variable holds openHAB's secret for authenticating and connecting to the openHAB Cloud
78      */
79     private final String secret;
80
81     /*
82      * This variable holds local openHAB's base URL for connecting to the local openHAB instance
83      */
84     private final String localBaseUrl;
85
86     /*
87      * This variable holds instance of Jetty HTTP client to make requests to local openHAB
88      */
89     private final HttpClient jettyClient;
90
91     /*
92      * This map holds HTTP requests to local openHAB which are currently running
93      */
94     private final Map<Integer, Request> runningRequests = new ConcurrentHashMap<>();
95
96     /*
97      * This variable indicates if connection to the openHAB Cloud is currently in an established state
98      */
99     private boolean isConnected;
100
101     /*
102      * This variable holds version of local openHAB
103      */
104     private String openHABVersion;
105
106     /*
107      * This variable holds instance of Socket.IO client class which provides communication
108      * with the openHAB Cloud
109      */
110     private Socket socket;
111
112     /*
113      * The protocol of the openHAB-cloud URL.
114      */
115     private String protocol = "https";
116
117     /*
118      * This variable holds instance of CloudClientListener which provides callbacks to communicate
119      * certain events from the openHAB Cloud back to openHAB
120      */
121     private CloudClientListener listener;
122     private boolean remoteAccessEnabled;
123     private Set<String> exposedItems;
124
125     /**
126      * Constructor of CloudClient
127      *
128      * @param uuid openHAB's UUID to connect to the openHAB Cloud
129      * @param secret openHAB's Secret to connect to the openHAB Cloud
130      * @param remoteAccessEnabled Allow the openHAB Cloud to be used as a remote proxy
131      * @param exposedItems Items that are made available to apps connected to the openHAB Cloud
132      */
133     public CloudClient(HttpClient httpClient, String uuid, String secret, String baseURL, String localBaseUrl,
134             boolean remoteAccessEnabled, Set<String> exposedItems) {
135         this.uuid = uuid;
136         this.secret = secret;
137         this.baseURL = baseURL;
138         this.localBaseUrl = localBaseUrl;
139         this.remoteAccessEnabled = remoteAccessEnabled;
140         this.exposedItems = exposedItems;
141         this.jettyClient = httpClient;
142     }
143
144     /**
145      * Connect to the openHAB Cloud
146      */
147
148     public void connect() {
149         try {
150             socket = IO.socket(baseURL);
151             URL parsed = new URL(baseURL);
152             protocol = parsed.getProtocol();
153         } catch (URISyntaxException e) {
154             logger.error("Error creating Socket.IO: {}", e.getMessage());
155         } catch (MalformedURLException e) {
156             logger.error("Error parsing baseURL to get protocol, assuming https. Error: {}", e.getMessage());
157         }
158         socket.io().on(Manager.EVENT_TRANSPORT, new Emitter.Listener() {
159             @Override
160             public void call(Object... args) {
161                 logger.trace("Manager.EVENT_TRANSPORT");
162                 Transport transport = (Transport) args[0];
163                 transport.on(Transport.EVENT_REQUEST_HEADERS, new Emitter.Listener() {
164                     @Override
165                     public void call(Object... args) {
166                         logger.trace("Transport.EVENT_REQUEST_HEADERS");
167                         @SuppressWarnings("unchecked")
168                         Map<String, List<String>> headers = (Map<String, List<String>>) args[0];
169                         headers.put("uuid", List.of(uuid));
170                         headers.put("secret", List.of(secret));
171                         headers.put("openhabversion", List.of(OpenHAB.getVersion()));
172                         headers.put("clientversion", List.of(CloudService.clientVersion));
173                         headers.put("remoteaccess", List.of(((Boolean) remoteAccessEnabled).toString()));
174                     }
175                 });
176             }
177         });
178         socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {
179             @Override
180             public void call(Object... args) {
181                 logger.debug("Socket.IO connected");
182                 isConnected = true;
183                 onConnect();
184             }
185         }).on(Socket.EVENT_DISCONNECT, new Emitter.Listener() {
186             @Override
187             public void call(Object... args) {
188                 logger.debug("Socket.IO disconnected");
189                 isConnected = false;
190                 onDisconnect();
191             }
192         }).on(Socket.EVENT_ERROR, new Emitter.Listener() {
193             @Override
194             public void call(Object... args) {
195                 if (logger.isDebugEnabled()) {
196                     logger.error("Error connecting to the openHAB Cloud instance: {}", args[0]);
197                 } else {
198                     logger.error("Error connecting to the openHAB Cloud instance");
199                 }
200             }
201         }).on("request", new Emitter.Listener() {
202             @Override
203             public void call(Object... args) {
204                 onEvent("request", (JSONObject) args[0]);
205             }
206         }).on("cancel", new Emitter.Listener() {
207             @Override
208             public void call(Object... args) {
209                 onEvent("cancel", (JSONObject) args[0]);
210             }
211         }).on("command", new Emitter.Listener() {
212
213             @Override
214             public void call(Object... args) {
215                 onEvent("command", (JSONObject) args[0]);
216             }
217         });
218         socket.connect();
219     }
220
221     /**
222      * Callback method for socket.io client which is called when connection is established
223      */
224
225     public void onConnect() {
226         logger.info("Connected to the openHAB Cloud service (UUID = {}, base URL = {})", this.uuid, this.localBaseUrl);
227         isConnected = true;
228     }
229
230     /**
231      * Callback method for socket.io client which is called when disconnect occurs
232      */
233
234     public void onDisconnect() {
235         logger.info("Disconnected from the openHAB Cloud service (UUID = {}, base URL = {})", this.uuid,
236                 this.localBaseUrl);
237         isConnected = false;
238         // And clean up the list of running requests
239         runningRequests.clear();
240     }
241
242     /**
243      * Callback method for socket.io client which is called when an error occurs
244      */
245
246     public void onError(IOException error) {
247         logger.debug("{}", error.getMessage());
248     }
249
250     /**
251      * Callback method for socket.io client which is called when a message is received
252      */
253
254     public void onEvent(String event, JSONObject data) {
255         logger.debug("on(): {}", event);
256         if ("command".equals(event)) {
257             handleCommandEvent(data);
258             return;
259         }
260         if (remoteAccessEnabled) {
261             if ("request".equals(event)) {
262                 handleRequestEvent(data);
263             } else if ("cancel".equals(event)) {
264                 handleCancelEvent(data);
265             } else {
266                 logger.warn("Unsupported event from openHAB Cloud: {}", event);
267             }
268         }
269     }
270
271     private void handleRequestEvent(JSONObject data) {
272         try {
273             // Get unique request Id
274             int requestId = data.getInt("id");
275             logger.debug("Got request {}", requestId);
276             // Get request path
277             String requestPath = data.getString("path");
278             logger.debug("Path {}", requestPath);
279             // Get request method
280             String requestMethod = data.getString("method");
281             logger.debug("Method {}", requestMethod);
282             // Get JSONObject for request headers
283             JSONObject requestHeadersJson = data.getJSONObject("headers");
284             logger.debug("Headers: {}", requestHeadersJson.toString());
285             // Get request body
286             String requestBody = data.getString("body");
287             logger.trace("Body {}", requestBody);
288             // Get JSONObject for request query parameters
289             JSONObject requestQueryJson = data.getJSONObject("query");
290             logger.debug("Query {}", requestQueryJson.toString());
291             // Create URI builder with base request URI of openHAB and path from request
292             String newPath = URIUtil.addPaths(localBaseUrl, requestPath);
293             Iterator<String> queryIterator = requestQueryJson.keys();
294             // Add query parameters to URI builder, if any
295             newPath += "?";
296             while (queryIterator.hasNext()) {
297                 String queryName = queryIterator.next();
298                 newPath += queryName;
299                 newPath += "=";
300                 newPath += URLEncoder.encode(requestQueryJson.getString(queryName), "UTF-8");
301                 if (queryIterator.hasNext()) {
302                     newPath += "&";
303                 }
304             }
305             // Finally get the future request URI
306             URI requestUri = new URI(newPath);
307             // All preparations which are common for different methods are done
308             // Now perform the request to openHAB
309             // If method is GET
310             logger.debug("Request method is {}", requestMethod);
311             Request request = jettyClient.newRequest(requestUri);
312             setRequestHeaders(request, requestHeadersJson);
313             String proto = protocol;
314             if (data.has("protocol")) {
315                 proto = data.getString("protocol");
316             }
317             request.header("X-Forwarded-Proto", proto);
318
319             if (requestMethod.equals("GET")) {
320                 request.method(HttpMethod.GET);
321             } else if (requestMethod.equals("POST")) {
322                 request.method(HttpMethod.POST);
323                 request.content(new BytesContentProvider(requestBody.getBytes()));
324             } else if (requestMethod.equals("PUT")) {
325                 request.method(HttpMethod.PUT);
326                 request.content(new BytesContentProvider(requestBody.getBytes()));
327             } else {
328                 // TODO: Reject unsupported methods
329                 logger.warn("Unsupported request method {}", requestMethod);
330                 return;
331             }
332
333             request.onResponseHeaders(response -> {
334                 logger.debug("onHeaders {}", requestId);
335                 JSONObject responseJson = new JSONObject();
336                 try {
337                     responseJson.put("id", requestId);
338                     responseJson.put("headers", getJSONHeaders(response.getHeaders()));
339                     responseJson.put("responseStatusCode", response.getStatus());
340                     responseJson.put("responseStatusText", "OK");
341                     socket.emit("responseHeader", responseJson);
342                     logger.trace("Sent headers to request {}", requestId);
343                     logger.trace("{}", responseJson.toString());
344                 } catch (JSONException e) {
345                     logger.debug("{}", e.getMessage());
346                 }
347             }).onResponseContent((theResponse, content) -> {
348                 logger.debug("onResponseContent: {}, content size {}", requestId, String.valueOf(content.remaining()));
349                 JSONObject responseJson = new JSONObject();
350                 try {
351                     responseJson.put("id", requestId);
352                     responseJson.put("body", BufferUtil.toArray(content));
353                     if (logger.isTraceEnabled()) {
354                         logger.trace("{}", StandardCharsets.UTF_8.decode(content).toString());
355                     }
356                     socket.emit("responseContentBinary", responseJson);
357                     logger.trace("Sent content to request {}", requestId);
358                 } catch (JSONException e) {
359                     logger.debug("{}", e.getMessage());
360                 }
361             }).onRequestFailure((origRequest, failure) -> {
362                 logger.debug("onRequestFailure: {},  {}", requestId, failure.getMessage());
363                 JSONObject responseJson = new JSONObject();
364                 try {
365                     responseJson.put("id", requestId);
366                     responseJson.put("responseStatusText", "openHAB connection error: " + failure.getMessage());
367                     socket.emit("responseError", responseJson);
368                 } catch (JSONException e) {
369                     logger.debug("{}", e.getMessage());
370                 }
371             }).send(result -> {
372                 logger.debug("onComplete: {}", requestId);
373                 // Remove this request from list of running requests
374                 runningRequests.remove(requestId);
375                 if ((result != null && result.isFailed())
376                         && (result.getResponse() != null && result.getResponse().getStatus() != HttpStatus.OK_200)) {
377                     if (result.getFailure() != null) {
378                         logger.debug("Jetty request {} failed: {}", requestId, result.getFailure().getMessage());
379                     }
380                     if (result.getRequestFailure() != null) {
381                         logger.debug("Request Failure: {}", result.getRequestFailure().getMessage());
382                     }
383                     if (result.getResponseFailure() != null) {
384                         logger.debug("Response Failure: {}", result.getResponseFailure().getMessage());
385                     }
386                 }
387                 JSONObject responseJson = new JSONObject();
388                 try {
389                     responseJson.put("id", requestId);
390                     socket.emit("responseFinished", responseJson);
391                     logger.debug("Finished responding to request {}", requestId);
392                 } catch (JSONException e) {
393                     logger.debug("{}", e.getMessage());
394                 }
395             });
396
397             // If successfully submitted request to http client, add it to the list of currently
398             // running requests to be able to cancel it if needed
399             runningRequests.put(requestId, request);
400         } catch (JSONException | IOException | URISyntaxException e) {
401             logger.debug("{}", e.getMessage());
402         }
403     }
404
405     private void setRequestHeaders(Request request, JSONObject requestHeadersJson) {
406         Iterator<String> headersIterator = requestHeadersJson.keys();
407         // Convert JSONObject of headers into Header ArrayList
408         while (headersIterator.hasNext()) {
409             String headerName = headersIterator.next();
410             String headerValue;
411             try {
412                 headerValue = requestHeadersJson.getString(headerName);
413                 logger.debug("Jetty set header {} = {}", headerName, headerValue);
414                 if (!headerName.equalsIgnoreCase("Content-Length")) {
415                     request.header(headerName, headerValue);
416                 }
417             } catch (JSONException e) {
418                 logger.warn("Error processing request headers: {}", e.getMessage());
419             }
420         }
421     }
422
423     private void handleCancelEvent(JSONObject data) {
424         try {
425             int requestId = data.getInt("id");
426             logger.debug("Received cancel for request {}", requestId);
427             // Find and abort running request
428             Request request = runningRequests.get(requestId);
429             if (request != null) {
430                 request.abort(new InterruptedException());
431                 runningRequests.remove(requestId);
432             }
433         } catch (JSONException e) {
434             logger.debug("{}", e.getMessage());
435         }
436     }
437
438     private void handleCommandEvent(JSONObject data) {
439         String itemName = data.getString("item");
440         if (exposedItems.contains(itemName)) {
441             try {
442                 logger.debug("Received command {} for item {}.", data.getString("command"), itemName);
443                 if (this.listener != null) {
444                     this.listener.sendCommand(itemName, data.getString("command"));
445                 }
446             } catch (JSONException e) {
447                 logger.debug("{}", e.getMessage());
448             }
449         } else {
450             logger.warn("Received command from openHAB Cloud for item '{}', which is not exposed.", itemName);
451         }
452     }
453
454     /**
455      * This method sends notification to the openHAB Cloud
456      *
457      * @param userId openHAB Cloud user id
458      * @param message notification message text
459      * @param icon name of the icon for this notification
460      * @param severity severity name for this notification
461      */
462     public void sendNotification(String userId, String message, @Nullable String icon, @Nullable String severity) {
463         if (isConnected()) {
464             JSONObject notificationMessage = new JSONObject();
465             try {
466                 notificationMessage.put("userId", userId);
467                 notificationMessage.put("message", message);
468                 notificationMessage.put("icon", icon);
469                 notificationMessage.put("severity", severity);
470                 socket.emit("notification", notificationMessage);
471             } catch (JSONException e) {
472                 logger.debug("{}", e.getMessage());
473             }
474         } else {
475             logger.debug("No connection, notification is not sent");
476         }
477     }
478
479     /**
480      * This method sends log notification to the openHAB Cloud
481      *
482      * @param message notification message text
483      * @param icon name of the icon for this notification
484      * @param severity severity name for this notification
485      */
486     public void sendLogNotification(String message, @Nullable String icon, @Nullable String severity) {
487         if (isConnected()) {
488             JSONObject notificationMessage = new JSONObject();
489             try {
490                 notificationMessage.put("message", message);
491                 notificationMessage.put("icon", icon);
492                 notificationMessage.put("severity", severity);
493                 socket.emit("lognotification", notificationMessage);
494             } catch (JSONException e) {
495                 logger.debug("{}", e.getMessage());
496             }
497         } else {
498             logger.debug("No connection, notification is not sent");
499         }
500     }
501
502     /**
503      * This method sends broadcast notification to the openHAB Cloud
504      *
505      * @param message notification message text
506      * @param icon name of the icon for this notification
507      * @param severity severity name for this notification
508      */
509     public void sendBroadcastNotification(String message, @Nullable String icon, @Nullable String severity) {
510         if (isConnected()) {
511             JSONObject notificationMessage = new JSONObject();
512             try {
513                 notificationMessage.put("message", message);
514                 notificationMessage.put("icon", icon);
515                 notificationMessage.put("severity", severity);
516                 socket.emit("broadcastnotification", notificationMessage);
517             } catch (JSONException e) {
518                 logger.debug("{}", e.getMessage());
519             }
520         } else {
521             logger.debug("No connection, notification is not sent");
522         }
523     }
524
525     /**
526      * Send item update to openHAB Cloud
527      *
528      * @param itemName the name of the item
529      * @param itemState updated item state
530      *
531      */
532     public void sendItemUpdate(String itemName, String itemState) {
533         if (isConnected()) {
534             logger.debug("Sending update '{}' for item '{}'", itemState, itemName);
535             JSONObject itemUpdateMessage = new JSONObject();
536             try {
537                 itemUpdateMessage.put("itemName", itemName);
538                 itemUpdateMessage.put("itemStatus", itemState);
539                 socket.emit("itemupdate", itemUpdateMessage);
540             } catch (JSONException e) {
541                 logger.debug("{}", e.getMessage());
542             }
543         } else {
544             logger.debug("No connection, Item update is not sent");
545         }
546     }
547
548     /**
549      * Returns true if openHAB Cloud connection is active
550      */
551     public boolean isConnected() {
552         return isConnected;
553     }
554
555     /**
556      * Disconnect from openHAB Cloud
557      */
558     public void shutdown() {
559         logger.info("Shutting down openHAB Cloud service connection");
560         socket.disconnect();
561     }
562
563     public String getOpenHABVersion() {
564         return openHABVersion;
565     }
566
567     public void setOpenHABVersion(String openHABVersion) {
568         this.openHABVersion = openHABVersion;
569     }
570
571     public void setListener(CloudClientListener listener) {
572         this.listener = listener;
573     }
574
575     private JSONObject getJSONHeaders(HttpFields httpFields) {
576         JSONObject headersJSON = new JSONObject();
577         try {
578             for (HttpField field : httpFields) {
579                 headersJSON.put(field.getName(), field.getValue());
580             }
581         } catch (JSONException e) {
582             logger.warn("Error forming response headers: {}", e.getMessage());
583         }
584         return headersJSON;
585     }
586 }