Skip to content

Commit 1df6b9d

Browse files
Add helper for subscribing to callback events
1 parent 039b2c1 commit 1df6b9d

2 files changed

Lines changed: 135 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package engineer.nightowl.sonos.api.util;
2+
3+
import engineer.nightowl.sonos.api.SonosApiClient;
4+
import engineer.nightowl.sonos.api.exception.SonosApiClientException;
5+
import org.apache.http.Header;
6+
import org.apache.http.HttpMessage;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
10+
import java.security.MessageDigest;
11+
import java.security.NoSuchAlgorithmException;
12+
import java.util.Arrays;
13+
import java.util.Base64;
14+
import java.util.Map;
15+
import java.util.stream.Collectors;
16+
17+
import static java.nio.charset.StandardCharsets.UTF_8;
18+
19+
public class SonosCallbackHelper
20+
{
21+
private static final Logger logger = LoggerFactory.getLogger(SonosCallbackHelper.class);
22+
23+
/**
24+
* Verify that the message was signed by Sonos.
25+
*
26+
* @param headers from the message
27+
* @return true if the message is cryptographically provable to be from Sonos
28+
* @throws SonosApiClientException if in an unsupported environment
29+
*/
30+
public static Boolean verifySignature(final Map<String, String> headers, final String apiKey, final String apiSecret) throws SonosApiClientException
31+
{
32+
MessageDigest messageDigest = null;
33+
try
34+
{
35+
messageDigest = MessageDigest.getInstance("SHA-256");
36+
} catch (NoSuchAlgorithmException e)
37+
{
38+
throw new SonosApiClientException("Unsupported execution environment", e);
39+
}
40+
41+
messageDigest.update(headers.get("X-Sonos-Event-Seq-Id").getBytes(UTF_8));
42+
messageDigest.update(headers.get("X-Sonos-Namespace").getBytes(UTF_8));
43+
messageDigest.update(headers.get("X-Sonos-Type").getBytes(UTF_8));
44+
messageDigest.update(headers.get("X-Sonos-Target-Type").getBytes(UTF_8));
45+
messageDigest.update(headers.get("X-Sonos-Target-Value").getBytes(UTF_8));
46+
messageDigest.update(apiKey.getBytes(UTF_8));
47+
messageDigest.update(apiSecret.getBytes(UTF_8));
48+
49+
final String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(messageDigest.digest());
50+
51+
logger.debug("Verifying signature: " + signature);
52+
53+
return signature.equals(headers.get("X-Sonos-Event-Signature"));
54+
}
55+
56+
public static Boolean verifySignature(final Map<String, String> headers, final SonosApiClient apiClient) throws SonosApiClientException
57+
{
58+
return SonosCallbackHelper.verifySignature(headers, apiClient.getConfiguration().getApiKey(), apiClient.getConfiguration().getApiSecret());
59+
}
60+
61+
public static Boolean verifySignature(final HttpMessage message, final SonosApiClient apiClient) throws SonosApiClientException
62+
{
63+
return SonosCallbackHelper.verifySignature(message, apiClient.getConfiguration().getApiKey(), apiClient.getConfiguration().getApiSecret());
64+
}
65+
66+
public static Boolean verifySignature(final HttpMessage message, final String apiKey, final String apiSecret) throws SonosApiClientException
67+
{
68+
// Map of headers - Name, Value
69+
Map<String, String> headers = convertHeadersToMap(message.getAllHeaders());
70+
return SonosCallbackHelper.verifySignature(headers, apiKey, apiSecret);
71+
}
72+
73+
public static Map<String, String> convertHeadersToMap(final Header[] headers)
74+
{
75+
return Arrays
76+
.stream(headers)
77+
.collect(Collectors.toMap(Header::getName, Header::getValue));
78+
}
79+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package engineer.nightowl.sonos.api.util;
2+
3+
import engineer.nightowl.sonos.api.exception.SonosApiClientException;
4+
import org.apache.http.entity.ContentType;
5+
import org.junit.Assert;
6+
import org.junit.Test;
7+
8+
import java.security.NoSuchAlgorithmException;
9+
import java.util.HashMap;
10+
import java.util.Map;
11+
12+
public class SonosCallbackHelperTest
13+
{
14+
final String apiKey = "sonos";
15+
final String apiSecret = "secret";
16+
17+
private Map<String, String> getHeaders()
18+
{
19+
final Map<String, String> headers = new HashMap<>();
20+
headers.put("X-Sonos-Event-Seq-Id", "event-seq-id");
21+
headers.put("X-Sonos-Namespace", "namespace");
22+
headers.put("X-Sonos-Type", "type");
23+
headers.put("X-Sonos-Target-Type", "target-type");
24+
headers.put("X-Sonos-Target-Value", "target-value");
25+
headers.put("Unrelated-Header", "sonos123");
26+
headers.put("Content-Type", ContentType.APPLICATION_JSON.getMimeType());
27+
28+
return headers;
29+
}
30+
31+
private Map<String, String> getValidHeaders()
32+
{
33+
final Map<String, String> headers = getHeaders();
34+
headers.put("X-Sonos-Event-Signature", "n9V0MCdGYaOPy_dJb_nUPqw5dHiBSd_g0NOQTe4IaVI");
35+
return headers;
36+
}
37+
38+
private Map<String, String> getInvalidHeaders()
39+
{
40+
final Map<String, String> headers = getHeaders();
41+
headers.put("X-Sonos-Event-Signature", "invalidSignature");
42+
return headers;
43+
}
44+
45+
@Test
46+
public void testVerifySignatureWithValidHeaders() throws SonosApiClientException, NoSuchAlgorithmException
47+
{
48+
Assert.assertTrue(SonosCallbackHelper.verifySignature(getValidHeaders(), apiKey, apiSecret));
49+
}
50+
51+
@Test
52+
public void testVerifySignatureWithInvalidHeaders() throws SonosApiClientException, NoSuchAlgorithmException
53+
{
54+
Assert.assertFalse(SonosCallbackHelper.verifySignature(getInvalidHeaders(), apiKey, apiSecret));
55+
}
56+
}

0 commit comments

Comments
 (0)