/* * Copyright (C) 2018 The Android Open Source Project * * 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 * * http://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 com.googlecode.android_scripting.jsonrpc; import com.googlecode.android_scripting.Log; import com.googlecode.android_scripting.Sl4aErrors; import com.googlecode.android_scripting.rpc.MethodDescriptor; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * A class that parses given JSON RPC Messages, and handles their execution. */ public class JsonRpcHandler { private int mNextSessionId = 1; private String mSessionId = ""; private final RpcReceiverManagerFactory mManagerFactory; public JsonRpcHandler(RpcReceiverManagerFactory managerFactory) { mManagerFactory = managerFactory; } /** * Returns the response object for the given request. *

* The response will be returned as a JSONObject, unless something fatal has occurred. In that * case, the resulting object will be a JSON formatted string. * * @param request the received request as a string * @return the response to the request */ public Object getResponse(String request) { try { return getResponse(new JSONObject(request)); } catch (JSONException ignored) { // May have been a bad request. } try { return Sl4aErrors.JSON_RPC_REQUEST_NOT_JSON.toJson(JSONObject.NULL); } catch (JSONException e) { // This error will never occur. Log.e("Received JSONException on invalid request.", e); return JsonRpcResult.wtf(); } } /** * Returns the response for the given request. *

* This message will be returned as a JSONObject, unless something fatal has occurred. In that * case, the resulting object will be a JSON formatted string. * * @param request the JSON-RPC request message as a JSONObject * @return the response to the request */ public Object getResponse(JSONObject request) { if (isSessionManagementRpc(request)) { return handleSessionRpc(request); } Object validationErrors = getValidationErrors(request); if (validationErrors != null) { return validationErrors; } try { Object id = request.get("id"); String method = request.getString("method"); JSONArray params = new JSONArray(); if (request.has("params")) { params = request.getJSONArray("params"); } RpcReceiverManager receiverManager = mManagerFactory.getRpcReceiverManagers().get(mSessionId); if (receiverManager == null) { Log.e("Unable to find sessionId \"" + mSessionId + "\"."); for (String key : mManagerFactory.getRpcReceiverManagers().keySet()) { Log.d("Available key: " + key); } return Sl4aErrors.SESSION_INVALID.toJson(id, mSessionId); } MethodDescriptor rpc = receiverManager.getMethodDescriptor(method); if (rpc == null) { return Sl4aErrors.JSON_RPC_UNKNOWN_RPC_METHOD.toJson(id, method); } Object rpcResult; try { rpcResult = rpc.invoke(receiverManager, params); } catch (Throwable t) { Log.e("RPC call threw an error.", t); return Sl4aErrors.JSON_RPC_FACADE_EXCEPTION_OCCURRED.toJson(id, t); } try { return JsonRpcResult.result(id, rpcResult); } catch (JSONException e) { // This error is rare, but may occur when the JsonBuilder fails to properly // convert the resulting class from the RPC into valid JSON. This may happen // when a resulting number is returned as NaN or infinite, but is not converted // to a String first. Log.e("Unable to build object of class \"" + rpcResult.getClass() + "\".", e); return Sl4aErrors.JSON_RPC_RESULT_NOT_JSON_VALID.toJson(id, e); } catch (Throwable t) { // This error will occur whenever the underlying Android APIs built against are // unavailable for the version of Android SL4A is installed onto. Log.e("Unable to build object of class \"" + rpcResult.getClass() + "\".", t); return Sl4aErrors.JSON_RPC_FAILED_TO_BUILD_RESULT.toJson(id, t); } } catch (JSONException exception) { // This error should never happen. NULL and all values found within Sl4aErrors are // guaranteed not to raise an error when being converted to JSON. // Also, all JSONObject.get() calls are only done after they have been validated to // exist after a corresponding JSONObject.has() call has been made. // Returning a raw string here to prevent the need to catch JsonRpcResult.error(). Log.e(exception); return JsonRpcResult.wtf(); } } /** * Returns a validation error if the request is invalid. Null otherwise. * @param message the request message * @return a JSONObject error response, or null if the message is valid */ private Object getValidationErrors(JSONObject message) { Object id = JSONObject.NULL; try { if (!message.has("id")) { return Sl4aErrors.JSON_RPC_MISSING_ID.toJson(id); } else { id = message.get("id"); } if (!message.has("method")) { return Sl4aErrors.JSON_RPC_MISSING_METHOD.toJson(id); } else if (!(message.get("method") instanceof String)) { return Sl4aErrors.JSON_RPC_METHOD_NOT_STRING.toJson(id); } if (message.has("params") && !(message.get("params") instanceof JSONArray)) { return Sl4aErrors.JSON_RPC_PARAMS_NOT_AN_ARRAY.toJson(id); } } catch (JSONException exception) { // This error should never happen. NULL and all values found within Sl4aException are // guaranteed not to raise an error when being converted to JSON. // Also, all JSONObject.get() calls are only done after they have been validated to // exist after a corresponding JSONObject.has() call has been made. // Returning a raw string here to prevent the need to wrap JsonRpcResult.error(). Log.e(exception); return JsonRpcResult.wtf(); } return null; } //TODO(markdr): Refactor this function into a class that handles session management. private boolean isSessionManagementRpc(JSONObject request) { return request.has("cmd") && request.has("uid"); } //TODO(markdr): Refactor this function into a class that handles session management. private Object handleSessionRpc(JSONObject request) { try { Object id = request.get("uid"); String cmd = request.getString("cmd"); switch (cmd) { case "initiate": { String newId; synchronized (mManagerFactory) { newId = "" + mNextSessionId; while (mManagerFactory.getRpcReceiverManagers().containsKey(newId)) { newId = "" + mNextSessionId++; } mSessionId = newId; mManagerFactory.create(newId); } return "{\"uid\": \"" + newId + "\", \"status\":true}"; } case "continue": { for (String key : mManagerFactory.getRpcReceiverManagers().keySet()) { Log.d("Available key: " + key); } String stringId = id.toString(); boolean canContinue = mManagerFactory.getRpcReceiverManagers().containsKey(stringId); if (canContinue) { mSessionId = stringId; return "{\"uid\":\"" + id + "\",\"status\":true}"; } return "{\"uid\":\"" + id + "\",\"status\":false," + "\"error\":\"Session does not exist.\"}"; } default: { // This case should never be reached if isSessionManagementRpc is called first. Log.e("Hit default case for handling session rpcs."); return JsonRpcResult.wtf(); } } } catch (JSONException jsonException) { // This error will never occur, because the above IDs and objects will always be JSON // serializable. Log.e("Exception during handleRpcCall: " + jsonException); return JsonRpcResult.wtf(); } } }