]> git.basschouten.com Git - openhab-addons.git/blob
8eb2854acb8ac3c400eed53634697e38095d662c
[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.binding.avmfritz.internal.hardware;
14
15 import static org.eclipse.jetty.http.HttpMethod.*;
16
17 import java.math.BigDecimal;
18 import java.nio.charset.StandardCharsets;
19 import java.security.MessageDigest;
20 import java.security.NoSuchAlgorithmException;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.client.api.ContentResponse;
31 import org.eclipse.jetty.client.util.StringContentProvider;
32 import org.openhab.binding.avmfritz.internal.config.AVMFritzBoxConfiguration;
33 import org.openhab.binding.avmfritz.internal.handler.AVMFritzBaseBridgeHandler;
34 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaApplyTemplateCallback;
35 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaCallback;
36 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetHeatingModeCallback;
37 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetHeatingTemperatureCallback;
38 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetSwitchCallback;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 /**
45  * This class handles requests to a FRITZ!OS web interface for interfacing with AVM home automation devices. It manages
46  * authentication and wraps commands.
47  *
48  * @author Robert Bausdorf, Christian Brauers - Initial contribution
49  * @author Christoph Weitkamp - Added support for AVM FRITZ!DECT 300 and Comet
50  *         DECT
51  * @author Christoph Weitkamp - Added support for groups
52  */
53 @NonNullByDefault
54 public class FritzAhaWebInterface {
55
56     private static final String WEBSERVICE_PATH = "login_sid.lua";
57     /**
58      * RegEx Pattern to grab the session ID from a login XML response
59      */
60     private static final Pattern SID_PATTERN = Pattern.compile("<SID>([a-fA-F0-9]*)</SID>");
61     /**
62      * RegEx Pattern to grab the challenge from a login XML response
63      */
64     private static final Pattern CHALLENGE_PATTERN = Pattern.compile("<Challenge>(\\w*)</Challenge>");
65     /**
66      * RegEx Pattern to grab the access privilege for home automation functions from a login XML response
67      */
68     private static final Pattern ACCESS_PATTERN = Pattern.compile("<Name>HomeAuto</Name>\\s*?<Access>([0-9])</Access>");
69
70     private final Logger logger = LoggerFactory.getLogger(FritzAhaWebInterface.class);
71     /**
72      * Configuration of the bridge from {@link AVMFritzBaseBridgeHandler}
73      */
74     private final AVMFritzBoxConfiguration config;
75     /**
76      * Bridge thing handler for updating thing status
77      */
78     private final AVMFritzBaseBridgeHandler handler;
79     /**
80      * Shared instance of HTTP client for asynchronous calls
81      */
82     private final HttpClient httpClient;
83     /**
84      * Current session ID
85      */
86     private @Nullable String sid;
87
88     /**
89      * This method authenticates with the FRITZ!OS Web Interface and updates the session ID accordingly
90      */
91     public void authenticate() {
92         sid = null;
93         String localPassword = config.password;
94         if (localPassword == null || localPassword.trim().isEmpty()) {
95             handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
96                     "Please configure the password.");
97             return;
98         }
99         String loginXml = syncGet(getURL(WEBSERVICE_PATH, addSID("")));
100         if (loginXml == null) {
101             handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
102                     "FRITZ!Box does not respond.");
103             return;
104         }
105         Matcher sidmatch = SID_PATTERN.matcher(loginXml);
106         if (!sidmatch.find()) {
107             handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
108                     "FRITZ!Box does not respond with SID.");
109             return;
110         }
111         String localSid = sidmatch.group(1);
112         Matcher accmatch = ACCESS_PATTERN.matcher(loginXml);
113         if (accmatch.find()) {
114             if ("2".equals(accmatch.group(1))) {
115                 sid = localSid;
116                 handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
117                 return;
118             }
119         }
120         Matcher challengematch = CHALLENGE_PATTERN.matcher(loginXml);
121         if (!challengematch.find()) {
122             handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
123                     "FRITZ!Box does not respond with challenge for authentication.");
124             return;
125         }
126         String challenge = challengematch.group(1);
127         String response = createResponse(challenge);
128         String localUser = config.user;
129         loginXml = syncGet(getURL(WEBSERVICE_PATH,
130                 (localUser == null || localUser.isEmpty() ? "" : ("username=" + localUser + "&")) + "response="
131                         + response));
132         if (loginXml == null) {
133             handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
134                     "FRITZ!Box does not respond.");
135             return;
136         }
137         sidmatch = SID_PATTERN.matcher(loginXml);
138         if (!sidmatch.find()) {
139             handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
140                     "FRITZ!Box does not respond with SID.");
141             return;
142         }
143         localSid = sidmatch.group(1);
144         accmatch = ACCESS_PATTERN.matcher(loginXml);
145         if (accmatch.find()) {
146             if ("2".equals(accmatch.group(1))) {
147                 sid = localSid;
148                 handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
149                 return;
150             }
151         }
152         handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "User "
153                 + (localUser == null ? "" : localUser) + " has no access to FRITZ!Box home automation functions.");
154         return;
155     }
156
157     /**
158      * Checks the authentication status of the web interface
159      *
160      * @return
161      */
162     public boolean isAuthenticated() {
163         return sid != null;
164     }
165
166     /**
167      * Creates the proper response to a given challenge based on the password stored
168      *
169      * @param challenge Challenge string as returned by the FRITZ!OS login script
170      * @return Response to the challenge
171      */
172     protected String createResponse(String challenge) {
173         String response = challenge.concat("-");
174         String handshake = response.concat(config.password);
175         MessageDigest md5;
176         try {
177             md5 = MessageDigest.getInstance("MD5");
178         } catch (NoSuchAlgorithmException e) {
179             logger.error("This version of Java does not support MD5 hashing");
180             return "";
181         }
182         byte[] handshakeHash = md5.digest(handshake.getBytes(StandardCharsets.UTF_16LE));
183         for (byte handshakeByte : handshakeHash) {
184             response = response.concat(String.format("%02x", handshakeByte));
185         }
186         return response;
187     }
188
189     /**
190      * Constructor to set up interface
191      *
192      * @param config Bridge configuration
193      */
194     public FritzAhaWebInterface(AVMFritzBoxConfiguration config, AVMFritzBaseBridgeHandler handler,
195             HttpClient httpClient) {
196         this.config = config;
197         this.handler = handler;
198         this.httpClient = httpClient;
199         authenticate();
200         logger.debug("Starting with SID {}", sid);
201     }
202
203     /**
204      * Constructs an URL from the stored information and a specified path
205      *
206      * @param path Path to include in URL
207      * @return URL
208      */
209     public String getURL(String path) {
210         return config.protocol + "://" + config.ipAddress + (config.port == null ? "" : ":" + config.port) + "/" + path;
211     }
212
213     /**
214      * Constructs an URL from the stored information, a specified path and a specified argument string
215      *
216      * @param path Path to include in URL
217      * @param args String of arguments, in standard HTTP format (arg1=value1&arg2=value2&...)
218      * @return URL
219      */
220     public String getURL(String path, String args) {
221         return getURL(args.isEmpty() ? path : path + "?" + args);
222     }
223
224     public String addSID(String path) {
225         if (sid == null) {
226             return path;
227         } else {
228             return (path.isEmpty() ? "" : path + "&") + "sid=" + sid;
229         }
230     }
231
232     /**
233      * Sends a HTTP GET request using the synchronous client
234      *
235      * @param path Path of the requested resource
236      * @return response
237      */
238     public @Nullable String syncGet(String url) {
239         try {
240             ContentResponse contentResponse = httpClient.newRequest(url)
241                     .timeout(config.syncTimeout, TimeUnit.MILLISECONDS).method(GET).send();
242             String content = contentResponse.getContentAsString();
243             logger.debug("GET response complete: {}", content);
244             return content;
245         } catch (ExecutionException | TimeoutException e) {
246             logger.debug("response failed: {}", e.getLocalizedMessage(), e);
247             return null;
248         } catch (InterruptedException e) {
249             logger.debug("response interrupted: {}", e.getLocalizedMessage(), e);
250             Thread.currentThread().interrupt();
251             return null;
252         }
253     }
254
255     /**
256      * Sends a HTTP GET request using the asynchronous client
257      *
258      * @param path Path of the requested resource
259      * @param args Arguments for the request
260      * @param callback Callback to handle the response with
261      */
262     public FritzAhaContentExchange asyncGet(String path, String args, FritzAhaCallback callback) {
263         if (!isAuthenticated()) {
264             authenticate();
265         }
266         FritzAhaContentExchange getExchange = new FritzAhaContentExchange(callback);
267         httpClient.newRequest(getURL(path, addSID(args))).method(GET).onResponseSuccess(getExchange)
268                 .onResponseFailure(getExchange).send(getExchange);
269         return getExchange;
270     }
271
272     public FritzAhaContentExchange asyncGet(FritzAhaCallback callback) {
273         return asyncGet(callback.getPath(), callback.getArgs(), callback);
274     }
275
276     /**
277      * Sends a HTTP POST request using the asynchronous client
278      *
279      * @param path Path of the requested resource
280      * @param args Arguments for the request
281      * @param callback Callback to handle the response with
282      */
283     public FritzAhaContentExchange asyncPost(String path, String args, FritzAhaCallback callback) {
284         if (!isAuthenticated()) {
285             authenticate();
286         }
287         FritzAhaContentExchange postExchange = new FritzAhaContentExchange(callback);
288         httpClient.newRequest(getURL(path)).timeout(config.asyncTimeout, TimeUnit.MILLISECONDS).method(POST)
289                 .onResponseSuccess(postExchange).onResponseFailure(postExchange)
290                 .content(new StringContentProvider(addSID(args), StandardCharsets.UTF_8)).send(postExchange);
291         return postExchange;
292     }
293
294     public FritzAhaContentExchange applyTemplate(String ain) {
295         FritzAhaApplyTemplateCallback callback = new FritzAhaApplyTemplateCallback(this, ain);
296         return asyncGet(callback);
297     }
298
299     public FritzAhaContentExchange setSwitch(String ain, boolean switchOn) {
300         FritzAhaSetSwitchCallback callback = new FritzAhaSetSwitchCallback(this, ain, switchOn);
301         return asyncGet(callback);
302     }
303
304     public FritzAhaContentExchange setSetTemp(String ain, BigDecimal temperature) {
305         FritzAhaSetHeatingTemperatureCallback callback = new FritzAhaSetHeatingTemperatureCallback(this, ain,
306                 temperature);
307         return asyncGet(callback);
308     }
309
310     public FritzAhaContentExchange setBoostMode(String ain, long endTime) {
311         return setHeatingMode(ain, FritzAhaSetHeatingModeCallback.BOOST_COMMAND, endTime);
312     }
313
314     public FritzAhaContentExchange setWindowOpenMode(String ain, long endTime) {
315         return setHeatingMode(ain, FritzAhaSetHeatingModeCallback.WINDOW_OPEN_COMMAND, endTime);
316     }
317
318     private FritzAhaContentExchange setHeatingMode(String ain, String command, long endTime) {
319         FritzAhaSetHeatingModeCallback callback = new FritzAhaSetHeatingModeCallback(this, ain, command, endTime);
320         return asyncGet(callback);
321     }
322 }