Added|Changed|Fixed: Refactor local socket server implementation and make client handling abstract

- Added `LocalSocketManager` to manage the server, `LocalServerSocket` to represent server socket, `LocalClientSocket` to represent client socket, `LocalSocketRunConfig` to store server run config and `ILocalSocketManager` as interface for the `LocalSocketManager` to handle callbacks from the server to handle clients.
- Added support to get full `PeerCred` for client socket, including `pid`, `pname`, `uid`, `uname`, `gid`, `gname` and `cmdline` instead of just `uid`. This should provide more info for error logs about which client failed or tried to connect in case of disallowed clients. Some data is filled in native code and some in java. Native support for added to get process name and `cmdline` of a process with a specific pid.
- Added `JniResult` to get results for JNI calls. Previously only an int was returned and incomplete errors logged. With `JniResult`, both `retval` and `errno` will be returned and full error messages in `errmsg`, including all `strerror()` output for `errno`s. This would provide more helpful info on errors.
- Added `Error` support via `LocalSocketErrno` which contains full error messages and stacktraces for all native and java calls, allowing much better error reporting to users and devs. The errors will be logged by `LocalSocketManagerClientBase` if log level is debug or higher since `PeerCred` `cmdline` may contain private info of users.
- Added support in java to check if socket path was an absolute path and not greater than `108` bytes, after canonicalizing it since otherwise it would result in creation of useless parent directories on failure.
- Added `readDataOnInputStream()` and `sendDataToOutputStream()` functions to `LocalClientSocket` so that server manager client can easily read and send data.

- Renamed the variables and functions as per convention, specially one letter variables. https://source.android.com/setup/contribute/code-style#follow-field-naming-conventions
- Rename `local-filesystem-socket` to `local-filesystem` since abstract namespace sockets can also be created.
- Previously, it was assumed that all local server would expect a shell command string that should be converted to command args with `ArgumentTokenizer` and then should be passed to `LocalSocketHandler.handle()` and then result sent back to client with exit code, stdout and stderr, but there could be any kind of servers in which behaviour is different. Such client handling should not be hard coded and the server manager client should handle the client themselves however they like, including closing the client socket. This will now be done with `ILocalSocketManager. onClientAccepted(LocalSocketManager, LocalClientSocket)`.

- Ensure app does not crash if `local-socket` library is not found or for any other exceptions in the server since anything running in the `Application` class is critical that it does not fail since user would not be able to recover from it, specially non rooted users without SAF support to disable the server with a prop.
- Make sure all reasonable JNI exceptions are caught instead of crashing the app.
- Fixed issue where client logic (`LocalSocketHandler.handle()` was being run in the same thread as the new client acceptable thread, basically blocking new clients until previous client's am command was fully processed. Now all client interface callbacks are started in new threads by `LocalSocketManager`.
- Fix bug where timeout would not be greater than `1000ms` due to only using `tv_usec` which caps at `999,999`.
This commit is contained in:
agnostic-apollo
2022-04-18 04:36:07 +05:00
parent 5f8a922201
commit 2aa7f43d1c
15 changed files with 2514 additions and 589 deletions

View File

@@ -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)

View File

@@ -0,0 +1 @@
APP_STL := c++_static

View File

@@ -1,197 +0,0 @@
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/ioctl.h>
#include <ctime>
#include <cstdio>
#include <unistd.h>
#include <cerrno>
#include <jni.h>
#include <android/log.h>
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<struct sockaddr*>(&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;
}

View File

@@ -0,0 +1,603 @@
#include <cstdio>
#include <ctime>
#include <cerrno>
#include <jni.h>
#include <sstream>
#include <string>
#include <unistd.h>
#include <android/log.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#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, "<init>", "(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<struct sockaddr*>(&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);
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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());
}
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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/<pid> 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);
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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");
}
}
}