termux-app
258 строк · 12.3 Кб
1package com.termux.shared.shell.am;
2
3import android.Manifest;
4import android.app.Application;
5import android.content.Context;
6
7import androidx.annotation.NonNull;
8import androidx.annotation.Nullable;
9
10import com.termux.am.Am;
11import com.termux.shared.R;
12import com.termux.shared.android.PackageUtils;
13import com.termux.shared.android.PermissionUtils;
14import com.termux.shared.errors.Error;
15import com.termux.shared.logger.Logger;
16import com.termux.shared.net.socket.local.ILocalSocketManager;
17import com.termux.shared.net.socket.local.LocalClientSocket;
18import com.termux.shared.net.socket.local.LocalServerSocket;
19import com.termux.shared.net.socket.local.LocalSocketManager;
20import com.termux.shared.net.socket.local.LocalSocketManagerClientBase;
21import com.termux.shared.net.socket.local.LocalSocketRunConfig;
22import com.termux.shared.shell.ArgumentTokenizer;
23import com.termux.shared.shell.command.ExecutionCommand;
24
25import java.io.ByteArrayOutputStream;
26import java.io.PrintStream;
27import java.nio.charset.StandardCharsets;
28import java.util.ArrayList;
29import java.util.Arrays;
30import java.util.List;
31
32/**
33* A AF_UNIX/SOCK_STREAM local server managed with {@link LocalSocketManager} whose
34* {@link LocalServerSocket} receives android activity manager (am) commands from {@link LocalClientSocket}
35* and runs them with termux-am-library. It would normally only allow processes belonging to the
36* server app's user and root user to connect to it.
37*
38* The client must send the am command as a string without the initial "am" arg on its output stream
39* and then wait for the result on its input stream. The result of the execution or error is sent
40* back in the format `exit_code\0stdout\0stderr\0` where `\0` represents a null character.
41* Check termux/termux-am-socket for implementation of a native c client.
42*
43* Usage:
44* 1. Optionally extend {@link AmSocketServerClient}, the implementation for
45* {@link ILocalSocketManager} that will receive call backs from the server including
46* when client connects via {@link ILocalSocketManager#onClientAccepted(LocalSocketManager, LocalClientSocket)}.
47* 2. Create a {@link AmSocketServerRunConfig} instance which extends from {@link LocalSocketRunConfig}
48* with the run config of the am server. It would be better to use a filesystem socket instead
49* of abstract namespace socket for security reasons.
50* 3. Call {@link #start(Context, LocalSocketRunConfig)} to start the server and store the {@link LocalSocketManager}
51* instance returned.
52* 4. Stop server if needed with a call to {@link LocalSocketManager#stop()} on the
53* {@link LocalSocketManager} instance returned by start call.
54*
55* https://github.com/termux/termux-am-library/blob/main/termux-am-library/src/main/java/com/termux/am/Am.java
56* https://github.com/termux/termux-am-socket
57* https://developer.android.com/studio/command-line/adb#am
58* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
59*/
60public class AmSocketServer {
61
62public static final String LOG_TAG = "AmSocketServer";
63
64/**
65* Create the {@link AmSocketServer} {@link LocalServerSocket} and start listening for new {@link LocalClientSocket}.
66*
67* @param context The {@link Context} for {@link LocalSocketManager}.
68* @param localSocketRunConfig The {@link LocalSocketRunConfig} for {@link LocalSocketManager}.
69*/
70public static synchronized LocalSocketManager start(@NonNull Context context,
71@NonNull LocalSocketRunConfig localSocketRunConfig) {
72LocalSocketManager localSocketManager = new LocalSocketManager(context, localSocketRunConfig);
73Error error = localSocketManager.start();
74if (error != null) {
75localSocketManager.onError(error);
76return null;
77}
78
79return localSocketManager;
80}
81
82public static void processAmClient(@NonNull LocalSocketManager localSocketManager,
83@NonNull LocalClientSocket clientSocket) {
84Error error;
85
86// Read amCommandString client sent and close input stream
87StringBuilder data = new StringBuilder();
88error = clientSocket.readDataOnInputStream(data, true);
89if (error != null) {
90sendResultToClient(localSocketManager, clientSocket, 1, null, error.toString());
91return;
92}
93
94String amCommandString = data.toString();
95
96Logger.logVerbose(LOG_TAG, "am command received from peer " + clientSocket.getPeerCred().getMinimalString() +
97"\nam command: `" + amCommandString + "`");
98
99// Parse am command string and convert it to a list of arguments
100List<String> amCommandList = new ArrayList<>();
101error = parseAmCommand(amCommandString, amCommandList);
102if (error != null) {
103sendResultToClient(localSocketManager, clientSocket, 1, null, error.toString());
104return;
105}
106
107String[] amCommandArray = amCommandList.toArray(new String[0]);
108
109Logger.logDebug(LOG_TAG, "am command received from peer " + clientSocket.getPeerCred().getMinimalString() +
110"\n" + ExecutionCommand.getArgumentsLogString("am command", amCommandArray));
111
112AmSocketServerRunConfig amSocketServerRunConfig = (AmSocketServerRunConfig) localSocketManager.getLocalSocketRunConfig();
113
114// Run am command and send its result to the client
115StringBuilder stdout = new StringBuilder();
116StringBuilder stderr = new StringBuilder();
117error = runAmCommand(localSocketManager.getContext(), amCommandArray, stdout, stderr,
118amSocketServerRunConfig.shouldCheckDisplayOverAppsPermission());
119if (error != null) {
120sendResultToClient(localSocketManager, clientSocket, 1, stdout.toString(),
121!stderr.toString().isEmpty() ? stderr + "\n\n" + error : error.toString());
122}
123
124sendResultToClient(localSocketManager, clientSocket, 0, stdout.toString(), stderr.toString());
125}
126
127/**
128* Send result to {@link LocalClientSocket} that requested the am command to be run.
129*
130* @param localSocketManager The {@link LocalSocketManager} instance for the local socket.
131* @param clientSocket The {@link LocalClientSocket} to which the result is to be sent.
132* @param exitCode The exit code value to send.
133* @param stdout The stdout value to send.
134* @param stderr The stderr value to send.
135*/
136public static void sendResultToClient(@NonNull LocalSocketManager localSocketManager,
137@NonNull LocalClientSocket clientSocket,
138int exitCode,
139@Nullable String stdout, @Nullable String stderr) {
140StringBuilder result = new StringBuilder();
141result.append(sanitizeExitCode(clientSocket, exitCode));
142result.append('\0');
143result.append(stdout != null ? stdout : "");
144result.append('\0');
145result.append(stderr != null ? stderr : "");
146
147// Send result to client and close output stream
148Error error = clientSocket.sendDataToOutputStream(result.toString(), true);
149if (error != null) {
150localSocketManager.onError(clientSocket, error);
151}
152}
153
154/**
155* Sanitize exitCode to between 0-255, otherwise it may be considered invalid.
156* Out of bound exit codes would return with exit code `44` `Channel number out of range` in shell.
157*
158* @param clientSocket The {@link LocalClientSocket} to which the exit code will be sent.
159* @param exitCode The current exit code.
160* @return Returns the sanitized exit code.
161*/
162public static int sanitizeExitCode(@NonNull LocalClientSocket clientSocket, int exitCode) {
163if (exitCode < 0 || exitCode > 255) {
164Logger.logWarn(LOG_TAG, "Ignoring invalid peer " + clientSocket.getPeerCred().getMinimalString() + " result value \"" + exitCode + "\" and force setting it to \"" + 1 + "\"");
165exitCode = 1;
166}
167
168return exitCode;
169}
170
171
172/**
173* Parse amCommandString into a list of arguments like normally done on shells like bourne shell.
174* Arguments are split on whitespaces unless quoted with single or double quotes.
175* Double quotes and backslashes can be escaped with backslashes in arguments surrounded.
176* Double quotes and backslashes can be escaped with backslashes in arguments surrounded with
177* double quotes.
178*
179* @param amCommandString The am command {@link String}.
180* @param amCommandList The {@link List<String>} to set list of arguments in.
181* @return Returns the {@code error} if parsing am command failed, otherwise {@code null}.
182*/
183public static Error parseAmCommand(String amCommandString, List<String> amCommandList) {
184
185if (amCommandString == null || amCommandString.isEmpty()) {
186return null;
187}
188
189try {
190amCommandList.addAll(ArgumentTokenizer.tokenize(amCommandString));
191} catch (Exception e) {
192return AmSocketServerErrno.ERRNO_PARSE_AM_COMMAND_FAILED_WITH_EXCEPTION.getError(e, amCommandString, e.getMessage());
193}
194
195return null;
196}
197
198/**
199* Call termux-am-library to run the am command.
200*
201* @param context The {@link Context} to run am command with.
202* @param amCommandArray The am command array.
203* @param stdout The {@link StringBuilder} to set stdout in that is returned by the am command.
204* @param stderr The {@link StringBuilder} to set stderr in that is returned by the am command.
205* @param checkDisplayOverAppsPermission Check if {@link Manifest.permission#SYSTEM_ALERT_WINDOW}
206* has been granted if running on Android `>= 10` and
207* starting activity or service.
208* @return Returns the {@code error} if am command failed, otherwise {@code null}.
209*/
210public static Error runAmCommand(@NonNull Context context,
211String[] amCommandArray,
212@NonNull StringBuilder stdout, @NonNull StringBuilder stderr,
213boolean checkDisplayOverAppsPermission) {
214try (ByteArrayOutputStream stdoutByteStream = new ByteArrayOutputStream();
215PrintStream stdoutPrintStream = new PrintStream(stdoutByteStream);
216ByteArrayOutputStream stderrByteStream = new ByteArrayOutputStream();
217PrintStream stderrPrintStream = new PrintStream(stderrByteStream)) {
218
219if (checkDisplayOverAppsPermission && amCommandArray.length >= 1 &&
220(amCommandArray[0].equals("start") || amCommandArray[0].equals("startservice")) &&
221!PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(context, true)) {
222throw new IllegalStateException(context.getString(R.string.error_display_over_other_apps_permission_not_granted,
223PackageUtils.getAppNameForPackage(context)));
224}
225
226new Am(stdoutPrintStream, stderrPrintStream, (Application) context.getApplicationContext()).run(amCommandArray);
227
228// Set stdout to value set by am command in stdoutPrintStream
229stdoutPrintStream.flush();
230stdout.append(stdoutByteStream.toString(StandardCharsets.UTF_8.name()));
231
232// Set stderr to value set by am command in stderrPrintStream
233stderrPrintStream.flush();
234stderr.append(stderrByteStream.toString(StandardCharsets.UTF_8.name()));
235} catch (Exception e) {
236return AmSocketServerErrno.ERRNO_RUN_AM_COMMAND_FAILED_WITH_EXCEPTION.getError(e, Arrays.toString(amCommandArray), e.getMessage());
237}
238
239return null;
240}
241
242
243
244
245
246/** Implementation for {@link ILocalSocketManager} for {@link AmSocketServer}. */
247public abstract static class AmSocketServerClient extends LocalSocketManagerClientBase {
248
249@Override
250public void onClientAccepted(@NonNull LocalSocketManager localSocketManager,
251@NonNull LocalClientSocket clientSocket) {
252AmSocketServer.processAmClient(localSocketManager, clientSocket);
253super.onClientAccepted(localSocketManager, clientSocket);
254}
255
256}
257
258}
259