2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.tapocontrol.internal.api.protocol.aes;
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.*;
21 import java.util.Objects;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
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;
44 * Handler class for TAPO-SECUREPASSTHROUGH-Protocol
46 * @author Christian Wild - Initial contribution
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;
55 /***********************
57 **********************/
59 public SecurePassthrough(TapoConnectorInterface httpDelegator) {
60 this.httpDelegator = httpDelegator;
61 session = new SecurePassthroughSession(this);
62 uid = httpDelegator.getThingUID() + " / HTTP-SecurePasstrhough";
65 /***********************
67 **********************/
70 public boolean login(TapoCredentials tapoCredentials) throws TapoErrorHandler {
71 logger.trace("({}) login to device", uid);
73 session.login(tapoCredentials);
74 return session.isHandshakeComplete();
78 public void logout() {
79 logger.trace("({}) logout from device", uid);
84 public boolean isLoggedIn() {
85 return session.isHandshakeComplete();
88 /***********************
90 **********************/
93 * send synchron request - request will be sent encrypted (secured)
94 * response will be handled in [responseReceived()] function
97 public void sendRequest(TapoRequest request) throws TapoErrorHandler {
98 sendRequest(request, true);
102 * send synchron request - response will be handled in [responseReceived()] funktion
104 * @param encrypt - if false response will be sent unsecured
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);
111 tapoRequest = session.encryptRequest(tapoRequest);
112 logger.trace("({}) encrypted request: '{}' with cookie '{}'", uid, tapoRequest, session.getCookie());
115 Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST);
118 httpRequest = setHeaders(httpRequest);
119 httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
121 /* add request body */
122 httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
125 responseReceived(httpRequest.send(), command);
126 } catch (Exception e) {
127 throw new TapoErrorHandler(e, "error sending request");
132 * send asynchron request - request will be sent encrypted (secured)
133 * response will be handled in [asyncResponseReceived()] function
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);
141 TapoRequest encryptedRequest = session.encryptRequest(tapoRequest);
142 logger.trace("({}) sending encrypted request to '{}' with cookie '{}'", uid, url, session.getCookie());
144 Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST);
147 httpRequest = setHeaders(httpRequest);
149 /* add request body */
150 httpRequest.content(new StringContentProvider(encryptedRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
152 httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
153 @NonNullByDefault({})
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));
166 logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
167 httpDelegator.handleError(new TapoErrorHandler(new Exception(e), errorMessage));
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()));
173 /* request successful */
174 String rBody = getContentAsString();
176 asyncResponseReceived(rBody, command);
177 } catch (TapoErrorHandler tapoError) {
178 httpDelegator.handleError(tapoError);
185 /************************
187 ************************/
190 * handle synchron request-response
191 * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
194 public void responseReceived(ContentResponse response, String command) throws TapoErrorHandler {
195 logger.trace("({}) received response: {}", uid, response.getContentAsString());
196 TapoResponse tapoResponse = getTapoResponse(response);
197 httpDelegator.handleResponse(tapoResponse, command);
198 httpDelegator.responsePasstrough(response.getContentAsString(), command);
202 * handle asynchron request-response
203 * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
206 public void asyncResponseReceived(String content, String command) throws TapoErrorHandler {
207 logger.trace("({}) asyncResponseReceived '{}'", uid, content);
209 TapoResponse tapoResponse = getTapoResponse(content);
210 httpDelegator.handleResponse(tapoResponse, command);
211 } catch (TapoErrorHandler tapoError) {
212 httpDelegator.handleError(tapoError);
217 * Get Tapo-Response from Contentresponse
218 * decrypt if is encrypted
220 protected TapoResponse getTapoResponse(ContentResponse response) throws TapoErrorHandler {
221 if (response.getStatus() == 200) {
222 return getTapoResponse(response.getContentAsString());
224 logger.debug("({}) invalid response received", uid);
225 throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
230 * Get Tapo-Response from responsestring
231 * decrypt if is encrypted
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);
239 if (tapoResponse.hasError()) {
240 throw new TapoErrorHandler(tapoResponse.errorCode(), tapoResponse.message());
244 logger.debug("({}) invalid response received", uid);
245 throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
249 /************************
251 ************************/
254 * Get Url requests are sent to
256 protected String getUrl() {
257 String baseUrl = String.format(TAPO_DEVICE_URL, httpDelegator.getBaseUrl());
258 if (session.isHandshakeComplete()) {
259 return baseUrl + "?token=" + session.getToken();
268 protected Request setHeaders(Request httpRequest) {
269 httpRequest.header("Accept", CONTENT_TYPE_JSON);
270 if (session.isHandshakeComplete()) {
271 httpRequest.header(HTTP_AUTH_TYPE_COOKIE, session.getCookie());