]> git.basschouten.com Git - openhab-addons.git/blob
2d3f65ba577c7b3df1d8cb6303a931e81032f27d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.tapocontrol.internal.api.protocol.aes;
14
15 import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
16 import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
17 import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
18 import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
19 import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
20
21 import java.util.Objects;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jetty.client.HttpResponse;
27 import org.eclipse.jetty.client.api.ContentResponse;
28 import org.eclipse.jetty.client.api.Request;
29 import org.eclipse.jetty.client.api.Result;
30 import org.eclipse.jetty.client.util.BufferingResponseListener;
31 import org.eclipse.jetty.client.util.StringContentProvider;
32 import org.eclipse.jetty.http.HttpMethod;
33 import org.openhab.binding.tapocontrol.internal.api.TapoConnectorInterface;
34 import org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolInterface;
35 import org.openhab.binding.tapocontrol.internal.dto.TapoBaseRequestInterface;
36 import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
37 import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
38 import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
39 import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 /**
44  * Handler class for TAPO-SECUREPASSTHROUGH-Protocol
45  *
46  * @author Christian Wild - Initial contribution
47  */
48 @NonNullByDefault
49 public class SecurePassthrough implements TapoProtocolInterface {
50     private final Logger logger = LoggerFactory.getLogger(SecurePassthrough.class);
51     protected final TapoConnectorInterface httpDelegator;
52     private final SecurePassthroughSession session;
53     private final String uid;
54
55     /***********************
56      * Init Class
57      **********************/
58
59     public SecurePassthrough(TapoConnectorInterface httpDelegator) {
60         this.httpDelegator = httpDelegator;
61         session = new SecurePassthroughSession(this);
62         uid = httpDelegator.getThingUID() + " / HTTP-SecurePasstrhough";
63     }
64
65     /***********************
66      * Login Handling
67      **********************/
68
69     @Override
70     public boolean login(TapoCredentials tapoCredentials) throws TapoErrorHandler {
71         logger.trace("({}) login to device", uid);
72         session.reset();
73         session.login(tapoCredentials);
74         return session.isHandshakeComplete();
75     }
76
77     @Override
78     public void logout() {
79         logger.trace("({}) logout from device", uid);
80         session.reset();
81     }
82
83     @Override
84     public boolean isLoggedIn() {
85         return session.isHandshakeComplete();
86     }
87
88     /***********************
89      * Request Sender
90      **********************/
91
92     /*
93      * send synchron request - request will be sent encrypted (secured)
94      * response will be handled in [responseReceived()] function
95      */
96     @Override
97     public void sendRequest(TapoRequest request) throws TapoErrorHandler {
98         sendRequest(request, true);
99     }
100
101     /*
102      * send synchron request - response will be handled in [responseReceived()] funktion
103      * 
104      * @param encrypt - if false response will be sent unsecured
105      */
106     private void sendRequest(TapoRequest tapoRequest, boolean encrypt) throws TapoErrorHandler {
107         String url = getUrl();
108         String command = tapoRequest.method();
109         logger.trace("({}) sending unencrypted request: '{}' to '{}' ", uid, tapoRequest, url);
110         if (encrypt) {
111             tapoRequest = session.encryptRequest(tapoRequest);
112             logger.trace("({}) encrypted request: '{}' with cookie '{}'", uid, tapoRequest, session.getCookie());
113         }
114
115         Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
116
117         /* set header */
118         httpRequest = setHeaders(httpRequest);
119         httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
120
121         /* add request body */
122         httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
123
124         try {
125             responseReceived(httpRequest.send(), command);
126         } catch (Exception e) {
127             throw new TapoErrorHandler(e, "error sending request");
128         }
129     }
130
131     /*
132      * send asynchron request - request will be sent encrypted (secured)
133      * response will be handled in [asyncResponseReceived()] function
134      */
135     @Override
136     public void sendAsyncRequest(TapoBaseRequestInterface tapoRequest) throws TapoErrorHandler {
137         String url = getUrl();
138         String command = tapoRequest.method();
139         logger.trace("({}) sendAsync unencrypted request: '{}' to '{}' ", uid, tapoRequest, url);
140
141         TapoRequest encryptedRequest = session.encryptRequest(tapoRequest);
142         logger.trace("({}) sending encrypted request to '{}' with cookie '{}'", uid, url, session.getCookie());
143
144         Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
145
146         /* set header */
147         httpRequest = setHeaders(httpRequest);
148
149         /* add request body */
150         httpRequest.content(new StringContentProvider(encryptedRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
151
152         httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
153             @NonNullByDefault({})
154             @Override
155             public void onComplete(Result result) {
156                 final HttpResponse response = (HttpResponse) result.getResponse();
157                 if (result.getFailure() != null) {
158                     /* handle result errors */
159                     Throwable e = result.getFailure();
160                     String errorMessage = getValueOrDefault(e.getMessage(), "");
161                     /* throw errors to delegator */
162                     if (e instanceof TimeoutException) {
163                         logger.debug("({}) sendAsyncRequest timeout'{}'", uid, errorMessage);
164                         httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, errorMessage));
165                     } else {
166                         logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
167                         httpDelegator.handleError(new TapoErrorHandler(new Exception(e), errorMessage));
168                     }
169                 } else if (response.getStatus() != 200) {
170                     logger.debug("({}) sendAsyncRequest response error'{}'", uid, response.getStatus());
171                     httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, getContentAsString()));
172                 } else {
173                     /* request successful */
174                     String rBody = getContentAsString();
175                     try {
176                         asyncResponseReceived(rBody, command);
177                     } catch (TapoErrorHandler tapoError) {
178                         httpDelegator.handleError(tapoError);
179                     }
180                 }
181             }
182         });
183     }
184
185     /************************
186      * RESPONSE HANDLERS
187      ************************/
188
189     /**
190      * handle synchron request-response
191      * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
192      */
193     @Override
194     public void responseReceived(ContentResponse response, String command) throws TapoErrorHandler {
195         logger.trace("({}) recived response: {}", uid, response.getContentAsString());
196         TapoResponse tapoResponse = getTapoResponse(response);
197         httpDelegator.handleResponse(tapoResponse, command);
198         httpDelegator.responsePasstrough(response.getContentAsString(), command);
199     }
200
201     /**
202      * handle asynchron request-response
203      * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
204      */
205     @Override
206     public void asyncResponseReceived(String content, String command) throws TapoErrorHandler {
207         logger.trace("({}) asyncResponseReceived '{}'", uid, content);
208         try {
209             TapoResponse tapoResponse = getTapoResponse(content);
210             httpDelegator.handleResponse(tapoResponse, command);
211         } catch (TapoErrorHandler tapoError) {
212             httpDelegator.handleError(tapoError);
213         }
214     }
215
216     /**
217      * Get Tapo-Response from Contentresponse
218      * decrypt if is encrypted
219      */
220     protected TapoResponse getTapoResponse(ContentResponse response) throws TapoErrorHandler {
221         if (response.getStatus() == 200) {
222             return getTapoResponse(response.getContentAsString());
223         } else {
224             logger.debug("({}) invalid response received", uid);
225             throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
226         }
227     }
228
229     /**
230      * Get Tapo-Response from responsestring
231      * decrypt if is encrypted
232      */
233     protected TapoResponse getTapoResponse(String responseString) throws TapoErrorHandler {
234         if (isValidJson(responseString)) {
235             TapoResponse tapoResponse = Objects.requireNonNull(GSON.fromJson(responseString, TapoResponse.class));
236             if (tapoResponse.result().has("response")) {
237                 tapoResponse = session.decryptResponse(tapoResponse);
238             }
239             if (tapoResponse.hasError()) {
240                 throw new TapoErrorHandler(tapoResponse.errorCode(), tapoResponse.message());
241             }
242             return tapoResponse;
243         } else {
244             logger.debug("({}) invalid response received", uid);
245             throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
246         }
247     }
248
249     /************************
250      * PRIVATE HELPERS
251      ************************/
252
253     /**
254      * Get Url requests are sent to
255      */
256     protected String getUrl() {
257         String baseUrl = String.format(TAPO_DEVICE_URL, httpDelegator.getBaseUrl());
258         if (session.isHandshakeComplete()) {
259             return baseUrl + "?token=" + session.getToken();
260         } else {
261             return baseUrl;
262         }
263     }
264
265     /**
266      * Set HTTP-Headers
267      */
268     protected Request setHeaders(Request httpRequest) {
269         httpRequest.header("content-type", CONTENT_TYPE_JSON);
270         httpRequest.header("Accept", CONTENT_TYPE_JSON);
271         if (session.isHandshakeComplete()) {
272             httpRequest.header(HTTP_AUTH_TYPE_COOKIE, session.getCookie());
273         }
274         return httpRequest;
275     }
276 }