2 * Copyright (c) 2010-2023 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.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;
27 import javax.crypto.Mac;
28 import javax.crypto.spec.SecretKeySpec;
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;
76 import com.google.gson.FieldNamingPolicy;
77 import com.google.gson.Gson;
78 import com.google.gson.GsonBuilder;
79 import com.google.gson.JsonSyntaxException;
82 * The {@link FreeboxApiManager} is responsible for the communication with the Freebox.
83 * It implements the different HTTP API calls provided by the Freebox
85 * @author Laurent Garnier - Initial contribution
87 public class FreeboxApiManager {
89 private final Logger logger = LoggerFactory.getLogger(FreeboxApiManager.class);
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";
96 private String appName;
97 private String appVersion;
98 private String deviceName;
99 private String baseAddress;
100 private String appToken;
101 private String sessionToken;
104 public FreeboxApiManager(String appId, String appName, String appVersion, String deviceName) {
106 this.appName = appName;
107 this.appVersion = appVersion;
108 this.deviceName = deviceName;
109 this.gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
112 public FreeboxDiscoveryResponse checkApi(String fqdn, boolean secureHttp) {
113 String url = String.format("%s://%s/api_version", secureHttp ? "https" : "http", fqdn);
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());
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];
130 this.baseAddress = (useHttps ? "https://" : "http://") + fqdn + apiBaseUrl + "v" + majorVersion + "/";
132 boolean granted = false;
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;
144 result = executeGetUrl("login/authorize/" + trackId, FreeboxAuthorizationStatusResponse.class,
146 } while (result.isStatusPending());
147 granted = result.isStatusGranted();
155 this.appToken = token;
158 } catch (FreeboxException e) {
159 logger.debug("Error while opening a session", e);
164 private synchronized void openSession() throws FreeboxException {
165 if (appToken == null) {
166 throw new FreeboxException("No app token to open a new session");
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();
175 public synchronized void closeSession() {
176 if (sessionToken != null) {
178 executePostUrl("login/logout/", null, FreeboxEmptyResponse.class, false);
179 } catch (FreeboxException e) {
185 public String getAppToken() {
189 public synchronized String getSessionToken() {
193 public FreeboxConnectionStatus getConnectionStatus() throws FreeboxException {
194 return executeGetUrl("connection/", FreeboxConnectionStatusResponse.class);
197 public String getxDslStatus() throws FreeboxException {
198 return executeGetUrl("connection/xdsl/", FreeboxXdslStatusResponse.class).getStatus();
201 public boolean getFtthPresent() throws FreeboxException {
202 return executeGetUrl("connection/ftth/", FreeboxFtthStatusResponse.class).getSfpPresent();
205 public boolean isWifiEnabled() throws FreeboxException {
206 return executeGetUrl("wifi/config/", FreeboxWifiGlobalConfigResponse.class).isEnabled();
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();
215 public boolean isFtpEnabled() throws FreeboxException {
216 return executeGetUrl("ftp/config/", FreeboxFtpConfigResponse.class).isEnabled();
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();
225 public boolean isAirMediaEnabled() throws FreeboxException {
226 return executeGetUrl("airmedia/config/", FreeboxAirMediaConfigResponse.class).isEnabled();
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();
235 public boolean isUPnPAVEnabled() throws FreeboxException {
236 return executeGetUrl("upnpav/config/", FreeboxUPnPAVConfigResponse.class).isEnabled();
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();
245 public FreeboxSambaConfig getSambaConfig() throws FreeboxException {
246 return executeGetUrl("netshare/samba/", FreeboxSambaConfigResponse.class);
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();
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();
263 public FreeboxLcdConfig getLcdConfig() throws FreeboxException {
264 return executeGetUrl("lcd/config/", FreeboxLcdConfigResponse.class);
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();
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();
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();
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);
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();
302 public FreeboxSystemConfig getSystemConfig() throws FreeboxException {
303 return executeGetUrl("system/", FreeboxSystemConfigResponse.class);
306 public boolean isInLanBridgeMode() throws FreeboxException {
307 return executeGetUrl("lan/config/", FreeboxLanConfigResponse.class).isInBridgeMode();
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);
327 private List<FreeboxLanHost> getLanHostsFromInterface(String lanInterface) throws FreeboxException {
328 return executeGetUrl("lan/browser/" + encodeUrl(lanInterface) + "/", FreeboxLanHostsResponse.class);
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);
338 public List<FreeboxCallEntry> getCallEntries() throws FreeboxException {
339 return executeGetUrl("call/log/", FreeboxCallEntryResponse.class);
342 public List<FreeboxAirMediaReceiver> getAirMediaReceivers() throws FreeboxException {
343 return executeGetUrl("airmedia/receivers/", FreeboxAirMediaReceiversResponse.class, true, true, false);
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);
353 request.setMedia(url);
354 executePostUrl("airmedia/receivers/" + encodeUrl(airPlayName) + "/", gson.toJson(request),
355 FreeboxEmptyResponse.class, true, false, true);
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);
365 executePostUrl("airmedia/receivers/" + encodeUrl(airPlayName) + "/", gson.toJson(request),
366 FreeboxEmptyResponse.class, true, false, true);
369 public void reboot() throws FreeboxException {
370 executePostUrl("system/reboot/", null, FreeboxEmptyResponse.class);
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);
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);
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);
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);
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);
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,
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);
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 {
414 Properties headers = null;
415 String token = getSessionToken();
417 headers = new Properties();
418 headers.setProperty(AUTH_HEADER, token);
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;
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) {
435 if (patchTableReponse) {
436 // Replace empty result by an empty table result
437 jsonResponse = jsonResponse.replace("\"result\":{}", "\"result\":[]");
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");
445 return executeUrl(httpMethod, relativeUrl, requestContent, responseClass, false, patchTableReponse,
449 } catch (IOException e) {
450 throw new FreeboxException(httpMethod + " request " + relativeUrl + ": execution failed: " + e.getMessage(),
452 } catch (JsonSyntaxException e) {
453 throw new FreeboxException(
454 httpMethod + " request " + relativeUrl + ": response parsing failed: " + e.getMessage(), e);
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 return fullResponse.getResult();
470 private String encodeUrl(String url) throws FreeboxException {
471 return URLEncoder.encode(url, StandardCharsets.UTF_8);
474 public static String hmacSha1(String key, String value) throws FreeboxException {
476 // Get an hmac_sha1 key from the raw key bytes
477 byte[] keyBytes = key.getBytes();
478 SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA1");
480 // Get an hmac_sha1 Mac instance and initialize with the signing key
481 Mac mac = Mac.getInstance("HmacSHA1");
482 mac.init(signingKey);
484 // Compute the hmac on input data bytes
485 byte[] rawHmac = mac.doFinal(value.getBytes());
487 // Convert raw bytes to a String
488 return HexUtils.bytesToHex(rawHmac).toLowerCase();
489 } catch (IllegalArgumentException | NoSuchAlgorithmException | InvalidKeyException | IllegalStateException e) {
490 throw new FreeboxException("Computing the hmac-sha1 of the challenge and the app token failed", e);