termux-app

Форк
0
258 строк · 12.3 Кб
1
package com.termux.shared.shell.am;
2

3
import android.Manifest;
4
import android.app.Application;
5
import android.content.Context;
6

7
import androidx.annotation.NonNull;
8
import androidx.annotation.Nullable;
9

10
import com.termux.am.Am;
11
import com.termux.shared.R;
12
import com.termux.shared.android.PackageUtils;
13
import com.termux.shared.android.PermissionUtils;
14
import com.termux.shared.errors.Error;
15
import com.termux.shared.logger.Logger;
16
import com.termux.shared.net.socket.local.ILocalSocketManager;
17
import com.termux.shared.net.socket.local.LocalClientSocket;
18
import com.termux.shared.net.socket.local.LocalServerSocket;
19
import com.termux.shared.net.socket.local.LocalSocketManager;
20
import com.termux.shared.net.socket.local.LocalSocketManagerClientBase;
21
import com.termux.shared.net.socket.local.LocalSocketRunConfig;
22
import com.termux.shared.shell.ArgumentTokenizer;
23
import com.termux.shared.shell.command.ExecutionCommand;
24

25
import java.io.ByteArrayOutputStream;
26
import java.io.PrintStream;
27
import java.nio.charset.StandardCharsets;
28
import java.util.ArrayList;
29
import java.util.Arrays;
30
import 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
 */
