Skip to content

Commit

Permalink
[amazonechocontrol] improve login handling (openhab#8207)
Browse files Browse the repository at this point in the history
Signed-off-by: Jan N. Klug <[email protected]>
Signed-off-by: Daan Meijer <[email protected]>
  • Loading branch information
J-N-K authored and DaanMeijer committed Sep 1, 2020
1 parent 14d6a17 commit 29e2cd8
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 18 deletions.
5 changes: 5 additions & 0 deletions bundles/org.openhab.binding.amazonechocontrol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,11 @@ E.g. to read out the history call from an installation on openhab:8080 with an a

http://openhab:8080/amazonechocontrol/account1/PROXY/api/activities?startTime=&size=50&offset=1

To resolve login problems the connection settings of an `account` thing can be reset via the karaf console.
The command `amazonechocontrol listAccounts` shows a list of all available `account` things.
The command `amazonechocontrol resetAccount <id>` resets the device id and all other connection settings.
After resetting a connection, a new login as described above is necessary.

## Note

This binding uses the same API as the Web-Browser-Based Alexa site (alexa.amazon.de).
Expand Down
4 changes: 3 additions & 1 deletion bundles/org.openhab.binding.amazonechocontrol/pom.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ void doVerb(String verb, @Nullable HttpServletRequest req, @Nullable HttpServlet
// handle post of login page
connection = this.connectionToInitialize;
if (connection == null) {
returnError(resp, "Connection not in intialize mode.");
returnError(resp, "Connection not in initialize mode.");
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
Expand Down Expand Up @@ -59,12 +61,14 @@
*
* @author Michael Geramb - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.amazonechocontrol")
@Component(service = { ThingHandlerFactory.class,
AmazonEchoControlHandlerFactory.class }, configurationPid = "binding.amazonechocontrol")
@NonNullByDefault
public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(AmazonEchoControlHandlerFactory.class);
private final Map<ThingUID, @Nullable List<ServiceRegistration<?>>> discoveryServiceRegistrations = new HashMap<>();

private final Set<AccountHandler> accountHandlers = new HashSet<>();
private final HttpService httpService;
private final StorageService storageService;
private final BindingServlet bindingServlet;
Expand Down Expand Up @@ -99,6 +103,7 @@ protected void deactivate(ComponentContext componentContext) {
Storage<String> storage = storageService.getStorage(thing.getUID().toString(),
String.class.getClassLoader());
AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, httpService, storage, gson);
accountHandlers.add(bridgeHandler);
registerDiscoveryService(bridgeHandler);
bindingServlet.addAccountThing(thing);
return bridgeHandler;
Expand Down Expand Up @@ -131,6 +136,7 @@ private synchronized void registerDiscoveryService(AccountHandler bridgeHandler)
@Override
protected synchronized void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof AccountHandler) {
accountHandlers.remove(thingHandler);
BindingServlet bindingServlet = this.bindingServlet;
bindingServlet.removeAccountThing(thingHandler.getThing());

Expand All @@ -154,4 +160,8 @@ protected synchronized void removeHandler(ThingHandler thingHandler) {
}
}
}

public Set<AccountHandler> getAccountHandlers() {
return accountHandlers;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
Expand Down Expand Up @@ -124,11 +123,13 @@ public class Connection {
private static final String THING_THREADPOOL_NAME = "thingHandler";
private static final long EXPIRES_IN = 432000; // five days
private static final Pattern CHARSET_PATTERN = Pattern.compile("(?i)\\bcharset=\\s*\"?([^\\s;\"]*)");
private static final String DEVICE_TYPE = "A2IVLV5VM2W81";

protected final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(THING_THREADPOOL_NAME);

private final Logger logger = LoggerFactory.getLogger(Connection.class);

private final Random rand = new Random();
private final CookieManager cookieManager = new CookieManager();
private String amazonSite = "amazon.com";
private String alexaServer = "https://alexa.amazon.com";
Expand All @@ -154,11 +155,10 @@ public Connection(@Nullable Connection oldConnection, Gson gson) {
String serial = null;
String deviceId = null;
if (oldConnection != null) {
deviceId = oldConnection.getDeviceId();
frc = oldConnection.getFrc();
serial = oldConnection.getSerial();
deviceId = oldConnection.getDeviceId();
}
Random rand = new Random();
if (frc != null) {
this.frc = frc;
} else {
Expand All @@ -178,11 +178,7 @@ public Connection(@Nullable Connection oldConnection, Gson gson) {
if (deviceId != null) {
this.deviceId = deviceId;
} else {
// generate device id
byte[] bytes = new byte[16];
rand.nextBytes(bytes);
String hexStr = HexUtils.bytesToHex(bytes).toUpperCase();
this.deviceId = HexUtils.bytesToHex(hexStr.getBytes()) + "23413249564c5635564d32573831";
this.deviceId = generateDeviceId();
}

// build user agent
Expand All @@ -193,6 +189,36 @@ public Connection(@Nullable Connection oldConnection, Gson gson) {
gsonWithNullSerialization = gsonBuilder.create();
}

/**
* Generate a new device id
*
* The device id consists of 16 random bytes in upper-case hex format, a # as separator and a fixed DEVICE_TYPE
*
* @return a string containing the new device-id
*/
private String generateDeviceId() {
byte[] bytes = new byte[16];
rand.nextBytes(bytes);
String hexStr = HexUtils.bytesToHex(bytes).toUpperCase() + "#" + DEVICE_TYPE;
return HexUtils.bytesToHex(hexStr.getBytes());
}

/**
* Check if deviceId is valid (consisting of hex(hex(16 random bytes)) + "#" + DEVICE_TYPE)
*
* @param deviceId the deviceId
* @return true if valid, false if invalid
*/
private boolean checkDeviceIdIsValid(@Nullable String deviceId) {
if (deviceId != null && deviceId.matches("^[0-9a-fA-F]{92}$")) {
String hexString = new String(HexUtils.hexToBytes(deviceId));
if (hexString.matches("^[0-9A-F]{32}#" + DEVICE_TYPE + "$")) {
return true;
}
}
return false;
}

private void setAmazonSite(@Nullable String amazonSite) {
String correctedAmazonSite = amazonSite != null ? amazonSite : "amazon.com";
if (correctedAmazonSite.toLowerCase().startsWith("http://")) {
Expand Down Expand Up @@ -802,14 +828,14 @@ private void exchangeToken() throws IOException, URISyntaxException {
this.renewTime = (long) (System.currentTimeMillis() + Connection.EXPIRES_IN * 1000d / 0.8d); // start renew at
}

public boolean checkRenewSession() throws UnknownHostException, URISyntaxException, IOException {
public boolean checkRenewSession() throws URISyntaxException, IOException {
if (System.currentTimeMillis() >= this.renewTime) {
String renewTokenPostData = "app_name=Amazon%20Alexa&app_version=2.2.223830.0&di.sdk.version=6.10.0&source_token="
+ URLEncoder.encode(refreshToken, StandardCharsets.UTF_8.name())
+ "&package_name=com.amazon.echo&di.hw.version=iPhone&platform=iOS&requested_token_type=access_token&source_token_type=refresh_token&di.os.name=iOS&di.os.version=11.4.1&current_version=6.10.0";
String renewTokenRepsonseJson = makeRequestAndReturnString("POST", "https://api.amazon.com/auth/token",
String renewTokenResponseJson = makeRequestAndReturnString("POST", "https://api.amazon.com/auth/token",
renewTokenPostData, false, null);
parseJson(renewTokenRepsonseJson, JsonRenewTokenResponse.class);
parseJson(renewTokenResponseJson, JsonRenewTokenResponse.class);

exchangeToken();
return true;
Expand All @@ -827,6 +853,11 @@ public String getLoginPage() throws IOException, URISyntaxException {

logger.debug("Start Login to {}", alexaServer);

if (!checkDeviceIdIsValid(deviceId)) {
deviceId = generateDeviceId();
logger.debug("Generating new device id (old device id had invalid format).");
}

String mapMdJson = "{\"device_user_dictionary\":[],\"device_registration_data\":{\"software_version\":\"1\"},\"app_identifier\":{\"app_version\":\"2.2.223830\",\"bundle_id\":\"com.amazon.echo\"}}";
String mapMdCookie = Base64.getEncoder().encodeToString(mapMdJson.getBytes());

Expand All @@ -835,8 +866,7 @@ public String getLoginPage() throws IOException, URISyntaxException {

Map<String, String> customHeaders = new HashMap<>();
customHeaders.put("authority", "www.amazon.com");
String loginFormHtml = makeRequestAndReturnString("GET",
"https://www.amazon.com"
String loginFormHtml = makeRequestAndReturnString("GET", "https://www.amazon.com"
+ "/ap/signin?openid.return_to=https://www.amazon.com/ap/maplanding&openid.assoc_handle=amzn_dp_project_dee_ios&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&pageId=amzn_dp_project_dee_ios&accountStatusPolicy=P1&openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns.oa2=http://www.amazon.com/ap/ext/oauth/2&openid.oa2.client_id=device:"
+ deviceId
+ "&openid.ns.pape=http://specs.openid.net/extensions/pape/1.0&openid.oa2.response_type=token&openid.ns=http://specs.openid.net/auth/2.0&openid.pape.max_auth_age=0&openid.oa2.scope=device_auth_access",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Copyright (c) 2010-2020 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.amazonechocontrol.internal;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.smarthome.io.console.Console;
import org.eclipse.smarthome.io.console.extensions.AbstractConsoleCommandExtension;
import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

/**
* The {@link ConsoleCommandExtension} class
*
* @author Jan N. Klug - Initial contribution
*/
@Component(service = org.eclipse.smarthome.io.console.extensions.ConsoleCommandExtension.class)
@NonNullByDefault
public class ConsoleCommandExtension extends AbstractConsoleCommandExtension {
private static final String LIST_ACCOUNTS = "listAccounts";
private static final String RESET_ACCOUNT = "resetAccount";

private final AmazonEchoControlHandlerFactory handlerFactory;

@Activate
public ConsoleCommandExtension(@Reference AmazonEchoControlHandlerFactory handlerFactory) {
super("amazonechocontrol", "Manage the AmazonEchoControl account");

this.handlerFactory = handlerFactory;
}

@Override
public void execute(String[] args, Console console) {
if (args.length > 0) {
String command = args[0];
switch (command) {
case LIST_ACCOUNTS:
listAccounts(console);
break;
case RESET_ACCOUNT:
if (args.length == 2) {
resetAccount(console, args[1]);
} else {
console.println("Invalid use of command '" + command + "'");
printUsage(console);
}
break;
default:
console.println("Unknown command '" + command + "'");
printUsage(console);
break;
}
} else {
printUsage(console);
}
}

private void listAccounts(Console console) {
Set<AccountHandler> accountHandlers = handlerFactory.getAccountHandlers();

accountHandlers.forEach(handler -> console.println(
"Thing-Id: " + handler.getThing().getUID().getId() + " ('" + handler.getThing().getLabel() + "')"));
}

private void resetAccount(Console console, String accountId) {
Optional<AccountHandler> accountHandler = handlerFactory.getAccountHandlers().stream()
.filter(handler -> handler.getThing().getUID().getId().equals(accountId)).findAny();
if (accountHandler.isPresent()) {
console.println("Resetting account '" + accountId + "'");
accountHandler.get().setConnection(null);
} else {
console.println("Account '" + accountId + "' not found.");
}
}

@Override
public List<String> getUsages() {
return Arrays.asList(buildCommandUsage(LIST_ACCOUNTS, "list all AmazonEchoControl accounts"), buildCommandUsage(
RESET_ACCOUNT + " <account_id>",
"resets the account connection (clears all authentication data) for the thing with the given id"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
* @author Michael Geramb - Initial contribution
*/
@NonNullByDefault
public class AmazonEchoDiscovery extends AbstractDiscoveryService {
public class AmazonEchoDiscovery extends AbstractDiscoveryService {

AccountHandler accountHandler;
private final Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class);
Expand Down Expand Up @@ -185,7 +185,8 @@ public synchronized void discoverFlashBriefingProfiles(String currentFlashBriefi

if (!discoveredFlashBriefings.contains(currentFlashBriefingJson)) {
ThingUID brigdeThingUID = this.accountHandler.getThing().getUID();
ThingUID freeThingUID = new ThingUID(THING_TYPE_FLASH_BRIEFING_PROFILE, brigdeThingUID, Integer.toString(currentFlashBriefingJson.hashCode()));
ThingUID freeThingUID = new ThingUID(THING_TYPE_FLASH_BRIEFING_PROFILE, brigdeThingUID,
Integer.toString(currentFlashBriefingJson.hashCode()));
DiscoveryResult result = DiscoveryResultBuilder.create(freeThingUID).withLabel("FlashBriefing")
.withProperty(DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE, currentFlashBriefingJson)
.withBridge(accountHandler.getThing().getUID()).build();
Expand Down

0 comments on commit 29e2cd8

Please sign in to comment.