[linky] Correcting authentication bug (#11406)
* Correcting authentication bug (issue #10360) Signed-off-by: clinique <gael@lhopital.org> * Reverting PR #11233 & PR #11266 Signed-off-by: Gaël L'hopital <gael@lhopital.org> * Addressing @lolodomo feed-back Signed-off-by: Gaël L'hopital <gael@lhopital.org> * One pointless comment left Signed-off-by: Gaël L'hopital <gael@lhopital.org> * Adding missing test on username Signed-off-by: Gaël L'hopital <gael@lhopital.org> * Reviewing configuration elements nullness and empty checks. Signed-off-by: Gaël L'hopital <gael@lhopital.org>
This commit is contained in:
parent
daea6481a7
commit
054518e345
@ -751,13 +751,11 @@
|
|||||||
<artifactId>org.openhab.binding.lifx</artifactId>
|
<artifactId>org.openhab.binding.lifx</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- linky binding suppressed from the distribution until it is fixed
|
<dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
<artifactId>org.openhab.binding.linky</artifactId>
|
<artifactId>org.openhab.binding.linky</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
-->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
<artifactId>org.openhab.binding.linuxinput</artifactId>
|
<artifactId>org.openhab.binding.linuxinput</artifactId>
|
||||||
|
|||||||
@ -12,15 +12,22 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.linky.internal;
|
package org.openhab.binding.linky.internal;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link LinkyConfiguration} is the class used to match the
|
* The {@link LinkyConfiguration} is the class used to match the
|
||||||
* thing configuration.
|
* thing configuration.
|
||||||
*
|
*
|
||||||
* @author Gaël L'hopital - Initial contribution
|
* @author Gaël L'hopital - Initial contribution
|
||||||
*/
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
public class LinkyConfiguration {
|
public class LinkyConfiguration {
|
||||||
public static final String INTERNAL_AUTH_ID = "internalAuthId";
|
public static final String INTERNAL_AUTH_ID = "internalAuthId";
|
||||||
public String username;
|
public String username = "";
|
||||||
public String password;
|
public String password = "";
|
||||||
public String internalAuthId;
|
public String internalAuthId = "";
|
||||||
|
|
||||||
|
public boolean seemsValid() {
|
||||||
|
return !username.isBlank() && !password.isBlank() && !internalAuthId.isBlank();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,9 +47,10 @@ import com.google.gson.JsonDeserializer;
|
|||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.linky")
|
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.linky")
|
||||||
public class LinkyHandlerFactory extends BaseThingHandlerFactory {
|
public class LinkyHandlerFactory extends BaseThingHandlerFactory {
|
||||||
|
private static final DateTimeFormatter LINKY_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(LinkyHandlerFactory.class);
|
private final Logger logger = LoggerFactory.getLogger(LinkyHandlerFactory.class);
|
||||||
|
|
||||||
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
|
|
||||||
private final LocaleProvider localeProvider;
|
private final LocaleProvider localeProvider;
|
||||||
private final Gson gson;
|
private final Gson gson;
|
||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
@ -60,7 +61,7 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory {
|
|||||||
this.localeProvider = localeProvider;
|
this.localeProvider = localeProvider;
|
||||||
this.gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
|
this.gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
|
||||||
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
|
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
|
||||||
.parse(json.getAsJsonPrimitive().getAsString(), formatter))
|
.parse(json.getAsJsonPrimitive().getAsString(), LINKY_FORMATTER))
|
||||||
.create();
|
.create();
|
||||||
this.httpClient = httpClientFactory.createHttpClient(LinkyBindingConstants.BINDING_ID);
|
this.httpClient = httpClientFactory.createHttpClient(LinkyBindingConstants.BINDING_ID);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,18 +64,19 @@ public class EnedisHttpApi {
|
|||||||
private final Logger logger = LoggerFactory.getLogger(EnedisHttpApi.class);
|
private final Logger logger = LoggerFactory.getLogger(EnedisHttpApi.class);
|
||||||
private final Gson gson;
|
private final Gson gson;
|
||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
private final LinkyConfiguration config;
|
|
||||||
private boolean connected = false;
|
private boolean connected = false;
|
||||||
|
private final CookieStore cookieStore;
|
||||||
|
private final LinkyConfiguration config;
|
||||||
|
|
||||||
public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient) {
|
public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient) {
|
||||||
this.gson = gson;
|
this.gson = gson;
|
||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.cookieStore = httpClient.getCookieStore();
|
||||||
|
addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() throws LinkyException {
|
public void initialize() throws LinkyException {
|
||||||
addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
|
|
||||||
|
|
||||||
logger.debug("Starting login process for user : {}", config.username);
|
logger.debug("Starting login process for user : {}", config.username);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -109,31 +110,39 @@ public class EnedisHttpApi {
|
|||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Step 3 : auth1 - retrieve the template, thanks to cookie internalAuthId, user is already set");
|
"Step 3 : auth1 - retrieve the template, thanks to cookie internalAuthId, user is already set");
|
||||||
result = httpClient.POST(url).send();
|
result = httpClient.POST(url).header("X-NoSession", "true").header("X-Password", "anonymous")
|
||||||
|
.header("X-Requested-With", "XMLHttpRequest").header("X-Username", "anonymous").send();
|
||||||
if (result.getStatus() != 200) {
|
if (result.getStatus() != 200) {
|
||||||
throw new LinkyException("Connection failed step 3 - auth1 : " + result.getContentAsString());
|
throw new LinkyException("Connection failed step 3 - auth1 : " + result.getContentAsString());
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthData authData = gson.fromJson(result.getContentAsString(), AuthData.class);
|
AuthData authData = gson.fromJson(result.getContentAsString(), AuthData.class);
|
||||||
if (authData.callbacks.size() < 2 || authData.callbacks.get(0).input.size() == 0
|
if (authData == null || authData.callbacks.size() < 2 || authData.callbacks.get(0).input.isEmpty()
|
||||||
|| authData.callbacks.get(1).input.size() == 0 || !config.username
|
|| authData.callbacks.get(1).input.isEmpty() || !config.username
|
||||||
.equals(Objects.requireNonNull(authData.callbacks.get(0).input.get(0)).valueAsString())) {
|
.equals(Objects.requireNonNull(authData.callbacks.get(0).input.get(0)).valueAsString())) {
|
||||||
throw new LinkyException("Authentication error, the authentication_cookie is probably wrong");
|
throw new LinkyException("Authentication error, the authentication_cookie is probably wrong");
|
||||||
}
|
}
|
||||||
|
|
||||||
authData.callbacks.get(1).input.get(0).value = config.password;
|
authData.callbacks.get(1).input.get(0).value = config.password;
|
||||||
url = "https://mon-compte.enedis.fr/auth/json/authenticate?realm=/enedis&spEntityID=SP-ODW-PROD&goto=/auth/SSOPOST/metaAlias/enedis/providerIDP?ReqID%"
|
url = URL_MON_COMPTE
|
||||||
|
+ "/auth/json/authenticate?realm=/enedis&spEntityID=SP-ODW-PROD&goto=/auth/SSOPOST/metaAlias/enedis/providerIDP?ReqID%"
|
||||||
+ reqId
|
+ reqId
|
||||||
+ "%26index%3Dnull%26acsURL%3Dhttps://apps.lincs.enedis.fr/saml/SSO%26spEntityID%3DSP-ODW-PROD%26binding%3Durn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST&AMAuthCookie=";
|
+ "%26index%3Dnull%26acsURL%3Dhttps://apps.lincs.enedis.fr/saml/SSO%26spEntityID%3DSP-ODW-PROD%26binding%3Durn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST&AMAuthCookie=";
|
||||||
|
|
||||||
logger.debug("Step 3 : auth2 - send the auth data");
|
logger.debug("Step 3 : auth2 - send the auth data");
|
||||||
result = httpClient.POST(url).header(HttpHeader.CONTENT_TYPE, "application/json")
|
result = httpClient.POST(url).header(HttpHeader.CONTENT_TYPE, "application/json")
|
||||||
|
.header("X-NoSession", "true").header("X-Password", "anonymous")
|
||||||
|
.header("X-Requested-With", "XMLHttpRequest").header("X-Username", "anonymous")
|
||||||
.content(new StringContentProvider(gson.toJson(authData))).send();
|
.content(new StringContentProvider(gson.toJson(authData))).send();
|
||||||
if (result.getStatus() != 200) {
|
if (result.getStatus() != 200) {
|
||||||
throw new LinkyException("Connection failed step 3 - auth2 : " + result.getContentAsString());
|
throw new LinkyException("Connection failed step 3 - auth2 : " + result.getContentAsString());
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthResult authResult = gson.fromJson(result.getContentAsString(), AuthResult.class);
|
AuthResult authResult = gson.fromJson(result.getContentAsString(), AuthResult.class);
|
||||||
|
if (authResult == null) {
|
||||||
|
throw new LinkyException("Invalid authentication result data");
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug("Add the tokenId cookie");
|
logger.debug("Add the tokenId cookie");
|
||||||
addCookie("enedisExt", authResult.tokenId);
|
addCookie("enedisExt", authResult.tokenId);
|
||||||
|
|
||||||
@ -155,18 +164,17 @@ public class EnedisHttpApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLocation(ContentResponse response) {
|
private String getLocation(ContentResponse response) {
|
||||||
return response.getHeaders().get(HttpHeader.LOCATION);
|
return response.getHeaders().get(HttpHeader.LOCATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disconnect() throws LinkyException {
|
private void disconnect() throws LinkyException {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
logger.debug("Logout process");
|
logger.debug("Logout process");
|
||||||
try { // Three times in a row to get disconnected
|
try { // Three times in a row to get disconnected
|
||||||
String location = getLocation(httpClient.GET(URL_APPS_LINCS + "/logout"));
|
String location = getLocation(httpClient.GET(URL_APPS_LINCS + "/logout"));
|
||||||
location = getLocation(httpClient.GET(location));
|
location = getLocation(httpClient.GET(location));
|
||||||
location = getLocation(httpClient.GET(location));
|
location = getLocation(httpClient.GET(location));
|
||||||
CookieStore cookieStore = httpClient.getCookieStore();
|
|
||||||
cookieStore.removeAll();
|
cookieStore.removeAll();
|
||||||
connected = false;
|
connected = false;
|
||||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||||
@ -184,7 +192,6 @@ public class EnedisHttpApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void addCookie(String key, String value) {
|
private void addCookie(String key, String value) {
|
||||||
CookieStore cookieStore = httpClient.getCookieStore();
|
|
||||||
HttpCookie cookie = new HttpCookie(key, value);
|
HttpCookie cookie = new HttpCookie(key, value);
|
||||||
cookie.setDomain(".enedis.fr");
|
cookie.setDomain(".enedis.fr");
|
||||||
cookie.setPath("/");
|
cookie.setPath("/");
|
||||||
@ -220,6 +227,9 @@ public class EnedisHttpApi {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
PrmInfo[] prms = gson.fromJson(data, PrmInfo[].class);
|
PrmInfo[] prms = gson.fromJson(data, PrmInfo[].class);
|
||||||
|
if (prms == null || prms.length < 1) {
|
||||||
|
throw new LinkyException("Invalid prms data received");
|
||||||
|
}
|
||||||
return prms[0];
|
return prms[0];
|
||||||
} catch (JsonSyntaxException e) {
|
} catch (JsonSyntaxException e) {
|
||||||
logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data);
|
logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data);
|
||||||
@ -259,6 +269,9 @@ public class EnedisHttpApi {
|
|||||||
logger.trace("getData returned {}", data);
|
logger.trace("getData returned {}", data);
|
||||||
try {
|
try {
|
||||||
ConsumptionReport report = gson.fromJson(data, ConsumptionReport.class);
|
ConsumptionReport report = gson.fromJson(data, ConsumptionReport.class);
|
||||||
|
if (report == null) {
|
||||||
|
throw new LinkyException("No report data received");
|
||||||
|
}
|
||||||
return report.firstLevel.consumptions;
|
return report.firstLevel.consumptions;
|
||||||
} catch (JsonSyntaxException e) {
|
} catch (JsonSyntaxException e) {
|
||||||
logger.debug("invalid JSON response not matching ConsumptionReport.class: {}", data);
|
logger.debug("invalid JSON response not matching ConsumptionReport.class: {}", data);
|
||||||
|
|||||||
@ -77,24 +77,6 @@ public class ExpiringDayCache<V> {
|
|||||||
return Optional.ofNullable(cachedValue);
|
return Optional.ofNullable(cachedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Puts a new value into the cache.
|
|
||||||
*
|
|
||||||
* @param value the new value
|
|
||||||
*/
|
|
||||||
public final synchronized void putValue(@Nullable V value) {
|
|
||||||
this.value = value;
|
|
||||||
expiresAt = calcNextExpiresAt();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidates the value in the cache.
|
|
||||||
*/
|
|
||||||
public final synchronized void invalidateValue() {
|
|
||||||
value = null;
|
|
||||||
expiresAt = calcAlreadyExpired();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refreshes and returns the value in the cache.
|
* Refreshes and returns the value in the cache.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -12,7 +12,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.linky.internal.dto;
|
package org.openhab.binding.linky.internal.dto;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
@ -31,22 +30,19 @@ public class AuthData {
|
|||||||
public @Nullable Object value;
|
public @Nullable Object value;
|
||||||
|
|
||||||
public @Nullable String valueAsString() {
|
public @Nullable String valueAsString() {
|
||||||
if (value instanceof String) {
|
return (value instanceof String) ? (String) value : null;
|
||||||
return (String) value;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable String type;
|
public @Nullable String type;
|
||||||
|
|
||||||
public List<NameValuePair> output = new ArrayList<>();
|
public List<NameValuePair> output = List.of();
|
||||||
public List<NameValuePair> input = new ArrayList<>();
|
public List<NameValuePair> input = List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable String authId;
|
public @Nullable String authId;
|
||||||
public @Nullable String template;
|
public @Nullable String template;
|
||||||
public @Nullable String stage;
|
public @Nullable String stage;
|
||||||
public @Nullable String header;
|
public @Nullable String header;
|
||||||
public List<AuthDataCallBack> callbacks = new ArrayList<>();
|
public List<AuthDataCallBack> callbacks = List.of();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,11 +64,11 @@ import com.google.gson.Gson;
|
|||||||
|
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class LinkyHandler extends BaseThingHandler {
|
public class LinkyHandler extends BaseThingHandler {
|
||||||
private final Logger logger = LoggerFactory.getLogger(LinkyHandler.class);
|
|
||||||
|
|
||||||
private static final int REFRESH_FIRST_HOUR_OF_DAY = 1;
|
private static final int REFRESH_FIRST_HOUR_OF_DAY = 1;
|
||||||
private static final int REFRESH_INTERVAL_IN_MIN = 120;
|
private static final int REFRESH_INTERVAL_IN_MIN = 120;
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(LinkyHandler.class);
|
||||||
|
|
||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
private final Gson gson;
|
private final Gson gson;
|
||||||
private final WeekFields weekFields;
|
private final WeekFields weekFields;
|
||||||
@ -146,12 +146,11 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
updateStatus(ThingStatus.UNKNOWN);
|
updateStatus(ThingStatus.UNKNOWN);
|
||||||
|
|
||||||
LinkyConfiguration config = getConfigAs(LinkyConfiguration.class);
|
LinkyConfiguration config = getConfigAs(LinkyConfiguration.class);
|
||||||
enedisApi = new EnedisHttpApi(config, gson, httpClient);
|
if (config.seemsValid()) {
|
||||||
|
enedisApi = new EnedisHttpApi(config, gson, httpClient);
|
||||||
scheduler.submit(() -> {
|
scheduler.submit(() -> {
|
||||||
try {
|
try {
|
||||||
EnedisHttpApi api = this.enedisApi;
|
EnedisHttpApi api = this.enedisApi;
|
||||||
if (api != null) {
|
|
||||||
api.initialize();
|
api.initialize();
|
||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
|
||||||
@ -179,13 +178,14 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
|
refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
|
||||||
ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
|
ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
|
||||||
REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
|
REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
|
||||||
} else {
|
} catch (LinkyException e) {
|
||||||
throw new LinkyException("Enedis Api is not initialized");
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||||
}
|
}
|
||||||
} catch (LinkyException e) {
|
});
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
} else {
|
||||||
}
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||||
});
|
"Username, password and authId are mandatory");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -470,7 +470,7 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
return consumption;
|
return consumption;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkData(Consumption consumption) throws LinkyException {
|
private void checkData(Consumption consumption) throws LinkyException {
|
||||||
if (consumption.aggregats.days.periodes.size() == 0) {
|
if (consumption.aggregats.days.periodes.size() == 0) {
|
||||||
throw new LinkyException("invalid consumptions data: no day period");
|
throw new LinkyException("invalid consumptions data: no day period");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -185,9 +185,7 @@
|
|||||||
<module>org.openhab.binding.lgtvserial</module>
|
<module>org.openhab.binding.lgtvserial</module>
|
||||||
<module>org.openhab.binding.lgwebos</module>
|
<module>org.openhab.binding.lgwebos</module>
|
||||||
<module>org.openhab.binding.lifx</module>
|
<module>org.openhab.binding.lifx</module>
|
||||||
<!-- linky binding suppressed from the distribution until it is fixed
|
<module>org.openhab.binding.linky</module>
|
||||||
<module>org.openhab.binding.linky</module>
|
|
||||||
-->
|
|
||||||
<module>org.openhab.binding.linuxinput</module>
|
<module>org.openhab.binding.linuxinput</module>
|
||||||
<module>org.openhab.binding.lirc</module>
|
<module>org.openhab.binding.lirc</module>
|
||||||
<module>org.openhab.binding.logreader</module>
|
<module>org.openhab.binding.logreader</module>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user