]> git.basschouten.com Git - openhab-addons.git/blob
e861c1f9073dcdd35a20c08f50ae5392d3392f30
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.freebox.internal.api;
14
15 import java.io.ByteArrayInputStream;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.net.URLEncoder;
19 import java.nio.charset.StandardCharsets;
20 import java.security.InvalidKeyException;
21 import java.security.NoSuchAlgorithmException;
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.Properties;
25 import java.util.concurrent.TimeUnit;
26
27 import javax.crypto.Mac;
28 import javax.crypto.spec.SecretKeySpec;
29
30 import org.openhab.binding.freebox.internal.api.model.FreeboxAirMediaConfig;
31 import org.openhab.binding.freebox.internal.api.model.FreeboxAirMediaConfigResponse;
32 import org.openhab.binding.freebox.internal.api.model.FreeboxAirMediaReceiver;
33 import org.openhab.binding.freebox.internal.api.model.FreeboxAirMediaReceiverRequest;
34 import org.openhab.binding.freebox.internal.api.model.FreeboxAirMediaReceiversResponse;
35 import org.openhab.binding.freebox.internal.api.model.FreeboxAuthorizationStatus;
36 import org.openhab.binding.freebox.internal.api.model.FreeboxAuthorizationStatusResponse;
37 import org.openhab.binding.freebox.internal.api.model.FreeboxAuthorizeRequest;
38 import org.openhab.binding.freebox.internal.api.model.FreeboxAuthorizeResponse;
39 import org.openhab.binding.freebox.internal.api.model.FreeboxAuthorizeResult;
40 import org.openhab.binding.freebox.internal.api.model.FreeboxCallEntry;
41 import org.openhab.binding.freebox.internal.api.model.FreeboxCallEntryResponse;
42 import org.openhab.binding.freebox.internal.api.model.FreeboxConnectionStatus;
43 import org.openhab.binding.freebox.internal.api.model.FreeboxConnectionStatusResponse;
44 import org.openhab.binding.freebox.internal.api.model.FreeboxDiscoveryResponse;
45 import org.openhab.binding.freebox.internal.api.model.FreeboxEmptyResponse;
46 import org.openhab.binding.freebox.internal.api.model.FreeboxFtpConfig;
47 import org.openhab.binding.freebox.internal.api.model.FreeboxFtpConfigResponse;
48 import org.openhab.binding.freebox.internal.api.model.FreeboxFtthStatusResponse;
49 import org.openhab.binding.freebox.internal.api.model.FreeboxLanConfigResponse;
50 import org.openhab.binding.freebox.internal.api.model.FreeboxLanHost;
51 import org.openhab.binding.freebox.internal.api.model.FreeboxLanHostsResponse;
52 import org.openhab.binding.freebox.internal.api.model.FreeboxLanInterface;
53 import org.openhab.binding.freebox.internal.api.model.FreeboxLanInterfacesResponse;
54 import org.openhab.binding.freebox.internal.api.model.FreeboxLcdConfig;
55 import org.openhab.binding.freebox.internal.api.model.FreeboxLcdConfigResponse;
56 import org.openhab.binding.freebox.internal.api.model.FreeboxLoginResponse;
57 import org.openhab.binding.freebox.internal.api.model.FreeboxOpenSessionRequest;
58 import org.openhab.binding.freebox.internal.api.model.FreeboxOpenSessionResponse;
59 import org.openhab.binding.freebox.internal.api.model.FreeboxPhoneStatus;
60 import org.openhab.binding.freebox.internal.api.model.FreeboxPhoneStatusResponse;
61 import org.openhab.binding.freebox.internal.api.model.FreeboxResponse;
62 import org.openhab.binding.freebox.internal.api.model.FreeboxSambaConfig;
63 import org.openhab.binding.freebox.internal.api.model.FreeboxSambaConfigResponse;
64 import org.openhab.binding.freebox.internal.api.model.FreeboxSystemConfig;
65 import org.openhab.binding.freebox.internal.api.model.FreeboxSystemConfigResponse;
66 import org.openhab.binding.freebox.internal.api.model.FreeboxUPnPAVConfig;
67 import org.openhab.binding.freebox.internal.api.model.FreeboxUPnPAVConfigResponse;
68 import org.openhab.binding.freebox.internal.api.model.FreeboxWifiGlobalConfig;
69 import org.openhab.binding.freebox.internal.api.model.FreeboxWifiGlobalConfigResponse;
70 import org.openhab.binding.freebox.internal.api.model.FreeboxXdslStatusResponse;
71 import org.openhab.core.io.net.http.HttpUtil;
72 import org.openhab.core.util.HexUtils;
73 import org.slf4j.Logger;
74 import org.slf4j.LoggerFactory;
75
76 import com.google.gson.FieldNamingPolicy;
77 import com.google.gson.Gson;
78 import com.google.gson.GsonBuilder;
79 import com.google.gson.JsonSyntaxException;
80
81 /**
82  * The {@link FreeboxApiManager} is responsible for the communication with the Freebox.
83  * It implements the different HTTP API calls provided by the Freebox
84  *
85  * @author Laurent Garnier - Initial contribution
86  */
87 public class FreeboxApiManager {
88
89     private final Logger logger = LoggerFactory.getLogger(FreeboxApiManager.class);
90
91     private static final int HTTP_CALL_DEFAULT_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(10);
92     private static final String AUTH_HEADER = "X-Fbx-App-Auth";
93     private static final String HTTP_CALL_CONTENT_TYPE = "application/json; charset=utf-8";
94
95     private String appId;
96     private String appName;
97     private String appVersion;
98     private String deviceName;
99     private String baseAddress;
100     private String appToken;
101     private String sessionToken;
102     private Gson gson;
103
104     public FreeboxApiManager(String appId, String appName, String appVersion, String deviceName) {
105         this.appId = appId;
106         this.appName = appName;
107         this.appVersion = appVersion;
108         this.deviceName = deviceName;
109         this.gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
110     }
111
112     public FreeboxDiscoveryResponse checkApi(String fqdn, boolean secureHttp) {
113         String url = String.format("%s://%s/api_version", secureHttp ? "https" : "http", fqdn);
114         try {
115             String jsonResponse = HttpUtil.executeUrl("GET", url, HTTP_CALL_DEFAULT_TIMEOUT_MS);
116             return gson.fromJson(jsonResponse, FreeboxDiscoveryResponse.class);
117         } catch (IOException | JsonSyntaxException e) {
118             logger.debug("checkApi with {} failed: {}", url, e.getMessage());
119             return null;
120         }
121     }
122
123     public boolean authorize(boolean useHttps, String fqdn, String apiBaseUrl, String apiVersion, String appToken)
124             throws InterruptedException {
125         String[] versionSplit = apiVersion.split("\\.");
126         String majorVersion = "5";
127         if (versionSplit.length > 0) {
128             majorVersion = versionSplit[0];
129         }
130         this.baseAddress = (useHttps ? "https://" : "http://") + fqdn + apiBaseUrl + "v" + majorVersion + "/";
131
132         boolean granted = false;
133         try {
134             String token = appToken;
135             if (token == null || token.isEmpty()) {
136                 FreeboxAuthorizeRequest request = new FreeboxAuthorizeRequest(appId, appName, appVersion, deviceName);
137                 FreeboxAuthorizeResult response = executePostUrl("login/authorize/", gson.toJson(request),
138                         FreeboxAuthorizeResponse.class, false, false, true);
139                 token = response.getAppToken();
140                 int trackId = response.getTrackId();
141                 FreeboxAuthorizationStatus result;
142                 do {
143                     Thread.sleep(2000);
144                     result = executeGetUrl("login/authorize/" + trackId, FreeboxAuthorizationStatusResponse.class,
145                             false);
146                 } while (result.isStatusPending());
147                 granted = result.isStatusGranted();
148             } else {
149                 granted = true;
150             }
151             if (!granted) {
152                 return false;
153             }
154
155             this.appToken = token;
156             openSession();
157             return true;
158         } catch (FreeboxException e) {
159             logger.debug("Error while opening a session", e);
160             return false;
161         }
162     }
163
164     private synchronized void openSession() throws FreeboxException {
165         if (appToken == null) {
166             throw new FreeboxException("No app token to open a new session");
167         }
168         sessionToken = null;
169         String challenge = executeGetUrl("login/", FreeboxLoginResponse.class, false).getChallenge();
170         FreeboxOpenSessionRequest request = new FreeboxOpenSessionRequest(appId, hmacSha1(appToken, challenge));
171         sessionToken = executePostUrl("login/session/", gson.toJson(request), FreeboxOpenSessionResponse.class, false,
172                 false, true).getSessionToken();
173     }
174
175     public synchronized void closeSession() {
176         if (sessionToken != null) {
177             try {
178                 executePostUrl("login/logout/", null, FreeboxEmptyResponse.class, false);
179             } catch (FreeboxException e) {
180             }
181             sessionToken = null;
182         }
183     }
184
185     public String getAppToken() {
186         return appToken;
187     }
188
189     public synchronized String getSessionToken() {
190         return sessionToken;
191     }
192
193     public FreeboxConnectionStatus getConnectionStatus() throws FreeboxException {
194         return executeGetUrl("connection/", FreeboxConnectionStatusResponse.class);
195     }
196
197     public String getxDslStatus() throws FreeboxException {
198         return executeGetUrl("connection/xdsl/", FreeboxXdslStatusResponse.class).getStatus();
199     }
200
201     public boolean getFtthPresent() throws FreeboxException {
202         return executeGetUrl("connection/ftth/", FreeboxFtthStatusResponse.class).getSfpPresent();
203     }
204
205     public boolean isWifiEnabled() throws FreeboxException {
206         return executeGetUrl("wifi/config/", FreeboxWifiGlobalConfigResponse.class).isEnabled();
207     }
208
209     public boolean enableWifi(boolean enable) throws FreeboxException {
210         FreeboxWifiGlobalConfig config = new FreeboxWifiGlobalConfig();
211         config.setEnabled(enable);
212         return executePutUrl("wifi/config/", gson.toJson(config), FreeboxWifiGlobalConfigResponse.class).isEnabled();
213     }
214
215     public boolean isFtpEnabled() throws FreeboxException {
216         return executeGetUrl("ftp/config/", FreeboxFtpConfigResponse.class).isEnabled();
217     }
218
219     public boolean enableFtp(boolean enable) throws FreeboxException {
220         FreeboxFtpConfig config = new FreeboxFtpConfig();
221         config.setEnabled(enable);
222         return executePutUrl("ftp/config/", gson.toJson(config), FreeboxFtpConfigResponse.class).isEnabled();
223     }
224
225     public boolean isAirMediaEnabled() throws FreeboxException {
226         return executeGetUrl("airmedia/config/", FreeboxAirMediaConfigResponse.class).isEnabled();
227     }
228
229     public boolean enableAirMedia(boolean enable) throws FreeboxException {
230         FreeboxAirMediaConfig config = new FreeboxAirMediaConfig();
231         config.setEnabled(enable);
232         return executePutUrl("airmedia/config/", gson.toJson(config), FreeboxAirMediaConfigResponse.class).isEnabled();
233     }
234
235     public boolean isUPnPAVEnabled() throws FreeboxException {
236         return executeGetUrl("upnpav/config/", FreeboxUPnPAVConfigResponse.class).isEnabled();
237     }
238
239     public boolean enableUPnPAV(boolean enable) throws FreeboxException {
240         FreeboxUPnPAVConfig config = new FreeboxUPnPAVConfig();
241         config.setEnabled(enable);
242         return executePutUrl("upnpav/config/", gson.toJson(config), FreeboxUPnPAVConfigResponse.class).isEnabled();
243     }
244
245     public FreeboxSambaConfig getSambaConfig() throws FreeboxException {
246         return executeGetUrl("netshare/samba/", FreeboxSambaConfigResponse.class);
247     }
248
249     public boolean enableSambaFileShare(boolean enable) throws FreeboxException {
250         FreeboxSambaConfig config = new FreeboxSambaConfig();
251         config.setFileShareEnabled(enable);
252         return executePutUrl("netshare/samba/", gson.toJson(config), FreeboxSambaConfigResponse.class)
253                 .isFileShareEnabled();
254     }
255
256     public boolean enableSambaPrintShare(boolean enable) throws FreeboxException {
257         FreeboxSambaConfig config = new FreeboxSambaConfig();
258         config.setPrintShareEnabled(enable);
259         return executePutUrl("netshare/samba/", gson.toJson(config), FreeboxSambaConfigResponse.class)
260                 .isPrintShareEnabled();
261     }
262
263     public FreeboxLcdConfig getLcdConfig() throws FreeboxException {
264         return executeGetUrl("lcd/config/", FreeboxLcdConfigResponse.class);
265     }
266
267     public int setLcdBrightness(int brightness) throws FreeboxException {
268         FreeboxLcdConfig config = getLcdConfig();
269         int newValue = Math.min(100, brightness);
270         newValue = Math.max(newValue, 0);
271         config.setBrightness(newValue);
272         return executePutUrl("lcd/config/", gson.toJson(config), FreeboxLcdConfigResponse.class).getBrightness();
273     }
274
275     public int increaseLcdBrightness() throws FreeboxException {
276         FreeboxLcdConfig config = getLcdConfig();
277         config.setBrightness(Math.min(100, config.getBrightness() + 1));
278         return executePutUrl("lcd/config/", gson.toJson(config), FreeboxLcdConfigResponse.class).getBrightness();
279     }
280
281     public int decreaseLcdBrightness() throws FreeboxException {
282         FreeboxLcdConfig config = getLcdConfig();
283         config.setBrightness(Math.max(0, config.getBrightness() - 1));
284         return executePutUrl("lcd/config/", gson.toJson(config), FreeboxLcdConfigResponse.class).getBrightness();
285     }
286
287     public FreeboxLcdConfig setLcdOrientation(int orientation) throws FreeboxException {
288         FreeboxLcdConfig config = getLcdConfig();
289         int newValue = Math.min(360, orientation);
290         newValue = Math.max(newValue, 0);
291         config.setOrientation(newValue);
292         config.setOrientationForced(true);
293         return executePutUrl("lcd/config/", gson.toJson(config), FreeboxLcdConfigResponse.class);
294     }
295
296     public boolean setLcdOrientationForced(boolean forced) throws FreeboxException {
297         FreeboxLcdConfig config = getLcdConfig();
298         config.setOrientationForced(forced);
299         return executePutUrl("lcd/config/", gson.toJson(config), FreeboxLcdConfigResponse.class).isOrientationForced();
300     }
301
302     public FreeboxSystemConfig getSystemConfig() throws FreeboxException {
303         return executeGetUrl("system/", FreeboxSystemConfigResponse.class);
304     }
305
306     public boolean isInLanBridgeMode() throws FreeboxException {
307         return executeGetUrl("lan/config/", FreeboxLanConfigResponse.class).isInBridgeMode();
308     }
309
310     public List<FreeboxLanHost> getLanHosts() throws FreeboxException {
311         List<FreeboxLanHost> hosts = new ArrayList<>();
312         List<FreeboxLanInterface> interfaces = executeGetUrl("lan/browser/interfaces/",
313                 FreeboxLanInterfacesResponse.class);
314         if (interfaces != null) {
315             for (FreeboxLanInterface lanInterface : interfaces) {
316                 if (lanInterface.getHostCount() > 0) {
317                     List<FreeboxLanHost> lanHosts = getLanHostsFromInterface(lanInterface.getName());
318                     if (lanHosts != null) {
319                         hosts.addAll(lanHosts);
320                     }
321                 }
322             }
323         }
324         return hosts;
325     }
326
327     private List<FreeboxLanHost> getLanHostsFromInterface(String lanInterface) throws FreeboxException {
328         return executeGetUrl("lan/browser/" + encodeUrl(lanInterface) + "/", FreeboxLanHostsResponse.class);
329     }
330
331     public FreeboxPhoneStatus getPhoneStatus() throws FreeboxException {
332         // This API is undocumented but working
333         // It is extracted from the freeboxos-java library
334         // https://github.com/MatMaul/freeboxos-java/blob/master/src/org/matmaul/freeboxos/phone/PhoneManager.java#L17
335         return executeGetUrl("phone/?_dc=1415032391207", FreeboxPhoneStatusResponse.class).get(0);
336     }
337
338     public List<FreeboxCallEntry> getCallEntries() throws FreeboxException {
339         return executeGetUrl("call/log/", FreeboxCallEntryResponse.class);
340     }
341
342     public List<FreeboxAirMediaReceiver> getAirMediaReceivers() throws FreeboxException {
343         return executeGetUrl("airmedia/receivers/", FreeboxAirMediaReceiversResponse.class, true, true, false);
344     }
345
346     public void playMedia(String url, String airPlayName, String airPlayPassword) throws FreeboxException {
347         FreeboxAirMediaReceiverRequest request = new FreeboxAirMediaReceiverRequest();
348         request.setStartAction();
349         request.setVideoMediaType();
350         if (airPlayPassword != null && !airPlayPassword.isEmpty()) {
351             request.setPassword(airPlayPassword);
352         }
353         request.setMedia(url);
354         executePostUrl("airmedia/receivers/" + encodeUrl(airPlayName) + "/", gson.toJson(request),
355                 FreeboxEmptyResponse.class, true, false, true);
356     }
357
358     public void stopMedia(String airPlayName, String airPlayPassword) throws FreeboxException {
359         FreeboxAirMediaReceiverRequest request = new FreeboxAirMediaReceiverRequest();
360         request.setStopAction();
361         request.setVideoMediaType();
362         if (airPlayPassword != null && !airPlayPassword.isEmpty()) {
363             request.setPassword(airPlayPassword);
364         }
365         executePostUrl("airmedia/receivers/" + encodeUrl(airPlayName) + "/", gson.toJson(request),
366                 FreeboxEmptyResponse.class, true, false, true);
367     }
368
369     public void reboot() throws FreeboxException {
370         executePostUrl("system/reboot/", null, FreeboxEmptyResponse.class);
371     }
372
373     private <T extends FreeboxResponse<F>, F> F executeGetUrl(String relativeUrl, Class<T> responseClass)
374             throws FreeboxException {
375         return executeUrl("GET", relativeUrl, null, responseClass, true, false, false);
376     }
377
378     private <T extends FreeboxResponse<F>, F> F executeGetUrl(String relativeUrl, Class<T> responseClass,
379             boolean retryAuth) throws FreeboxException {
380         return executeUrl("GET", relativeUrl, null, responseClass, retryAuth, false, false);
381     }
382
383     private <T extends FreeboxResponse<F>, F> F executeGetUrl(String relativeUrl, Class<T> responseClass,
384             boolean retryAuth, boolean patchTableReponse, boolean doNotLogData) throws FreeboxException {
385         return executeUrl("GET", relativeUrl, null, responseClass, retryAuth, patchTableReponse, doNotLogData);
386     }
387
388     private <T extends FreeboxResponse<F>, F> F executePostUrl(String relativeUrl, String requestContent,
389             Class<T> responseClass) throws FreeboxException {
390         return executeUrl("POST", relativeUrl, requestContent, responseClass, true, false, false);
391     }
392
393     private <T extends FreeboxResponse<F>, F> F executePostUrl(String relativeUrl, String requestContent,
394             Class<T> responseClass, boolean retryAuth) throws FreeboxException {
395         return executeUrl("POST", relativeUrl, requestContent, responseClass, retryAuth, false, false);
396     }
397
398     private <T extends FreeboxResponse<F>, F> F executePostUrl(String relativeUrl, String requestContent,
399             Class<T> responseClass, boolean retryAuth, boolean patchTableReponse, boolean doNotLogData)
400             throws FreeboxException {
401         return executeUrl("POST", relativeUrl, requestContent, responseClass, retryAuth, patchTableReponse,
402                 doNotLogData);
403     }
404
405     private <T extends FreeboxResponse<F>, F> F executePutUrl(String relativeUrl, String requestContent,
406             Class<T> responseClass) throws FreeboxException {
407         return executeUrl("PUT", relativeUrl, requestContent, responseClass, true, false, false);
408     }
409
410     private <T extends FreeboxResponse<F>, F> F executeUrl(String httpMethod, String relativeUrl, String requestContent,
411             Class<T> responseClass, boolean retryAuth, boolean patchTableReponse, boolean doNotLogData)
412             throws FreeboxException {
413         try {
414             Properties headers = null;
415             String token = getSessionToken();
416             if (token != null) {
417                 headers = new Properties();
418                 headers.setProperty(AUTH_HEADER, token);
419             }
420             InputStream stream = null;
421             String contentType = null;
422             if (requestContent != null) {
423                 stream = new ByteArrayInputStream(requestContent.getBytes(StandardCharsets.UTF_8));
424                 contentType = HTTP_CALL_CONTENT_TYPE;
425             }
426             logger.debug("executeUrl {} {} requestContent {}", httpMethod, relativeUrl,
427                     doNotLogData ? "***" : requestContent);
428             String jsonResponse = HttpUtil.executeUrl(httpMethod, baseAddress + relativeUrl, headers, stream,
429                     contentType, HTTP_CALL_DEFAULT_TIMEOUT_MS);
430             if (stream != null) {
431                 stream.close();
432                 stream = null;
433             }
434
435             if (patchTableReponse) {
436                 // Replace empty result by an empty table result
437                 jsonResponse = jsonResponse.replace("\"result\":{}", "\"result\":[]");
438             }
439
440             return evaluateJsonReesponse(jsonResponse, responseClass, doNotLogData);
441         } catch (FreeboxException e) {
442             if (retryAuth && e.isAuthRequired()) {
443                 logger.debug("Authentication required: open a new session and retry the request");
444                 openSession();
445                 return executeUrl(httpMethod, relativeUrl, requestContent, responseClass, false, patchTableReponse,
446                         doNotLogData);
447             }
448             throw e;
449         } catch (IOException e) {
450             throw new FreeboxException(httpMethod + " request " + relativeUrl + ": execution failed: " + e.getMessage(),
451                     e);
452         } catch (JsonSyntaxException e) {
453             throw new FreeboxException(
454                     httpMethod + " request " + relativeUrl + ": response parsing failed: " + e.getMessage(), e);
455         }
456     }
457
458     private <T extends FreeboxResponse<F>, F> F evaluateJsonReesponse(String jsonResponse, Class<T> responseClass,
459             boolean doNotLogData) throws JsonSyntaxException, FreeboxException {
460         logger.debug("evaluateJsonReesponse Json {}", doNotLogData ? "***" : jsonResponse);
461         // First check only if the result is successful
462         FreeboxResponse<Object> partialResponse = gson.fromJson(jsonResponse, FreeboxEmptyResponse.class);
463         partialResponse.evaluate();
464         // Parse the full response in case of success
465         T fullResponse = gson.fromJson(jsonResponse, responseClass);
466         fullResponse.evaluate();
467         F result = fullResponse.getResult();
468         return result;
469     }
470
471     private String encodeUrl(String url) throws FreeboxException {
472         return URLEncoder.encode(url, StandardCharsets.UTF_8);
473     }
474
475     public static String hmacSha1(String key, String value) throws FreeboxException {
476         try {
477             // Get an hmac_sha1 key from the raw key bytes
478             byte[] keyBytes = key.getBytes();
479             SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA1");
480
481             // Get an hmac_sha1 Mac instance and initialize with the signing key
482             Mac mac = Mac.getInstance("HmacSHA1");
483             mac.init(signingKey);
484
485             // Compute the hmac on input data bytes
486             byte[] rawHmac = mac.doFinal(value.getBytes());
487
488             // Convert raw bytes to a String
489             return HexUtils.bytesToHex(rawHmac).toLowerCase();
490         } catch (IllegalArgumentException | NoSuchAlgorithmException | InvalidKeyException | IllegalStateException e) {
491             throw new FreeboxException("Computing the hmac-sha1 of the challenge and the app token failed", e);
492         }
493     }
494 }