diff --git a/hal.conf.example b/hal.conf.example index 89561b76..d7eed31d 100755 --- a/hal.conf.example +++ b/hal.conf.example @@ -36,3 +36,4 @@ powerchallenge.sync_port=6666 ## Google Assistant assistant.google.port=8081 +assistant.google.client_id=https://oauth-redirect.googleusercontent.com/r/optimal-comfort-XXXXX diff --git a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/SmartHomeDaemon.java b/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/SmartHomeDaemon.java index 38a7457c..170ecee4 100644 --- a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/SmartHomeDaemon.java +++ b/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/SmartHomeDaemon.java @@ -26,12 +26,14 @@ package se.hal.plugin.assistant.google; import se.hal.HalContext; import se.hal.intf.HalDaemon; -import se.hal.plugin.assistant.google.endpoint.OAuth2AuthPage; -import se.hal.plugin.assistant.google.endpoint.OAuth2TokenPage; import se.hal.plugin.assistant.google.endpoint.SmartHomePage; import zutil.log.LogUtil; import zutil.net.http.HttpServer; +import zutil.net.http.page.oauth.OAuth2AuthorizationPage; +import zutil.net.http.page.oauth.OAuth2Registry; +import zutil.net.http.page.oauth.OAuth2TokenPage; +import java.util.Date; import java.util.concurrent.ScheduledExecutorService; import java.util.logging.Logger; @@ -39,32 +41,33 @@ import java.util.logging.Logger; public class SmartHomeDaemon implements HalDaemon { private static final Logger logger = LogUtil.getLogger(); - private static final String PARAM_PORT = "assistant.google.port"; - private static final String PARAM_KEYSTORE_PATH = "assistant.google.keystore"; - private static final String PARAM_KEYSTORE_PASSWORD = "assistant.google.keystore_psw"; - private static final String PARAM_GOOGLE_CREDENTIALS = "assistant.google.credentials"; + public static final String ENDPOINT_AUTH = "api/assistant/google/auth/token"; + public static final String ENDPOINT_TOKEN = "api/assistant/google/auth/authorize"; + + private static final String PARAM_PORT = "assistant.google.port"; + private static final String PARAM_CLIENT_ID = "assistant.google.client_id"; - private HttpServer httpServer; private SmartHomeImpl smartHome; + private OAuth2Registry oAuth2Registry; + private HttpServer httpServer; @Override public void initiate(ScheduledExecutorService executor) { if (smartHome == null) { if (!HalContext.containsProperty(PARAM_PORT) || - !HalContext.containsProperty(PARAM_KEYSTORE_PATH) || - !HalContext.containsProperty(PARAM_KEYSTORE_PASSWORD) || - !HalContext.containsProperty(PARAM_GOOGLE_CREDENTIALS)) { + !HalContext.containsProperty(PARAM_CLIENT_ID)) { logger.severe("Missing configuration, abort initializations."); + return; } - smartHome = new SmartHomeImpl( - HalContext.RESOURCE_ROOT + "/" + HalContext.getStringProperty(PARAM_GOOGLE_CREDENTIALS) - ); + smartHome = new SmartHomeImpl("token", new Date(System.currentTimeMillis() + 24*60*60*1000)); + + oAuth2Registry = new OAuth2Registry(); + oAuth2Registry.addWhitelist(HalContext.getStringProperty(PARAM_CLIENT_ID)); httpServer = new HttpServer(HalContext.getIntegerProperty(PARAM_PORT)); - httpServer.setPage(OAuth2AuthPage.ENDPOINT_URL, new OAuth2AuthPage(smartHome, - "https://oauth-redirect.googleusercontent.com/r/optimal-comfort-93608")); - httpServer.setPage(OAuth2TokenPage.ENDPOINT_URL, new OAuth2TokenPage(smartHome)); + httpServer.setPage(ENDPOINT_AUTH, new OAuth2AuthorizationPage(oAuth2Registry)); + httpServer.setPage(ENDPOINT_TOKEN, new OAuth2TokenPage(oAuth2Registry)); httpServer.setPage(SmartHomePage.ENDPOINT_URL, new SmartHomePage(smartHome)); httpServer.start(); } diff --git a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/SmartHomeImpl.java b/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/SmartHomeImpl.java index 85f23039..b1b3a80e 100644 --- a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/SmartHomeImpl.java +++ b/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/SmartHomeImpl.java @@ -16,13 +16,12 @@ package se.hal.plugin.assistant.google; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.logging.Logger; import com.google.actions.api.smarthome.*; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; import zutil.log.LogUtil; @@ -30,9 +29,9 @@ public class SmartHomeImpl extends SmartHomeApp { private static final Logger logger = LogUtil.getLogger(); - public SmartHomeImpl(String googleCredentials) { - /*try { - GoogleCredentials credentials = GoogleCredentials.fromStream(FileUtil.getInputStream(googleCredentials)); + public SmartHomeImpl(String accessToken, Date accessExpirationTime) { +/* try { + GoogleCredentials credentials = GoogleCredentials.create(new AccessToken(accessToken, accessExpirationTime)); this.setCredentials(credentials); } catch (Exception e) { logger.severe("Could not load google credentials"); @@ -43,6 +42,8 @@ public class SmartHomeImpl extends SmartHomeApp { @Override public SyncResponse onSync(SyncRequest syncRequest, Map headers) { + logger.fine("Received sync request."); + SyncResponse res = new SyncResponse(); res.setRequestId(syncRequest.requestId); res.setPayload(new SyncResponse.Payload()); @@ -100,6 +101,8 @@ public class SmartHomeImpl extends SmartHomeApp { @Override public QueryResponse onQuery(QueryRequest queryRequest, Map headers) { + logger.fine("Received query request."); + QueryRequest.Inputs.Payload.Device[] devices = ((QueryRequest.Inputs) queryRequest.getInputs()[0]).payload.devices; QueryResponse res = new QueryResponse(); res.setRequestId(queryRequest.requestId); @@ -125,6 +128,8 @@ public class SmartHomeImpl extends SmartHomeApp { @Override public ExecuteResponse onExecute(ExecuteRequest executeRequest, Map headers) { + logger.fine("Received execute request."); + ExecuteResponse res = new ExecuteResponse(); List commandsResponse = new ArrayList<>(); @@ -218,6 +223,6 @@ public class SmartHomeImpl extends SmartHomeApp { @Override public void onDisconnect(DisconnectRequest disconnectRequest, Map headers) { - + logger.fine("Received disconnect request."); } } diff --git a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2AuthPage.java b/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2AuthPage.java deleted file mode 100644 index 3dcbcd29..00000000 --- a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2AuthPage.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2020 Ziver Koc - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package se.hal.plugin.assistant.google.endpoint; - -import se.hal.plugin.assistant.google.SmartHomeImpl; -import zutil.net.http.HttpHeader; -import zutil.net.http.HttpPage; -import zutil.net.http.HttpPrintStream; -import zutil.net.http.HttpURL; - -import java.net.MalformedURLException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.Map; - -/** - * This endpoint is the first step in the OAuth 2 procedure. - * The purpose of this page is get authorization from the user to share a resource. - * - * RFC 6749: Chapter 4.1.2: - * https://tools.ietf.org/html/rfc6749 - */ -public class OAuth2AuthPage implements HttpPage { - public static final String ENDPOINT_URL = "api/assistant/google/auth/authorize"; - - /** The request is missing a required parameter, includes an invalid parameter value, includes a parameter - more than once, or is otherwise malformed. **/ - protected static final String ERROR_INVALID_REQUEST = "invalid_request"; - /** The client is not authorized to request an authorization code using this method. **/ - protected static final String ERROR_UNAUTHORIZED_CLIENT = "unauthorized_client"; - /** The resource owner or authorization server denied the request. **/ - protected static final String ERROR_ACCESS_DENIED = "access_denied"; - /** The authorization server does not support obtaining an authorization code using this method. **/ - protected static final String ERROR_UNSUPPORTED_RESP_TYPE = "unsupported_response_type"; - /** The requested scope is invalid, unknown, or malformed. **/ - protected static final String ERROR_INVALID_SCOPE = "invalid_scope"; - /** The authorization server encountered an unexpected condition that prevented it from fulfilling the request. - (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to the client - via an HTTP redirect.) **/ - protected static final String ERROR_SERVER_ERROR = "server_error"; - /** The authorization server is currently unable to handle the request due to a temporary overloading or maintenance - of the server. (This error code is needed because a 503 Service Unavailable HTTP status code cannot be returned - to the client via an HTTP redirect.) **/ - protected static final String ERROR_TEMPORARILY_UNAVAILABLE = "temporarily_unavailable"; - - private static final String RESPONSE_TYPE_CODE = "code"; - private static final String RESPONSE_TYPE_PASSWORD = "password"; - private static final String RESPONSE_TYPE_CREDENTIALS = "client_credentials"; - - protected static final String SECRET_CODE = "SUPER-SECURE-CODE"; - - - private String clientId; - - - public OAuth2AuthPage(SmartHomeImpl smartHome) {} - public OAuth2AuthPage(SmartHomeImpl smartHome, String clientId) { - this.clientId = clientId; - } - - - @Override - public void respond( - HttpPrintStream out, - HttpHeader headers, - Map session, - Map cookie, - Map request) throws MalformedURLException { - - if (!request.containsKey("redirect_uri")) { - errorResponse(out, "Bad Request, missing property: redirect_uri"); - return; - } - - if (!request.containsKey("client_id") || clientId != null && clientId.equals(request.containsKey("client_id"))) { - errorResponse(out, "Bad Request, missing or invalid client_id property."); - return; - } - - HttpURL url = new HttpURL(URLDecoder.decode(request.get("redirect_uri"), StandardCharsets.UTF_8)); - - if (!"HTTPS".equalsIgnoreCase(url.getProtocol())) { - errorResponse(out, "Bad redirect protocol: " + url.getProtocol()); - return; - } - - switch (request.get("response_type")) { - case RESPONSE_TYPE_CODE: - url.setParameter("state", request.get("state")); - url.setParameter("code", SECRET_CODE); - break; - case RESPONSE_TYPE_PASSWORD: - case RESPONSE_TYPE_CREDENTIALS: - default: - errorRedirect(out, url, ERROR_INVALID_REQUEST, request.get("state"), - "unsupported response_type: " + request.get("response_type")); - return; - } - - // Setup the redirect - - redirect(out, url); - } - - - private static void errorResponse(HttpPrintStream out, String description) { - out.setResponseStatusCode(400); - out.println(description); - } - - private static void errorRedirect(HttpPrintStream out, HttpURL url, String error, String state, String description) { - out.setHeader(HttpHeader.HEADER_CONTENT_TYPE, "application/x-www-form-urlencoded"); - url.setParameter("error", error); - if (description != null) url.setParameter("error_description", description); - //if (uri != null) url.setParameter("error_uri", uri); - if (state != null) url.setParameter("state", state); - - redirect(out, url); - } - - private static void redirect(HttpPrintStream out, HttpURL url) { - out.setResponseStatusCode(302); - out.setHeader(HttpHeader.HEADER_LOCATION, url.toString()); - } -} diff --git a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2TokenPage.java b/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2TokenPage.java deleted file mode 100644 index 68df0d5c..00000000 --- a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2TokenPage.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2020 Ziver Koc - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package se.hal.plugin.assistant.google.endpoint; - -import java.util.Map; - -import se.hal.plugin.assistant.google.SmartHomeImpl; -import zutil.net.http.HttpHeader; -import zutil.net.http.HttpPrintStream; -import zutil.net.http.page.HttpJsonPage; -import zutil.parser.DataNode; - -/** - * This endpoint is the second step in the OAuth 2 procedure. - * The purpose of this page is give a token that should be used for all consequent HTTP. - * - * RFC 6749: Chapter 4.1 - */ -public class OAuth2TokenPage extends HttpJsonPage { - private static final int SECONDS_IN_DAY = 86400; - public static final String ENDPOINT_URL = "api/assistant/google/auth/token"; - - /** The request is missing a required parameter, includes an unsupported parameter value (other than grant type), - repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the - client, or is otherwise malformed. **/ - protected static final String ERROR_INVALID_REQUEST = "invalid_request"; - /** Client authentication failed (e.g., unknown client, no client authentication included, or unsupported - authentication method). **/ - protected static final String ERROR_INVALID_CLIENT = "invalid_client"; - /** The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is - invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to - another client. **/ - protected static final String ERROR_INVALID_GRANT = "invalid_grant"; - /** The authenticated client is not authorized to use this authorization grant type. **/ - protected static final String ERROR_UNAUTHORIZED_CLIENT = "unauthorized_client"; - /** The authorization grant type is not supported by the authorization server. **/ - protected static final String ERROR_UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type"; - /** The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner. **/ - protected static final String ERROR_INVALID_SCOPE = "invalid_scope"; - - protected final String ACCESS_TOKEN = "SUPER-SECURE-TOKEN"; - protected final String REFRESH_ACCESS_TOKEN = "SUPER-SECURE-REFRESH-TOKEN"; - - - private String clientId; - - - public OAuth2TokenPage(SmartHomeImpl smartHome) {} - public OAuth2TokenPage(SmartHomeImpl smartHome, String clientId) { - this.clientId = clientId; - } - - - @Override - public DataNode jsonRespond( - HttpPrintStream out, - HttpHeader headers, - Map session, - Map cookie, - Map request) { - - // POST - - out.setHeader("Access-Control-Allow-Origin", "*"); - out.setHeader("Cache-Control", "no-store"); - out.setHeader("Pragma", "no-cache"); - - DataNode jsonRes = new DataNode(DataNode.DataType.Map); - - if (!request.containsKey("client_id")) - return errorResponse(out, ERROR_INVALID_REQUEST , request.get("state"), "Missing mandatory parameter client_id."); - - if (clientId != null && clientId.equals(request.containsKey("client_id"))) - return errorResponse(out, ERROR_INVALID_CLIENT , request.get("state"), "Invalid client_id provided."); - - if (!OAuth2AuthPage.SECRET_CODE.equals(request.get("code"))) - return errorResponse(out, ERROR_INVALID_GRANT, request.get("state"), "Invalid code value provided."); - - String grantType = request.get("grant_type"); - - switch (grantType) { - case "authorization_code": - jsonRes.set("refresh_token", REFRESH_ACCESS_TOKEN); - break; - default: - return errorResponse(out, ERROR_UNSUPPORTED_GRANT_TYPE, request.get("state"), "Unsupported grant_type: " + request.containsKey("grant_type")); - } - - jsonRes.set("access_token", ACCESS_TOKEN); - jsonRes.set("token_type", "bearer"); - jsonRes.set("expires_in", SECONDS_IN_DAY); - //jsonRes.set("scope", SECONDS_IN_DAY); - if (request.containsKey("state")) jsonRes.set("state", request.get("state")); - - return jsonRes; - } - - private static DataNode errorResponse(HttpPrintStream out, String error, String state, String description) { - out.setResponseStatusCode(400); - - DataNode jsonErr = new DataNode(DataNode.DataType.Map); - jsonErr.set("error", error); - if (description != null) jsonErr.set("error_description", description); - //if (uri != null) jsonErr.set("error_uri", uri); - if (state != null) jsonErr.set("state", state); - - return jsonErr; - } -}