60
public class AmSocketServer {
61

62
    public 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
     */
70
    public static synchronized LocalSocketManager start(@NonNull Context context,
71
                                                        @NonNull LocalSocketRunConfig localSocketRunConfig) {
72
        LocalSocketManager localSocketManager = new LocalSocketManager(context, localSocketRunConfig);
73
        Error error = localSocketManager.start();
74
        if (error != null) {
75
            localSocketManager.onError(error);
76
            return null;
77
        }
78

79
        return localSocketManager;
80
    }
81

82
    public static void processAmClient(@NonNull LocalSocketManager localSocketManager,
83
                                       @NonNull LocalClientSocket clientSocket) {
84
        Error error;
85

86
        // Read amCommandString client sent and close input stream
87
        StringBuilder data = new StringBuilder();
88
        error = clientSocket.readDataOnInputStream(data, true);
89
        if (error != null) {
90
            sendResultToClient(localSocketManager, clientSocket, 1, null, error.toString());
91
            return;
92
        }
93

94
        String amCommandString = data.toString();
95

96
        Logger.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
100
        List<String> amCommandList = new ArrayList<>();
101
        error = parseAmCommand(amCommandString, amCommandList);
102
        if (error != null) {
103
            sendResultToClient(localSocketManager, clientSocket, 1, null, error.toString());
104
            return;
105
        }
106

107
        String[] amCommandArray = amCommandList.toArray(new String[0]);
108

109
        Logger.logDebug(LOG_TAG, "am command received from peer " + clientSocket.getPeerCred().getMinimalString() +
110
            "\n" + ExecutionCommand.getArgumentsLogString("am command", amCommandArray));
111

112
        AmSocketServerRunConfig amSocketServerRunConfig = (AmSocketServerRunConfig) localSocketManager.getLocalSocketRunConfig();
113

114
        // Run am command and send its result to the client
115
        StringBuilder stdout = new StringBuilder();
116
        StringBuilder stderr = new StringBuilder();
117
        error = runAmCommand(localSocketManager.getContext(), amCommandArray, stdout, stderr,
118
            amSocketServerRunConfig.shouldCheckDisplayOverAppsPermission());
119
        if (error != null) {
120
            sendResultToClient(localSocketManager, clientSocket, 1, stdout.toString(),
121
                !stderr.toString().isEmpty() ? stderr + "\n\n" + error : error.toString());
122
        }
123

124
        sendResultToClient(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
     */
136
    public static void sendResultToClient(@NonNull LocalSocketManager localSocketManager,
137
                                          @NonNull LocalClientSocket clientSocket,
138
                                          int exitCode,
139
                                          @Nullable String stdout, @Nullable String stderr) {
140
        StringBuilder result = new StringBuilder();
141
        result.append(sanitizeExitCode(clientSocket, exitCode));
142
        result.append('\0');
143
        result.append(stdout != null ? stdout : "");
144
        result.append('\0');
145
        result.append(stderr != null ? stderr : "");
146

147
        // Send result to client and close output stream
148
        Error error = clientSocket.sendDataToOutputStream(result.toString(), true);
149
        if (error != null) {
150
            localSocketManager.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
     */
162
    public static int sanitizeExitCode(@NonNull LocalClientSocket clientSocket, int exitCode) {
163
        if (exitCode < 0 || exitCode > 255) {
164
            Logger.logWarn(LOG_TAG, "Ignoring invalid peer "  + clientSocket.getPeerCred().getMinimalString() + " result value \"" + exitCode + "\" and force setting it to \"" + 1 + "\"");
165
            exitCode = 1;
166
        }
167

168
        return 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
     */
183
    public static Error parseAmCommand(String amCommandString, List<String> amCommandList) {
184

185
        if (amCommandString == null || amCommandString.isEmpty()) {
186
            return null;
187
        }
188

189
        try {
190
            amCommandList.addAll(ArgumentTokenizer.tokenize(amCommandString));
191
        } catch (Exception e) {
192
            return AmSocketServerErrno.ERRNO_PARSE_AM_COMMAND_FAILED_WITH_EXCEPTION.getError(e, amCommandString, e.getMessage());
193
        }
194

195
        return 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
     */
210
    public static Error runAmCommand(@NonNull Context context,
211
                                     String[] amCommandArray,
212
                                     @NonNull StringBuilder stdout, @NonNull StringBuilder stderr,
213
                                     boolean checkDisplayOverAppsPermission) {
214
        try (ByteArrayOutputStream stdoutByteStream = new ByteArrayOutputStream();
215
             PrintStream stdoutPrintStream = new PrintStream(stdoutByteStream);
216
             ByteArrayOutputStream stderrByteStream = new ByteArrayOutputStream();
217
             PrintStream stderrPrintStream = new PrintStream(stderrByteStream)) {
218

219
            if (checkDisplayOverAppsPermission && amCommandArray.length >= 1 &&
220
                (amCommandArray[0].equals("start") || amCommandArray[0].equals("startservice")) &&
221
                !PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(context, true)) {
222
                throw new IllegalStateException(context.getString(R.string.error_display_over_other_apps_permission_not_granted,
223
                    PackageUtils.getAppNameForPackage(context)));
224
            }
225

226
            new Am(stdoutPrintStream, stderrPrintStream, (Application) context.getApplicationContext()).run(amCommandArray);
227

228
            // Set stdout to value set by am command in stdoutPrintStream
229
            stdoutPrintStream.flush();
230
            stdout.append(stdoutByteStream.toString(StandardCharsets.UTF_8.name()));
231

232
            // Set stderr to value set by am command in stderrPrintStream
233
            stderrPrintStream.flush();
234
            stderr.append(stderrByteStream.toString(StandardCharsets.UTF_8.name()));
235
        } catch (Exception e) {
236
            return AmSocketServerErrno.ERRNO_RUN_AM_COMMAND_FAILED_WITH_EXCEPTION.getError(e, Arrays.toString(amCommandArray), e.getMessage());
237
        }
238

239
        return null;
240
    }
241

242

243

244

245

246
    /** Implementation for {@link ILocalSocketManager} for {@link AmSocketServer}. */
247
    public abstract static class AmSocketServerClient extends LocalSocketManagerClientBase {
248

249
        @Override
250
        public void onClientAccepted(@NonNull LocalSocketManager localSocketManager,
251
                                     @NonNull LocalClientSocket clientSocket) {
252
            AmSocketServer.processAmClient(localSocketManager, clientSocket);
253
            super.onClientAccepted(localSocketManager, clientSocket);
254
        }
255

256
    }
257

258
}
259

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.