Skip to content

Commit

Permalink
add azure authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
goopilot committed Jan 10, 2025
1 parent 2aabbae commit d5543d7
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 132 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**

Check failure on line 1 in bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/AzureActions.java

View workflow job for this annotation

GitHub Actions / Build (Java 21, ubuntu-24.04)

Header line doesn't match pattern ^/\*$
* Copyright (c) 2010-2023 Contributors to the openHAB project
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
Expand Down Expand Up @@ -32,6 +32,9 @@
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.openhab.binding.folderwatcher.internal.api.auth.Azure4SignerForAuthorizationHeader;
import org.openhab.binding.folderwatcher.internal.api.exception.AuthException;
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtilException;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
Expand All @@ -48,14 +51,25 @@ public class AzureActions {
private static final Duration REQUEST_TIMEOUT = Duration.ofMinutes(1);
private static final String CONTENT_TYPE = "application/xml";
private URL containerUri;
private String azureAccessKey;
private String accountName;
private String containerName;

public AzureActions(HttpClientFactory httpClientFactory, String accountName, String containerName) {
this(httpClientFactory, accountName, containerName, "");
}

public AzureActions(HttpClientFactory httpClientFactory, String accountName, String containerName,
String azureAccessKey) {
this.httpClient = httpClientFactory.getCommonHttpClient();
try {
this.containerUri = new URL("http://" + accountName + ".blob.core.windows.net/" + containerName);
this.containerUri = new URL("https://" + accountName + ".blob.core.windows.net/" + containerName);
} catch (MalformedURLException e) {
throw new RuntimeException("Unable to parse service endpoint: " + e.getMessage());
}
this.azureAccessKey = azureAccessKey;
this.accountName = accountName;
this.containerName = containerName;
}

public List<String> listContainer(String prefix) throws Exception {
Expand All @@ -72,7 +86,18 @@ public List<String> listBlob(String prefix, Map<String, String> headers, Map<Str
params.put("maxresults", "1000");
params.put("prefix", prefix);
headers.put(ACCEPT.toString(), CONTENT_TYPE);
// headers.put("x-ms-version", "2020-04-08");

if (!azureAccessKey.isEmpty()) {
Azure4SignerForAuthorizationHeader signer = new Azure4SignerForAuthorizationHeader("GET",
this.containerUri);
String authorization;
try {
authorization = signer.computeSignature(headers, params, accountName, azureAccessKey, containerName);
} catch (HttpUtilException e) {
throw new AuthException(e);
}
headers.put("Authorization", authorization);
}

Request request = httpClient.newRequest(this.containerUri.toString()) //
.method(GET) //
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/**

Check failure on line 1 in bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/auth/AWS4SignerBase.java

View workflow job for this annotation

GitHub Actions / Build (Java 21, ubuntu-24.04)

Header line doesn't match pattern ^/\*$
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
Expand All @@ -13,25 +13,13 @@
package org.openhab.binding.folderwatcher.internal.api.auth;

import java.net.URL;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SimpleTimeZone;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.folderwatcher.internal.api.exception.AuthException;
import org.openhab.binding.folderwatcher.internal.api.util.BinaryUtils;
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtilException;
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtils;

/**
* The {@link AWS4SignerBase} class contains based methods for AWS S3 API authentication.
Expand All @@ -41,15 +29,15 @@
* @author Alexandr Salamatov - Initial contribution
*/
@NonNullByDefault
public abstract class AWS4SignerBase {
public abstract class AWS4SignerBase extends SignerBase {

public static final String EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
public static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
public static final String SCHEME = "AWS4";
public static final String ALGORITHM = "HMAC-SHA256";
// public static final String ALGORITHM = "HMAC-SHA256";
public static final String TERMINATOR = "aws4_request";
public static final String ISO8601_BASIC_FORMAT = "yyyyMMdd'T'HHmmss'Z'";
public static final String DATESTRING_FORMAT = "yyyyMMdd";
protected static String PAIR_SEPARATOR = "&";
protected static String VALEU_SEPARATOR = "=";
protected URL endpointUrl;
protected String httpMethod;
protected String serviceName;
Expand All @@ -58,6 +46,9 @@ public abstract class AWS4SignerBase {
protected final SimpleDateFormat dateStampFormat;

public AWS4SignerBase(URL endpointUrl, String httpMethod, String serviceName, String regionName) {

super(PAIR_SEPARATOR, VALEU_SEPARATOR);

this.endpointUrl = endpointUrl;
this.httpMethod = httpMethod;
this.serviceName = serviceName;
Expand All @@ -69,124 +60,15 @@ public AWS4SignerBase(URL endpointUrl, String httpMethod, String serviceName, St
dateStampFormat.setTimeZone(new SimpleTimeZone(0, "UTC"));
}

protected static String getCanonicalizeHeaderNames(Map<String, String> headers) {
List<String> sortedHeaders = new ArrayList<>();
sortedHeaders.addAll(headers.keySet());
Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);

StringBuilder buffer = new StringBuilder();
for (String header : sortedHeaders) {
if (buffer.length() > 0) {
buffer.append(";");
}
buffer.append(header.toLowerCase());
}
return buffer.toString();
}

protected static String getCanonicalizedHeaderString(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
return "";
}

List<String> sortedHeaders = new ArrayList<>();
sortedHeaders.addAll(headers.keySet());
Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);

StringBuilder buffer = new StringBuilder();
for (String key : sortedHeaders) {
buffer.append(key.toLowerCase().replaceAll("\\s+", " ") + ":" + headers.get(key).replaceAll("\\s+", " "));
buffer.append("\n");
}
return buffer.toString();
}

protected static String getCanonicalRequest(URL endpoint, String httpMethod, String queryParameters,
String canonicalizedHeaderNames, String canonicalizedHeaders, String bodyHash) throws HttpUtilException {
return httpMethod + "\n" + getCanonicalizedResourcePath(endpoint) + "\n" + queryParameters + "\n"
+ canonicalizedHeaders + "\n" + canonicalizedHeaderNames + "\n" + bodyHash;
}

protected static String getCanonicalizedResourcePath(URL endpoint) throws HttpUtilException {
if (endpoint == null) {
return "/";
}
String path = endpoint.getPath();
if (path == null || path.isEmpty()) {
return "/";
}

String encodedPath = HttpUtils.urlEncode(path, true);
if (encodedPath.startsWith("/")) {
return encodedPath;
} else {
return "/".concat(encodedPath);
}
}

public static String getCanonicalizedQueryString(Map<String, String> parameters) throws HttpUtilException {
if (parameters == null || parameters.isEmpty()) {
return "";
}

SortedMap<String, String> sorted = new TreeMap<>();
Iterator<Map.Entry<String, String>> pairs = parameters.entrySet().iterator();

while (pairs.hasNext()) {
Map.Entry<String, String> pair = pairs.next();
String key = pair.getKey();
String value = pair.getValue();
sorted.put(HttpUtils.urlEncode(key, false), HttpUtils.urlEncode(value, false));
}

StringBuilder builder = new StringBuilder();
pairs = sorted.entrySet().iterator();
while (pairs.hasNext()) {
Map.Entry<String, String> pair = pairs.next();
builder.append(pair.getKey());
builder.append("=");
builder.append(pair.getValue());
if (pairs.hasNext()) {
builder.append("&");
}
}
return builder.toString();
}

protected static String getStringToSign(String scheme, String algorithm, String dateTime, String scope,
String canonicalRequest) throws AuthException {
return scheme + "-" + algorithm + "\n" + dateTime + "\n" + scope + "\n"
+ BinaryUtils.toHex(hash(canonicalRequest));
}

public static byte[] hash(String text) throws AuthException {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(text.getBytes("UTF-8"));
return md.digest();
} catch (Exception e) {
throw new AuthException("Unable to compute hash while signing request: " + e.getMessage(), e);
}
}

public static byte[] hash(byte[] data) throws AuthException {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(data);
return md.digest();
} catch (Exception e) {
throw new AuthException("Unable to compute hash while signing request: " + e.getMessage(), e);
}
}

protected static byte[] sign(String stringData, byte[] key, String algorithm) throws AuthException {
try {
byte[] data = stringData.getBytes("UTF-8");
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(key, algorithm));
return mac.doFinal(data);
} catch (Exception e) {
throw new AuthException("Unable to calculate a request signature: " + e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**

Check failure on line 1 in bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/auth/Azure4SignerForAuthorizationHeader.java

View workflow job for this annotation

GitHub Actions / Build (Java 21, ubuntu-24.04)

Header line doesn't match pattern ^/\*$
* 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.folderwatcher.internal.api.auth;

import java.net.URL;
import java.util.Base64;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.folderwatcher.internal.api.exception.AuthException;
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtilException;

/**
* The {@link Azure4SignerForAuthorizationHeader} class contains methods for Azure Blob API authentication using HTTPS
* headers.
*
* @author Alexandr Salamatov - Initial contribution
*/
@NonNullByDefault
public class Azure4SignerForAuthorizationHeader extends AzureSignerBase {

public Azure4SignerForAuthorizationHeader(String httpMethod, URL endpointUrl) {
super(endpointUrl, httpMethod, "serviceName", "regionName");
}

public String computeSignature(Map<String, String> headers, Map<String, String> queryParameters,
String AzureAccount, String AzureSecretKey, String azureContainerName)
throws AuthException, HttpUtilException {
String dateTimeStamp = dateTimeFormat.format(java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC));
headers.put("x-ms-date", dateTimeStamp);
headers.put("x-ms-version", "2020-08-04");

Map<String, String> filteredHeaders = new HashMap<>();
filteredHeaders.putAll(headers);
Iterator<Map.Entry<String, String>> iterator = filteredHeaders.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
if (!entry.getKey().startsWith(HEADER_FILTER)) {
iterator.remove();
}
}
String canonicalizedHeaders = getCanonicalizedHeaderString(filteredHeaders);
String canonicalizedQueryParameters = getCanonicalizedQueryString(queryParameters);
String canonicalilezdResource = getCanonicalResource(endpointUrl, canonicalizedQueryParameters);
String stringToSign = getStringToSign("GET", "", "", "", "", "", "", "", "", "", "", "", canonicalizedHeaders,
canonicalilezdResource);
byte[] kSecret = Base64.getDecoder().decode(AzureSecretKey);
byte[] signed = sign(stringToSign, kSecret, "HmacSHA256");
return "SharedKey" + " " + AzureAccount + ":" + Base64.getEncoder().encodeToString(signed);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**

Check failure on line 1 in bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/auth/AzureSignerBase.java

View workflow job for this annotation

GitHub Actions / Build (Java 21, ubuntu-24.04)

Header line doesn't match pattern ^/\*$
* 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.folderwatcher.internal.api.auth;

import java.net.URL;
import java.time.format.DateTimeFormatter;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.folderwatcher.internal.api.exception.AuthException;
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtilException;
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtils;

/**
* The {@link AzureSignerBase} class contains based methods for Azure Blob API authentication.
*
* @author Alexandr Salamatov - Initial contribution
*/
@NonNullByDefault
public abstract class AzureSignerBase extends SignerBase {
protected static String PAIR_SEPARATOR = "\n";
protected static String VALEU_SEPARATOR = ":";
protected static String HEADER_FILTER = "x-ms-";
protected URL endpointUrl;
protected String httpMethod;
protected String serviceName;
protected String regionName;
protected DateTimeFormatter dateTimeFormat;

public AzureSignerBase(URL endpointUrl, String httpMethod, String serviceName, String regionName) {

super(PAIR_SEPARATOR, VALEU_SEPARATOR);

this.endpointUrl = endpointUrl;
this.httpMethod = httpMethod;
this.serviceName = serviceName;
this.regionName = regionName;

dateTimeFormat = java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
}

protected static String getCanonicalResource(URL endpoint, String queryParameters) throws HttpUtilException {
return getCanonicalizedResourceName(endpoint) + getCanonicalizedResourcePath(endpoint) + "\n" + queryParameters;
}

protected static String getCanonicalizedResourceName(URL endpoint) throws HttpUtilException {
if (endpoint == null) {
return "/";
}
String path = endpoint.getHost().split("\\.")[0];
if (path == null || path.isEmpty()) {
return "/";
}

String encodedPath = HttpUtils.urlEncode(path, true);
if (encodedPath.startsWith("/")) {
return encodedPath;
} else {
return "/".concat(encodedPath);
}
}

protected static String getStringToSign(String VERB, String ContentEncoding, String ContentLanguage,
String ContentLength, String ContentMD5, String ContentType, String Date, String IfModifiedSince,
String IfMatch, String IfNoneMatch, String IfUnmodifiedSince, String Range, String CanonicalizedHeaders,
String CanonicalizedResource) throws AuthException {

return VERB + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentLength + "\n" + ContentMD5 + "\n"
+ ContentType + "\n" + Date + "\n" + IfModifiedSince + "\n" + IfMatch + "\n" + IfNoneMatch + "\n"
+ IfUnmodifiedSince + "\n" + Range + "\n" + CanonicalizedHeaders + CanonicalizedResource;
}
}
Loading

0 comments on commit d5543d7

Please sign in to comment.