Files
termux-app/termux-shared/src/main/java/com/termux/shared/shell/LocalFilesystemSocket.java
2022-04-23 00:36:12 +05:00

274 lines
8.8 KiB
Java

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