diff --git a/termux-shared/src/main/cpp/Android.mk b/termux-shared/src/main/cpp/Android.mk index bc34d6ac..abc213c3 100644 --- a/termux-shared/src/main/cpp/Android.mk +++ b/termux-shared/src/main/cpp/Android.mk @@ -1,6 +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 +LOCAL_MODULE := local-socket +LOCAL_SRC_FILES := local-socket.cpp include $(BUILD_SHARED_LIBRARY) diff --git a/termux-shared/src/main/cpp/Application.mk b/termux-shared/src/main/cpp/Application.mk new file mode 100644 index 00000000..ce095350 --- /dev/null +++ b/termux-shared/src/main/cpp/Application.mk @@ -0,0 +1 @@ +APP_STL := c++_static diff --git a/termux-shared/src/main/cpp/local-filesystem-socket.cpp b/termux-shared/src/main/cpp/local-filesystem-socket.cpp deleted file mode 100644 index 4c5bc3e1..00000000 --- a/termux-shared/src/main/cpp/local-filesystem-socket.cpp +++ /dev/null @@ -1,197 +0,0 @@ -#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 = {}; - cred.uid = 1; // initialize uid to 1 here because I'm paranoid and a failed getsockopt that somehow doesn't report as failed would report the uid of root - 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/cpp/local-socket.cpp b/termux-shared/src/main/cpp/local-socket.cpp new file mode 100644 index 00000000..6457aca8 --- /dev/null +++ b/termux-shared/src/main/cpp/local-socket.cpp @@ -0,0 +1,603 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#define LOG_TAG "local-socket" +#define JNI_EXCEPTION "jni-exception" + +using namespace std; + + +/* Convert a jstring to a std:string. */ +string jstring_to_stdstr(JNIEnv *env, jstring jString) { + jclass stringClass = env->FindClass("java/lang/String"); + jmethodID getBytes = env->GetMethodID(stringClass, "getBytes", "()[B"); + jbyteArray jStringBytesArray = (jbyteArray) env->CallObjectMethod(jString, getBytes); + jsize length = env->GetArrayLength(jStringBytesArray); + jbyte* jStringBytes = env->GetByteArrayElements(jStringBytesArray, nullptr); + std::string stdString((char *)jStringBytes, length); + env->ReleaseByteArrayElements(jStringBytesArray, jStringBytes, JNI_ABORT); + return stdString; +} + +/* Get characters before first occurrence of the delim in a std:string. */ +string get_string_till_first_delim(string str, char delim) { + if (!str.empty()) { + stringstream cmdline_args(str); + string tmp; + if (getline(cmdline_args, tmp, delim)) + return tmp; + } + return ""; +} + +/* Replace `\0` values with spaces in a std:string. */ +string replace_null_with_space(string str) { + if (str.empty()) + return ""; + + stringstream tokens(str); + string tmp; + string str_spaced; + while (getline(tokens, tmp, '\0')){ + str_spaced.append(" " + tmp); + } + + if (!str_spaced.empty()) { + if (str_spaced.front() == ' ') + str_spaced.erase(0, 1); + } + + return str_spaced; +} + +/* Get class name of a jclazz object with a call to `Class.getName()`. */ +string get_class_name(JNIEnv *env, jclass clazz) { + jclass classClass = env->FindClass("java/lang/Class"); + jmethodID getName = env->GetMethodID(classClass, "getName", "()Ljava/lang/String;"); + jstring className = (jstring) env->CallObjectMethod(clazz, getName); + return jstring_to_stdstr(env, className); +} + + + +/* + * Get /proc/[pid]/cmdline for a process with pid. + * + * https://manpages.debian.org/testing/manpages/proc.5.en.html + */ +string get_process_cmdline(const pid_t pid) { + string cmdline; + char buf[BUFSIZ]; + size_t len; + char procfile[BUFSIZ]; + sprintf(procfile, "/proc/%d/cmdline", pid); + FILE *fp = fopen(procfile, "rb"); + if (fp) { + while ((len = fread(buf, 1, sizeof(buf), fp)) > 0) { + cmdline.append(buf, len); + } + fclose(fp); + } + + return cmdline; +} + +/* Extract process name from /proc/[pid]/cmdline value of a process. */ +string get_process_name_from_cmdline(string cmdline) { + return get_string_till_first_delim(cmdline, '\0'); +} + +/* Replace `\0` values with spaces in /proc/[pid]/cmdline value of a process. */ +string get_process_cmdline_spaced(string cmdline) { + return replace_null_with_space(cmdline); +} + + +/* Send an ERROR log message to android logcat. */ +void log_error(string message) { + __android_log_write(ANDROID_LOG_ERROR, LOG_TAG, message.c_str()); +} + +/* Send an WARN log message to android logcat. */ +void log_warn(string message) { + __android_log_write(ANDROID_LOG_WARN, LOG_TAG, message.c_str()); +} + +/* Get "title: message" formatted string. */ +string get_title_and_message(JNIEnv *env, jstring title, string message) { + if (title) + message = jstring_to_stdstr(env, title) + ": " + message; + return message; +} + + +/* Convert timespec to milliseconds. */ +int64_t timespec_to_milliseconds(const struct timespec* const time) { + return (((int64_t)time->tv_sec) * 1000) + (((int64_t)time->tv_nsec)/1000000); +} + +/* Convert milliseconds to timeval. */ +timeval milliseconds_to_timeval(int milliseconds) { + struct timeval tv = {}; + tv.tv_sec = milliseconds / 1000; + tv.tv_usec = (milliseconds % 1000) * 1000; + return tv; +} + + +// Note: Exceptions thrown from JNI must be caught with Throwable class instead of Exception, +// otherwise exception will be sent to UncaughtExceptionHandler of the thread. +// Android studio complains that getJniResult functions always return nullptr since linter is broken +// for jboolean and jobject if comparisons. +bool checkJniException(JNIEnv *env) { + if (env->ExceptionCheck()) { + jthrowable throwable = env->ExceptionOccurred(); + if (throwable != NULL) { + env->ExceptionClear(); + env->Throw(throwable); + return true; + } + } + + return false; +} + +string getJniResultString(const int retvalParam, const int errnoParam, + string errmsgParam, const int intDataParam) { + return "retval=" + to_string(retvalParam) + ", errno=" + to_string(errnoParam) + + ", errmsg=\"" + errmsgParam + "\"" + ", intData=" + to_string(intDataParam); +} + +/* Get "com/termux/shared/jni/models/JniResult" object that can be returned as result for a JNI call. */ +jobject getJniResult(JNIEnv *env, jstring title, const int retvalParam, const int errnoParam, + string errmsgParam, const int intDataParam) { + jclass clazz = env->FindClass("com/termux/shared/jni/models/JniResult"); + if (checkJniException(env)) return NULL; + if (!clazz) { + log_error(get_title_and_message(env, title, + "Failed to find JniResult class to create object for " + + getJniResultString(retvalParam, errnoParam, errmsgParam, intDataParam))); + return NULL; + } + + jmethodID constructor = env->GetMethodID(clazz, "", "(IILjava/lang/String;I)V"); + if (checkJniException(env)) return NULL; + if (!constructor) { + log_error(get_title_and_message(env, title, + "Failed to get constructor for JniResult class to create object for " + + getJniResultString(retvalParam, errnoParam, errmsgParam, intDataParam))); + return NULL; + } + + if (!errmsgParam.empty()) + errmsgParam = get_title_and_message(env, title, string(errmsgParam)); + + jobject obj = env->NewObject(clazz, constructor, retvalParam, errnoParam, env->NewStringUTF(errmsgParam.c_str()), intDataParam); + if (checkJniException(env)) return NULL; + if (obj == NULL) { + log_error(get_title_and_message(env, title, + "Failed to get JniResult object for " + + getJniResultString(retvalParam, errnoParam, errmsgParam, intDataParam))); + return NULL; + } + + return obj; +} + + +jobject getJniResult(JNIEnv *env, jstring title, const int retvalParam, const int errnoParam) { + return getJniResult(env, title, retvalParam, errnoParam, strerror(errnoParam), 0); +} + +jobject getJniResult(JNIEnv *env, jstring title, const int retvalParam, string errmsgPrefixParam) { + return getJniResult(env, title, retvalParam, 0, errmsgPrefixParam, 0); +} + +jobject getJniResult(JNIEnv *env, jstring title, const int retvalParam, const int errnoParam, string errmsgPrefixParam) { + return getJniResult(env, title, retvalParam, errnoParam, errmsgPrefixParam + ": " + string(strerror(errnoParam)), 0); +} + +jobject getJniResult(JNIEnv *env, jstring title, const int intDataParam) { + return getJniResult(env, title, 0, 0, "", intDataParam); +} + +jobject getJniResult(JNIEnv *env, jstring title) { + return getJniResult(env, title, 0, 0, "", 0); +} + + +/* Set int fieldName field for clazz to value. */ +string setIntField(JNIEnv *env, jobject obj, jclass clazz, const string fieldName, const int value) { + jfieldID field = env->GetFieldID(clazz, fieldName.c_str(), "I"); + if (checkJniException(env)) return JNI_EXCEPTION; + if (!field) { + return "Failed to get int \"" + string(fieldName) + "\" field of \"" + + get_class_name(env, clazz) + "\" class to set value \"" + to_string(value) + "\""; + } + + env->SetIntField(obj, field, value); + if (checkJniException(env)) return JNI_EXCEPTION; + + return ""; +} + +/* Set String fieldName field for clazz to value. */ +string setStringField(JNIEnv *env, jobject obj, jclass clazz, const string fieldName, const string value) { + jfieldID field = env->GetFieldID(clazz, fieldName.c_str(), "Ljava/lang/String;"); + if (checkJniException(env)) return JNI_EXCEPTION; + if (!field) { + return "Failed to get String \"" + string(fieldName) + "\" field of \"" + + get_class_name(env, clazz) + "\" class to set value \"" + value + "\""; + } + + env->SetObjectField(obj, field, env->NewStringUTF(value.c_str())); + if (checkJniException(env)) return JNI_EXCEPTION; + + return ""; +} + + + +extern "C" +JNIEXPORT jobject JNICALL +Java_com_termux_shared_net_socket_local_LocalSocketManager_createServerSocketNative(JNIEnv *env, jclass clazz, + jstring logTitle, + jbyteArray pathArray, + jint backlog) { + if (backlog < 1 || backlog > 500) { + return getJniResult(env, logTitle, -1, "createServerSocketNative(): Backlog \"" + + to_string(backlog) + "\" is not between 1-500"); + } + + // Create server socket + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) { + return getJniResult(env, logTitle, -1, errno, "createServerSocketNative(): Create local socket failed"); + } + + jbyte* path = env->GetByteArrayElements(pathArray, nullptr); + if (checkJniException(env)) return NULL; + if (path == nullptr) { + close(fd); + return getJniResult(env, logTitle, -1, "createServerSocketNative(): Path passed is null"); + } + + // On Linux, sun_path is 108 bytes (UNIX_PATH_MAX) in size + int chars = env->GetArrayLength(pathArray); + if (checkJniException(env)) return NULL; + if (chars >= 108 || chars >= sizeof(struct sockaddr_un) - sizeof(sa_family_t)) { + env->ReleaseByteArrayElements(pathArray, path, JNI_ABORT); + if (checkJniException(env)) return NULL; + close(fd); + return getJniResult(env, logTitle, -1, "createServerSocketNative(): Path passed is too long"); + } + + struct sockaddr_un adr = {.sun_family = AF_UNIX}; + memcpy(&adr.sun_path, path, chars); + + // Bind path to server socket + if (::bind(fd, reinterpret_cast(&adr), sizeof(adr)) == -1) { + int errnoBackup = errno; + env->ReleaseByteArrayElements(pathArray, path, JNI_ABORT); + if (checkJniException(env)) return NULL; + close(fd); + return getJniResult(env, logTitle, -1, errnoBackup, + "createServerSocketNative(): Bind to local socket at path \"" + string(adr.sun_path) + "\" with fd " + to_string(fd) + " failed"); + } + + // Start listening for client sockets on server socket + if (listen(fd, backlog) == -1) { + int errnoBackup = errno; + env->ReleaseByteArrayElements(pathArray, path, JNI_ABORT); + if (checkJniException(env)) return NULL; + close(fd); + return getJniResult(env, logTitle, -1, errnoBackup, + "createServerSocketNative(): Listen on local socket at path \"" + string(adr.sun_path) + "\" with fd " + to_string(fd) + " failed"); + } + + env->ReleaseByteArrayElements(pathArray, path, JNI_ABORT); + if (checkJniException(env)) return NULL; + + // Return success and server socket fd in JniResult.intData field + return getJniResult(env, logTitle, fd); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_com_termux_shared_net_socket_local_LocalSocketManager_closeSocketNative(JNIEnv *env, jclass clazz, + jstring logTitle, jint fd) { + if (fd < 0) { + return getJniResult(env, logTitle, -1, "closeSocketNative(): Invalid fd \"" + to_string(fd) + "\" passed"); + } + + if (close(fd) == -1) { + return getJniResult(env, logTitle, -1, errno, "closeSocketNative(): Failed to close socket fd " + to_string(fd)); + } + + // Return success + return getJniResult(env, logTitle); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_com_termux_shared_net_socket_local_LocalSocketManager_acceptNative(JNIEnv *env, jclass clazz, + jstring logTitle, jint fd) { + if (fd < 0) { + return getJniResult(env, logTitle, -1, "acceptNative(): Invalid fd \"" + to_string(fd) + "\" passed"); + } + + // Accept client socket + int clientFd = accept(fd, nullptr, nullptr); + if (clientFd == -1) { + return getJniResult(env, logTitle, -1, errno, "acceptNative(): Failed to accept client on fd " + to_string(fd)); + } + + // Return success and client socket fd in JniResult.intData field + return getJniResult(env, logTitle, clientFd); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_com_termux_shared_net_socket_local_LocalSocketManager_readNative(JNIEnv *env, jclass clazz, + jstring logTitle, + jint fd, jbyteArray dataArray, + jlong deadline) { + if (fd < 0) { + return getJniResult(env, logTitle, -1, "readNative(): Invalid fd \"" + to_string(fd) + "\" passed"); + } + + jbyte* data = env->GetByteArrayElements(dataArray, nullptr); + if (checkJniException(env)) return NULL; + if (data == nullptr) { + return getJniResult(env, logTitle, -1, "readNative(): data passed is null"); + } + + struct timespec time = {}; + jbyte* current = data; + int bytes = env->GetArrayLength(dataArray); + if (checkJniException(env)) return NULL; + int bytesRead = 0; + while (bytesRead < bytes) { + if (deadline > 0) { + if (clock_gettime(CLOCK_REALTIME, &time) != -1) { + // If current time is greater than the time defined in deadline + if (timespec_to_milliseconds(&time) > deadline) { + env->ReleaseByteArrayElements(dataArray, data, 0); + if (checkJniException(env)) return NULL; + return getJniResult(env, logTitle, -1, + "readNative(): Deadline \"" + to_string(deadline) + "\" timeout"); + } + } else { + log_warn(get_title_and_message(env, logTitle, + "readNative(): Deadline \"" + to_string(deadline) + + "\" timeout will not work since failed to get current time")); + } + } + + // Read data from socket + int ret = read(fd, current, bytes); + if (ret == -1) { + int errnoBackup = errno; + env->ReleaseByteArrayElements(dataArray, data, 0); + if (checkJniException(env)) return NULL; + return getJniResult(env, logTitle, -1, errnoBackup, "readNative(): Failed to read on fd " + to_string(fd)); + } + // EOF, peer closed writing end + if (ret == 0) { + break; + } + + bytesRead += ret; + current += ret; + } + + env->ReleaseByteArrayElements(dataArray, data, 0); + if (checkJniException(env)) return NULL; + + // Return success and bytes read in JniResult.intData field + return getJniResult(env, logTitle, bytesRead); +} + + +extern "C" +JNIEXPORT jobject JNICALL +Java_com_termux_shared_net_socket_local_LocalSocketManager_sendNative(JNIEnv *env, jclass clazz, + jstring logTitle, + jint fd, jbyteArray dataArray, + jlong deadline) { + if (fd < 0) { + return getJniResult(env, logTitle, -1, "sendNative(): Invalid fd \"" + to_string(fd) + "\" passed"); + } + + jbyte* data = env->GetByteArrayElements(dataArray, nullptr); + if (checkJniException(env)) return NULL; + if (data == nullptr) { + return getJniResult(env, logTitle, -1, "sendNative(): data passed is null"); + } + + struct timespec time = {}; + jbyte* current = data; + int bytes = env->GetArrayLength(dataArray); + if (checkJniException(env)) return NULL; + while (bytes > 0) { + if (deadline > 0) { + if (clock_gettime(CLOCK_REALTIME, &time) != -1) { + // If current time is greater than the time defined in deadline + if (timespec_to_milliseconds(&time) > deadline) { + env->ReleaseByteArrayElements(dataArray, data, JNI_ABORT); + if (checkJniException(env)) return NULL; + return getJniResult(env, logTitle, -1, + "sendNative(): Deadline \"" + to_string(deadline) + "\" timeout"); + } + } else { + log_warn(get_title_and_message(env, logTitle, + "sendNative(): Deadline \"" + to_string(deadline) + + "\" timeout will not work since failed to get current time")); + } + } + + // Send data to socket + int ret = send(fd, current, bytes, MSG_NOSIGNAL); + if (ret == -1) { + int errnoBackup = errno; + env->ReleaseByteArrayElements(dataArray, data, JNI_ABORT); + if (checkJniException(env)) return NULL; + return getJniResult(env, logTitle, -1, errnoBackup, "sendNative(): Failed to send on fd " + to_string(fd)); + } + + bytes -= ret; + current += ret; + } + + env->ReleaseByteArrayElements(dataArray, data, JNI_ABORT); + if (checkJniException(env)) return NULL; + + // Return success + return getJniResult(env, logTitle); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_com_termux_shared_net_socket_local_LocalSocketManager_availableNative(JNIEnv *env, jclass clazz, + jstring logTitle, jint fd) { + if (fd < 0) { + return getJniResult(env, logTitle, -1, "availableNative(): Invalid fd \"" + to_string(fd) + "\" passed"); + } + + int available = 0; + if (ioctl(fd, SIOCINQ, &available) == -1) { + return getJniResult(env, logTitle, -1, errno, + "availableNative(): Failed to get number of unread bytes in the receive buffer of fd " + to_string(fd)); + } + + // Return success and bytes available in JniResult.intData field + return getJniResult(env, logTitle, available); +} + +/* Sets socket option timeout in milliseconds. */ +int set_socket_timeout(int fd, int option, int timeout) { + struct timeval tv = milliseconds_to_timeval(timeout); + socklen_t len = sizeof(tv); + return setsockopt(fd, SOL_SOCKET, option, &tv, len); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_com_termux_shared_net_socket_local_LocalSocketManager_setSocketReadTimeoutNative(JNIEnv *env, jclass clazz, + jstring logTitle, + jint fd, jint timeout) { + if (fd < 0) { + return getJniResult(env, logTitle, -1, "setSocketReadTimeoutNative(): Invalid fd \"" + to_string(fd) + "\" passed"); + } + + if (set_socket_timeout(fd, SO_RCVTIMEO, timeout) == -1) { + return getJniResult(env, logTitle, -1, errno, + "setSocketReadTimeoutNative(): Failed to set socket receiving (SO_RCVTIMEO) timeout for fd " + to_string(fd)); + } + + // Return success + return getJniResult(env, logTitle); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_com_termux_shared_net_socket_local_LocalSocketManager_setSocketSendTimeoutNative(JNIEnv *env, jclass clazz, + jstring logTitle, + jint fd, jint timeout) { + if (fd < 0) { + return getJniResult(env, logTitle, -1, "setSocketSendTimeoutNative(): Invalid fd \"" + + to_string(fd) + "\" passed"); + } + + if (set_socket_timeout(fd, SO_SNDTIMEO, timeout) == -1) { + return getJniResult(env, logTitle, -1, errno, + "setSocketSendTimeoutNative(): Failed to set socket sending (SO_SNDTIMEO) timeout for fd " + to_string(fd)); + } + + // Return success + return getJniResult(env, logTitle); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_com_termux_shared_net_socket_local_LocalSocketManager_getPeerCredNative(JNIEnv *env, jclass clazz, + jstring logTitle, + jint fd, jobject peerCred) { + if (fd < 0) { + return getJniResult(env, logTitle, -1, "getPeerCredNative(): Invalid fd \"" + to_string(fd) + "\" passed"); + } + + if (peerCred == nullptr) { + return getJniResult(env, logTitle, -1, "getPeerCredNative(): peerCred passed is null"); + } + + // Initialize to -1 instead of 0 in case a failed getsockopt() call somehow doesn't report failure and returns the uid of root + struct ucred cred = {}; + cred.pid = -1; cred.uid = -1; cred.gid = -1; + + socklen_t len = sizeof(cred); + + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) { + return getJniResult(env, logTitle, -1, errno, "getPeerCredNative(): Failed to get peer credentials for fd " + to_string(fd)); + } + + // Fill "com.termux.shared.net.socket.local.PeerCred" object. + // The pid, uid and gid will always be set based on ucred. + // The pname and cmdline will only be set if current process has access to "/proc/[pid]/cmdline" + // of peer process. Processes of other users/apps are not normally accessible. + jclass peerCredClazz = env->GetObjectClass(peerCred); + if (checkJniException(env)) return NULL; + if (!peerCredClazz) { + return getJniResult(env, logTitle, -1, errno, "getPeerCredNative(): Failed to get PeerCred class"); + } + + string error; + + error = setIntField(env, peerCred, peerCredClazz, "pid", cred.pid); + if (!error.empty()) { + if (error == JNI_EXCEPTION) return NULL; + return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error); + } + + error = setIntField(env, peerCred, peerCredClazz, "uid", cred.uid); + if (!error.empty()) { + if (error == JNI_EXCEPTION) return NULL; + return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error); + } + + error = setIntField(env, peerCred, peerCredClazz, "gid", cred.gid); + if (!error.empty()) { + if (error == JNI_EXCEPTION) return NULL; + return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error); + } + + string cmdline = get_process_cmdline(cred.pid); + if (!cmdline.empty()) { + error = setStringField(env, peerCred, peerCredClazz, "pname", get_process_name_from_cmdline(cmdline)); + if (!error.empty()) { + if (error == JNI_EXCEPTION) return NULL; + return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error); + } + + error = setStringField(env, peerCred, peerCredClazz, "cmdline", get_process_cmdline_spaced(cmdline)); + if (!error.empty()) { + if (error == JNI_EXCEPTION) return NULL; + return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error); + } + } + + // Return success since PeerCred was filled successfully + return getJniResult(env, logTitle); +} diff --git a/termux-shared/src/main/java/com/termux/shared/jni/models/JniResult.java b/termux-shared/src/main/java/com/termux/shared/jni/models/JniResult.java new file mode 100644 index 00000000..34c38a8f --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/jni/models/JniResult.java @@ -0,0 +1,107 @@ +package com.termux.shared.jni.models; + +import androidx.annotation.NonNull; + +import com.termux.shared.logger.Logger; + +/** + * A class that can be used to return result for JNI calls with support for multiple fields to easily + * return success and error states. + * + * https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html + * https://developer.android.com/training/articles/perf-jni + */ +public class JniResult { + + /** + * The return value for the JNI call. + * This should be 0 for success. + */ + public int retval; + + /** + * The errno value for any failed native system or library calls if {@link #retval} does not equal 0. + * This should be 0 if no errno was set. + * + * https://manpages.debian.org/testing/manpages-dev/errno.3.en.html + */ + public int errno; + + /** + * The error message for the failure if {@link #retval} does not equal 0. + * The message will contain errno message returned by strerror() if errno was set. + * + * https://manpages.debian.org/testing/manpages-dev/strerror.3.en.html + */ + public String errmsg; + + /** + * Optional additional int data that needs to be returned by JNI call, like bytes read on success. + */ + public int intData; + + /** + * Create an new instance of {@link JniResult}. + * + * @param retval The {@link #retval} value. + * @param errno The {@link #errno} value. + * @param errmsg The {@link #errmsg} value. + */ + public JniResult(int retval, int errno, String errmsg) { + this.retval = retval; + this.errno = errno; + this.errmsg = errmsg; + } + + /** + * Create an new instance of {@link JniResult}. + * + * @param retval The {@link #retval} value. + * @param errno The {@link #errno} value. + * @param errmsg The {@link #errmsg} value. + * @param intData The {@link #intData} value. + */ + public JniResult(int retval, int errno, String errmsg, int intData) { + this(retval, errno, errmsg); + this.intData = intData; + } + + /** + * Create an new instance of {@link JniResult} from a {@link Throwable} with {@link #retval} -1. + * + * @param message The error message. + * @param throwable The {@link Throwable} value. + */ + public JniResult(String message, Throwable throwable) { + this(-1, 0, Logger.getMessageAndStackTraceString(message, throwable)); + } + + /** + * Get error {@link String} for {@link JniResult}. + * + * @param result The {@link JniResult} to get error from. + * @return Returns the error {@link String}. + */ + @NonNull + public static String getErrorString(final JniResult result) { + if (result == null) return "null"; + return result.getErrorString(); + } + + /** Get error {@link String} for {@link JniResult}. */ + @NonNull + public String getErrorString() { + StringBuilder logString = new StringBuilder(); + + logString.append(Logger.getSingleLineLogStringEntry("Retval", retval, "-")); + + if (errno != 0) + logString.append("\n").append(Logger.getSingleLineLogStringEntry("Errno", errno, "-")); + + if (errmsg != null && !errmsg.isEmpty()) + logString.append("\n").append(Logger.getMultiLineLogStringEntry("Errmsg", errmsg, "-")); + + return logString.toString(); + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/ILocalSocketManager.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/ILocalSocketManager.java new file mode 100644 index 00000000..a520254e --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/ILocalSocketManager.java @@ -0,0 +1,72 @@ +package com.termux.shared.net.socket.local; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.termux.shared.errors.Error; + +/** + * The interface for the {@link LocalSocketManager} for callbacks to manager client/server starter. + */ +public interface ILocalSocketManager { + + /** + * This should return the {@link Thread.UncaughtExceptionHandler} that should be used for the + * client socket listener and client logic runner threads started for other interface methods. + * + * @param localSocketManager The {@link LocalSocketManager} for the server. + * @return Should return {@link Thread.UncaughtExceptionHandler} or {@code null}, if default + * handler should be used which just logs the exception. + */ + @Nullable + Thread.UncaughtExceptionHandler getLocalSocketManagerClientThreadUEH( + @NonNull LocalSocketManager localSocketManager); + + /** + * This is called if any error is raised by {@link LocalSocketManager}, {@link LocalServerSocket} + * or {@link LocalClientSocket}. The server will automatically close the client socket + * with a call to {@link LocalClientSocket#closeClientSocket(boolean)} if the error occurred due + * to the client. + * + * The {@link LocalClientSocket#getPeerCred()} can be used to get the {@link PeerCred} object + * containing info for the connected client/peer. + * + * @param localSocketManager The {@link LocalSocketManager} for the server. + * @param clientSocket The {@link LocalClientSocket} that connected. This will be {@code null} + * if error is not for a {@link LocalClientSocket}. + * @param error The {@link Error} auto generated that can be used for logging purposes. + */ + void onError(@NonNull LocalSocketManager localSocketManager, + @Nullable LocalClientSocket clientSocket, @NonNull Error error); + + /** + * This is called if a {@link LocalServerSocket} connects to the server which **does not** have + * the server app's user id or root user id. The server will automatically close the client socket + * with a call to {@link LocalClientSocket#closeClientSocket(boolean)}. + * + * The {@link LocalClientSocket#getPeerCred()} can be used to get the {@link PeerCred} object + * containing info for the connected client/peer. + * + * @param localSocketManager The {@link LocalSocketManager} for the server. + * @param clientSocket The {@link LocalClientSocket} that connected. + * @param error The {@link Error} auto generated that can be used for logging purposes. + */ + void onDisallowedClientConnected(@NonNull LocalSocketManager localSocketManager, + @NonNull LocalClientSocket clientSocket, @NonNull Error error); + + /** + * This is called if a {@link LocalServerSocket} connects to the server which has the + * the server app's user id or root user id. It is the responsibility of the interface + * implementation to close the client socket with a call to + * {@link LocalClientSocket#closeClientSocket(boolean)} once its done processing. + * + * The {@link LocalClientSocket#getPeerCred()} can be used to get the {@link PeerCred} object + * containing info for the connected client/peer. + * + * @param localSocketManager The {@link LocalSocketManager} for the server. + * @param clientSocket The {@link LocalClientSocket} that connected. + */ + void onClientAccepted(@NonNull LocalSocketManager localSocketManager, + @NonNull LocalClientSocket clientSocket); + +} diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalClientSocket.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalClientSocket.java new file mode 100644 index 00000000..75a7e6a8 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalClientSocket.java @@ -0,0 +1,483 @@ +package com.termux.shared.net.socket.local; + +import androidx.annotation.NonNull; + +import com.termux.shared.data.DataUtils; +import com.termux.shared.errors.Error; +import com.termux.shared.jni.models.JniResult; +import com.termux.shared.logger.Logger; +import com.termux.shared.markdown.MarkdownUtils; + +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +/** The client socket for {@link LocalSocketManager}. */ +public class LocalClientSocket implements Closeable { + + public static final String LOG_TAG = "LocalClientSocket"; + + /** The {@link LocalSocketManager} instance for the local socket. */ + @NonNull protected final LocalSocketManager mLocalSocketManager; + + /** The {@link LocalSocketRunConfig} containing run config for the {@link LocalClientSocket}. */ + @NonNull protected final LocalSocketRunConfig mLocalSocketRunConfig; + + /** + * The {@link LocalClientSocket} file descriptor. + * Value will be `>= 0` if socket has been connected and `-1` if closed. + */ + protected int mFD; + + /** The creation time of {@link LocalClientSocket}. This is also used for deadline. */ + protected final long mCreationTime; + + /** The {@link PeerCred} of the {@link LocalClientSocket} containing info of client/peer. */ + @NonNull protected final PeerCred mPeerCred; + + /** The {@link OutputStream} implementation for the {@link LocalClientSocket}. */ + @NonNull protected final SocketOutputStream mOutputStream; + + /** The {@link InputStream} implementation for the {@link LocalClientSocket}. */ + @NonNull protected final SocketInputStream mInputStream; + + /** + * Create an new instance of {@link LocalClientSocket}. + * + * @param localSocketManager The {@link #mLocalSocketManager} value. + * @param fd The {@link #mFD} value. + * @param peerCred The {@link #mPeerCred} value. + */ + LocalClientSocket(@NonNull LocalSocketManager localSocketManager, int fd, @NonNull PeerCred peerCred) { + mLocalSocketManager = localSocketManager; + mLocalSocketRunConfig = localSocketManager.getLocalSocketRunConfig(); + mCreationTime = System.currentTimeMillis(); + mOutputStream = new SocketOutputStream(); + mInputStream = new SocketInputStream(); + mPeerCred = peerCred; + + setFD(fd); + mPeerCred.fillPeerCred(localSocketManager.getContext()); + } + + + /** Close client socket. */ + public synchronized Error closeClientSocket(boolean logErrorMessage) { + try { + close(); + } catch (IOException e) { + Error error = LocalSocketErrno.ERRNO_CLOSE_CLIENT_SOCKET_FAILED_WITH_EXCEPTION.getError(e, mLocalSocketRunConfig.getTitle(), e.getMessage()); + if (logErrorMessage) + Logger.logErrorExtended(LOG_TAG, error.getErrorLogString()); + return error; + } + + return null; + } + + /** Close client socket that exists at fd. */ + public static void closeClientSocket(@NonNull LocalSocketManager localSocketManager, int fd) { + new LocalClientSocket(localSocketManager, fd, new PeerCred()).closeClientSocket(true); + } + + /** Implementation for {@link Closeable#close()} to close client socket. */ + @Override + public void close() throws IOException { + if (mFD >= 0) { + Logger.logVerbose(LOG_TAG, "Client socket close for \"" + mLocalSocketRunConfig.getTitle() + "\" server: " + getPeerCred().getMinimalString()); + JniResult result = LocalSocketManager.closeSocket(mLocalSocketRunConfig.getLogTitle() + " (client)", mFD); + if (result == null || result.retval != 0) { + throw new IOException(JniResult.getErrorString(result)); + } + // Update fd to signify that client socket has been closed + setFD(-1); + } + } + + + /** + * Attempts to read up to data buffer length bytes from file descriptor into the data buffer. + * On success, the number of bytes read is returned (zero indicates end of file) in bytesRead. + * It is not an error if bytesRead is smaller than the number of bytes requested; this may happen + * for example because fewer bytes are actually available right now (maybe because we were close + * to end-of-file, or because we are reading from a pipe), or because read() was interrupted by + * a signal. + * + * If while reading the {@link #mCreationTime} + the milliseconds returned by + * {@link LocalSocketRunConfig#getDeadline()} elapses but all the data has not been read, an + * error would be returned. + * + * This is a wrapper for {@link LocalSocketManager#read(String, int, byte[], long)}, which can + * be called instead if you want to get access to errno int value instead of {@link JniResult} + * error {@link String}. + * + * @param data The data buffer to read bytes into. + * @param bytesRead The actual bytes read. + * @return Returns the {@code error} if reading was not successful containing {@link JniResult} + * error {@link String}, otherwise {@code null}. + */ + public Error read(@NonNull byte[] data, MutableInt bytesRead) { + bytesRead.value = 0; + + if (mFD < 0) { + return LocalSocketErrno.ERRNO_USING_CLIENT_SOCKET_WITH_INVALID_FD.getError(mFD, + mLocalSocketRunConfig.getTitle()); + } + + JniResult result = LocalSocketManager.read(mLocalSocketRunConfig.getLogTitle() + " (client)", + mFD, data, + mLocalSocketRunConfig.getDeadline() > 0 ? mCreationTime + mLocalSocketRunConfig.getDeadline() : 0); + if (result == null || result.retval != 0) { + return LocalSocketErrno.ERRNO_READ_DATA_FROM_CLIENT_SOCKET_FAILED.getError( + mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result)); + } + + bytesRead.value = result.intData; + return null; + } + + /** + * Attempts to send data buffer to the file descriptor. + * + * If while sending the {@link #mCreationTime} + the milliseconds returned by + * {@link LocalSocketRunConfig#getDeadline()} elapses but all the data has not been sent, an + * error would be returned. + * + * This is a wrapper for {@link LocalSocketManager#send(String, int, byte[], long)}, which can + * be called instead if you want to get access to errno int value instead of {@link JniResult} + * error {@link String}. + * + * @param data The data buffer containing bytes to send. + * @return Returns the {@code error} if sending was not successful containing {@link JniResult} + * error {@link String}, otherwise {@code null}. + */ + public Error send(@NonNull byte[] data) { + if (mFD < 0) { + return LocalSocketErrno.ERRNO_USING_CLIENT_SOCKET_WITH_INVALID_FD.getError(mFD, + mLocalSocketRunConfig.getTitle()); + } + + JniResult result = LocalSocketManager.send(mLocalSocketRunConfig.getLogTitle() + " (client)", + mFD, data, + mLocalSocketRunConfig.getDeadline() > 0 ? mCreationTime + mLocalSocketRunConfig.getDeadline() : 0); + if (result == null || result.retval != 0) { + return LocalSocketErrno.ERRNO_SEND_DATA_TO_CLIENT_SOCKET_FAILED.getError( + mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result)); + } + + return null; + } + + /** + * Attempts to read all the bytes available on {@link SocketInputStream} and appends them to + * {@code data} {@link StringBuilder}. + * + * This is a wrapper for {@link #read(byte[], MutableInt)} called via {@link SocketInputStream#read()}. + * + * @param data The data {@link StringBuilder} to append the bytes read into. + * @param closeStreamOnFinish If set to {@code true}, then underlying input stream will closed + * and further attempts to read from socket will fail. + * @return Returns the {@code error} if reading was not successful containing {@link JniResult} + * error {@link String}, otherwise {@code null}. + */ + public Error readDataOnInputStream(@NonNull StringBuilder data, boolean closeStreamOnFinish) { + int c; + InputStreamReader inputStreamReader = getInputStreamReader(); + try { + while ((c = inputStreamReader.read()) > 0) { + data.append((char) c); + } + } catch (IOException e) { + // The SocketInputStream.read() throws the Error message in an IOException, + // so just read the exception message and not the stack trace, otherwise it would result + // in a messy nested error message. + return LocalSocketErrno.ERRNO_READ_DATA_FROM_INPUT_STREAM_OF_CLIENT_SOCKET_FAILED_WITH_EXCEPTION.getError( + mLocalSocketRunConfig.getTitle(), DataUtils.getSpaceIndentedString(e.getMessage(), 1)); + } catch (Exception e) { + return LocalSocketErrno.ERRNO_READ_DATA_FROM_INPUT_STREAM_OF_CLIENT_SOCKET_FAILED_WITH_EXCEPTION.getError( + e, mLocalSocketRunConfig.getTitle(), e.getMessage()); + } finally { + if (closeStreamOnFinish) { + try { inputStreamReader.close(); + } catch (IOException e) { + // Ignore + } + } + } + + return null; + } + + /** + * Attempts to send all the bytes passed to {@link SocketOutputStream} . + * + * This is a wrapper for {@link #send(byte[])} called via {@link SocketOutputStream#write(int)}. + * + * @param data The {@link String} bytes to send. + * @param closeStreamOnFinish If set to {@code true}, then underlying output stream will closed + * and further attempts to send to socket will fail. + * @return Returns the {@code error} if sending was not successful containing {@link JniResult} + * error {@link String}, otherwise {@code null}. + */ + public Error sendDataToOutputStream(@NonNull String data, boolean closeStreamOnFinish) { + + OutputStreamWriter outputStreamWriter = getOutputStreamWriter(); + + try (BufferedWriter byteStreamWriter = new BufferedWriter(outputStreamWriter)) { + byteStreamWriter.write(data); + byteStreamWriter.flush(); + } catch (IOException e) { + // The SocketOutputStream.write() throws the Error message in an IOException, + // so just read the exception message and not the stack trace, otherwise it would result + // in a messy nested error message. + return LocalSocketErrno.ERRNO_SEND_DATA_TO_OUTPUT_STREAM_OF_CLIENT_SOCKET_FAILED_WITH_EXCEPTION.getError( + mLocalSocketRunConfig.getTitle(), DataUtils.getSpaceIndentedString(e.getMessage(), 1)); + } catch (Exception e) { + return LocalSocketErrno.ERRNO_SEND_DATA_TO_OUTPUT_STREAM_OF_CLIENT_SOCKET_FAILED_WITH_EXCEPTION.getError( + e, mLocalSocketRunConfig.getTitle(), e.getMessage()); + } finally { + if (closeStreamOnFinish) { + try { + outputStreamWriter.close(); + } catch (IOException e) { + // Ignore + } + } + } + + return null; + } + + /** Wrapper for {@link #available(MutableInt, boolean)} that checks deadline. The + * {@link SocketInputStream} calls this. */ + public Error available(MutableInt available) { + return available(available, true); + } + + /** + * Get available bytes on {@link #mInputStream} and optionally check if value returned by + * {@link LocalSocketRunConfig#getDeadline()} has passed. + */ + public Error available(MutableInt available, boolean checkDeadline) { + available.value = 0; + + if (mFD < 0) { + return LocalSocketErrno.ERRNO_USING_CLIENT_SOCKET_WITH_INVALID_FD.getError(mFD, + mLocalSocketRunConfig.getTitle()); + } + + if (checkDeadline && mLocalSocketRunConfig.getDeadline() > 0 && System.currentTimeMillis() > (mCreationTime + mLocalSocketRunConfig.getDeadline())) { + return null; + } + + JniResult result = LocalSocketManager.available(mLocalSocketRunConfig.getLogTitle() + " (client)", mLocalSocketRunConfig.getFD()); + if (result == null || result.retval != 0) { + return LocalSocketErrno.ERRNO_CHECK_AVAILABLE_DATA_ON_CLIENT_SOCKET_FAILED.getError( + mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result)); + } + + available.value = result.intData; + return null; + } + + + + /** Set {@link LocalClientSocket} receiving (SO_RCVTIMEO) timeout to value returned by {@link LocalSocketRunConfig#getReceiveTimeout()}. */ + public Error setReadTimeout() { + if (mFD >= 0) { + JniResult result = LocalSocketManager.setSocketReadTimeout(mLocalSocketRunConfig.getLogTitle() + " (client)", + mFD, mLocalSocketRunConfig.getReceiveTimeout()); + if (result == null || result.retval != 0) { + return LocalSocketErrno.ERRNO_SET_CLIENT_SOCKET_READ_TIMEOUT_FAILED.getError( + mLocalSocketRunConfig.getTitle(), mLocalSocketRunConfig.getReceiveTimeout(), JniResult.getErrorString(result)); + } + } + return null; + } + + /** Set {@link LocalClientSocket} sending (SO_SNDTIMEO) timeout to value returned by {@link LocalSocketRunConfig#getSendTimeout()}. */ + public Error setWriteTimeout() { + if (mFD >= 0) { + JniResult result = LocalSocketManager.setSocketSendTimeout(mLocalSocketRunConfig.getLogTitle() + " (client)", + mFD, mLocalSocketRunConfig.getSendTimeout()); + if (result == null || result.retval != 0) { + return LocalSocketErrno.ERRNO_SET_CLIENT_SOCKET_SEND_TIMEOUT_FAILED.getError( + mLocalSocketRunConfig.getTitle(), mLocalSocketRunConfig.getSendTimeout(), JniResult.getErrorString(result)); + } + } + return null; + } + + + + /** Get {@link #mFD} for the client socket. */ + public int getFD() { + return mFD; + } + + /** Set {@link #mFD}. Value must be greater than 0 or -1. */ + private void setFD(int fd) { + if (fd >= 0) + mFD = fd; + else + mFD = -1; + } + + /** Get {@link #mPeerCred} for the client socket. */ + public PeerCred getPeerCred() { + return mPeerCred; + } + + /** Get {@link #mCreationTime} for the client socket. */ + public long getCreationTime() { + return mCreationTime; + } + + /** Get {@link #mOutputStream} for the client socket. The stream will automatically close when client socket is closed. */ + public OutputStream getOutputStream() { + return mOutputStream; + } + + /** Get {@link OutputStreamWriter} for {@link #mOutputStream} for the client socket. The stream will automatically close when client socket is closed. */ + @NonNull + public OutputStreamWriter getOutputStreamWriter() { + return new OutputStreamWriter(getOutputStream()); + } + + /** Get {@link #mInputStream} for the client socket. The stream will automatically close when client socket is closed. */ + public InputStream getInputStream() { + return mInputStream; + } + + /** Get {@link InputStreamReader} for {@link #mInputStream} for the client socket. The stream will automatically close when client socket is closed. */ + @NonNull + public InputStreamReader getInputStreamReader() { + return new InputStreamReader(getInputStream()); + } + + + + /** Get a log {@link String} for the {@link LocalClientSocket}. */ + @NonNull + public String getLogString() { + StringBuilder logString = new StringBuilder(); + + logString.append("Client Socket:"); + logString.append("\n").append(Logger.getSingleLineLogStringEntry("FD", mFD, "-")); + logString.append("\n").append(Logger.getSingleLineLogStringEntry("Creation Time", mCreationTime, "-")); + logString.append("\n\n\n"); + + logString.append(mPeerCred.getLogString()); + + return logString.toString(); + } + + /** Get a markdown {@link String} for the {@link LocalClientSocket}. */ + @NonNull + public String getMarkdownString() { + StringBuilder markdownString = new StringBuilder(); + + markdownString.append("## ").append("Client Socket"); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("FD", mFD, "-")); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Creation Time", mCreationTime, "-")); + markdownString.append("\n\n\n"); + + markdownString.append(mPeerCred.getMarkdownString()); + + return markdownString.toString(); + } + + + + + + /** Wrapper class to allow pass by reference of int values. */ + public static final class MutableInt { + public int value; + + public MutableInt(int value) { + this.value = value; + } + } + + + + /** The {@link InputStream} implementation for the {@link LocalClientSocket}. */ + protected class SocketInputStream extends InputStream { + private final byte[] mBytes = new byte[1]; + + @Override + public int read() throws IOException { + MutableInt bytesRead = new MutableInt(0); + Error error = LocalClientSocket.this.read(mBytes, bytesRead); + if (error != null) { + throw new IOException(error.getErrorMarkdownString()); + } + + if (bytesRead.value == 0) { + return -1; + } + + return mBytes[0]; + } + + @Override + public int read(byte[] bytes) throws IOException { + if (bytes == null) { + throw new NullPointerException("Read buffer can't be null"); + } + + MutableInt bytesRead = new MutableInt(0); + Error error = LocalClientSocket.this.read(bytes, bytesRead); + if (error != null) { + throw new IOException(error.getErrorMarkdownString()); + } + + if (bytesRead.value == 0) { + return -1; + } + + return bytesRead.value; + } + + @Override + public int available() throws IOException { + MutableInt available = new MutableInt(0); + Error error = LocalClientSocket.this.available(available); + if (error != null) { + throw new IOException(error.getErrorMarkdownString()); + } + return available.value; + } + } + + + + /** The {@link OutputStream} implementation for the {@link LocalClientSocket}. */ + protected class SocketOutputStream extends OutputStream { + private final byte[] mBytes = new byte[1]; + + @Override + public void write(int b) throws IOException { + mBytes[0] = (byte) b; + + Error error = LocalClientSocket.this.send(mBytes); + if (error != null) { + throw new IOException(error.getErrorMarkdownString()); + } + } + + @Override + public void write(byte[] bytes) throws IOException { + Error error = LocalClientSocket.this.send(bytes); + if (error != null) { + throw new IOException(error.getErrorMarkdownString()); + } + } + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java new file mode 100644 index 00000000..385c2034 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java @@ -0,0 +1,303 @@ +package com.termux.shared.net.socket.local; + +import androidx.annotation.NonNull; + +import com.termux.shared.errors.Error; +import com.termux.shared.file.FileUtils; +import com.termux.shared.jni.models.JniResult; +import com.termux.shared.logger.Logger; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** The server socket for {@link LocalSocketManager}. */ +public class LocalServerSocket implements Closeable { + + public static final String LOG_TAG = "LocalServerSocket"; + + /** The {@link LocalSocketManager} instance for the local socket. */ + @NonNull protected final LocalSocketManager mLocalSocketManager; + + /** The {@link LocalSocketRunConfig} containing run config for the {@link LocalServerSocket}. */ + @NonNull protected final LocalSocketRunConfig mLocalSocketRunConfig; + + /** The {@link ILocalSocketManager} client for the {@link LocalSocketManager}. */ + @NonNull protected final ILocalSocketManager mLocalSocketManagerClient; + + /** The {@link ClientSocketListener} {@link Thread} for the {@link LocalServerSocket}. */ + @NonNull protected final Thread mClientSocketListener; + + /** + * The required permissions for server socket file parent directory. + * Creation of a new socket will fail if the server starter app process does not have + * write and search (execute) permission on the directory in which the socket is created. + */ + public static final String SERVER_SOCKET_PARENT_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx" + + /** + * Create an new instance of {@link LocalServerSocket}. + * + * @param localSocketManager The {@link #mLocalSocketManager} value. + */ + protected LocalServerSocket(@NonNull LocalSocketManager localSocketManager) { + mLocalSocketManager = localSocketManager; + mLocalSocketRunConfig = localSocketManager.getLocalSocketRunConfig(); + mLocalSocketManagerClient = mLocalSocketRunConfig.getLocalSocketManagerClient(); + mClientSocketListener = new Thread(new ClientSocketListener()); + } + + /** Start server by creating server socket. */ + public synchronized Error start() { + Logger.logDebug(LOG_TAG, "start"); + + String path = mLocalSocketRunConfig.getPath(); + if (path == null || path.isEmpty()) { + return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_NULL_OR_EMPTY.getError(mLocalSocketRunConfig.getTitle()); + } + if (!mLocalSocketRunConfig.isAbstractNamespaceSocket()) { + path = FileUtils.getCanonicalPath(path, null); + } + + // On Linux, sun_path is 108 bytes (UNIX_PATH_MAX) in size, so do an early check here to + // prevent useless parent directory creation since createServerSocket() call will fail since + // there is a native check as well. + if (path.getBytes(StandardCharsets.UTF_8).length > 108) { + return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_TOO_LONG.getError(mLocalSocketRunConfig.getTitle(), path); + } + + int backlog = mLocalSocketRunConfig.getBacklog(); + if (backlog <= 0) { + return LocalSocketErrno.ERRNO_SERVER_SOCKET_BACKLOG_INVALID.getError(mLocalSocketRunConfig.getTitle(), backlog); + } + + Error error; + + // If server socket is not in abstract namespace + if (!mLocalSocketRunConfig.isAbstractNamespaceSocket()) { + if (!path.startsWith("/")) + return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_NOT_ABSOLUTE.getError(mLocalSocketRunConfig.getTitle(), path); + + // Create the server socket file parent directory and set SERVER_SOCKET_PARENT_DIRECTORY_PERMISSIONS if missing + String socketParentPath = new File(path).getParent(); + error = FileUtils.validateDirectoryFileExistenceAndPermissions(mLocalSocketRunConfig.getTitle() + " server socket file parent", + socketParentPath, + null, true, + SERVER_SOCKET_PARENT_DIRECTORY_PERMISSIONS, true, true, + false, false); + if (error != null) + return error; + + + // Delete the server socket file to stop any existing servers and for bind() to succeed + error = deleteServerSocketFile(); + if (error != null) + return error; + } + + // Create the server socket + JniResult result = LocalSocketManager.createServerSocket(mLocalSocketRunConfig.getLogTitle() + " (server)", + path.getBytes(StandardCharsets.UTF_8), backlog); + if (result == null || result.retval != 0) { + return LocalSocketErrno.ERRNO_CREATE_SERVER_SOCKET_FAILED.getError(mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result)); + } + + int fd = result.intData; + if (fd < 0) { + return LocalSocketErrno.ERRNO_SERVER_SOCKET_FD_INVALID.getError(fd, mLocalSocketRunConfig.getTitle()); + } + + // Update fd to signify that server socket has been created successfully + mLocalSocketRunConfig.setFD(fd); + + mClientSocketListener.setUncaughtExceptionHandler(mLocalSocketManager.getLocalSocketManagerClientThreadUEH()); + + try { + // Start listening to server clients + mClientSocketListener.start(); + } catch (Exception e) { + Logger.logStackTraceWithMessage(LOG_TAG, "mClientSocketListener start failed", e); + } + + return null; + } + + /** Stop server. */ + public synchronized Error stop() { + Logger.logDebug(LOG_TAG, "stop"); + + try { + // Stop the LocalClientSocket listener. + mClientSocketListener.interrupt(); + } catch (Exception ignored) {} + + Error error = closeServerSocket(false); + if (error != null) + return error; + + return deleteServerSocketFile(); + } + + /** Close server socket. */ + public synchronized Error closeServerSocket(boolean logErrorMessage) { + Logger.logDebug(LOG_TAG, "closeServerSocket"); + + try { + close(); + } catch (IOException e) { + Error error = LocalSocketErrno.ERRNO_CLOSE_SERVER_SOCKET_FAILED_WITH_EXCEPTION.getError(e, mLocalSocketRunConfig.getTitle(), e.getMessage()); + if (logErrorMessage) + Logger.logErrorExtended(LOG_TAG, error.getErrorLogString()); + return error; + } + + return null; + } + + /** Implementation for {@link Closeable#close()} to close server socket. */ + @Override + public synchronized void close() throws IOException { + Logger.logDebug(LOG_TAG, "close"); + + int fd = mLocalSocketRunConfig.getFD(); + + if (fd >= 0) { + JniResult result = LocalSocketManager.closeSocket(mLocalSocketRunConfig.getLogTitle() + " (server)", fd); + if (result == null || result.retval != 0) { + throw new IOException(JniResult.getErrorString(result)); + } + // Update fd to signify that server socket has been closed + mLocalSocketRunConfig.setFD(-1); + } + } + + /** + * Delete server socket file if not an abstract namespace socket. This will cause any existing + * running server to stop. + */ + private Error deleteServerSocketFile() { + if (!mLocalSocketRunConfig.isAbstractNamespaceSocket()) + return FileUtils.deleteSocketFile(mLocalSocketRunConfig.getTitle() + " server socket file", mLocalSocketRunConfig.getPath(), true); + else + return null; + } + + /** Listen and accept new {@link LocalClientSocket}. */ + public LocalClientSocket accept() { + Logger.logVerbose(LOG_TAG, "accept"); + + int clientFD; + while (true) { + // If server socket closed + int fd = mLocalSocketRunConfig.getFD(); + if (fd < 0) { + return null; + } + + JniResult result = LocalSocketManager.accept(mLocalSocketRunConfig.getLogTitle() + " (client)", fd); + if (result == null || result.retval != 0) { + mLocalSocketManager.onError( + LocalSocketErrno.ERRNO_ACCEPT_CLIENT_SOCKET_FAILED.getError(mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result))); + continue; + } + + clientFD = result.intData; + if (clientFD < 0) { + mLocalSocketManager.onError( + LocalSocketErrno.ERRNO_CLIENT_SOCKET_FD_INVALID.getError(clientFD, mLocalSocketRunConfig.getTitle())); + continue; + } + + PeerCred peerCred = new PeerCred(); + result = LocalSocketManager.getPeerCred(mLocalSocketRunConfig.getLogTitle() + " (client)", clientFD, peerCred); + if (result == null || result.retval != 0) { + mLocalSocketManager.onError( + LocalSocketErrno.ERRNO_GET_CLIENT_SOCKET_PEER_UID_FAILED.getError(mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result))); + LocalClientSocket.closeClientSocket(mLocalSocketManager, clientFD); + continue; + } + + int peerUid = peerCred.uid; + if (peerUid < 0) { + mLocalSocketManager.onError( + LocalSocketErrno.ERRNO_CLIENT_SOCKET_PEER_UID_INVALID.getError(peerUid, mLocalSocketRunConfig.getTitle())); + LocalClientSocket.closeClientSocket(mLocalSocketManager, clientFD); + continue; + } + + LocalClientSocket clientSocket = new LocalClientSocket(mLocalSocketManager, clientFD, peerCred); + Logger.logVerbose(LOG_TAG, "Client socket accept for \"" + mLocalSocketRunConfig.getTitle() + "\" server\n" + clientSocket.getLogString()); + + // Only allow connection if the peer has the same uid as server app's user id or root user id + if (peerUid != mLocalSocketManager.getContext().getApplicationInfo().uid && peerUid != 0) { + mLocalSocketManager.onDisallowedClientConnected(clientSocket, + LocalSocketErrno.ERRNO_CLIENT_SOCKET_PEER_UID_DISALLOWED.getError(clientSocket.getPeerCred().getMinimalString(), + mLocalSocketManager.getLocalSocketRunConfig().getTitle())); + clientSocket.closeClientSocket(true); + continue; + } + + return clientSocket; + } + } + + + + + /** The {@link LocalClientSocket} listener {@link java.lang.Runnable} for {@link LocalServerSocket}. */ + protected class ClientSocketListener implements Runnable { + + @Override + public void run() { + try { + Logger.logVerbose(LOG_TAG, "ClientSocketListener start"); + + while (!Thread.currentThread().isInterrupted()) { + LocalClientSocket clientSocket = null; + try { + // Listen for new client socket connections + clientSocket = null; + clientSocket = accept(); + // If server socket is closed, then stop listener thread. + if (clientSocket == null) + break; + + Error error; + + error = clientSocket.setReadTimeout(); + if (error != null) { + mLocalSocketManager.onError(clientSocket, error); + clientSocket.closeClientSocket(true); + continue; + } + + error = clientSocket.setWriteTimeout(); + if (error != null) { + mLocalSocketManager.onError(clientSocket, error); + clientSocket.closeClientSocket(true); + continue; + } + + // Start new thread for client logic and pass control to ILocalSocketManager implementation + mLocalSocketManager.onClientAccepted(clientSocket); + } catch (Throwable t) { + mLocalSocketManager.onError(clientSocket, + LocalSocketErrno.ERRNO_CLIENT_SOCKET_LISTENER_FAILED_WITH_EXCEPTION.getError(t, mLocalSocketRunConfig.getTitle(), t.getMessage())); + if (clientSocket != null) + clientSocket.closeClientSocket(true); + } + } + } catch (Exception ignored) { + } finally { + try { + close(); + } catch (Exception ignored) {} + } + + Logger.logVerbose(LOG_TAG, "ClientSocketListener end"); + } + + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketErrno.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketErrno.java new file mode 100644 index 00000000..251f5c67 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketErrno.java @@ -0,0 +1,43 @@ +package com.termux.shared.net.socket.local; + +import com.termux.shared.errors.Errno; + +public class LocalSocketErrno extends Errno { + + public static final String TYPE = "LocalSocket Error"; + + + /** Errors for {@link LocalSocketManager} (100-150) */ + public static final Errno ERRNO_START_LOCAL_SOCKET_LIB_LOAD_FAILED_WITH_EXCEPTION = new Errno(TYPE, 100, "Failed to load \"%1$s\" library.\nException: %2$s"); + + /** Errors for {@link LocalServerSocket} (150-200) */ + public static final Errno ERRNO_SERVER_SOCKET_PATH_NULL_OR_EMPTY = new Errno(TYPE, 150, "The \"%1$s\" server socket path is null or empty."); + public static final Errno ERRNO_SERVER_SOCKET_PATH_TOO_LONG = new Errno(TYPE, 151, "The \"%1$s\" server socket path \"%2$s\" is greater than 108 bytes."); + public static final Errno ERRNO_SERVER_SOCKET_PATH_NOT_ABSOLUTE = new Errno(TYPE, 152, "The \"%1$s\" server socket path \"%2$s\" is not an absolute file path."); + public static final Errno ERRNO_SERVER_SOCKET_BACKLOG_INVALID = new Errno(TYPE, 153, "The \"%1$s\" server socket backlog \"%2$s\" is not greater than 0."); + public static final Errno ERRNO_CREATE_SERVER_SOCKET_FAILED = new Errno(TYPE, 154, "Create \"%1$s\" server socket failed.\n%2$s"); + public static final Errno ERRNO_SERVER_SOCKET_FD_INVALID = new Errno(TYPE, 155, "Invalid file descriptor \"%1$s\" returned when creating \"%2$s\" server socket."); + public static final Errno ERRNO_ACCEPT_CLIENT_SOCKET_FAILED = new Errno(TYPE, 156, "Accepting client socket for \"%1$s\" server failed.\n%2$s"); + public static final Errno ERRNO_CLIENT_SOCKET_FD_INVALID = new Errno(TYPE, 157, "Invalid file descriptor \"%1$s\" returned when accept new client for \"%2$s\" server."); + public static final Errno ERRNO_GET_CLIENT_SOCKET_PEER_UID_FAILED = new Errno(TYPE, 158, "Getting peer uid for client socket for \"%1$s\" server failed.\n%2$s"); + public static final Errno ERRNO_CLIENT_SOCKET_PEER_UID_INVALID = new Errno(TYPE, 158, "Invalid peer uid \"%1$s\" returned for new client for \"%2$s\" server."); + public static final Errno ERRNO_CLIENT_SOCKET_PEER_UID_DISALLOWED = new Errno(TYPE, 160, "Disallowed peer %1$s tried to connect with \"%2$s\" server."); + public static final Errno ERRNO_CLOSE_SERVER_SOCKET_FAILED_WITH_EXCEPTION = new Errno(TYPE, 161, "Close \"%1$s\" server socket failed.\nException: %2$s"); + public static final Errno ERRNO_CLIENT_SOCKET_LISTENER_FAILED_WITH_EXCEPTION = new Errno(TYPE, 162, "Exception in client socket listener for \"%1$s\" server.\nException: %2$s"); + + /** Errors for {@link LocalClientSocket} (200-250) */ + public static final Errno ERRNO_SET_CLIENT_SOCKET_READ_TIMEOUT_FAILED = new Errno(TYPE, 200, "Set \"%1$s\" client socket read (SO_RCVTIMEO) timeout to \"%2$s\" failed.\n%3$s"); + public static final Errno ERRNO_SET_CLIENT_SOCKET_SEND_TIMEOUT_FAILED = new Errno(TYPE, 201, "Set \"%1$s\" client socket send (SO_SNDTIMEO) timeout \"%2$s\" failed.\n%3$s"); + public static final Errno ERRNO_READ_DATA_FROM_CLIENT_SOCKET_FAILED = new Errno(TYPE, 202, "Read data from \"%1$s\" client socket failed.\n%2$s"); + public static final Errno ERRNO_READ_DATA_FROM_INPUT_STREAM_OF_CLIENT_SOCKET_FAILED_WITH_EXCEPTION = new Errno(TYPE, 203, "Read data from \"%1$s\" client socket input stream failed.\n%2$s"); + public static final Errno ERRNO_SEND_DATA_TO_CLIENT_SOCKET_FAILED = new Errno(TYPE, 204, "Send data to \"%1$s\" client socket failed.\n%2$s"); + public static final Errno ERRNO_SEND_DATA_TO_OUTPUT_STREAM_OF_CLIENT_SOCKET_FAILED_WITH_EXCEPTION = new Errno(TYPE, 205, "Send data to \"%1$s\" client socket output stream failed.\n%2$s"); + public static final Errno ERRNO_CHECK_AVAILABLE_DATA_ON_CLIENT_SOCKET_FAILED = new Errno(TYPE, 206, "Check available data on \"%1$s\" client socket failed.\n%2$s"); + public static final Errno ERRNO_CLOSE_CLIENT_SOCKET_FAILED_WITH_EXCEPTION = new Errno(TYPE, 207, "Close \"%1$s\" client socket failed.\n%2$s"); + public static final Errno ERRNO_USING_CLIENT_SOCKET_WITH_INVALID_FD = new Errno(TYPE, 208, "Trying to use client socket with invalid file descriptor \"%1$s\" for \"%2$s\" server."); + + LocalSocketErrno(final String type, final int code, final String message) { + super(type, code, message); + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java new file mode 100644 index 00000000..0b7c3dc9 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java @@ -0,0 +1,448 @@ +package com.termux.shared.net.socket.local; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.termux.shared.errors.Error; +import com.termux.shared.jni.models.JniResult; +import com.termux.shared.logger.Logger; + +/** + * Manager for an AF_UNIX/SOCK_STREAM local server. + * + * Usage: + * 1. Implement the {@link ILocalSocketManager} that will receive call backs from the server including + * when client connects via {@link ILocalSocketManager#onClientAccepted(LocalSocketManager, LocalClientSocket)}. + * Optionally extend the {@link LocalSocketManagerClientBase} class that provides base implementation. + * 2. Create a {@link LocalSocketRunConfig} instance with the run config of the server. + * 3. Create a {@link LocalSocketManager} instance and call {@link #start()}. + * 4. Stop server if needed with a call to {@link #stop()}. + */ +public class LocalSocketManager { + + public static final String LOG_TAG = "LocalSocketManager"; + + /** The native JNI local socket library. */ + protected static String LOCAL_SOCKET_LIBRARY = "local-socket"; + + /** Whether {@link #LOCAL_SOCKET_LIBRARY} has been loaded or not. */ + protected static boolean localSocketLibraryLoaded; + + /** The {@link Context} that may needed for various operations. */ + @NonNull protected final Context mContext; + + /** The {@link LocalSocketRunConfig} containing run config for the {@link LocalSocketManager}. */ + @NonNull protected final LocalSocketRunConfig mLocalSocketRunConfig; + + /** The {@link LocalServerSocket} for the {@link LocalSocketManager}. */ + @NonNull protected final LocalServerSocket mServerSocket; + + /** The {@link ILocalSocketManager} client for the {@link LocalSocketManager}. */ + @NonNull protected final ILocalSocketManager mLocalSocketManagerClient; + + /** The {@link Thread.UncaughtExceptionHandler} used for client thread started by {@link LocalSocketManager}. */ + @NonNull protected final Thread.UncaughtExceptionHandler mLocalSocketManagerClientThreadUEH; + + /** Whether the {@link LocalServerSocket} managed by {@link LocalSocketManager} in running or not. */ + protected boolean mIsRunning; + + + /** + * Create an new instance of {@link LocalSocketManager}. + * + * @param context The {@link #mContext} value. + * @param localSocketRunConfig The {@link #mLocalSocketRunConfig} value. + */ + public LocalSocketManager(@NonNull Context context, @NonNull LocalSocketRunConfig localSocketRunConfig) { + mContext = context.getApplicationContext(); + mLocalSocketRunConfig = localSocketRunConfig; + mServerSocket = new LocalServerSocket(this); + mLocalSocketManagerClient = mLocalSocketRunConfig.getLocalSocketManagerClient(); + mLocalSocketManagerClientThreadUEH = getLocalSocketManagerClientThreadUEHOrDefault(); + mIsRunning = false; + } + + /** + * Create the {@link LocalServerSocket} and start listening for new {@link LocalClientSocket}. + */ + public synchronized Error start() { + Logger.logDebugExtended(LOG_TAG, "start\n" + mLocalSocketRunConfig); + + if (!localSocketLibraryLoaded) { + try { + Logger.logDebug(LOG_TAG, "Loading \"" + LOCAL_SOCKET_LIBRARY + "\" library"); + System.loadLibrary(LOCAL_SOCKET_LIBRARY); + localSocketLibraryLoaded = true; + } catch (Exception e) { + return LocalSocketErrno.ERRNO_START_LOCAL_SOCKET_LIB_LOAD_FAILED_WITH_EXCEPTION.getError(e, LOCAL_SOCKET_LIBRARY, e.getMessage()); + } + } + + mIsRunning = true; + return mServerSocket.start(); + } + + /** + * Stop the {@link LocalServerSocket} and stop listening for new {@link LocalClientSocket}. + */ + public synchronized Error stop() { + if (mIsRunning) { + Logger.logDebugExtended(LOG_TAG, "stop\n" + mLocalSocketRunConfig); + mIsRunning = false; + return mServerSocket.stop(); + } + return null; + } + + + + + /* + Note: Exceptions thrown from JNI must be caught with Throwable class instead of Exception, + otherwise exception will be sent to UncaughtExceptionHandler of the thread. + */ + + /** + * Creates an AF_UNIX/SOCK_STREAM local server socket at {@code path}, with the specified backlog. + * + * @param serverTitle The server title used for logging and errors. + * @param path The path at which to create the socket. + * For a filesystem socket, this must be an absolute path to the socket file. + * For an abstract namespace socket, the first byte must be a null `\0` character. + * Max allowed length is 108 bytes as per sun_path size (UNIX_PATH_MAX) on Linux. + * @param backlog The maximum length to which the queue of pending connections for the socket + * may grow. This value may be ignored or may not have one-to-one mapping + * in kernel implementation. Value must be greater than 0. + * @return Returns the {@link JniResult}. If server creation was successful, then + * {@link JniResult#retval} will be 0 and {@link JniResult#intData} will contain the server socket + * fd. + */ + @Nullable + public static JniResult createServerSocket(@NonNull String serverTitle, @NonNull byte[] path, int backlog) { + try { + return createServerSocketNative(serverTitle, path, backlog); + } catch (Throwable t) { + String message = "Exception in createServerSocketNative()"; + Logger.logStackTraceWithMessage(LOG_TAG, message, t); + return new JniResult(message, t); + } + } + + /** + * Closes the socket with fd. + * + * @param serverTitle The server title used for logging and errors. + * @param fd The socket fd. + * @return Returns the {@link JniResult}. If closing socket was successful, then + * {@link JniResult#retval} will be 0. + */ + @Nullable + public static JniResult closeSocket(@NonNull String serverTitle, int fd) { + try { + return closeSocketNative(serverTitle, fd); + } catch (Throwable t) { + String message = "Exception in closeSocketNative()"; + Logger.logStackTraceWithMessage(LOG_TAG, message, t); + return new JniResult(message, t); + } + } + + /** + * Accepts a connection on the supplied server socket fd. + * + * @param serverTitle The server title used for logging and errors. + * @param fd The server socket fd. + * @return Returns the {@link JniResult}. If accepting socket was successful, then + * {@link JniResult#retval} will be 0 and {@link JniResult#intData} will contain the client socket + * fd. + */ + @Nullable + public static JniResult accept(@NonNull String serverTitle, int fd) { + try { + return acceptNative(serverTitle, fd); + } catch (Throwable t) { + String message = "Exception in acceptNative()"; + Logger.logStackTraceWithMessage(LOG_TAG, message, t); + return new JniResult(message, t); + } + } + + /** + * Attempts to read up to data buffer length bytes from file descriptor fd into the data buffer. + * On success, the number of bytes read is returned (zero indicates end of file). + * It is not an error if bytes read is smaller than the number of bytes requested; this may happen + * for example because fewer bytes are actually available right now (maybe because we were close + * to end-of-file, or because we are reading from a pipe), or because read() was interrupted by + * a signal. On error, the {@link JniResult#errno} and {@link JniResult#errmsg} will be set. + * + * If while reading the deadline elapses but all the data has not been read, the call will fail. + * + * @param serverTitle The server title used for logging and errors. + * @param fd The socket fd. + * @param data The data buffer to read bytes into. + * @param deadline The deadline milliseconds since epoch. + * @return Returns the {@link JniResult}. If reading was successful, then {@link JniResult#retval} + * will be 0 and {@link JniResult#intData} will contain the bytes read. + */ + @Nullable + public static JniResult read(@NonNull String serverTitle, int fd, @NonNull byte[] data, long deadline) { + try { + return readNative(serverTitle, fd, data, deadline); + } catch (Throwable t) { + String message = "Exception in readNative()"; + Logger.logStackTraceWithMessage(LOG_TAG, message, t); + return new JniResult(message, t); + } + } + + /** + * Attempts to send data buffer to the file descriptor. On error, the {@link JniResult#errno} and + * {@link JniResult#errmsg} will be set. + * + * If while sending the deadline elapses but all the data has not been sent, the call will fail. + * + * @param serverTitle The server title used for logging and errors. + * @param fd The socket fd. + * @param data The data buffer containing bytes to send. + * @param deadline The deadline milliseconds since epoch. + * @return Returns the {@link JniResult}. If sending was successful, then {@link JniResult#retval} + * will be 0. + */ + @Nullable + public static JniResult send(@NonNull String serverTitle, int fd, @NonNull byte[] data, long deadline) { + try { + return sendNative(serverTitle, fd, data, deadline); + } catch (Throwable t) { + String message = "Exception in sendNative()"; + Logger.logStackTraceWithMessage(LOG_TAG, message, t); + return new JniResult(message, t); + } + } + + /** + * Gets the number of bytes available to read on the socket. + * + * @param serverTitle The server title used for logging and errors. + * @param fd The socket fd. + * @return Returns the {@link JniResult}. If checking availability was successful, then + * {@link JniResult#retval} will be 0 and {@link JniResult#intData} will contain the bytes available. + */ + @Nullable + public static JniResult available(@NonNull String serverTitle, int fd) { + try { + return availableNative(serverTitle, fd); + } catch (Throwable t) { + String message = "Exception in availableNative()"; + Logger.logStackTraceWithMessage(LOG_TAG, message, t); + return new JniResult(message, t); + } + } + + /** + * Set receiving (SO_RCVTIMEO) timeout in milliseconds for socket. + * + * @param serverTitle The server title used for logging and errors. + * @param fd The socket fd. + * @param timeout The timeout value in milliseconds. + * @return Returns the {@link JniResult}. If setting timeout was successful, then + * {@link JniResult#retval} will be 0. + */ + @Nullable + public static JniResult setSocketReadTimeout(@NonNull String serverTitle, int fd, int timeout) { + try { + return setSocketReadTimeoutNative(serverTitle, fd, timeout); + } catch (Throwable t) { + String message = "Exception in setSocketReadTimeoutNative()"; + Logger.logStackTraceWithMessage(LOG_TAG, message, t); + return new JniResult(message, t); + } + } + + /** + * Set sending (SO_SNDTIMEO) timeout in milliseconds for fd. + * + * @param serverTitle The server title used for logging and errors. + * @param fd The socket fd. + * @param timeout The timeout value in milliseconds. + * @return Returns the {@link JniResult}. If setting timeout was successful, then + * {@link JniResult#retval} will be 0. + */ + @Nullable + public static JniResult setSocketSendTimeout(@NonNull String serverTitle, int fd, int timeout) { + try { + return setSocketSendTimeoutNative(serverTitle, fd, timeout); + } catch (Throwable t) { + String message = "Exception in setSocketSendTimeoutNative()"; + Logger.logStackTraceWithMessage(LOG_TAG, message, t); + return new JniResult(message, t); + } + } + + /** + * Get the {@link PeerCred} for the socket. + * + * @param serverTitle The server title used for logging and errors. + * @param fd The socket fd. + * @param peerCred The {@link PeerCred} object that should be filled. + * @return Returns the {@link JniResult}. If setting timeout was successful, then + * {@link JniResult#retval} will be 0. + */ + @Nullable + public static JniResult getPeerCred(@NonNull String serverTitle, int fd, PeerCred peerCred) { + try { + return getPeerCredNative(serverTitle, fd, peerCred); + } catch (Throwable t) { + String message = "Exception in getPeerCredNative()"; + Logger.logStackTraceWithMessage(LOG_TAG, message, t); + return new JniResult(message, t); + } + } + + + + /** Wrapper for {@link #onError(LocalClientSocket, Error)} for {@code null} {@link LocalClientSocket}. */ + public void onError(@NonNull Error error) { + onError(null, error); + } + + /** Wrapper to call {@link ILocalSocketManager#onError(LocalSocketManager, LocalClientSocket, Error)} in a new thread. */ + public void onError(@Nullable LocalClientSocket clientSocket, @NonNull Error error) { + startLocalSocketManagerClientThread(() -> + mLocalSocketManagerClient.onError(this, clientSocket, error)); + } + + /** Wrapper to call {@link ILocalSocketManager#onDisallowedClientConnected(LocalSocketManager, LocalClientSocket, Error)} in a new thread. */ + public void onDisallowedClientConnected(@NonNull LocalClientSocket clientSocket, @NonNull Error error) { + startLocalSocketManagerClientThread(() -> + mLocalSocketManagerClient.onDisallowedClientConnected(this, clientSocket, error)); + } + + /** Wrapper to call {@link ILocalSocketManager#onClientAccepted(LocalSocketManager, LocalClientSocket)} in a new thread. */ + public void onClientAccepted(@NonNull LocalClientSocket clientSocket) { + startLocalSocketManagerClientThread(() -> + mLocalSocketManagerClient.onClientAccepted(this, clientSocket)); + } + + /** All client accept logic must be run on separate threads so that incoming client acceptance is not blocked. */ + public void startLocalSocketManagerClientThread(@NonNull Runnable runnable) { + Thread thread = new Thread(runnable); + thread.setUncaughtExceptionHandler(getLocalSocketManagerClientThreadUEH()); + try { + thread.start(); + } catch (Exception e) { + Logger.logStackTraceWithMessage(LOG_TAG, "LocalSocketManagerClientThread start failed", e); + } + } + + + + /** Get {@link #mContext}. */ + public Context getContext() { + return mContext; + } + + /** Get {@link #mLocalSocketRunConfig}. */ + public LocalSocketRunConfig getLocalSocketRunConfig() { + return mLocalSocketRunConfig; + } + + /** Get {@link #mLocalSocketManagerClient}. */ + public ILocalSocketManager getLocalSocketManagerClient() { + return mLocalSocketManagerClient; + } + + /** Get {@link #mServerSocket}. */ + public LocalServerSocket getServerSocket() { + return mServerSocket; + } + + /** Get {@link #mLocalSocketManagerClientThreadUEH}. */ + public Thread.UncaughtExceptionHandler getLocalSocketManagerClientThreadUEH() { + return mLocalSocketManagerClientThreadUEH; + } + + /** + * Get {@link Thread.UncaughtExceptionHandler} returned by call to + * {@link ILocalSocketManager#getLocalSocketManagerClientThreadUEH(LocalSocketManager)} + * or the default handler that just logs the exception. + */ + protected Thread.UncaughtExceptionHandler getLocalSocketManagerClientThreadUEHOrDefault() { + Thread.UncaughtExceptionHandler uncaughtExceptionHandler = + mLocalSocketManagerClient.getLocalSocketManagerClientThreadUEH(this); + if (uncaughtExceptionHandler == null) + uncaughtExceptionHandler = (t, e) -> + Logger.logStackTraceWithMessage(LOG_TAG, "Uncaught exception for " + t + " in " + mLocalSocketRunConfig.getTitle() + " server", e); + return uncaughtExceptionHandler; + } + + /** Get {@link #mIsRunning}. */ + public boolean isRunning() { + return mIsRunning; + } + + + + /** Get an error log {@link String} for the {@link LocalSocketManager}. */ + public static String getErrorLogString(@NonNull Error error, + @NonNull LocalSocketRunConfig localSocketRunConfig, + @Nullable LocalClientSocket clientSocket) { + StringBuilder logString = new StringBuilder(); + + logString.append(localSocketRunConfig.getTitle()).append(" Socket Server Error:\n"); + logString.append(error.getErrorLogString()); + logString.append("\n\n\n"); + + logString.append(localSocketRunConfig.getLogString()); + + if (clientSocket != null) { + logString.append("\n\n\n"); + logString.append(clientSocket.getLogString()); + } + + return logString.toString(); + } + + /** Get an error markdown {@link String} for the {@link LocalSocketManager}. */ + public static String getErrorMarkdownString(@NonNull Error error, + @NonNull LocalSocketRunConfig localSocketRunConfig, + @Nullable LocalClientSocket clientSocket) { + StringBuilder markdownString = new StringBuilder(); + + markdownString.append(error.getErrorMarkdownString()); + markdownString.append("\n##\n\n\n"); + + markdownString.append(localSocketRunConfig.getMarkdownString()); + + if (clientSocket != null) { + markdownString.append("\n\n\n"); + markdownString.append(clientSocket.getMarkdownString()); + } + + return markdownString.toString(); + } + + + + + + @Nullable private static native JniResult createServerSocketNative(@NonNull String serverTitle, @NonNull byte[] path, int backlog); + + @Nullable private static native JniResult closeSocketNative(@NonNull String serverTitle, int fd); + + @Nullable private static native JniResult acceptNative(@NonNull String serverTitle, int fd); + + @Nullable private static native JniResult readNative(@NonNull String serverTitle, int fd, @NonNull byte[] data, long deadline); + + @Nullable private static native JniResult sendNative(@NonNull String serverTitle, int fd, @NonNull byte[] data, long deadline); + + @Nullable private static native JniResult availableNative(@NonNull String serverTitle, int fd); + + private static native JniResult setSocketReadTimeoutNative(@NonNull String serverTitle, int fd, int timeout); + + @Nullable private static native JniResult setSocketSendTimeoutNative(@NonNull String serverTitle, int fd, int timeout); + + @Nullable private static native JniResult getPeerCredNative(@NonNull String serverTitle, int fd, PeerCred peerCred); + +} diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManagerClientBase.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManagerClientBase.java new file mode 100644 index 00000000..4d7657c4 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManagerClientBase.java @@ -0,0 +1,47 @@ +package com.termux.shared.net.socket.local; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.termux.shared.errors.Error; +import com.termux.shared.logger.Logger; + +/** Base helper implementation for {@link ILocalSocketManager}. */ +public abstract class LocalSocketManagerClientBase implements ILocalSocketManager { + + @Nullable + @Override + public Thread.UncaughtExceptionHandler getLocalSocketManagerClientThreadUEH( + @NonNull LocalSocketManager localSocketManager) { + return null; + } + + @Override + public void onError(@NonNull LocalSocketManager localSocketManager, + @Nullable LocalClientSocket clientSocket, @NonNull Error error) { + // Only log if log level is debug or higher since PeerCred.cmdline may contain private info + Logger.logErrorPrivate(getLogTag(), "onError"); + Logger.logErrorPrivateExtended(getLogTag(), LocalSocketManager.getErrorLogString(error, + localSocketManager.getLocalSocketRunConfig(), clientSocket)); + } + + @Override + public void onDisallowedClientConnected(@NonNull LocalSocketManager localSocketManager, + @NonNull LocalClientSocket clientSocket, @NonNull Error error) { + Logger.logWarn(getLogTag(), "onDisallowedClientConnected"); + Logger.logWarnExtended(getLogTag(), LocalSocketManager.getErrorLogString(error, + localSocketManager.getLocalSocketRunConfig(), clientSocket)); + } + + @Override + public void onClientAccepted(@NonNull LocalSocketManager localSocketManager, + @NonNull LocalClientSocket clientSocket) { + // Just close socket and let child class handle any required communication + clientSocket.closeClientSocket(true); + } + + + + protected abstract String getLogTag(); + +} diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java new file mode 100644 index 00000000..53ef8957 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java @@ -0,0 +1,265 @@ +package com.termux.shared.net.socket.local; + +import androidx.annotation.NonNull; + +import com.termux.shared.file.FileUtils; +import com.termux.shared.logger.Logger; +import com.termux.shared.markdown.MarkdownUtils; + +import java.io.Serializable; +import java.nio.charset.StandardCharsets; + + +/** + * Run config for {@link LocalSocketManager}. + */ +public class LocalSocketRunConfig implements Serializable { + + /** The {@link LocalSocketManager} title. */ + private final String mTitle; + + /** + * The {@link LocalServerSocket} path. + * + * For a filesystem socket, this must be an absolute path to the socket file. Creation of a new + * socket will fail if the server starter app process does not have write and search (execute) + * permission on the directory in which the socket is created. The client process must have write + * permission on the socket to connect to it. Other app will not be able to connect to socket + * if its created in private app data directory. + * + * For an abstract namespace socket, the first byte must be a null `\0` character. Note that on + * Android 9+, if server app is using `targetSdkVersion` `28`, then other apps will not be able + * to connect to it due to selinux restrictions. + * > Per-app SELinux domains + * > Apps that target Android 9 or higher cannot share data with other apps using world-accessible + * Unix permissions. This change improves the integrity of the Android Application Sandbox, + * particularly the requirement that an app's private data is accessible only by that app. + * https://developer.android.com/about/versions/pie/android-9.0-changes-28 + * https://github.com/android/ndk/issues/1469 + * https://stackoverflow.com/questions/63806516/avc-denied-connectto-when-using-uds-on-android-10 + * + * Max allowed length is 108 bytes as per sun_path size (UNIX_PATH_MAX) on Linux. + */ + private final String mPath; + + /** If abstract namespace {@link LocalServerSocket} instead of filesystem. */ + protected final boolean mAbstractNamespaceSocket; + + /** The {@link ILocalSocketManager} client for the {@link LocalSocketManager}. */ + private final ILocalSocketManager mLocalSocketManagerClient; + + /** + * The {@link LocalServerSocket} file descriptor. + * Value will be `>= 0` if socket has been created successfully and `-1` if not created or closed. + */ + private int mFD = -1; + + /** + * The {@link LocalClientSocket} receiving (SO_RCVTIMEO) timeout in milliseconds. + * + * https://manpages.debian.org/testing/manpages/socket.7.en.html + * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/am/NativeCrashListener.java;l=55 + * Defaults to {@link #DEFAULT_RECEIVE_TIMEOUT}. + */ + private Integer mReceiveTimeout; + public static final int DEFAULT_RECEIVE_TIMEOUT = 10000; + + /** + * The {@link LocalClientSocket} sending (SO_SNDTIMEO) timeout in milliseconds. + * + * https://manpages.debian.org/testing/manpages/socket.7.en.html + * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/am/NativeCrashListener.java;l=55 + * Defaults to {@link #DEFAULT_SEND_TIMEOUT}. + */ + private Integer mSendTimeout; + public static final int DEFAULT_SEND_TIMEOUT = 10000; + + /** + * The {@link LocalClientSocket} deadline in milliseconds. When the deadline has elapsed after + * creation time of client socket, all reads and writes will error out. Set to 0, for no + * deadline. + * Defaults to {@link #DEFAULT_DEADLINE}. + */ + private Long mDeadline; + public static final int DEFAULT_DEADLINE = 0; + + /** + * The {@link LocalServerSocket} backlog for the maximum length to which the queue of pending connections + * for the socket may grow. This value may be ignored or may not have one-to-one mapping + * in kernel implementation. Value must be greater than 0. + * + * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/net/LocalSocketManager.java;l=31 + * Defaults to {@link #DEFAULT_BACKLOG}. + */ + private Integer mBacklog; + public static final int DEFAULT_BACKLOG = 50; + + + /** + * Create an new instance of {@link LocalSocketRunConfig}. + * + * @param title The {@link #mTitle} value. + * @param path The {@link #mPath} value. + * @param localSocketManagerClient The {@link #mLocalSocketManagerClient} value. + */ + public LocalSocketRunConfig(@NonNull String title, @NonNull String path, @NonNull ILocalSocketManager localSocketManagerClient) { + mTitle = title; + mLocalSocketManagerClient = localSocketManagerClient; + mAbstractNamespaceSocket = path.getBytes(StandardCharsets.UTF_8)[0] == 0; + + if (mAbstractNamespaceSocket) + mPath = path; + else + mPath = FileUtils.getCanonicalPath(path, null); + } + + + + /** Get {@link #mTitle}. */ + public String getTitle() { + return mTitle; + } + + /** Get log title that should be used for {@link LocalSocketManager}. */ + public String getLogTitle() { + return Logger.getDefaultLogTag() + "." + mTitle; + } + + /** Get {@link #mPath}. */ + public String getPath() { + return mPath; + } + + /** Get {@link #mAbstractNamespaceSocket}. */ + public boolean isAbstractNamespaceSocket() { + return mAbstractNamespaceSocket; + } + + /** Get {@link #mLocalSocketManagerClient}. */ + public ILocalSocketManager getLocalSocketManagerClient() { + return mLocalSocketManagerClient; + } + + /** Get {@link #mFD}. */ + public Integer getFD() { + return mFD; + } + + /** Set {@link #mFD}. Value must be greater than 0 or -1. */ + public void setFD(int fd) { + if (fd >= 0) + mFD = fd; + else + mFD = -1; + } + + /** Get {@link #mReceiveTimeout} if set, otherwise {@link #DEFAULT_RECEIVE_TIMEOUT}. */ + public Integer getReceiveTimeout() { + return mReceiveTimeout != null ? mReceiveTimeout : DEFAULT_RECEIVE_TIMEOUT; + } + + /** Set {@link #mReceiveTimeout}. */ + public void setReceiveTimeout(Integer receiveTimeout) { + mReceiveTimeout = receiveTimeout; + } + + /** Get {@link #mSendTimeout} if set, otherwise {@link #DEFAULT_SEND_TIMEOUT}. */ + public Integer getSendTimeout() { + return mSendTimeout != null ? mSendTimeout : DEFAULT_SEND_TIMEOUT; + } + + /** Set {@link #mSendTimeout}. */ + public void setSendTimeout(Integer sendTimeout) { + mSendTimeout = sendTimeout; + } + + /** Get {@link #mDeadline} if set, otherwise {@link #DEFAULT_DEADLINE}. */ + public Long getDeadline() { + return mDeadline != null ? mDeadline : DEFAULT_DEADLINE; + } + + /** Set {@link #mDeadline}. */ + public void setDeadline(Long deadline) { + mDeadline = deadline; + } + + /** Get {@link #mBacklog} if set, otherwise {@link #DEFAULT_BACKLOG}. */ + public Integer getBacklog() { + return mBacklog != null ? mBacklog : DEFAULT_BACKLOG; + } + + /** Set {@link #mBacklog}. Value must be greater than 0. */ + public void setBacklog(Integer backlog) { + if (backlog > 0) + mBacklog = backlog; + } + + + /** + * Get a log {@link String} for {@link LocalSocketRunConfig}. + * + * @param config The {@link LocalSocketRunConfig} to get info of. + * @return Returns the log {@link String}. + */ + @NonNull + public static String getRunConfigLogString(final LocalSocketRunConfig config) { + if (config == null) return "null"; + return config.getLogString(); + } + + /** Get a log {@link String} for the {@link LocalSocketRunConfig}. */ + @NonNull + public String getLogString() { + StringBuilder logString = new StringBuilder(); + + logString.append(mTitle).append(" Socket Server Run Config:"); + logString.append("\n").append(Logger.getSingleLineLogStringEntry("Path", mPath, "-")); + logString.append("\n").append(Logger.getSingleLineLogStringEntry("AbstractNamespaceSocket", mAbstractNamespaceSocket, "-")); + logString.append("\n").append(Logger.getSingleLineLogStringEntry("LocalSocketManagerClient", mLocalSocketManagerClient.getClass().getName(), "-")); + logString.append("\n").append(Logger.getSingleLineLogStringEntry("FD", mFD, "-")); + logString.append("\n").append(Logger.getSingleLineLogStringEntry("ReceiveTimeout", getReceiveTimeout(), "-")); + logString.append("\n").append(Logger.getSingleLineLogStringEntry("SendTimeout", getSendTimeout(), "-")); + logString.append("\n").append(Logger.getSingleLineLogStringEntry("Deadline", getDeadline(), "-")); + logString.append("\n").append(Logger.getSingleLineLogStringEntry("Backlog", getBacklog(), "-")); + + return logString.toString(); + } + + /** + * Get a markdown {@link String} for {@link LocalSocketRunConfig}. + * + * @param config The {@link LocalSocketRunConfig} to get info of. + * @return Returns the markdown {@link String}. + */ + public static String getRunConfigMarkdownString(final LocalSocketRunConfig config) { + if (config == null) return "null"; + return config.getMarkdownString(); + } + + /** Get a markdown {@link String} for the {@link LocalSocketRunConfig}. */ + @NonNull + public String getMarkdownString() { + StringBuilder markdownString = new StringBuilder(); + + markdownString.append("## ").append(mTitle).append(" Socket Server Run Config"); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Path", mPath, "-")); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("AbstractNamespaceSocket", mAbstractNamespaceSocket, "-")); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("LocalSocketManagerClient", mLocalSocketManagerClient.getClass().getName(), "-")); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("FD", mFD, "-")); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("ReceiveTimeout", getReceiveTimeout(), "-")); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("SendTimeout", getSendTimeout(), "-")); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Deadline", getDeadline(), "-")); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Backlog", getBacklog(), "-")); + + return markdownString.toString(); + } + + + + @NonNull + @Override + public String toString() { + return getLogString(); + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/PeerCred.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/PeerCred.java new file mode 100644 index 00000000..8adab1f7 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/PeerCred.java @@ -0,0 +1,140 @@ +package com.termux.shared.net.socket.local; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.termux.shared.android.ProcessUtils; +import com.termux.shared.android.UserUtils; +import com.termux.shared.logger.Logger; +import com.termux.shared.markdown.MarkdownUtils; + +/** The {@link PeerCred} of the {@link LocalClientSocket} containing info of client/peer. */ +public class PeerCred { + + public static final String LOG_TAG = "PeerCred"; + + /** Process Id. */ + public int pid; + /** Process Name. */ + public String pname; + + /** User Id. */ + public int uid; + /** User name. */ + public String uname; + + /** Group Id. */ + public int gid; + /** Group name. */ + public String gname; + + /** Command line that started the process. */ + public String cmdline; + + PeerCred() { + // Initialize to -1 instead of 0 in case a failed getPeerCred()/getsockopt() call somehow doesn't report failure and returns the uid of root + pid = -1; uid = -1; gid = -1; + } + + /** Set data that was not set by JNI. */ + public void fillPeerCred(@NonNull Context context) { + fillUnameAndGname(context); + fillPname(context); + } + + /** Set {@link #uname} and {@link #gname} if not set. */ + public void fillUnameAndGname(@NonNull Context context) { + uname = UserUtils.getNameForUid(context, uid); + + if (gid != uid) + gname = UserUtils.getNameForUid(context, gid); + else + gname = uname; + } + + /** Set {@link #pname} if not set. */ + public void fillPname(@NonNull Context context) { + // If jni did not set process name since it wouldn't be able to access /proc/ of other + // users/apps, then try to see if any app has that pid, but this wouldn't check child + // processes of the app. + if (pid > 0 && pname == null) + pname = ProcessUtils.getAppProcessNameForPid(context, pid); + } + + /** + * Get a log {@link String} for {@link PeerCred}. + * + * @param peerCred The {@link PeerCred} to get info of. + * @return Returns the log {@link String}. + */ + @NonNull + public static String getPeerCredLogString(final PeerCred peerCred) { + if (peerCred == null) return "null"; + return peerCred.getLogString(); + } + + /** Get a log {@link String} for the {@link PeerCred}. */ + @NonNull + public String getLogString() { + StringBuilder logString = new StringBuilder(); + + logString.append("Peer Cred:"); + logString.append("\n").append(Logger.getSingleLineLogStringEntry("Process", getProcessString(), "-")); + logString.append("\n").append(Logger.getSingleLineLogStringEntry("User", getUserString(), "-")); + logString.append("\n").append(Logger.getSingleLineLogStringEntry("Group", getGroupString(), "-")); + + if (cmdline != null) + logString.append("\n").append(Logger.getMultiLineLogStringEntry("Cmdline", cmdline, "-")); + + return logString.toString(); + } + + /** + * Get a markdown {@link String} for {@link PeerCred}. + * + * @param peerCred The {@link PeerCred} to get info of. + * @return Returns the markdown {@link String}. + */ + public static String getPeerCredMarkdownString(final PeerCred peerCred) { + if (peerCred == null) return "null"; + return peerCred.getMarkdownString(); + } + + /** Get a markdown {@link String} for the {@link PeerCred}. */ + @NonNull + public String getMarkdownString() { + StringBuilder markdownString = new StringBuilder(); + + markdownString.append("## ").append("Peer Cred"); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Process", getProcessString(), "-")); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("User", getUserString(), "-")); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Group", getGroupString(), "-")); + + if (cmdline != null) + markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Cmdline", cmdline, "-")); + + return markdownString.toString(); + } + + @NonNull + public String getMinimalString() { + return "process=" + getProcessString() + ", user=" + getUserString() + ", group=" + getGroupString(); + } + + @NonNull + public String getProcessString() { + return pname != null && !pname.isEmpty() ? pid + " (" + pname + ")" : String.valueOf(pid); + } + + @NonNull + public String getUserString() { + return uname != null ? uid + " (" + uname + ")" : String.valueOf(uid); + } + + @NonNull + public String getGroupString() { + return gname != null ? gid + " (" + gname + ")" : String.valueOf(gid); + } + +} 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 deleted file mode 100644 index 951e668c..00000000 --- a/termux-shared/src/main/java/com/termux/shared/shell/LocalFilesystemSocket.java +++ /dev/null @@ -1,273 +0,0 @@ -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 || path.length() == 0) { - throw new IllegalArgumentException("path cannot be null or empty"); - } - this.path = path; - if (path.getBytes(StandardCharsets.UTF_8)[0] != 0) { - // not a socket in the abstract linux namespace, make sure the path is accessible and clear - 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 the peer has the same uid or is root, allow the connection - if (peeruid == app.getApplicationInfo().uid || peeruid == 0) { - 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 deleted file mode 100644 index 19322bf3..00000000 --- a/termux-shared/src/main/java/com/termux/shared/shell/LocalSocketListener.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.termux.shared.shell; - -import android.app.Application; - -import androidx.annotation.NonNull; - -import com.termux.shared.logger.Logger; - -import java.io.BufferedWriter; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintStream; - -public class LocalSocketListener -{ - public interface LocalSocketHandler { - int handle(String[] args, PrintStream out, PrintStream err); - } - - private static final String LOG_TAG = "LocalSocketListener"; - - private final Thread thread; - private final LocalFilesystemSocket.ServerSocket server; - private final int timeoutMillis; - private final int deadlineMillis; - - - private LocalSocketListener(@NonNull Application a, @NonNull LocalSocketHandler h, @NonNull String path, int timeoutMillis, int deadlineMillis) throws IOException { - this.timeoutMillis = timeoutMillis; - 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 { - thread.interrupt(); - server.close(); - } catch (Exception ignored) {} - } - - private class LocalSocketListenerRunnable implements Runnable { - private final LocalSocketHandler h; - public LocalSocketListenerRunnable(@NonNull LocalSocketHandler h) { - this.h = h; - } - - @Override - public void run() { - try { - while (! Thread.currentThread().isInterrupted()) { - try (LocalFilesystemSocket.Socket s = server.accept(); - OutputStream sockout = s.getOutputStream(); - InputStreamReader r = new InputStreamReader(s.getInputStream())) { - - - - s.setTimeout(timeoutMillis); - s.setDeadline(System.currentTimeMillis()+deadlineMillis); - - StringBuilder b = new StringBuilder(); - int c; - while ((c = r.read()) > 0) { - b.append((char) c); - } - String outString; - String errString; - int ret; - try (ByteArrayOutputStream out = new ByteArrayOutputStream(); - PrintStream outp = new PrintStream(out); - ByteArrayOutputStream err = new ByteArrayOutputStream(); - PrintStream errp = new PrintStream(err)) { - - ret = h.handle(ArgumentTokenizer.tokenize(b.toString()).toArray(new String[0]), outp, errp); - - outp.flush(); - outString = out.toString("UTF-8"); - - errp.flush(); - errString = err.toString("UTF-8"); - } - try (BufferedWriter w = new BufferedWriter(new OutputStreamWriter(sockout))) { - w.write(Integer.toString(ret)); - w.write('\0'); - w.write(outString); - w.write('\0'); - w.write(errString); - w.flush(); - } - } catch (Exception e) { - Logger.logStackTraceWithMessage(LOG_TAG, "Exception while handling connection", e); - } - } - } - finally { - try { - server.close(); - } catch (Exception ignored) {} - } - Logger.logDebug(LOG_TAG, "LocalSocketListenerRunnable returned"); - } - } -}