From 8f77e16e18a8b0a843ec3eead441799b32d03136 Mon Sep 17 00:00:00 2001 From: Florian Hotze Date: Sun, 19 Jan 2025 17:05:38 +0100 Subject: [PATCH] [fronius] Fix invalid credentials lead to unexpected exception (#18130) With the changes from this PR, the status code is now properly read and for 401, a meaningful warnings is logged instead. Signed-off-by: Florian Hotze --- .../internal/api/FroniusBatteryControl.java | 24 ++++-- .../internal/api/FroniusConfigAuthUtil.java | 85 +++++++++++++++---- .../api/FroniusUnauthorizedException.java | 24 ++++++ .../handler/FroniusSymoInverterHandler.java | 13 +++ 4 files changed, 121 insertions(+), 25 deletions(-) create mode 100644 bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusUnauthorizedException.java diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusBatteryControl.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusBatteryControl.java index 7eb4bef03a8..4eb2cc80658 100644 --- a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusBatteryControl.java +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusBatteryControl.java @@ -77,8 +77,9 @@ public class FroniusBatteryControl { * * @return the time of use settings * @throws FroniusCommunicationException if an error occurs during communication with the inverter + * @throws FroniusUnauthorizedException when the login failed due to invalid credentials */ - private TimeOfUseRecords getTimeOfUse() throws FroniusCommunicationException { + private TimeOfUseRecords getTimeOfUse() throws FroniusCommunicationException, FroniusUnauthorizedException { // Login and get the auth header for the next request String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.GET, timeOfUseUri.getPath(), API_TIMEOUT); @@ -107,8 +108,10 @@ public class FroniusBatteryControl { * * @param records the time of use settings * @throws FroniusCommunicationException if an error occurs during communication with the inverter + * @throws FroniusUnauthorizedException when the login failed due to invalid credentials */ - private void setTimeOfUse(TimeOfUseRecords records) throws FroniusCommunicationException { + private void setTimeOfUse(TimeOfUseRecords records) + throws FroniusCommunicationException, FroniusUnauthorizedException { // Login and get the auth header for the next request String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.POST, timeOfUseUri.getPath(), API_TIMEOUT); @@ -127,8 +130,9 @@ public class FroniusBatteryControl { * inverter. * * @throws FroniusCommunicationException when an error occurs during communication with the inverter + * @throws FroniusUnauthorizedException when the login failed due to invalid credentials */ - public void reset() throws FroniusCommunicationException { + public void reset() throws FroniusCommunicationException, FroniusUnauthorizedException { setTimeOfUse(new TimeOfUseRecords(new TimeOfUseRecord[0])); } @@ -136,8 +140,9 @@ public class FroniusBatteryControl { * Holds the battery charge right now, i.e. prevents the battery from discharging. * * @throws FroniusCommunicationException when an error occurs during communication with the inverter + * @throws FroniusUnauthorizedException when the login failed due to invalid credentials */ - public void holdBatteryCharge() throws FroniusCommunicationException { + public void holdBatteryCharge() throws FroniusCommunicationException, FroniusUnauthorizedException { reset(); addHoldBatteryChargeSchedule(BEGIN_OF_DAY, END_OF_DAY); } @@ -149,8 +154,10 @@ public class FroniusBatteryControl { * @param from start time of the hold charge period * @param until end time of the hold charge period * @throws FroniusCommunicationException when an error occurs during communication with the inverter + * @throws FroniusUnauthorizedException when the login failed due to invalid credentials */ - public void addHoldBatteryChargeSchedule(LocalTime from, LocalTime until) throws FroniusCommunicationException { + public void addHoldBatteryChargeSchedule(LocalTime from, LocalTime until) + throws FroniusCommunicationException, FroniusUnauthorizedException { TimeOfUseRecord[] currentTimeOfUse = getTimeOfUse().records(); TimeOfUseRecord[] timeOfUse = new TimeOfUseRecord[currentTimeOfUse.length + 1]; System.arraycopy(currentTimeOfUse, 0, timeOfUse, 0, currentTimeOfUse.length); @@ -166,8 +173,10 @@ public class FroniusBatteryControl { * * @param power the power to charge the battery with * @throws FroniusCommunicationException when an error occurs during communication with the inverter + * @throws FroniusUnauthorizedException when the login failed due to invalid credentials */ - public void forceBatteryCharging(QuantityType power) throws FroniusCommunicationException { + public void forceBatteryCharging(QuantityType power) + throws FroniusCommunicationException, FroniusUnauthorizedException { reset(); addForcedBatteryChargingSchedule(BEGIN_OF_DAY, END_OF_DAY, power); } @@ -179,9 +188,10 @@ public class FroniusBatteryControl { * @param until end time of the forced charge period * @param power the power to charge the battery with * @throws FroniusCommunicationException when an error occurs during communication with the inverter + * @throws FroniusUnauthorizedException when the login failed due to invalid credentials */ public void addForcedBatteryChargingSchedule(LocalTime from, LocalTime until, QuantityType power) - throws FroniusCommunicationException { + throws FroniusCommunicationException, FroniusUnauthorizedException { TimeOfUseRecords currentTimeOfUse = getTimeOfUse(); TimeOfUseRecord[] timeOfUse = new TimeOfUseRecord[currentTimeOfUse.records().length + 1]; System.arraycopy(currentTimeOfUse.records(), 0, timeOfUse, 0, currentTimeOfUse.records().length); diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusConfigAuthUtil.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusConfigAuthUtil.java index 918f98ccdc8..f3f3a2e5cce 100644 --- a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusConfigAuthUtil.java +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusConfigAuthUtil.java @@ -19,14 +19,11 @@ import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.http.HttpHeader; @@ -69,10 +66,10 @@ public class FroniusConfigAuthUtil { throws IOException { LOGGER.debug("Sending login request to get authentication challenge"); CountDownLatch latch = new CountDownLatch(1); - Request initialRequest = httpClient.newRequest(loginUri).timeout(timeout, TimeUnit.MILLISECONDS); - XWwwAuthenticateHeaderListener XWwwAuthenticateHeaderListener = new XWwwAuthenticateHeaderListener(latch); - initialRequest.onResponseHeaders(XWwwAuthenticateHeaderListener); - initialRequest.send(result -> latch.countDown()); + Request request = httpClient.newRequest(loginUri).timeout(timeout, TimeUnit.MILLISECONDS); + XWwwAuthenticateHeaderListener xWwwAuthenticateHeaderListener = new XWwwAuthenticateHeaderListener(latch); + request.onResponseHeaders(xWwwAuthenticateHeaderListener); + request.send(result -> latch.countDown()); // Wait for the request to complete try { latch.await(); @@ -80,7 +77,7 @@ public class FroniusConfigAuthUtil { throw new RuntimeException(ie); } - String authHeader = XWwwAuthenticateHeaderListener.getAuthHeader(); + String authHeader = xWwwAuthenticateHeaderListener.getAuthHeader(); if (authHeader == null) { throw new IOException("No authentication header found in login response"); } @@ -161,21 +158,40 @@ public class FroniusConfigAuthUtil { * @param authHeader the authentication header to use for the login request * @throws InterruptedException when the request is interrupted * @throws FroniusCommunicationException when the login request failed + * @throws FroniusUnauthorizedException when the login failed due to invalid credentials */ private static void performLoginRequest(HttpClient httpClient, URI loginUri, String authHeader, int timeout) - throws InterruptedException, FroniusCommunicationException { - Request loginRequest = httpClient.newRequest(loginUri).header(HttpHeader.AUTHORIZATION, authHeader) - .timeout(timeout, TimeUnit.MILLISECONDS); - ContentResponse loginResponse; + throws InterruptedException, FroniusCommunicationException, FroniusUnauthorizedException { + CountDownLatch latch = new CountDownLatch(1); + Request request = httpClient.newRequest(loginUri).header(HttpHeader.AUTHORIZATION, authHeader).timeout(timeout, + TimeUnit.MILLISECONDS); + StatusListener statusListener = new StatusListener(latch); + request.onResponseBegin(statusListener); + Integer status; try { - loginResponse = loginRequest.send(); - if (loginResponse.getStatus() != 200) { - throw new FroniusCommunicationException( - "Failed to send login request, status code: " + loginResponse.getStatus()); + request.send(result -> latch.countDown()); + // Wait for the request to complete + try { + latch.await(); + } catch (InterruptedException ie) { + throw new RuntimeException(ie); } - } catch (TimeoutException | ExecutionException e) { + + status = statusListener.getStatus(); + if (status == null) { + throw new FroniusCommunicationException("Failed to send login request: No status code received."); + } + } catch (IOException e) { throw new FroniusCommunicationException("Failed to send login request", e); } + + if (status == 401) { + throw new FroniusUnauthorizedException( + "Failed to send login request, status code: 401 Unauthorized. Please check your credentials."); + } + if (status != 200) { + throw new FroniusCommunicationException("Failed to send login request, status code: " + status); + } } /** @@ -191,9 +207,11 @@ public class FroniusConfigAuthUtil { * @param timeout the timeout in milliseconds for the login requests * @return the authentication header for the next request * @throws FroniusCommunicationException when the login failed or interrupted + * @throws FroniusUnauthorizedException when the login failed due to invalid credentials */ public static synchronized String login(HttpClient httpClient, URI baseUri, String username, String password, - HttpMethod method, String relativeUrl, int timeout) throws FroniusCommunicationException { + HttpMethod method, String relativeUrl, int timeout) + throws FroniusCommunicationException, FroniusUnauthorizedException { // Perform request to get authentication parameters LOGGER.debug("Getting authentication parameters"); URI loginUri = baseUri.resolve(URI.create(LOGIN_ENDPOINT + "?user=" + username)); @@ -246,6 +264,8 @@ public class FroniusConfigAuthUtil { Thread.sleep(500 * attemptCount); attemptCount++; lastException = e; + } catch (FroniusUnauthorizedException e) { + throw e; } if (attemptCount >= 3) { @@ -269,6 +289,9 @@ public class FroniusConfigAuthUtil { /** * Listener to extract the X-Www-Authenticate header from the response of a {@link Request}. + * Required to mitigate {@link org.eclipse.jetty.client.HttpResponseException}: HTTP protocol violation: + * Authentication challenge without WWW-Authenticate header being thrown due to Fronius non-standard authentication + * header. */ private static class XWwwAuthenticateHeaderListener extends Response.Listener.Adapter { private final CountDownLatch latch; @@ -288,4 +311,30 @@ public class FroniusConfigAuthUtil { return authHeader; } } + + /** + * Listener to extract the HTTP status code from the response of a {@link Request} on response begin. + * Required to mitigate {@link org.eclipse.jetty.client.HttpResponseException}: HTTP protocol violation: + * Authentication challenge without WWW-Authenticate header being thrown due to Fronius non-standard authentication + * header. + */ + private static class StatusListener extends Response.Listener.Adapter { + private final CountDownLatch latch; + private @Nullable Integer status; + + public StatusListener(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void onBegin(Response response) { + this.status = response.getStatus(); + latch.countDown(); + super.onBegin(response); + } + + public @Nullable Integer getStatus() { + return status; + } + } } diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusUnauthorizedException.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusUnauthorizedException.java new file mode 100644 index 00000000000..497be1b5b2d --- /dev/null +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusUnauthorizedException.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.fronius.internal.api; + +/** + * Exception for 401 response from the Fronius controller. + * + * @author Florian Hotze - Initial contribution + */ +public class FroniusUnauthorizedException extends Exception { + public FroniusUnauthorizedException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusSymoInverterHandler.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusSymoInverterHandler.java index cd2eb54c34f..c568619333c 100644 --- a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusSymoInverterHandler.java +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusSymoInverterHandler.java @@ -29,6 +29,7 @@ import org.openhab.binding.fronius.internal.FroniusBridgeConfiguration; import org.openhab.binding.fronius.internal.action.FroniusSymoInverterActions; import org.openhab.binding.fronius.internal.api.FroniusBatteryControl; import org.openhab.binding.fronius.internal.api.FroniusCommunicationException; +import org.openhab.binding.fronius.internal.api.FroniusUnauthorizedException; import org.openhab.binding.fronius.internal.api.dto.ValueUnit; import org.openhab.binding.fronius.internal.api.dto.inverter.InverterDeviceStatus; import org.openhab.binding.fronius.internal.api.dto.inverter.InverterRealtimeBody; @@ -115,6 +116,8 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler { return true; } catch (FroniusCommunicationException e) { logger.warn("Failed to reset battery control", e); + } catch (FroniusUnauthorizedException e) { + logger.warn("Failed to reset battery control: Invalid username or password"); } } return false; @@ -128,6 +131,8 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler { return true; } catch (FroniusCommunicationException e) { logger.warn("Failed to set battery control to hold battery charge", e); + } catch (FroniusUnauthorizedException e) { + logger.warn("Failed to set battery control to hold battery charge: Invalid username or password"); } } return false; @@ -141,6 +146,9 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler { return true; } catch (FroniusCommunicationException e) { logger.warn("Failed to add hold battery charge schedule to battery control", e); + } catch (FroniusUnauthorizedException e) { + logger.warn( + "Failed to add hold battery charge schedule to battery control: Invalid username or password"); } } return false; @@ -154,6 +162,8 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler { return true; } catch (FroniusCommunicationException e) { logger.warn("Failed to set battery control to force battery charge", e); + } catch (FroniusUnauthorizedException e) { + logger.warn("Failed to set battery control to force battery charge: Invalid username or password"); } } return false; @@ -167,6 +177,9 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler { return true; } catch (FroniusCommunicationException e) { logger.warn("Failed to add forced battery charge schedule to battery control", e); + } catch (FroniusUnauthorizedException e) { + logger.warn( + "Failed to add forced battery charge schedule to battery control: Invalid username or password"); } } return false;