/*
* Copyright 2022 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 androidx.javascriptengine;
import android.content.res.AssetFileDescriptor;
import android.os.Binder;
import android.os.RemoteException;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresFeature;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.javascriptengine.common.Utils;
import com.google.common.util.concurrent.ListenableFuture;
import org.chromium.android_webview.js_sandbox.common.IJsSandboxConsoleCallback;
import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolate;
import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolateCallback;
import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolateSyncCallback;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.concurrent.GuardedBy;
/**
* Environment within a {@link JavaScriptSandbox} where Javascript is executed.
*
* A single {@link JavaScriptSandbox} process can contain any number of {@link JavaScriptIsolate}
* instances where JS can be evaluated independently and in parallel.
*
* Each isolate has its own state and JS global object,
* and cannot interact with any other isolate through JS APIs. There is only a moderate
* security boundary between isolates in a single {@link JavaScriptSandbox}. If the code in one
* {@link JavaScriptIsolate} is able to compromise the security of the JS engine then it may be
* able to observe or manipulate other isolates, since they run in the same process. For strong
* isolation multiple {@link JavaScriptSandbox} processes should be used, but it is not supported
* at the moment. Please find the feature request here.
*
* Each isolate object must only be used from one thread.
*/
public final class JavaScriptIsolate implements AutoCloseable {
private static final String TAG = "JavaScriptIsolate";
private final Object mSetLock = new Object();
/**
* Interface to underlying service-backed implementation.
*
* mJsIsolateStub should only be null when the Isolate has been explicitly closed - not when the
* isolate has crashed or simply had its pending and future evaluations cancelled.
*/
@Nullable
private IJsSandboxIsolate mJsIsolateStub;
private CloseGuardHelper mGuard = CloseGuardHelper.create();
final JavaScriptSandbox mJsSandbox;
@Nullable
@GuardedBy("mSetLock")
private HashSet> mPendingCompleterSet =
new HashSet>();
/**
* If mSandboxClosed is true, new evaluations will throw this exception asynchronously.
*
* Note that if the isolate is closed, IllegalStateException is thrown synchronously instead.
*/
@Nullable
private Exception mExceptionForNewEvaluations;
private AtomicBoolean mSandboxClosed = new AtomicBoolean(false);
IsolateStartupParameters mStartupParameters;
private class IJsSandboxIsolateSyncCallbackStubWrapper extends
IJsSandboxIsolateSyncCallback.Stub {
private CallbackToFutureAdapter.Completer mCompleter;
IJsSandboxIsolateSyncCallbackStubWrapper(
CallbackToFutureAdapter.Completer completer) {
mCompleter = completer;
}
@Override
public void reportResultWithFd(AssetFileDescriptor afd) {
mJsSandbox.mThreadPoolTaskExecutor.execute(
() -> {
String result;
try {
result = Utils.readToString(afd,
mStartupParameters.getMaxEvaluationReturnSizeBytes(), false);
} catch (IOException | UnsupportedOperationException ex) {
mCompleter.setException(
new JavaScriptException(
"Retrieving result failed: " + ex.getMessage()));
removePending(mCompleter);
return;
} catch (IllegalArgumentException ex) {
if (ex.getMessage() != null) {
mCompleter.setException(
new EvaluationResultSizeLimitExceededException(
ex.getMessage()));
} else {
mCompleter.setException(
new EvaluationResultSizeLimitExceededException());
}
removePending(mCompleter);
return;
}
handleEvaluationResult(mCompleter, result);
});
}
@Override
public void reportErrorWithFd(@ExecutionErrorTypes int type, AssetFileDescriptor afd) {
mJsSandbox.mThreadPoolTaskExecutor.execute(
() -> {
String error;
try {
error = Utils.readToString(afd,
mStartupParameters.getMaxEvaluationReturnSizeBytes(), true);
} catch (IOException | UnsupportedOperationException ex) {
mCompleter.setException(
new JavaScriptException(
"Retrieving error failed: " + ex.getMessage()));
removePending(mCompleter);
return;
}
handleEvaluationError(mCompleter, type, error);
});
}
}
private class IJsSandboxIsolateCallbackStubWrapper extends IJsSandboxIsolateCallback.Stub {
private CallbackToFutureAdapter.Completer mCompleter;
IJsSandboxIsolateCallbackStubWrapper(CallbackToFutureAdapter.Completer completer) {
mCompleter = completer;
}
@Override
public void reportResult(String result) {
handleEvaluationResult(mCompleter, result);
}
@Override
public void reportError(@ExecutionErrorTypes int type, String error) {
handleEvaluationError(mCompleter, type, error);
}
}
private static final class JsSandboxConsoleCallbackRelay
extends IJsSandboxConsoleCallback.Stub {
private final Executor mExecutor;
private final JavaScriptConsoleCallback mCallback;
JsSandboxConsoleCallbackRelay(Executor executor, JavaScriptConsoleCallback callback) {
mExecutor = executor;
mCallback = callback;
}
@Override
public void consoleMessage(final int contextGroupId, final int level, final String message,
final String source, final int line, final int column, final String trace) {
final long identity = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> {
if ((level & JavaScriptConsoleCallback.ConsoleMessage.LEVEL_ALL) == 0
|| ((level - 1) & level) != 0) {
throw new IllegalArgumentException(
"invalid console level " + level + " provided by isolate");
}
if (message == null) {
throw new IllegalArgumentException("null message provided by isolate");
}
if (source == null) {
throw new IllegalArgumentException("null source provided by isolate");
}
mCallback.onConsoleMessage(
new JavaScriptConsoleCallback.ConsoleMessage(
level, message, source, line, column, trace));
});
} catch (RejectedExecutionException e) {
Log.e(TAG, "Console message dropped", e);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void consoleClear(int contextGroupId) {
final long identity = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> mCallback.onConsoleClear());
} catch (RejectedExecutionException e) {
Log.e(TAG, "Console clear dropped", e);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
JavaScriptIsolate(IJsSandboxIsolate jsIsolateStub, JavaScriptSandbox sandbox,
IsolateStartupParameters settings) {
mJsSandbox = sandbox;
mJsIsolateStub = jsIsolateStub;
mStartupParameters = settings;
mGuard.open("close");
// This should be at the end of the constructor.
}
/**
* Evaluates the given JavaScript code and returns the result.
*
* There are 3 possible behaviors based on the output of the expression:
*
* - If the JS expression returns a JS String, then the Java Future
* resolves to Java String.
* - If the JS expression returns a JS Promise,
* and if {@link JavaScriptSandbox#isFeatureSupported(String)} for
* {@link JavaScriptSandbox#JS_FEATURE_PROMISE_RETURN} returns {@code true}, Java Future
* resolves to Java String once the promise resolves. If it returns {@code false}, then the
* Future resolves to an empty string.
* - If the JS expression returns another data type, then Java Future
* resolves to empty Java String.
*
* The environment uses a single JS global object for all the calls to {@link
* #evaluateJavaScriptAsync(String)} and {@link #provideNamedData(String, byte[])} methods.
* These calls are queued up and are run one at a time in sequence, using the single JS
* environment for the isolate. The global variables set by one evaluation are visible for
* later evaluations. This is similar to adding multiple {@code