2 * Copyright (c) 2010-2021 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.freebox.internal.api;
15 import java.io.ByteArrayInputStream;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.io.UnsupportedEncodingException;
19 import java.net.URLEncoder;
20 import java.nio.charset.StandardCharsets;
21 import java.security.InvalidKeyException;
22 import java.security.NoSuchAlgorithmException;
23 import java.util.ArrayList;
24 import java.util.List;
25 import java.util.Properties;
26 import java.util.concurrent.TimeUnit;
28 import javax.crypto.Mac;
29 import javax.crypto.spec.SecretKeySpec;
31 import org.openhab.binding.freebox.internal.api.model.FreeboxAirMediaConfig;
32 import org.openhab.binding.freebox.internal.api.model.FreeboxAirMediaConfigResponse;
33 import org.openhab.binding.freebox.internal.api.model.FreeboxAirMediaReceiver;
34 import org.openhab.binding.freebox.internal.api.model.FreeboxAirMediaReceiverRequest;
35 import org.openhab.binding.freebox.internal.api.model.FreeboxAirMediaReceiversResponse;
36 import org.openhab.binding.freebox.internal.api.model.FreeboxAuthorizationStatus;
37 import org.openhab.binding.freebox.internal.api.model.FreeboxAuthorizationStatusResponse;
38 import org.openhab.binding.freebox.internal.api.model.FreeboxAuthorizeRequest;
39 import org.openhab.binding.freebox.internal.api.model.FreeboxAuthorizeResponse;
40 import org.openhab.binding.freebox.internal.api.model.FreeboxAuthorizeResult;
41 import org.openhab.binding.freebox.internal.api.model.FreeboxCallEntry;
42 import org.openhab.binding.freebox.internal.api.model.FreeboxCallEntryResponse;
43 import org.openhab.binding.freebox.internal.api.model.FreeboxConnectionStatus;
44 import org.openhab.binding.freebox.internal.api.model.FreeboxConnectionStatusResponse;
45 import org.openhab.binding.freebox.internal.api.model.FreeboxDiscoveryResponse;
46 import org.openhab.binding.freebox.internal.api.model.FreeboxEmptyResponse;
47 import org.openhab.binding.freebox.internal.api.model.FreeboxFtpConfig;
48 import org.openhab.binding.freebox.internal.api.model.FreeboxFtpConfigResponse;
49 import org.openhab.binding.freebox.internal.api.model.FreeboxFtthStatusResponse;
50 import org.openhab.binding.freebox.internal.api.model.FreeboxLanConfigResponse;
51 import org.openhab.binding.freebox.internal.api.model.FreeboxLanHost;
52 import org.openhab.binding.freebox.internal.api.model.FreeboxLanHostsResponse;
53 import org.openhab.binding.freebox.internal.api.model.FreeboxLanInterface;
54 import org.openhab.binding.freebox.internal.api.model.FreeboxLanInterfacesResponse;
55 import org.openhab.binding.freebox.internal.api.model.FreeboxLcdConfig;
56 import org.openhab.binding.freebox.internal.api.model.FreeboxLcdConfigResponse;
57 import org.openhab.binding.freebox.internal.api.model.FreeboxLoginResponse;
58 import org.openhab.binding.freebox.internal.api.model.FreeboxOpenSessionRequest;
59 import org.openhab.binding.freebox.internal.api.model.FreeboxOpenSessionResponse;
60 import org.openhab.binding.freebox.internal.api.model.FreeboxPhoneStatus;
61 import org.openhab.binding.freebox.internal.api.model.FreeboxPhoneStatusResponse;
62 import org.openhab.binding.freebox.internal.api.model.FreeboxResponse;
63 import org.openhab.binding.freebox.internal.api.model.FreeboxSambaConfig;
64 import org.openhab.binding.freebox.internal.api.model.FreeboxSambaConfigResponse;
65 import org.openhab.binding.freebox.internal.api.model.FreeboxSystemConfig;
66 import org.openhab.binding.freebox.internal.api.model.FreeboxSystemConfigResponse;
67 import org.openhab.binding.freebox.internal.api.model.FreeboxUPnPAVConfig;
68 import org.openhab.binding.freebox.internal.api.model.FreeboxUPnPAVConfigResponse;
69 import org.openhab.binding.freebox.internal.api.model.FreeboxWifiGlobalConfig;
70 import org.openhab.binding.freebox.internal.api.model.FreeboxWifiGlobalConfigResponse;
71 import org.openhab.binding.freebox.internal.api.model.FreeboxXdslStatusResponse;
72 import org.openhab.core.io.net.http.HttpUtil;
73 import org.openhab.core.util.HexUtils;
74 import org.slf4j.Logger;
75 import org.slf4j.LoggerFactory;
77 import com.google.gson.FieldNamingPolicy;
78 import com.google.gson.Gson;
79 import com.google.gson.GsonBuilder;
80 import com.google.gson.JsonSyntaxException;
83 * The {@link FreeboxApiManager} is responsible for the communication with the Freebox.
84 * It implements the different HTTP API calls provided by the Freebox
86 * @author Laurent Garnier - Initial contribution
88 public class FreeboxApiManager {
90 private final Logger logger = LoggerFactory.getLogger(FreeboxApiManager.class);
92 private static final int HTTP_CALL_DEFAULT_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(10);
93 private static final String AUTH_HEADER = "X-Fbx-App-Auth";
94 private static final String HTTP_CALL_CONTENT_TYPE = "application/json; charset=utf-8";
97 private String appName;
98 private String appVersion;
99 private String deviceName;
100 private String baseAddress;
101 private String appToken;
102 private String sessionToken;
105 public FreeboxApiManager(String appId, String appName, String appVersion, String deviceName) {
107 this.appName = appName;
108 this.appVersion = appVersion;
109 this.deviceName = deviceName;
110 this.gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
113 public FreeboxDiscoveryResponse checkApi(String fqdn, boolean secureHttp) {
114 String url = String.format("%s://%s/api_version", secureHttp ? "https" : "http", fqdn);
116 String jsonResponse = HttpUtil.executeUrl("GET", url, HTTP_CALL_DEFAULT_TIMEOUT_MS);
117 return gson.fromJson(jsonResponse, FreeboxDiscoveryResponse.class);
118 } catch (IOException | JsonSyntaxException e) {
119 logger.debug("checkApi with {} failed: {}", url, e.getMessage());
124 public boolean authorize(boolean useHttps, String fqdn, String apiBaseUrl, String apiVersion, String appToken)
125 throws InterruptedException {
126 String[] versionSplit = apiVersion.split("\\.");
127 String majorVersion = "5";
128 if (versionSplit.length > 0) {
129 majorVersion = versionSplit[0];
131 this.baseAddress = (useHttps ? "https://" : "http://") + fqdn + apiBaseUrl + "v" + majorVersion + "/";
133 boolean granted = false;
135 String token = appToken;
136 if (token == null || token.isEmpty()) {
137 FreeboxAuthorizeRequest request = new FreeboxAuthorizeRequest(appId, appName, appVersion, deviceName);
138 FreeboxAuthorizeResult response = executePostUrl("login/authorize/", gson.toJson(request),
139 FreeboxAuthorizeResponse.class, false, false, true);
140 token = response.getAppToken();
141 int trackId = response.getTrackId();
142 FreeboxAuthorizationStatus result;
145 result = executeGetUrl("login/authorize/" + trackId, FreeboxAuthorizationStatusResponse.class,
147 } while (result.isStatusPending());
148 granted = result.isStatusGranted();
156 this.appToken = token;
159 } catch (FreeboxException e) {
160 logger.debug("Error while opening a session", e);
165 private synchronized void openSession() throws FreeboxException {
166 if (appToken == null) {
167 throw new FreeboxException("No app token to open a new session");
170 String challenge = executeGetUrl("login/", FreeboxLoginResponse.class, false).getChallenge();
171 FreeboxOpenSessionRequest request = new FreeboxOpenSessionRequest(appId, hmacSha1(appToken, challenge));
172 sessionToken = executePostUrl("login/session/", gson.toJson(request), FreeboxOpenSessionResponse.class, false,
173 false, true).getSessionToken();
176 public synchronized void closeSession() {
177 if (sessionToken != null) {
179 executePostUrl("login/logout/", null, FreeboxEmptyResponse.class, false);
180 } catch (FreeboxException e) {
186 public String getAppToken() {
190 public synchronized String getSessionToken() {
194 public FreeboxConnectionStatus getConnectionStatus() throws FreeboxException {
195 return executeGetUrl("connection/", FreeboxConnectionStatusResponse.class);
198 public String getxDslStatus() throws FreeboxException {
199 return executeGetUrl("connection/xdsl/", FreeboxXdslStatusResponse.class).getStatus();
202 public boolean getFtthPresent() throws FreeboxException {
203 return executeGetUrl("connection/ftth/", FreeboxFtthStatusResponse.class).getSfpPresent();
206 public boolean isWifiEnabled() throws FreeboxException {
207 return executeGetUrl("wifi/config/", FreeboxWifiGlobalConfigResponse.class).isEnabled();
210 public boolean enableWifi(boolean enable) throws FreeboxException {
211 FreeboxWifiGlobalConfig config = new FreeboxWifiGlobalConfig();
212 config.setEnabled(enable);
213 return executePutUrl("wifi/config/", gson.toJson(config), FreeboxWifiGlobalConfigResponse.class).isEnabled();
216 public boolean isFtpEnabled() throws FreeboxException {
217 return executeGetUrl("ftp/config/", FreeboxFtpConfigResponse.class).isEnabled();
220 public boolean enableFtp(boolean enable) throws FreeboxException {
221 FreeboxFtpConfig config = new FreeboxFtpConfig();
222 config.setEnabled(enable);
223 return executePutUrl("ftp/config/", gson.toJson(config), FreeboxFtpConfigResponse.class).isEnabled();
226 public boolean isAirMediaEnabled() throws FreeboxException {
227 return executeGetUrl("airmedia/config/", FreeboxAirMediaConfigResponse.class).isEnabled();
230 public boolean enableAirMedia(boolean enable) throws FreeboxException {
231 FreeboxAirMediaConfig config = new FreeboxAirMediaConfig();
232 config.setEnabled(enable);
233 return executePutUrl("airmedia/config/", gson.toJson(config), FreeboxAirMediaConfigResponse.class).isEnabled();
236 public boolean isUPnPAVEnabled() throws FreeboxException {
237 return executeGetUrl("upnpav/config/", FreeboxUPnPAVConfigResponse.class).isEnabled();
240 public boolean enableUPnPAV(boolean enable) throws FreeboxException {
241 FreeboxUPnPAVConfig config = new FreeboxUPnPAVConfig();
242 config.setEnabled(enable);
243 return executePutUrl("upnpav/config/", gson.toJson(config), FreeboxUPnPAVConfigResponse.class).isEnabled();
246 public FreeboxSambaConfig getSambaConfig() throws FreeboxException {
247 return executeGetUrl("netshare/samba/", FreeboxSambaConfigResponse.class);
250 public boolean enableSambaFileShare(boolean enable) throws FreeboxException {
251 FreeboxSambaConfig config = new FreeboxSambaConfig();
252 config.setFileShareEnabled(enable);
253 return executePutUrl("netshare/samba/", gson.toJson(config), FreeboxSambaConfigResponse.class)
254 .isFileShareEnabled();
257 public boolean enableSambaPrintShare(boolean enable) throws FreeboxException {
258 FreeboxSambaConfig config = new FreeboxSambaConfig();
259 config.setPrintShareEnabled(enable);
260 return executePutUrl("netshare/samba/", gson.toJson(config), FreeboxSambaConfigResponse.class)
261 .isPrintShareEnabled();
264 public FreeboxLcdConfig getLcdConfig() throws FreeboxException {
265 return executeGetUrl("lcd/config/", FreeboxLcdConfigResponse.class);
268 public int setLcdBrightness(int brightness) throws FreeboxException {
269 FreeboxLcdConfig config = getLcdConfig();
270 int newValue = Math.min(100, brightness);
271 newValue = Math.max(newValue, 0);
272 config.setBrightness(newValue);
273 return executePutUrl("lcd/config/", gson.toJson(config), FreeboxLcdConfigResponse.class).getBrightness();
276 public int increaseLcdBrightness() throws FreeboxException {
277 FreeboxLcdConfig config = getLcdConfig();
278 config.setBrightness(Math.min(100, config.getBrightness() + 1));
279 return executePutUrl("lcd/config/", gson.toJson(config), FreeboxLcdConfigResponse.class).getBrightness();
282 public int decreaseLcdBrightness() throws FreeboxException {
283 FreeboxLcdConfig config = getLcdConfig();
284 config.setBrightness(Math.max(0, config.getBrightness() - 1));
285 return executePutUrl("lcd/config/", gson.toJson(config), FreeboxLcdConfigResponse.class).getBrightness();
288 public FreeboxLcdConfig setLcdOrientation(int orientation) throws FreeboxException {
289 FreeboxLcdConfig config = getLcdConfig();
290 int newValue = Math.min(360, orientation);
291 newValue = Math.max(newValue, 0);
292 config.setOrientation(newValue);
293 config.setOrientationForced(true);
294 return executePutUrl("lcd/config/", gson.toJson(config), FreeboxLcdConfigResponse.class);
297 public boolean setLcdOrientationForced(boolean forced) throws FreeboxException {
298 FreeboxLcdConfig config = getLcdConfig();
299 config.setOrientationForced(forced);
300 return executePutUrl("lcd/config/", gson.toJson(config), FreeboxLcdConfigResponse.class).isOrientationForced();
303 public FreeboxSystemConfig getSystemConfig() throws FreeboxException {
304 return executeGetUrl("system/", FreeboxSystemConfigResponse.class);
307 public boolean isInLanBridgeMode() throws FreeboxException {
308 return executeGetUrl("lan/config/", FreeboxLanConfigResponse.class).isInBridgeMode();
311 public List<FreeboxLanHost> getLanHosts() throws FreeboxException {
312 List<FreeboxLanHost> hosts = new ArrayList<>();
313 List<FreeboxLanInterface> interfaces = executeGetUrl("lan/browser/interfaces/",
314 FreeboxLanInterfacesResponse.class);
315 if (interfaces != null) {
316 for (FreeboxLanInterface lanInterface : interfaces) {
317 if (lanInterface.getHostCount() > 0) {
318 List<FreeboxLanHost> lanHosts = getLanHostsFromInterface(lanInterface.getName());
319 if (lanHosts != null) {
320 hosts.addAll(lanHosts);
328 private List<FreeboxLanHost> getLanHostsFromInterface(String lanInterface) throws FreeboxException {
329 return executeGetUrl("lan/browser/" + encodeUrl(lanInterface) + "/", FreeboxLanHostsResponse.class);
332 public FreeboxPhoneStatus getPhoneStatus() throws FreeboxException {
333 // This API is undocumented but working
334 // It is extracted from the freeboxos-java library
335 // https://github.com/MatMaul/freeboxos-java/blob/master/src/org/matmaul/freeboxos/phone/PhoneManager.java#L17
336 return executeGetUrl("phone/?_dc=1415032391207", FreeboxPhoneStatusResponse.class).get(0);
339 public List<FreeboxCallEntry> getCallEntries() throws FreeboxException {
340 return executeGetUrl("call/log/", FreeboxCallEntryResponse.class);
343 public List<FreeboxAirMediaReceiver> getAirMediaReceivers() throws FreeboxException {
344 return executeGetUrl("airmedia/receivers/", FreeboxAirMediaReceiversResponse.class, true, true, false);
347 public void playMedia(String url, String airPlayName, String airPlayPassword) throws FreeboxException {
348 FreeboxAirMediaReceiverRequest request = new FreeboxAirMediaReceiverRequest();
349 request.setStartAction();
350 request.setVideoMediaType();
351 if (airPlayPassword != null && !airPlayPassword.isEmpty()) {
352 request.setPassword(airPlayPassword);
354 request.setMedia(url);
355 executePostUrl("airmedia/receivers/" + encodeUrl(airPlayName) + "/", gson.toJson(request),
356 FreeboxEmptyResponse.class, true, false, true);
359 public void stopMedia(String airPlayName, String airPlayPassword) throws FreeboxException {
360 FreeboxAirMediaReceiverRequest request = new FreeboxAirMediaReceiverRequest();
361 request.setStopAction();
362 request.setVideoMediaType();
363 if (airPlayPassword != null && !airPlayPassword.isEmpty()) {
364 request.setPassword(airPlayPassword);
366 executePostUrl("airmedia/receivers/" + encodeUrl(airPlayName) + "/", gson.toJson(request),
367 FreeboxEmptyResponse.class, true, false, true);
370 public void reboot() throws FreeboxException {
371 executePostUrl("system/reboot/", null, FreeboxEmptyResponse.class);
374 private <T extends FreeboxResponse<F>, F> F executeGetUrl(String relativeUrl, Class<T> responseClass)
375 throws FreeboxException {
376 return executeUrl("GET", relativeUrl, null, responseClass, true, false, false);
379 private <T extends FreeboxResponse<F>, F> F executeGetUrl(String relativeUrl, Class<T> responseClass,
380 boolean retryAuth) throws FreeboxException {
381 return executeUrl("GET", relativeUrl, null, responseClass, retryAuth, false, false);
384 private <T extends FreeboxResponse<F>, F> F executeGetUrl(String relativeUrl, Class<T> responseClass,
385 boolean retryAuth, boolean patchTableReponse, boolean doNotLogData) throws FreeboxException {
386 return executeUrl("GET", relativeUrl, null, responseClass, retryAuth, patchTableReponse, doNotLogData);
389 private <T extends FreeboxResponse<F>, F> F executePostUrl(String relativeUrl, String requestContent,
390 Class<T> responseClass) throws FreeboxException {
391 return executeUrl("POST", relativeUrl, requestContent, responseClass, true, false, false);
394 private <T extends FreeboxResponse<F>, F> F executePostUrl(String relativeUrl, String requestContent,
395 Class<T> responseClass, boolean retryAuth) throws FreeboxException {
396 return executeUrl("POST", relativeUrl, requestContent, responseClass, retryAuth, false, false);
399 private <T extends FreeboxResponse<F>, F> F executePostUrl(String relativeUrl, String requestContent,
400 Class<T> responseClass, boolean retryAuth, boolean patchTableReponse, boolean doNotLogData)
401 throws FreeboxException {
402 return executeUrl("POST", relativeUrl, requestContent, responseClass, retryAuth, patchTableReponse,
406 private <T extends FreeboxResponse<F>, F> F executePutUrl(String relativeUrl, String requestContent,
407 Class<T> responseClass) throws FreeboxException {
408 return executeUrl("PUT", relativeUrl, requestContent, responseClass, true, false, false);
411 private <T extends FreeboxResponse<F>, F> F executeUrl(String httpMethod, String relativeUrl, String requestContent,
412 Class<T> responseClass, boolean retryAuth, boolean patchTableReponse, boolean doNotLogData)
413 throws FreeboxException {
415 Properties headers = null;
416 String token = getSessionToken();
418 headers = new Properties();
419 headers.setProperty(AUTH_HEADER, token);
421 InputStream stream = null;
422 String contentType = null;
423 if (requestContent != null) {
424 stream = new ByteArrayInputStream(requestContent.getBytes(StandardCharsets.UTF_8));
425 contentType = HTTP_CALL_CONTENT_TYPE;
427 logger.debug("executeUrl {} {} requestContent {}", httpMethod, relativeUrl,
428 doNotLogData ? "***" : requestContent);
429 String jsonResponse = HttpUtil.executeUrl(httpMethod, baseAddress + relativeUrl, headers, stream,
430 contentType, HTTP_CALL_DEFAULT_TIMEOUT_MS);
431 if (stream != null) {
436 if (patchTableReponse) {
437 // Replace empty result by an empty table result
438 jsonResponse = jsonResponse.replace("\"result\":{}", "\"result\":[]");
441 return evaluateJsonReesponse(jsonResponse, responseClass, doNotLogData);
442 } catch (FreeboxException e) {
443 if (retryAuth && e.isAuthRequired()) {
444 logger.debug("Authentication required: open a new session and retry the request");
446 return executeUrl(httpMethod, relativeUrl, requestContent, responseClass, false, patchTableReponse,
450 } catch (IOException e) {
451 throw new FreeboxException(httpMethod + " request " + relativeUrl + ": execution failed: " + e.getMessage(),
453 } catch (JsonSyntaxException e) {
454 throw new FreeboxException(
455 httpMethod + " request " + relativeUrl + ": response parsing failed: " + e.getMessage(), e);
459 private <T extends FreeboxResponse<F>, F> F evaluateJsonReesponse(String jsonResponse, Class<T> responseClass,
460 boolean doNotLogData) throws JsonSyntaxException, FreeboxException {
461 logger.debug("evaluateJsonReesponse Json {}", doNotLogData ? "***" : jsonResponse);
462 // First check only if the result is successful
463 FreeboxResponse<Object> partialResponse = gson.fromJson(jsonResponse, FreeboxEmptyResponse.class);
464 partialResponse.evaluate();
465 // Parse the full response in case of success
466 T fullResponse = gson.fromJson(jsonResponse, responseClass);
467 fullResponse.evaluate();
468 F result = fullResponse.getResult();
472 private String encodeUrl(String url) throws FreeboxException {
474 return URLEncoder.encode(url, StandardCharsets.UTF_8.name());
475 } catch (UnsupportedEncodingException e) {
476 throw new FreeboxException("Encoding the URL \"" + url + "\" in UTF-8 failed", e);
480 public static String hmacSha1(String key, String value) throws FreeboxException {
482 // Get an hmac_sha1 key from the raw key bytes
483 byte[] keyBytes = key.getBytes();
484 SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA1");
486 // Get an hmac_sha1 Mac instance and initialize with the signing key
487 Mac mac = Mac.getInstance("HmacSHA1");
488 mac.init(signingKey);
490 // Compute the hmac on input data bytes
491 byte[] rawHmac = mac.doFinal(value.getBytes());
493 // Convert raw bytes to a String
494 return HexUtils.bytesToHex(rawHmac).toLowerCase();
495 } catch (IllegalArgumentException | NoSuchAlgorithmException | InvalidKeyException | IllegalStateException e) {
496 throw new FreeboxException("Computing the hmac-sha1 of the challenge and the app token failed", e);