diff --git a/app/src/main/java/com/termux/app/TermuxApplication.java b/app/src/main/java/com/termux/app/TermuxApplication.java index 1d0eed04..84dd0e6c 100644 --- a/app/src/main/java/com/termux/app/TermuxApplication.java +++ b/app/src/main/java/com/termux/app/TermuxApplication.java @@ -4,15 +4,15 @@ import android.app.Application; import android.content.Context; import com.termux.am.Am; +import com.termux.shared.logger.Logger; +import com.termux.shared.shell.LocalSocketListener; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.crash.TermuxCrashUtils; import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; -import com.termux.shared.logger.Logger; import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties; import com.termux.shared.termux.theme.TermuxThemeUtils; -import com.termux.shared.shell.LocalSocketListener; -import java.io.IOException; +import java.io.File; public class TermuxApplication extends Application { @@ -35,18 +35,15 @@ public class TermuxApplication extends Application { // Set NightMode.APP_NIGHT_MODE TermuxThemeUtils.setAppNightMode(properties.getNightMode()); - try { - new LocalSocketListener(this, (args, out, err) -> { - try { - new Am(out, err, this).run(args); - return 0; - } catch (Exception e) { - return 1; - } - }, TermuxConstants.TERMUX_PACKAGE_NAME+"://call-am", 1000); - } - catch (IOException e) { - Logger.logDebug("TermuxApplication", "am socket already in use"); + if (LocalSocketListener.tryEstablishLocalSocketListener(this, (args, out, err) -> { + try { + new Am(out, err, this).run(args); + return 0; + } catch (Exception e) { + return 1; + } + }, new File(getFilesDir(), "am-socket").getAbsolutePath(), 100, 1000) == null) { + Logger.logWarn("TermuxApplication", "am socket cannot be created"); } } diff --git a/termux-shared/build.gradle b/termux-shared/build.gradle index 4883864e..97f60d49 100644 --- a/termux-shared/build.gradle +++ b/termux-shared/build.gradle @@ -32,6 +32,11 @@ android { minSdkVersion project.properties.minSdkVersion.toInteger() targetSdkVersion project.properties.targetSdkVersion.toInteger() testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + externalNativeBuild { + ndkBuild { + cppFlags '' + } + } } buildTypes { @@ -45,6 +50,11 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + externalNativeBuild { + ndkBuild { + path file('src/main/cpp/Android.mk') + } + } } dependencies { diff --git a/termux-shared/src/main/cpp/Android.mk b/termux-shared/src/main/cpp/Android.mk new file mode 100644 index 00000000..bc34d6ac --- /dev/null +++ b/termux-shared/src/main/cpp/Android.mk @@ -0,0 +1,6 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) +LOCAL_LDLIBS := -llog +LOCAL_MODULE := local-filesystem-socket +LOCAL_SRC_FILES := local-filesystem-socket.cpp +include $(BUILD_SHARED_LIBRARY) diff --git a/termux-shared/src/main/cpp/local-filesystem-socket.cpp b/termux-shared/src/main/cpp/local-filesystem-socket.cpp new file mode 100644 index 00000000..eb0ca32b --- /dev/null +++ b/termux-shared/src/main/cpp/local-filesystem-socket.cpp @@ -0,0 +1,196 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include + + + + + +int64_t millisecondtime(const struct timespec* const time) { + return (((int64_t)time->tv_sec)*1000)+(((int64_t)time->tv_nsec)/1000000); +} + + + +/// Sets the timeout in microseconds +void settimeout_micro(int fd, int timeout) { + struct timeval t = {}; + t.tv_sec = 0; + t.tv_usec = timeout; + socklen_t len = sizeof(t); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &t, len); + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &t, len); +} + +/// Sets the timeout in milliseconds +void settimeout(int fd, int timeout) { + settimeout_micro(fd, timeout*1000); +} + + +extern "C" +JNIEXPORT jint JNICALL +Java_com_termux_shared_shell_LocalFilesystemSocket_createserversocket(JNIEnv *env, jclass clazz, jbyteArray path, jint backlog) { + if (backlog < 1) { + return -1; + } + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) { + return -1; + } + jbyte* p = env->GetByteArrayElements(path, nullptr); + if (p == nullptr) { + close(fd); + return -1; + } + int chars = env->GetArrayLength(path); + if (chars >= sizeof(struct sockaddr_un)-sizeof(sa_family_t)) { + env->ReleaseByteArrayElements(path, p, JNI_ABORT); + close(fd); + return -1; + } + struct sockaddr_un adr = {.sun_family = AF_UNIX}; + memcpy(&adr.sun_path, p, chars); + if (bind(fd, reinterpret_cast(&adr), sizeof(adr)) == -1) { + env->ReleaseByteArrayElements(path, p, JNI_ABORT); + close(fd); + return -1; + } + if (listen(fd, backlog) == -1) { + env->ReleaseByteArrayElements(path, p, JNI_ABORT); + close(fd); + return -1; + } + return fd; +} + + +extern "C" +JNIEXPORT void JNICALL +Java_com_termux_shared_shell_LocalFilesystemSocket_closefd(JNIEnv *env, jclass clazz, jint fd) { + close(fd); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_com_termux_shared_shell_LocalFilesystemSocket_accept(JNIEnv *env, jclass clazz, jint fd) { + int c = accept(fd, nullptr, nullptr); + if (c == -1) { + return -1; + } + return c; +} + + + + +extern "C" +JNIEXPORT jboolean JNICALL +Java_com_termux_shared_shell_LocalFilesystemSocket_send(JNIEnv *env, jclass clazz, jbyteArray data, jint fd, jlong deadline) { + if (fd == -1) { + return false; + } + jbyte* d = env->GetByteArrayElements(data, nullptr); + if (d == nullptr) { + return false; + } + struct timespec time = {}; + jbyte* current = d; + int bytes = env->GetArrayLength(data); + while (bytes > 0) { + if (clock_gettime(CLOCK_REALTIME, &time) != -1) { + if (millisecondtime(&time) > deadline) { + env->ReleaseByteArrayElements(data, d, JNI_ABORT); + return false; + } + } else { + __android_log_write(ANDROID_LOG_WARN, "LocalFilesystemSocket.send", "Could not get the current time, deadline will not work"); + } + int ret = send(fd, current, bytes, MSG_NOSIGNAL); + if (ret == -1) { + __android_log_print(ANDROID_LOG_DEBUG, "LocalFilesystemSocket.send", "%s", strerror(errno)); + env->ReleaseByteArrayElements(data, d, JNI_ABORT); + return false; + } + bytes -= ret; + current += ret; + } + env->ReleaseByteArrayElements(data, d, JNI_ABORT); + return true; +} + +extern "C" +JNIEXPORT jint JNICALL +Java_com_termux_shared_shell_LocalFilesystemSocket_recv(JNIEnv *env, jclass clazz, jbyteArray data, jint fd, jlong deadline) { + if (fd == -1) { + return -1; + } + jbyte* d = env->GetByteArrayElements(data, nullptr); + if (d == nullptr) { + return -1; + } + struct timespec time = {}; + jbyte* current = d; + int bytes = env->GetArrayLength(data); + int transferred = 0; + while (transferred < bytes) { + if (clock_gettime(CLOCK_REALTIME, &time) != -1) { + if (millisecondtime(&time) > deadline) { + env->ReleaseByteArrayElements(data, d, 0); + return -1; + } + } else { + __android_log_write(ANDROID_LOG_WARN, "LocalFilesystemSocket.recv", "Could not get the current time, deadline will not work"); + } + int ret = read(fd, current, bytes); + if (ret == -1) { + env->ReleaseByteArrayElements(data, d, 0); + return -1; + } + // EOF, peer closed writing end + if (ret == 0) { + break; + } + transferred += ret; + current += ret; + } + env->ReleaseByteArrayElements(data, d, 0); + return transferred; +} + +extern "C" +JNIEXPORT void JNICALL +Java_com_termux_shared_shell_LocalFilesystemSocket_settimeout(JNIEnv *env, jclass clazz, jint fd, jint timeout) { + settimeout(fd, timeout); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_com_termux_shared_shell_LocalFilesystemSocket_available(JNIEnv *env, jclass clazz, jint fd) { + int size = 0; + if (ioctl(fd, SIOCINQ, &size) == -1) { + return 0; + } + return size; +} + +extern "C" +JNIEXPORT jint JNICALL +Java_com_termux_shared_shell_LocalFilesystemSocket_getpeeruid(JNIEnv *env, jclass clazz, jint fd) { + struct ucred cred = {}; + socklen_t len = sizeof(cred); + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) { + return -1; + } + return cred.uid; +} diff --git a/termux-shared/src/main/java/com/termux/shared/shell/LocalFilesystemSocket.java b/termux-shared/src/main/java/com/termux/shared/shell/LocalFilesystemSocket.java new file mode 100644 index 00000000..3a4db47c --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/shell/LocalFilesystemSocket.java @@ -0,0 +1,269 @@ +package com.termux.shared.shell; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.termux.shared.logger.Logger; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +public class LocalFilesystemSocket +{ + static { + System.loadLibrary("local-filesystem-socket"); + } + + /** + * Creates as UNIX server socket at {@code path}, with a backlog of {@code backlog}. + * @return The file descriptor of the server socket or -1 if an error occurred. + */ + private static native int createserversocket(@NonNull byte[] path, int backlog); + + /** + * Accepts a connection on the supplied server socket. + * @return The file descriptor of the connection socket or -1 in case of an error. + */ + private static native int accept(int fd); + + private static native void closefd(int fd); + + + /** + * Returns the UID of the socket peer, or -1 in case of an error. + */ + private static native int getpeeruid(int fd); + + + /** + * Sends {@code data} over the socket and decrements the timeout by the elapsed time. If the timeout hits or an error occurs, false is returned, true otherwise. + */ + private static native boolean send(@NonNull byte[] data, int fd, long deadline); + + /** + * Receives data from the socket and decrements the timeout by the elapsed time. If the timeout hits or an error occurs, -1 is returned, otherwise the number of received bytes. + */ + private static native int recv(@NonNull byte[] data, int fd, long deadline); + + /** + * Sets the send and receive timeout for the socket in milliseconds. + */ + private static native void settimeout(int fd, int timeout); + + /** + * Gets the number of bytes available to read on the socket. + */ + private static native int available(int fd); + + + + + static class Socket implements Closeable { + private int fd; + private long deadline = 0; + private final SocketOutputStream out = new SocketOutputStream(); + private final SocketInputStream in = new SocketInputStream(); + + class SocketInputStream extends InputStream { + private final byte[] readb = new byte[1]; + + @Override + public int read() throws IOException { + int ret = recv(readb); + if (ret == -1) { + throw new IOException("Could not read from socket"); + } + if (ret == 0) { + return -1; + } + return readb[0]; + } + + @Override + public int read(byte[] b) throws IOException { + if (b == null) { + throw new NullPointerException("Read buffer can't be null"); + } + int ret = recv(b); + if (ret == -1) { + throw new IOException("Could not read from socket"); + } + if (ret == 0) { + return -1; + } + return ret; + } + + @Override + public int available() { + return Socket.this.available(); + } + } + + class SocketOutputStream extends OutputStream { + private final byte[] writeb = new byte[1]; + + @Override + public void write(int b) throws IOException { + writeb[0] = (byte) b; + if (! send(writeb)) { + throw new IOException("Could not write to socket"); + } + } + + @Override + public void write(byte[] b) throws IOException { + if (! send(b)) { + throw new IOException("Could not write to socket"); + } + } + } + + private Socket(int fd) { + this.fd = fd; + } + + + /** + * Sets the socket timeout, that makes a single send/recv error out if it triggers. For a deadline after which the socket should be finished see setDeadline() + */ + public void setTimeout(int timeout) { + if (fd != -1) { + settimeout(fd, timeout); + } + } + + + /** + * Sets the deadline in unix milliseconds. When the deadline has elapsed and the socket timeout triggers, all reads and writes will error. + */ + public void setDeadline(long deadline) { + this.deadline = deadline; + } + + public boolean send(@NonNull byte[] data) { + if (fd == -1) { + return false; + } + return LocalFilesystemSocket.send(data, fd, deadline); + } + + public int recv(@NonNull byte[] data) { + if (fd == -1) { + return -1; + } + return LocalFilesystemSocket.recv(data, fd, deadline); + } + + public int available() { + if (fd == -1 || System.currentTimeMillis() > deadline) { + return 0; + } + return LocalFilesystemSocket.available(fd); + } + + @Override + public void close() throws IOException { + if (fd != -1) { + closefd(fd); + fd = -1; + } + } + + /** + * Returns the UID of the socket peer, or -1 in case of an error. + */ + public int getPeerUID() { + return getpeeruid(fd); + } + + /** + * Returns an {@link OutputStream} for the socket. You don't need to close the stream, it's automatically closed when closing the socket. + */ + public OutputStream getOutputStream() { + return out; + } + + /** + * Returns an {@link InputStream} for the socket. You don't need to close the stream, it's automatically closed when closing the socket. + */ + public InputStream getInputStream() { + return in; + } + } + + + static class ServerSocket implements Closeable + { + private final String path; + private final Context app; + private int fd; + + public ServerSocket(Context c, String path, int backlog) throws IOException { + app = c.getApplicationContext(); + if (backlog <= 0) { + throw new IllegalArgumentException("Backlog has to be at least 1"); + } + if (path == null) { + throw new IllegalArgumentException("path cannot be null"); + } + this.path = path; + File f = new File(path); + File parent = f.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + f.delete(); + fd = createserversocket(path.getBytes(StandardCharsets.UTF_8), backlog); + if (fd == -1) { + throw new IOException("Could not create UNIX server socket at \""+path+"\""); + } + } + + public ServerSocket(Context c, String path) throws IOException { + this(c, path, 50); // 50 is the default value for the Android LocalSocket implementation, so this should be good here, too + } + + public Socket accept() { + if (fd == -1) { + return null; + } + int c = -1; + while (true) { + while (c == -1) { + c = LocalFilesystemSocket.accept(fd); + } + int peeruid = getpeeruid(c); + if (peeruid == -1) { + Logger.logWarn("LocalFilesystemSocket.ServerSocket", "Could not verify peer uid, closing socket"); + closefd(c); + c = -1; + continue; + } + + if (peeruid == app.getApplicationInfo().uid) { + // if the peer has the same uid, allow the connection + break; + } else { + Logger.logWarn("LocalFilesystemSocket.ServerSocket", "WARNING: An app with the uid of "+peeruid+" tried to connect to the socket at \""+path+"\", closing connection."); + closefd(c); + c = -1; + } + } + return new Socket(c); + } + + @Override + public void close() throws IOException { + if (fd != -1) { + closefd(fd); + fd = -1; + } + } + } +} diff --git a/termux-shared/src/main/java/com/termux/shared/shell/LocalSocketListener.java b/termux-shared/src/main/java/com/termux/shared/shell/LocalSocketListener.java index 3e58d328..551562f2 100644 --- a/termux-shared/src/main/java/com/termux/shared/shell/LocalSocketListener.java +++ b/termux-shared/src/main/java/com/termux/shared/shell/LocalSocketListener.java @@ -1,8 +1,6 @@ package com.termux.shared.shell; import android.app.Application; -import android.net.LocalServerSocket; -import android.net.LocalSocket; import androidx.annotation.NonNull; @@ -22,21 +20,31 @@ public class LocalSocketListener int handle(String[] args, PrintStream out, PrintStream err); } - private static final String LOG_TAG = "LocalSocketListener"; private final Thread thread; - private final LocalServerSocket server; + private final LocalFilesystemSocket.ServerSocket server; private final int timeoutMillis; + private final int deadlineMillis; - public LocalSocketListener(@NonNull Application a, @NonNull LocalSocketHandler h, String address, int timeoutMillis) throws IOException { + + private LocalSocketListener(@NonNull Application a, @NonNull LocalSocketHandler h, @NonNull String path, int timeoutMillis, int deadlineMillis) throws IOException { this.timeoutMillis = timeoutMillis; - server = new LocalServerSocket(address); - thread = new Thread(new LocalSocketListenerRunnable(a, h)); + this.deadlineMillis = deadlineMillis; + server = new LocalFilesystemSocket.ServerSocket(a, path); + thread = new Thread(new LocalSocketListenerRunnable(h)); thread.setUncaughtExceptionHandler((t, e) -> Logger.logStackTraceWithMessage(LOG_TAG, "Uncaught exception in LocalSocketListenerRunnable", e)); thread.start(); } + public static LocalSocketListener tryEstablishLocalSocketListener(@NonNull Application a, @NonNull LocalSocketHandler h, @NonNull String address, int timeoutMillis, int deadlineMillis) { + try { + return new LocalSocketListener(a, h, address, timeoutMillis, deadlineMillis); + } catch (IOException e) { + return null; + } + } + @SuppressWarnings("unused") public void stop() { try { @@ -46,78 +54,30 @@ public class LocalSocketListener } private class LocalSocketListenerRunnable implements Runnable { - private final Application a; - private final TimeoutWatcher timeoutWatcher; - private final Thread timeoutWatcherThread; private final LocalSocketHandler h; - public LocalSocketListenerRunnable(@NonNull Application a, @NonNull LocalSocketHandler h) { - this.a = a; + public LocalSocketListenerRunnable(@NonNull LocalSocketHandler h) { this.h = h; - timeoutWatcher = new TimeoutWatcher(); - timeoutWatcherThread = new Thread(timeoutWatcher); - timeoutWatcherThread.start(); - } - - // the socket timeout for LocalSocket doesn't seem to work, so close the socket if the timeout is over, so the processing Thread doesn't get blocked. - private class TimeoutWatcher implements Runnable { - private final Object lock = new Object(); - private LocalSocket current = null; - @Override - public void run() { - while (! Thread.currentThread().isInterrupted()) { - LocalSocket watch = current; - synchronized (lock) { - while (watch == null) { - try { - lock.wait(); - } - catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - watch = current; - } - } - try { - //noinspection BusyWait - Thread.sleep(timeoutMillis); - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - try { - watch.shutdownInput(); - } catch (Exception ignored) {} - try { - watch.shutdownOutput(); - } catch (Exception ignored) {} - try { - watch.close(); - } catch (Exception ignored) {} - } - } } @Override public void run() { try { while (! Thread.currentThread().isInterrupted()) { - try (LocalSocket s = server.accept(); + try (LocalFilesystemSocket.Socket s = server.accept(); OutputStream sockout = s.getOutputStream(); InputStreamReader r = new InputStreamReader(s.getInputStream())) { - timeoutWatcher.current = s; - synchronized (timeoutWatcher.lock) { - timeoutWatcher.lock.notifyAll(); - } - // ensure only Termux programs can connect - if (s.getPeerCredentials().getUid() != a.getApplicationInfo().uid) { - Logger.logDebug(LOG_TAG, "A program with another UID tried to connect"); - continue; - } + + + + s.setTimeout(timeoutMillis); + s.setDeadline(System.currentTimeMillis()+deadlineMillis); + StringBuilder b = new StringBuilder(); int c; while ((c = r.read()) > 0) { b.append((char) c); } + Logger.logDebug(LOG_TAG, b.toString()); String outString; String errString; int ret; @@ -151,9 +111,6 @@ public class LocalSocketListener try { server.close(); } catch (Exception ignored) {} - if (timeoutWatcherThread.isAlive()) { - timeoutWatcherThread.interrupt(); - } } Logger.logDebug(LOG_TAG, "LocalSocketListenerRunnable returned"); }