Skip to content

Commit

Permalink
Fix ArrayStoreException
Browse files Browse the repository at this point in the history
Fixes openhab#15313

Partially reverts openhab#13967

Signed-off-by: Jacob Laursen <[email protected]>
  • Loading branch information
markus7017 authored and jlaur committed Jul 28, 2023
1 parent ae36108 commit 2a0aa9e
Show file tree
Hide file tree
Showing 11 changed files with 153 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ public class BluetoothUtils {
*/
public static int[] toIntArray(byte[] value) {
int[] ret = new int[value.length];
System.arraycopy(value, 0, ret, 0, value.length);
for (int i = 0; i < value.length; i++) {
ret[i] = value[i];
}
return ret;
}

Expand Down
1 change: 0 additions & 1 deletion bundles/org.openhab.binding.shelly/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,6 @@ You should calibrate the valve using the device Web UI or Shelly App before star
| ------- | --------------- | -------- | --------- | ------------------------------------------------------------------- |
| sensors | temperature | Number | yes | Current Temperature in °C |
| | state | Contact | yes | Valve status: OPEN or CLOSED (position = 0) |
| | open | Contact | yes | ON: "window is open" was detected, OFF: window is closed |
| | lastUpdate | DateTime | yes | Timestamp of the last update (any sensor value changed) |
| control | targetTemp | Number | no | Temperature in °C: 4=Low/Min; 5..30=target temperature;31=Hi/Max |
| | position | Dimmer | no | Set valve to manual mode (0..100%) disables auto-temp) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ public class ShellyBindingConstants {
public static final String SHELLY_API_MIN_FWCOIOT = "v1.6";// v1.6.0+
public static final String SHELLY_API_FWCOIOT2 = "v1.8";// CoAP 2 with FW 1.8+
public static final String SHELLY_API_FW_110 = "v1.10"; // FW 1.10 or newer detected, activates some add feature
public static final String SHELLY2_API_MIN_FWVERSION = "v0.10.2"; // Gen 2 minimum FW
public static final String SHELLY2_API_MIN_FWVERSION = "v0.10.1"; // Gen 2 minimum FW

// Alarm types/messages
public static final String ALARM_TYPE_NONE = "NONE";
Expand Down Expand Up @@ -327,7 +327,7 @@ public class ShellyBindingConstants {
public static final int DIGITS_LUX = 0;
public static final int DIGITS_PERCENT = 1;

public static final int SHELLY_API_TIMEOUT_MS = 15000;
public static final int SHELLY_API_TIMEOUT_MS = 10000;
public static final int UPDATE_STATUS_INTERVAL_SECONDS = 3; // check for updates every x sec
public static final int UPDATE_SKIP_COUNT = 20; // update every x triggers or when a key was pressed
public static final int UPDATE_MIN_DELAY = 15;// update every x triggers or when a key was pressed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class ShellyApiResult {
public String response = "";
public int httpCode = -1;
public String httpReason = "";
public String authResponse = "";
public String authChallenge = "";

public ShellyApiResult() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,21 @@

import static org.openhab.binding.shelly.internal.ShellyBindingConstants.SHELLY_API_TIMEOUT_MS;
import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;

import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.ws.rs.core.HttpHeaders;

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;
Expand All @@ -32,6 +37,8 @@
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthChallenge;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthRsp;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
Expand All @@ -49,8 +56,9 @@
public class ShellyHttpClient {
private final Logger logger = LoggerFactory.getLogger(ShellyHttpClient.class);

public static final String HTTP_HEADER_AUTH = "Authorization";
public static final String HTTP_HEADER_AUTH = HttpHeaders.AUTHORIZATION;
public static final String HTTP_AUTH_TYPE_BASIC = "Basic";
public static final String HTTP_AUTH_TYPE_DIGEST = "Digest";
public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8";
public static final String CONTENT_TYPE_FORM_URLENC = "application/x-www-form-urlencoded";

Expand All @@ -72,6 +80,7 @@ public ShellyHttpClient(String thingName, ShellyThingConfiguration config, HttpC
this.thingName = thingName;
setConfig(thingName, config);
this.httpClient = httpClient;
this.httpClient.setConnectTimeout(SHELLY_API_TIMEOUT_MS);
}

public void initialize() throws ShellyApiException {
Expand Down Expand Up @@ -103,7 +112,7 @@ protected String httpRequest(String uri) throws ShellyApiException {
boolean timeout = false;
while (retries > 0) {
try {
apiResult = innerRequest(HttpMethod.GET, uri, "");
apiResult = innerRequest(HttpMethod.GET, uri, null, "");
if (timeout) {
logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
apiResult.getUrl());
Expand All @@ -128,10 +137,15 @@ protected String httpRequest(String uri) throws ShellyApiException {
}

public String httpPost(String uri, String data) throws ShellyApiException {
return innerRequest(HttpMethod.POST, uri, data).response;
return innerRequest(HttpMethod.POST, uri, null, data).response;
}

public String httpPost(@Nullable Shelly2AuthChallenge auth, String data) throws ShellyApiException {
return innerRequest(HttpMethod.POST, SHELLYRPC_ENDPOINT, auth, data).response;
}

private ShellyApiResult innerRequest(HttpMethod method, String uri, String data) throws ShellyApiException {
private ShellyApiResult innerRequest(HttpMethod method, String uri, @Nullable Shelly2AuthChallenge auth,
String data) throws ShellyApiException {
Request request = null;
String url = "http://" + config.deviceIp + uri;
ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url);
Expand All @@ -140,10 +154,24 @@ private ShellyApiResult innerRequest(HttpMethod method, String uri, String data)
request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
TimeUnit.MILLISECONDS);

if (!config.password.isEmpty() && !getString(data).contains("\"auth\":{")) {
String value = config.userId + ":" + config.password;
request.header(HTTP_HEADER_AUTH,
HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
if (!uri.equals(SHELLY_URL_DEVINFO) && !config.password.isEmpty()) { // not for /shelly or no password
// configured
// Add Auth info
// Gen 1: Basic Auth
// Gen 2: Digest Auth
String authHeader = "";
if (auth != null) { // only if we received an Auth challenge
authHeader = formatAuthResponse(uri,
buildAuthResponse(uri, auth, SHELLY2_AUTHDEF_USER, config.password));
} else {
if (!uri.equals(SHELLYRPC_ENDPOINT)) {
String bearer = config.userId + ":" + config.password;
authHeader = HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(bearer.getBytes());
}
}
if (!authHeader.isEmpty()) {
request.header(HTTP_HEADER_AUTH, authHeader);
}
}
fillPostData(request, data);
logger.trace("{}: HTTP {} for {} {}\n{}", thingName, method, url, data, request.getHeaders());
Expand All @@ -162,14 +190,14 @@ private ShellyApiResult innerRequest(HttpMethod method, String uri, String data)
apiResult.httpCode = message.error.code;
apiResult.response = message.error.message;
if (getInteger(message.error.code) == HttpStatus.UNAUTHORIZED_401) {
apiResult.authResponse = getString(message.error.message).replaceAll("\\\"", "\"");
apiResult.authChallenge = getString(message.error.message).replaceAll("\\\"", "\"");
}
}
}
HttpFields headers = contentResponse.getHeaders();
String auth = headers.get(HttpHeader.WWW_AUTHENTICATE);
if (!getString(auth).isEmpty()) {
apiResult.authResponse = auth;
String authChallenge = headers.get(HttpHeader.WWW_AUTHENTICATE);
if (!getString(authChallenge).isEmpty()) {
apiResult.authChallenge = authChallenge;
}

// validate response, API errors are reported as Json
Expand All @@ -191,6 +219,36 @@ private ShellyApiResult innerRequest(HttpMethod method, String uri, String data)
return apiResult;
}

protected @Nullable Shelly2AuthRsp buildAuthResponse(String uri, @Nullable Shelly2AuthChallenge challenge,
String user, String password) throws ShellyApiException {
if (challenge == null) {
return null; // not required
}
if (!SHELLY2_AUTHTTYPE_DIGEST.equalsIgnoreCase(challenge.authType)
|| !SHELLY2_AUTHALG_SHA256.equalsIgnoreCase(challenge.algorithm)) {
throw new IllegalArgumentException("Unsupported Auth type/algorithm requested by device");
}
Shelly2AuthRsp response = new Shelly2AuthRsp();
response.username = user;
response.realm = challenge.realm;
response.nonce = challenge.nonce;
response.cnonce = Long.toHexString((long) Math.floor(Math.random() * 10e8));
response.nc = "00000001";
response.authType = challenge.authType;
response.algorithm = challenge.algorithm;
String ha1 = sha256(response.username + ":" + response.realm + ":" + password);
String ha2 = sha256(HttpMethod.POST + ":" + uri);// SHELLY2_AUTH_NOISE;
response.response = sha256(
ha1 + ":" + response.nonce + ":" + response.nc + ":" + response.cnonce + ":" + "auth" + ":" + ha2);
return response;
}

protected String formatAuthResponse(String uri, @Nullable Shelly2AuthRsp rsp) {
return rsp != null ? MessageFormat.format(HTTP_AUTH_TYPE_DIGEST
+ " username=\"{0}\", realm=\"{1}\", uri=\"{2}\", nonce=\"{3}\", cnonce=\"{4}\", nc=\"{5}\", qop=\"auth\",response=\"{6}\", algorithm=\"{7}\", ",
rsp.username, rsp.realm, uri, rsp.nonce, rsp.cnonce, rsp.nc, rsp.response, rsp.algorithm) : "";
}

/**
* Fill in POST data, set http headers
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorBat;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorHum;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorLux;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthRequest;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthResponse;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthRsp;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigCover;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigInput;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigSwitch;
Expand Down Expand Up @@ -92,7 +91,7 @@ public class Shelly2ApiClient extends ShellyHttpClient {
protected final ShellyStatusSensor sensorData = new ShellyStatusSensor();
protected final ArrayList<ShellyRollerStatus> rollerStatus = new ArrayList<>();
protected @Nullable ShellyThingInterface thing;
protected @Nullable Shelly2AuthRequest authReq;
protected @Nullable Shelly2AuthRsp authReq;

public Shelly2ApiClient(String thingName, ShellyThingInterface thing) {
super(thingName, thing);
Expand Down Expand Up @@ -793,23 +792,6 @@ protected Shelly2RpcBaseMessage buildRequest(String method, @Nullable Object par
return request;
}

protected Shelly2AuthRequest buildAuthRequest(Shelly2AuthResponse authParm, String user, String realm,
String password) throws ShellyApiException {
Shelly2AuthRequest authReq = new Shelly2AuthRequest();
authReq.username = "admin";
authReq.realm = realm;
authReq.nonce = authParm.nonce;
authReq.cnonce = (long) Math.floor(Math.random() * 10e8);
authReq.nc = authParm.nc != null ? authParm.nc : 1;
authReq.authType = SHELLY2_AUTHTTYPE_DIGEST;
authReq.algorithm = SHELLY2_AUTHALG_SHA256;
String ha1 = sha256(authReq.username + ":" + authReq.realm + ":" + password);
String ha2 = SHELLY2_AUTH_NOISE;
authReq.response = sha256(
ha1 + ":" + authReq.nonce + ":" + authReq.nc + ":" + authReq.cnonce + ":" + "auth" + ":" + ha2);
return authReq;
}

protected String mapValue(Map<String, String> map, @Nullable String key) {
String value;
boolean known = key != null && !key.isEmpty() && map.containsKey(key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
* @author Markus Michels - Initial contribution
*/
public class Shelly2ApiJsonDTO {
public static final String SHELLYRPC_ENDPOINT = "/rpc";

public static final String SHELLYRPC_METHOD_CLASS_SHELLY = "Shelly";
public static final String SHELLYRPC_METHOD_CLASS_SWITCH = "Switch";

Expand Down Expand Up @@ -1004,7 +1006,7 @@ public class Shelly2RpcMessageError {
public Object params;
public String event;
public Object result;
public Shelly2AuthRequest auth;
public Shelly2AuthRsp auth;
public Shelly2RpcMessageError error;
}

Expand All @@ -1022,31 +1024,32 @@ public static class Shelly2NotifyStatus extends Shelly2DeviceStatusResult {
public Shelly2RpcMessageError error;
}

public static String SHELLY2_AUTHDEF_USER = "admin";
public static String SHELLY2_AUTHTTYPE_DIGEST = "digest";
public static String SHELLY2_AUTHTTYPE_STRING = "string";
public static String SHELLY2_AUTHALG_SHA256 = "SHA-256";
// = ':auth:'+HexHash("dummy_method:dummy_uri");
public static String SHELLY2_AUTH_NOISE = "6370ec69915103833b5222b368555393393f098bfbfbb59f47e0590af135f062";

public static class Shelly2AuthRequest {
public String username;
public Long nonce;
public Long cnonce;
public Integer nc;
public String realm;
public String algorithm;
public String response;
public static class Shelly2AuthChallenge { // on 401 message contains the auth info
@SerializedName("auth_type")
public String authType;
public String nonce;
public String nc;
public String realm;
public String algorithm;
}

public static class Shelly2AuthResponse { // on 401 message contains the auth info
@SerializedName("auth_type")
public String authType;
public Long nonce;
public Integer nc;
public static class Shelly2AuthRsp {
public String username;
public String nonce;
public String cnonce;
public String nc;
public String realm;
public String algorithm;
public String response;
@SerializedName("auth_type")
public String authType;
}

// BTHome samples
Expand Down
Loading

0 comments on commit 2a0aa9e

Please sign in to comment.