[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
bom/openhab-addons
bundles
@ -751,13 +751,11 @@
|
||||
<artifactId>org.openhab.binding.lifx</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<!-- linky binding suppressed from the distribution until it is fixed
|
||||
<dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.linky</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
-->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.linuxinput</artifactId>
|
||||
|
@ -12,15 +12,22 @@
|
||||
*/
|
||||
package org.openhab.binding.linky.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link LinkyConfiguration} is the class used to match the
|
||||
* thing configuration.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LinkyConfiguration {
|
||||
public static final String INTERNAL_AUTH_ID = "internalAuthId";
|
||||
public String username;
|
||||
public String password;
|
||||
public String internalAuthId;
|
||||
public String username = "";
|
||||
public String password = "";
|
||||
public String internalAuthId = "";
|
||||
|
||||
public boolean seemsValid() {
|
||||
return !username.isBlank() && !password.isBlank() && !internalAuthId.isBlank();
|
||||
}
|
||||
}
|
||||
|
@ -47,9 +47,10 @@ import com.google.gson.JsonDeserializer;
|
||||
@NonNullByDefault
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.linky")
|
||||
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 static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
|
||||
private final LocaleProvider localeProvider;
|
||||
private final Gson gson;
|
||||
private final HttpClient httpClient;
|
||||
@ -60,7 +61,7 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory {
|
||||
this.localeProvider = localeProvider;
|
||||
this.gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
|
||||
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
|
||||
.parse(json.getAsJsonPrimitive().getAsString(), formatter))
|
||||
.parse(json.getAsJsonPrimitive().getAsString(), LINKY_FORMATTER))
|
||||
.create();
|
||||
this.httpClient = httpClientFactory.createHttpClient(LinkyBindingConstants.BINDING_ID);
|
||||
}
|
||||
|
@ -64,18 +64,19 @@ public class EnedisHttpApi {
|
||||
private final Logger logger = LoggerFactory.getLogger(EnedisHttpApi.class);
|
||||
private final Gson gson;
|
||||
private final HttpClient httpClient;
|
||||
private final LinkyConfiguration config;
|
||||
private boolean connected = false;
|
||||
private final CookieStore cookieStore;
|
||||
private final LinkyConfiguration config;
|
||||
|
||||
public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient) {
|
||||
this.gson = gson;
|
||||
this.httpClient = httpClient;
|
||||
this.config = config;
|
||||
this.cookieStore = httpClient.getCookieStore();
|
||||
addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
|
||||
}
|
||||
|
||||
public void initialize() throws LinkyException {
|
||||
addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
|
||||
|
||||
logger.debug("Starting login process for user : {}", config.username);
|
||||
|
||||
try {
|
||||
@ -109,31 +110,39 @@ public class EnedisHttpApi {
|
||||
|
||||
logger.debug(
|
||||
"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) {
|
||||
throw new LinkyException("Connection failed step 3 - auth1 : " + result.getContentAsString());
|
||||
}
|
||||
|
||||
AuthData authData = gson.fromJson(result.getContentAsString(), AuthData.class);
|
||||
if (authData.callbacks.size() < 2 || authData.callbacks.get(0).input.size() == 0
|
||||
|| authData.callbacks.get(1).input.size() == 0 || !config.username
|
||||
if (authData == null || authData.callbacks.size() < 2 || authData.callbacks.get(0).input.isEmpty()
|
||||
|| authData.callbacks.get(1).input.isEmpty() || !config.username
|
||||
.equals(Objects.requireNonNull(authData.callbacks.get(0).input.get(0)).valueAsString())) {
|
||||
throw new LinkyException("Authentication error, the authentication_cookie is probably wrong");
|
||||
}
|
||||
|
||||
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
|
||||
+ "%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");
|
||||
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();
|
||||
if (result.getStatus() != 200) {
|
||||
throw new LinkyException("Connection failed step 3 - auth2 : " + result.getContentAsString());
|
||||
}
|
||||
|
||||
AuthResult authResult = gson.fromJson(result.getContentAsString(), AuthResult.class);
|
||||
if (authResult == null) {
|
||||
throw new LinkyException("Invalid authentication result data");
|
||||
}
|
||||
|
||||
logger.debug("Add the tokenId cookie");
|
||||
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);
|
||||
}
|
||||
|
||||
public void disconnect() throws LinkyException {
|
||||
private void disconnect() throws LinkyException {
|
||||
if (connected) {
|
||||
logger.debug("Logout process");
|
||||
try { // Three times in a row to get disconnected
|
||||
String location = getLocation(httpClient.GET(URL_APPS_LINCS + "/logout"));
|
||||
location = getLocation(httpClient.GET(location));
|
||||
location = getLocation(httpClient.GET(location));
|
||||
CookieStore cookieStore = httpClient.getCookieStore();
|
||||
cookieStore.removeAll();
|
||||
connected = false;
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
@ -184,7 +192,6 @@ public class EnedisHttpApi {
|
||||
}
|
||||
|
||||
private void addCookie(String key, String value) {
|
||||
CookieStore cookieStore = httpClient.getCookieStore();
|
||||
HttpCookie cookie = new HttpCookie(key, value);
|
||||
cookie.setDomain(".enedis.fr");
|
||||
cookie.setPath("/");
|
||||
@ -220,6 +227,9 @@ public class EnedisHttpApi {
|
||||
}
|
||||
try {
|
||||
PrmInfo[] prms = gson.fromJson(data, PrmInfo[].class);
|
||||
if (prms == null || prms.length < 1) {
|
||||
throw new LinkyException("Invalid prms data received");
|
||||
}
|
||||
return prms[0];
|
||||
} catch (JsonSyntaxException e) {
|
||||
logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data);
|
||||
@ -259,6 +269,9 @@ public class EnedisHttpApi {
|
||||
logger.trace("getData returned {}", data);
|
||||
try {
|
||||
ConsumptionReport report = gson.fromJson(data, ConsumptionReport.class);
|
||||
if (report == null) {
|
||||
throw new LinkyException("No report data received");
|
||||
}
|
||||
return report.firstLevel.consumptions;
|
||||
} catch (JsonSyntaxException e) {
|
||||
logger.debug("invalid JSON response not matching ConsumptionReport.class: {}", data);
|
||||
|
@ -77,24 +77,6 @@ public class ExpiringDayCache<V> {
|
||||
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.
|
||||
*
|
||||
|
12
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthData.java
12
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthData.java
@ -12,7 +12,6 @@
|
||||
*/
|
||||
package org.openhab.binding.linky.internal.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
@ -31,22 +30,19 @@ public class AuthData {
|
||||
public @Nullable Object value;
|
||||
|
||||
public @Nullable String valueAsString() {
|
||||
if (value instanceof String) {
|
||||
return (String) value;
|
||||
}
|
||||
return null;
|
||||
return (value instanceof String) ? (String) value : null;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable String type;
|
||||
|
||||
public List<NameValuePair> output = new ArrayList<>();
|
||||
public List<NameValuePair> input = new ArrayList<>();
|
||||
public List<NameValuePair> output = List.of();
|
||||
public List<NameValuePair> input = List.of();
|
||||
}
|
||||
|
||||
public @Nullable String authId;
|
||||
public @Nullable String template;
|
||||
public @Nullable String stage;
|
||||
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
|
||||
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_INTERVAL_IN_MIN = 120;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LinkyHandler.class);
|
||||
|
||||
private final HttpClient httpClient;
|
||||
private final Gson gson;
|
||||
private final WeekFields weekFields;
|
||||
@ -146,12 +146,11 @@ public class LinkyHandler extends BaseThingHandler {
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
LinkyConfiguration config = getConfigAs(LinkyConfiguration.class);
|
||||
enedisApi = new EnedisHttpApi(config, gson, httpClient);
|
||||
|
||||
scheduler.submit(() -> {
|
||||
try {
|
||||
EnedisHttpApi api = this.enedisApi;
|
||||
if (api != null) {
|
||||
if (config.seemsValid()) {
|
||||
enedisApi = new EnedisHttpApi(config, gson, httpClient);
|
||||
scheduler.submit(() -> {
|
||||
try {
|
||||
EnedisHttpApi api = this.enedisApi;
|
||||
api.initialize();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
|
||||
@ -179,13 +178,14 @@ public class LinkyHandler extends BaseThingHandler {
|
||||
refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
|
||||
ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
|
||||
REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
|
||||
} else {
|
||||
throw new LinkyException("Enedis Api is not initialized");
|
||||
} catch (LinkyException e) {
|
||||
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;
|
||||
}
|
||||
|
||||
public void checkData(Consumption consumption) throws LinkyException {
|
||||
private void checkData(Consumption consumption) throws LinkyException {
|
||||
if (consumption.aggregats.days.periodes.size() == 0) {
|
||||
throw new LinkyException("invalid consumptions data: no day period");
|
||||
}
|
||||
|
@ -185,9 +185,7 @@
|
||||
<module>org.openhab.binding.lgtvserial</module>
|
||||
<module>org.openhab.binding.lgwebos</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.lirc</module>
|
||||
<module>org.openhab.binding.logreader</module>
|
||||
|
Loading…
x
Reference in New Issue
Block a user