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