Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[amazonechocontrol] improve login handling #8207

Merged
merged 2 commits into from
Jul 28, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -154,9 +154,13 @@ public Connection(@Nullable Connection oldConnection, Gson gson) {
String serial = null;
String deviceId = null;
if (oldConnection != null) {
frc = oldConnection.getFrc();
serial = oldConnection.getSerial();
deviceId = oldConnection.getDeviceId();
if (checkDeviceIdIsValid(deviceId)) {
frc = oldConnection.getFrc();
serial = oldConnection.getSerial();} else {
// reset connection if device-id is not valid
deviceId = null;
}
}
Random rand = new Random();
if (frc != null) {
Expand Down Expand Up @@ -193,6 +197,26 @@ public Connection(@Nullable Connection oldConnection, Gson gson) {
gsonWithNullSerialization = gsonBuilder.create();
}

/**
* Check if deviceId is valid (consisting of hex(hex(16 random bytes)) + correct ending)
*
* @param deviceId the deviceId
* @return true if valid, false if invalid
*/
private boolean checkDeviceIdIsValid(@Nullable String deviceId) {
if (deviceId != null && deviceId.length() == 92 && deviceId.endsWith("23413249564c5635564d32573831")) {
try {
String randomPart = deviceId.substring(0, 64);
String randomPartHex = new String(HexUtils.hexToBytes(randomPart));
byte[] randomBytes = HexUtils.hexToBytes(randomPartHex);
return true;
} catch (IllegalArgumentException e) {
logger.debug("Failed to decode deviceId: {}", e.getMessage());
}
}
return false;
}

private void setAmazonSite(@Nullable String amazonSite) {
String correctedAmazonSite = amazonSite != null ? amazonSite : "amazon.com";
if (correctedAmazonSite.toLowerCase().startsWith("http://")) {
Expand Down Expand Up @@ -360,6 +384,12 @@ public boolean tryRestoreLogin(@Nullable String data, @Nullable String overloade
serial = scanner.nextLine();
deviceId = scanner.nextLine();

if (!checkDeviceIdIsValid(deviceId)) {
logger.debug("Device id is invalid. Cannot restore login.");
scanner.close();
return null;
}

// Recreate session and cookies
refreshToken = scanner.nextLine();
String domain = scanner.nextLine();
Expand Down Expand Up @@ -802,14 +832,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 @@ -835,8 +865,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