mirror of
https://github.com/Benjamin-Wiegand/Flywheel.git
synced 2026-07-20 05:22:25 -07:00
implement IPC to privd
This commit is contained in:
@@ -34,6 +34,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":libprivd"))
|
||||
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.material)
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- needed to launch arbitrary apps -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
|
||||
|
||||
|
||||
@@ -26,11 +26,13 @@ import java.io.IOException;
|
||||
import io.benwiegand.projection.geargrinder.logs.LogUiAdapter;
|
||||
import io.benwiegand.projection.geargrinder.logs.LogcatReader;
|
||||
import io.benwiegand.projection.geargrinder.privileged.RootPrivdLauncher;
|
||||
import io.benwiegand.projection.libprivd.ipc.IPCConstants;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = MainActivity.class.getSimpleName();
|
||||
|
||||
private LogcatReader logcatReader;
|
||||
private RootPrivdLauncher privdLauncher;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
@@ -68,7 +70,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
.setAction(ConnectionRequestActivity.INTENT_ACTION_REQUEST_MEDIA_PROJECTION)));
|
||||
|
||||
findViewById(R.id.launch_privd_button).setOnClickListener(v -> {
|
||||
RootPrivdLauncher privdLauncher = new RootPrivdLauncher(this);
|
||||
try {
|
||||
privdLauncher.launchRoot();
|
||||
} catch (IOException e) {
|
||||
@@ -115,6 +116,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
});
|
||||
|
||||
privdLauncher = new RootPrivdLauncher(this, connection -> {
|
||||
Log.i(TAG, "IPC connected");
|
||||
connection.send(IPCConstants.COMMAND_PING)
|
||||
.doOnResult(r -> Log.i(TAG, "pong from daemon"))
|
||||
.doOnError(t -> Log.e(TAG, "failed to ping daemon", t))
|
||||
.callMeWhenDone();
|
||||
});
|
||||
|
||||
RecyclerView logRecyclerView = findViewById(R.id.log_recycler);
|
||||
LogUiAdapter logUiAdapter = new LogUiAdapter();
|
||||
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.benwiegand.projection.geargrinder.callback;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.privileged.PrivdIPCConnection;
|
||||
|
||||
public interface IPCConnectionListener {
|
||||
void onPrivdConnected(PrivdIPCConnection connection);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package io.benwiegand.projection.geargrinder.privileged;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.callback.IPCConnectionListener;
|
||||
|
||||
public class IPCServer {
|
||||
private static final String TAG = IPCServer.class.getSimpleName();
|
||||
|
||||
private static final InetAddress BIND_ADDRESS = InetAddress.getLoopbackAddress();
|
||||
private static final int BIND_PORT = 0; // auto
|
||||
private static final int MAX_CONNECTIONS = 5; // for resiliency if something else connects first
|
||||
|
||||
private static final int AUTHENTICATION_SECRET_LENGTH = 1024;
|
||||
|
||||
private final Thread serverThread;
|
||||
private final ServerSocket server;
|
||||
private int port = -1;
|
||||
private boolean dead = false;
|
||||
|
||||
private final SecureRandom random;
|
||||
private final byte[] tokenA = new byte[AUTHENTICATION_SECRET_LENGTH];
|
||||
private final byte[] tokenB = new byte[AUTHENTICATION_SECRET_LENGTH];
|
||||
|
||||
private final PrivdIPCConnection[] connections = new PrivdIPCConnection[MAX_CONNECTIONS];
|
||||
private int activeConnection = 0;
|
||||
|
||||
private final IPCConnectionListener connectionListener;
|
||||
|
||||
public IPCServer(IPCConnectionListener connectionListener) {
|
||||
this.connectionListener = connectionListener;
|
||||
try {
|
||||
serverThread = new Thread(this::serverLoop, "geargrinder-ipc-server");
|
||||
// server = SSLServerSocketFactory.getDefault().createServerSocket();
|
||||
server = new ServerSocket();
|
||||
random = SecureRandom.getInstanceStrong();
|
||||
rotate();
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "failed to init IPC server", t);
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ConnectionInitCallback {
|
||||
void onInitComplete(boolean success);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (dead) return;
|
||||
dead = true;
|
||||
serverThread.interrupt();
|
||||
try {
|
||||
if (!server.isClosed())
|
||||
server.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "IOException while closing socket", e);
|
||||
}
|
||||
|
||||
synchronized (connections) {
|
||||
for (int i = 0; i < connections.length; i++) {
|
||||
if (connections[i] == null) continue;
|
||||
connections[i].close();
|
||||
connections[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void rotate() {
|
||||
// keeps the same port number but with different tokens.
|
||||
// this is preferred over spinning up a new server for security.
|
||||
Log.i(TAG, "generating new tokens");
|
||||
random.nextBytes(tokenA);
|
||||
random.nextBytes(tokenB);
|
||||
}
|
||||
|
||||
public void start() throws IOException {
|
||||
server.bind(new InetSocketAddress(BIND_ADDRESS, BIND_PORT));
|
||||
Log.i(TAG, "IPC server started on " + server.getLocalSocketAddress());
|
||||
port = server.getLocalPort();
|
||||
serverThread.start();
|
||||
}
|
||||
|
||||
public byte[] getTokenA() {
|
||||
return tokenA;
|
||||
}
|
||||
|
||||
public byte[] getTokenB() {
|
||||
return tokenB;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public PrivdIPCConnection getActiveConnection() {
|
||||
synchronized (connections) {
|
||||
if (connections[activeConnection] == null) return null;
|
||||
if (!connections[activeConnection].isAlive()) return null;
|
||||
return connections[activeConnection];
|
||||
}
|
||||
}
|
||||
|
||||
private int findFreeConnectionIdLocked() {
|
||||
int id = -1;
|
||||
for (int i = 0; i < connections.length; i++) {
|
||||
if (connections[i] != null) continue;
|
||||
id = i;
|
||||
break;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
private void serverLoop() {
|
||||
try {
|
||||
Log.d(TAG, "server loop start");
|
||||
while (!dead) {
|
||||
Socket socket = server.accept();
|
||||
if (getActiveConnection() != null) {
|
||||
Log.e(TAG, "already connected! refusing connection from " + socket.getRemoteSocketAddress());
|
||||
try {
|
||||
socket.close();
|
||||
} catch (Throwable ignored) {}
|
||||
continue;
|
||||
}
|
||||
|
||||
synchronized (connections) {
|
||||
Log.d(TAG, "connection from " + socket.getRemoteSocketAddress());
|
||||
|
||||
int connectionId = findFreeConnectionIdLocked();
|
||||
if (connectionId == -1) {
|
||||
Log.e(TAG, "too many open connections, dropping");
|
||||
continue;
|
||||
}
|
||||
|
||||
connections[connectionId] = new PrivdIPCConnection(socket, tokenA, tokenB, success -> {
|
||||
synchronized (connections) {
|
||||
if (!success) {
|
||||
connections[connectionId] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
activeConnection = connectionId;
|
||||
connectionListener.onPrivdConnected(connections[connectionId]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "IOException during IPC server loop", e);
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.benwiegand.projection.geargrinder.privileged;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
|
||||
import io.benwiegand.projection.libprivd.ipc.IPCConnection;
|
||||
import io.benwiegand.projection.libprivd.ipc.IPCConstants;
|
||||
|
||||
public class PrivdIPCConnection extends IPCConnection {
|
||||
private static final String TAG = PrivdIPCConnection.class.getSimpleName();
|
||||
|
||||
private final IPCServer.ConnectionInitCallback initCallback;
|
||||
|
||||
public PrivdIPCConnection(Socket socket, byte[] tokenA, byte[] tokenB, IPCServer.ConnectionInitCallback initCallback) throws IOException {
|
||||
super(socket, tokenA, tokenB);
|
||||
this.initCallback = initCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInitComplete(boolean success) {
|
||||
initCallback.onInitComplete(success);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Reply onCommand(int command, byte[] data, int offset, int length) {
|
||||
return switch (command) {
|
||||
case IPCConstants.COMMAND_PING -> {
|
||||
Log.d(TAG, "ping");
|
||||
yield new Reply(IPCConstants.REPLY_SUCCESS);
|
||||
}
|
||||
default -> {
|
||||
Log.wtf(TAG, "unhandled command: " + command);
|
||||
yield new Reply(IPCConstants.REPLY_FAILURE);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClose() {
|
||||
// TODO
|
||||
Log.e(TAG, "connection closed");
|
||||
}
|
||||
}
|
||||
+48
-9
@@ -1,6 +1,11 @@
|
||||
package io.benwiegand.projection.geargrinder.privileged;
|
||||
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_PORT;
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_TOKEN_A;
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_TOKEN_B;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
@@ -8,41 +13,75 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.callback.IPCConnectionListener;
|
||||
|
||||
public class RootPrivdLauncher {
|
||||
private static final String TAG = RootPrivdLauncher.class.getSimpleName();
|
||||
|
||||
private static final String PRIVD_FILE_NAME = "privd.jar";
|
||||
private static final String ENV_CLASSPATH = "CLASSPATH";
|
||||
|
||||
private final Context context;
|
||||
private final IPCConnectionListener connectionListener;
|
||||
private final File daemonFile;
|
||||
|
||||
private IPCServer server = null;
|
||||
private Process rootProcess = null;
|
||||
|
||||
public RootPrivdLauncher(Context context) {
|
||||
public RootPrivdLauncher(Context context, IPCConnectionListener connectionListener) {
|
||||
this.context = context;
|
||||
this.connectionListener = connectionListener;
|
||||
daemonFile = context.getFilesDir().toPath().resolve(PRIVD_FILE_NAME).toFile();
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
killRootProcess();
|
||||
killServer();
|
||||
}
|
||||
|
||||
public void launchRoot() throws IOException {
|
||||
killRootProcess();
|
||||
copyDaemon();
|
||||
startServer();
|
||||
executeDaemon();
|
||||
}
|
||||
|
||||
Log.i(TAG, "launching privd as root");
|
||||
ProcessBuilder procBuilder = new ProcessBuilder("su", "-c", "app_process /system/bin --nice-name=Geargrinder-privd io.benwiegand.projection.geargrinder.privd.Main");
|
||||
procBuilder.environment().put("CLASSPATH", daemonFile.getAbsolutePath());
|
||||
rootProcess = procBuilder.start();
|
||||
private void killServer() {
|
||||
if (server == null) return;
|
||||
Log.i(TAG, "closing IPC server");
|
||||
server.close();
|
||||
server = null;
|
||||
}
|
||||
|
||||
private void killRootProcess() {
|
||||
if (rootProcess != null) {
|
||||
Log.i(TAG, "killing privd root process");
|
||||
rootProcess.destroyForcibly();
|
||||
rootProcess = null;
|
||||
if (rootProcess == null) return;
|
||||
Log.i(TAG, "killing privd root process");
|
||||
rootProcess.destroyForcibly();
|
||||
rootProcess = null;
|
||||
}
|
||||
|
||||
private void startServer() throws IOException {
|
||||
if (server != null) {
|
||||
server.rotate();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "starting IPC server");
|
||||
server = new IPCServer(connectionListener);
|
||||
server.start();
|
||||
}
|
||||
|
||||
private void executeDaemon() throws IOException {
|
||||
assert rootProcess == null;
|
||||
assert server != null;
|
||||
Log.i(TAG, "launching privd as root");
|
||||
ProcessBuilder procBuilder = new ProcessBuilder("su", "-c", "app_process /system/bin --nice-name=Geargrinder-privd io.benwiegand.projection.geargrinder.privd.Main");
|
||||
procBuilder.environment().put(ENV_CLASSPATH, daemonFile.getAbsolutePath());
|
||||
procBuilder.environment().put(ENV_PORT, String.valueOf(server.getPort()));
|
||||
// tokens are swapped for app/daemon
|
||||
procBuilder.environment().put(ENV_TOKEN_A, Base64.encodeToString(server.getTokenB(), Base64.NO_WRAP));
|
||||
procBuilder.environment().put(ENV_TOKEN_B, Base64.encodeToString(server.getTokenA(), Base64.NO_WRAP));
|
||||
rootProcess = procBuilder.start();
|
||||
}
|
||||
|
||||
private void copyDaemon() throws IOException {
|
||||
|
||||
@@ -19,4 +19,5 @@ localbroadcastmanager = { group = "androidx.localbroadcastmanager", name = "loca
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
android-library = { id = "com.android.library" }
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
/build
|
||||
/release
|
||||
/debug
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.benwiegand.projection.libprivd"
|
||||
compileSdk = 36
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package io.benwiegand.projection.libprivd.ipc;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import io.benwiegand.projection.libprivd.sec.Sec;
|
||||
import io.benwiegand.projection.libprivd.sec.SecAdapter;
|
||||
|
||||
public abstract class IPCConnection {
|
||||
private final String TAG = getTag();
|
||||
|
||||
private static final int AUTH_TIMEOUT = 1000;
|
||||
|
||||
private final Map<Integer, SecAdapter<Reply>> pendingReplyMap = new ConcurrentHashMap<>();
|
||||
|
||||
private final Object writeLock = new Object();
|
||||
private final Thread connectionThread;
|
||||
|
||||
private final Socket socket;
|
||||
private final InputStream is;
|
||||
private final OutputStream os;
|
||||
|
||||
private final CountDownLatch initLatch = new CountDownLatch(1);
|
||||
|
||||
private final byte[] readBuffer = new byte[IPCMessage.HEADER_LENGTH + 65535];
|
||||
private final byte[] writeBuffer = new byte[IPCMessage.HEADER_LENGTH + 65535];
|
||||
|
||||
private final byte[] tokenA;
|
||||
private final byte[] tokenB;
|
||||
|
||||
private int msgIdCounter = 0;
|
||||
private boolean dead = false;
|
||||
|
||||
public IPCConnection(Socket socket, byte[] tokenA, byte[] tokenB) throws IOException {
|
||||
this.connectionThread = new Thread(this::connectionLoop, "geargrinder-ipc");
|
||||
this.socket = socket;
|
||||
is = socket.getInputStream();
|
||||
os = socket.getOutputStream();
|
||||
|
||||
this.tokenA = tokenA;
|
||||
this.tokenB = tokenB;
|
||||
|
||||
connectionThread.start();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (dead) return;
|
||||
|
||||
try {
|
||||
if (!socket.isClosed())
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "IOException while closing socket", e);
|
||||
}
|
||||
|
||||
dead = true;
|
||||
connectionThread.interrupt();
|
||||
|
||||
while (!pendingReplyMap.isEmpty()) {
|
||||
Set<Integer> messageIds = new HashSet<>(pendingReplyMap.keySet());
|
||||
for (int msgId : messageIds) {
|
||||
SecAdapter<Reply> adapter = pendingReplyMap.remove(msgId);
|
||||
assert adapter != null;
|
||||
adapter.throwError(new IOException("connection closed"));
|
||||
}
|
||||
}
|
||||
|
||||
onClose();
|
||||
}
|
||||
|
||||
public static final class Reply {
|
||||
public final int status;
|
||||
public final byte[] data;
|
||||
public final int offset;
|
||||
public final int length;
|
||||
|
||||
public Reply(int status, byte[] data, int offset, int length) {
|
||||
this.status = status;
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public Reply(int status, byte[] data) {
|
||||
this(status, data, 0, data.length);
|
||||
}
|
||||
|
||||
public Reply(int status) {
|
||||
this(status, new byte[0]);
|
||||
}
|
||||
|
||||
public static Reply copyFromMessage(IPCMessage msg) {
|
||||
assert msg.isReply();
|
||||
byte[] data = new byte[msg.getDataLength()];
|
||||
System.arraycopy(msg.getBuffer(), IPCMessage.HEADER_LENGTH, data, 0, data.length);
|
||||
return new Reply(msg.getCommand(), data);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract String getTag();
|
||||
|
||||
protected void onInitComplete(boolean success) {
|
||||
|
||||
}
|
||||
|
||||
protected abstract Reply onCommand(int command, byte[] data, int offset, int length);
|
||||
|
||||
protected abstract void onClose();
|
||||
|
||||
public void waitForInit(long timeout) throws InterruptedException, TimeoutException {
|
||||
if (!initLatch.await(timeout, TimeUnit.MILLISECONDS)) {
|
||||
throw new TimeoutException("timed out waiting for IPC init");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
return !dead && initLatch.getCount() != 1 && socket.isConnected();
|
||||
}
|
||||
|
||||
public Sec<Reply> send(int command, byte[] data, int offset, int length) {
|
||||
if (dead) return Sec.premeditatedError(new IOException("connection closed"));
|
||||
SecAdapter.SecWithAdapter<Reply> secWithAdapter = SecAdapter.createThreadless();
|
||||
|
||||
synchronized (writeLock) {
|
||||
int msgId = nextMsgId();
|
||||
pendingReplyMap.put(msgId, secWithAdapter.secAdapter());
|
||||
|
||||
try {
|
||||
writeMsg(new IPCMessage(writeBuffer)
|
||||
.clear()
|
||||
.setMessageId(msgId)
|
||||
.setCommand(command)
|
||||
.copyData(data, offset, length));
|
||||
} catch (Throwable t) {
|
||||
pendingReplyMap.remove(msgId);
|
||||
return Sec.premeditatedError(t);
|
||||
}
|
||||
}
|
||||
|
||||
return secWithAdapter.sec();
|
||||
}
|
||||
|
||||
public Sec<Reply> send(int command, byte[] data) {
|
||||
return send(command, data, 0, data.length);
|
||||
}
|
||||
|
||||
public Sec<Reply> send(int command, Parcelable parcelable) {
|
||||
Parcel parcel = Parcel.obtain();
|
||||
parcelable.writeToParcel(parcel, 0);
|
||||
|
||||
Sec<Reply> sec = send(command, parcel.marshall());
|
||||
|
||||
parcel.recycle();
|
||||
return sec;
|
||||
}
|
||||
|
||||
public Sec<Reply> send(int command) {
|
||||
return send(command, new byte[0]);
|
||||
}
|
||||
|
||||
private int nextMsgId() {
|
||||
if (msgIdCounter++ >= 0xffff) msgIdCounter = 0;
|
||||
return msgIdCounter;
|
||||
}
|
||||
|
||||
private void readAll(InputStream is, byte[] buffer, int offset, int length) throws IOException {
|
||||
int len, i = 0;
|
||||
while (i < length) {
|
||||
len = is.read(buffer, i + offset, length - i);
|
||||
if (len < 0) throw new IOException("EOS (" + len + ")");
|
||||
i += len;
|
||||
}
|
||||
}
|
||||
|
||||
private void readAllTimeout(InputStream is, byte[] buffer, int offset, int length, int timeout) throws IOException {
|
||||
int initialSocketTimeout = socket.getSoTimeout();
|
||||
long deadline = SystemClock.elapsedRealtime() + timeout;
|
||||
int len, i = 0;
|
||||
try {
|
||||
while (i < length && SystemClock.elapsedRealtime() < deadline) {
|
||||
int remaining = (int) (deadline - SystemClock.elapsedRealtime());
|
||||
if (remaining > 0) socket.setSoTimeout(remaining);
|
||||
|
||||
len = is.read(buffer, i + offset, length - i);
|
||||
if (len < 0) throw new IOException("EOS (" + len + ")");
|
||||
i += len;
|
||||
}
|
||||
} catch (SocketTimeoutException e) {
|
||||
throw new IOException("read timed out", e);
|
||||
}
|
||||
|
||||
if (i < length) throw new IOException("read timed out");
|
||||
socket.setSoTimeout(initialSocketTimeout);
|
||||
}
|
||||
|
||||
private void writeMsg(IPCMessage msg) throws IOException {
|
||||
os.write(msg.getBuffer(), 0, IPCMessage.HEADER_LENGTH + msg.getDataLength());
|
||||
}
|
||||
|
||||
private void readMsg(IPCMessage msg) throws IOException {
|
||||
readAll(is, msg.getBuffer(), 0, IPCMessage.HEADER_LENGTH);
|
||||
if (msg.getDataLength() > 0)
|
||||
readAll(is, msg.getBuffer(), IPCMessage.HEADER_LENGTH, msg.getDataLength());
|
||||
}
|
||||
|
||||
private void sendReply(Reply reply, int messageId) throws IOException {
|
||||
synchronized (writeLock) {
|
||||
writeMsg(new IPCMessage(writeBuffer)
|
||||
.clear()
|
||||
.setMessageId(messageId)
|
||||
.setFlags(IPCConstants.FLAG_REPLY)
|
||||
.setCommand(reply.status)
|
||||
.copyData(reply.data, reply.offset, reply.length));
|
||||
}
|
||||
}
|
||||
|
||||
private void connectionLoop() {
|
||||
// read loop, write is handled by calling thread
|
||||
Log.d(TAG, "start connection loop");
|
||||
try {
|
||||
// auth
|
||||
socket.setSoTimeout(AUTH_TIMEOUT);
|
||||
os.write(tokenA, 0, tokenA.length);
|
||||
readAllTimeout(is, readBuffer, 0, tokenB.length, AUTH_TIMEOUT);
|
||||
|
||||
boolean valid = true;
|
||||
for (int i = 0; i < tokenB.length; i++) {
|
||||
if (tokenB[i] == readBuffer[i]) continue;
|
||||
valid = false;
|
||||
// don't break: timing could give away token
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
Log.e(TAG, "auth failed for " + socket.getRemoteSocketAddress());
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "auth succeeded for " + socket.getRemoteSocketAddress());
|
||||
socket.setSoTimeout(0);
|
||||
initLatch.countDown();
|
||||
onInitComplete(true);
|
||||
|
||||
IPCMessage msg = new IPCMessage(readBuffer);
|
||||
while (socket.isConnected()) {
|
||||
readMsg(msg);
|
||||
|
||||
if (msg.isReply()) {
|
||||
SecAdapter<Reply> adapter = pendingReplyMap.remove(msg.getMessageId());
|
||||
if (adapter == null) {
|
||||
Log.wtf(TAG, "couldn't find pending reply entry for reply message: " + msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
adapter.provideResult(Reply.copyFromMessage(msg));
|
||||
continue;
|
||||
}
|
||||
|
||||
Reply reply = onCommand(msg.getCommand(), msg.getBuffer(), IPCMessage.HEADER_LENGTH, msg.getDataLength());
|
||||
sendReply(reply, msg.getMessageId());
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "IOException in connection loop", e);
|
||||
} finally {
|
||||
if (initLatch.getCount() > 0) {
|
||||
initLatch.countDown();
|
||||
onInitComplete(false);
|
||||
}
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.benwiegand.projection.libprivd.ipc;
|
||||
|
||||
public class IPCConstants {
|
||||
|
||||
public static final long PING_INTERVAL = 1000;
|
||||
public static final String ENV_PORT = "PORT";
|
||||
public static final String ENV_TOKEN_A = "TOKEN_A";
|
||||
public static final String ENV_TOKEN_B = "TOKEN_B";
|
||||
|
||||
|
||||
public static final int FLAG_REPLY = 1;
|
||||
|
||||
public static final int COMMAND_PING = 0;
|
||||
|
||||
public static final int REPLY_SUCCESS = 0;
|
||||
public static final int REPLY_FAILURE = 1;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package io.benwiegand.projection.libprivd.ipc;
|
||||
|
||||
import static io.benwiegand.projection.libprivd.util.ByteUtil.readUInt16;
|
||||
import static io.benwiegand.projection.libprivd.util.ByteUtil.writeUInt16;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class IPCMessage {
|
||||
private static final int MESSAGE_ID_OFFSET = 0;
|
||||
private static final int FLAGS_OFFSET = 2;
|
||||
private static final int CMD_OFFSET = 3;
|
||||
private static final int DATA_LEN_OFFSET = 4;
|
||||
private static final int DATA_OFFSET = 6;
|
||||
|
||||
public static final int HEADER_LENGTH = 6;
|
||||
private final byte[] buffer;
|
||||
|
||||
public IPCMessage(byte[] buffer) {
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
public byte[] getBuffer() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public int getMessageId() {
|
||||
return readUInt16(buffer, MESSAGE_ID_OFFSET);
|
||||
}
|
||||
|
||||
public IPCMessage setMessageId(int msgId) {
|
||||
assert msgId >= 0 && msgId <= 0xffff;
|
||||
writeUInt16(msgId, buffer, MESSAGE_ID_OFFSET);
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getFlags() {
|
||||
return buffer[FLAGS_OFFSET];
|
||||
}
|
||||
|
||||
public IPCMessage setFlags(int flags) {
|
||||
assert flags >= 0 && flags <= 0xff;
|
||||
buffer[FLAGS_OFFSET] = (byte) flags;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getCommand() {
|
||||
return buffer[CMD_OFFSET];
|
||||
}
|
||||
|
||||
public IPCMessage setCommand(int cmd) {
|
||||
assert cmd >= 0 && cmd <= 0xff;
|
||||
buffer[CMD_OFFSET] = (byte) cmd;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getDataLength() {
|
||||
return readUInt16(buffer, DATA_LEN_OFFSET);
|
||||
}
|
||||
|
||||
public IPCMessage setDataLength(int dataLength) {
|
||||
assert dataLength >= 0 && dataLength <= 0xffff;
|
||||
writeUInt16(dataLength, buffer, DATA_LEN_OFFSET);
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isReply() {
|
||||
return (getFlags() & IPCConstants.FLAG_REPLY) != 0;
|
||||
}
|
||||
|
||||
public IPCMessage copyData(byte[] src, int offset, int length) {
|
||||
System.arraycopy(src, offset, buffer, DATA_OFFSET, length);
|
||||
setDataLength(length);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IPCMessage clear() {
|
||||
Arrays.fill(buffer, 0, HEADER_LENGTH, (byte) 0);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IPCMessage{" +
|
||||
"buffer=(" + buffer.length + " bytes)" +
|
||||
", messageId=" + getMessageId() +
|
||||
", flags=" + getFlags() +
|
||||
", command=" + getCommand() +
|
||||
", dataLength=" + getDataLength() +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package io.benwiegand.projection.libprivd.sec;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class PendingSec<T> {
|
||||
private final Object lock = new Object();
|
||||
|
||||
private boolean started = false;
|
||||
private boolean wrapped = false;
|
||||
|
||||
private final Supplier<Sec<T>> secStarter;
|
||||
|
||||
PendingSec(Supplier<Sec<T>> secStarter) {
|
||||
this.secStarter = secStarter;
|
||||
}
|
||||
|
||||
public Sec<T> start() {
|
||||
synchronized (lock) {
|
||||
if (started) throw new IllegalStateException("already started");
|
||||
started = true;
|
||||
}
|
||||
return secStarter.get();
|
||||
}
|
||||
|
||||
private <U> PendingSec<U> wrapSec(Function<Sec<T>, Sec<U>> mapping) {
|
||||
synchronized (lock) {
|
||||
if (started) throw new IllegalStateException("can't wrap after call to start()");
|
||||
if (wrapped) throw new IllegalStateException("already wrapped!");
|
||||
wrapped = true;
|
||||
return new PendingSec<>(() -> mapping.apply(this.start()));
|
||||
}
|
||||
}
|
||||
|
||||
public <U> PendingSec<U> flatMapSec(Function<T, Sec<U>> mapping) {
|
||||
return wrapSec(sec -> sec.flatMap(mapping));
|
||||
}
|
||||
|
||||
public <U> PendingSec<U> mapSec(Function<T, U> mapping) {
|
||||
return wrapSec(sec -> sec.map(mapping));
|
||||
}
|
||||
|
||||
public <U> PendingSec<U> flatMap(Function<T, PendingSec<U>> mapping) {
|
||||
return flatMapSec(r -> mapping.apply(r).start());
|
||||
}
|
||||
|
||||
public static PendingSec<Void> createDelay(long delay) {
|
||||
return new PendingSec<>(() -> {
|
||||
SecAdapter.SecWithAdapter<Void> secWithAdapter = SecAdapter.createThreadless();
|
||||
boolean started = new Handler(Looper.getMainLooper())
|
||||
.postDelayed(() -> secWithAdapter.secAdapter().provideResult(null), delay);
|
||||
if (!started) return Sec.premeditatedError(new RejectedExecutionException("main looper is dead"));
|
||||
return secWithAdapter.sec();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package io.benwiegand.projection.libprivd.sec;
|
||||
|
||||
import static io.benwiegand.projection.libprivd.sec.SecAdapter.createThreadless;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
// as in "just a sec"
|
||||
public class Sec<T> {
|
||||
private static final String TAG = Sec.class.getSimpleName();
|
||||
|
||||
private final Object lock = new Object();
|
||||
private boolean finished = false;
|
||||
private T result = null;
|
||||
private Throwable error = null;
|
||||
|
||||
private boolean callbacksSet = false;
|
||||
private boolean callbacksCalled = false;
|
||||
private Consumer<T> onResult = null;
|
||||
private Consumer<Throwable> onError = null;
|
||||
|
||||
Sec() {}
|
||||
|
||||
Adapter createAdapter() {
|
||||
return new Adapter();
|
||||
}
|
||||
|
||||
public boolean isFinished() {
|
||||
synchronized (lock) {
|
||||
return finished;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSuccessful() {
|
||||
synchronized (lock) {
|
||||
if (!finished) throw new IllegalStateException("not finished, success is not yet known");
|
||||
|
||||
return error == null;
|
||||
}
|
||||
}
|
||||
|
||||
public T getResult() {
|
||||
synchronized (lock) {
|
||||
if (!finished) throw new IllegalStateException("not finished, result doesn't exist yet");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public Throwable getError() {
|
||||
synchronized (lock) {
|
||||
if (!finished) throw new IllegalStateException("not finished, error doesn't exist yet");
|
||||
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
public T getResultOrThrow() throws Throwable {
|
||||
synchronized (lock) {
|
||||
if (!finished) throw new IllegalStateException("not finished, result/error doesn't exist yet");
|
||||
|
||||
if (error != null) throw error;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public Sec<T> doOnResult(Consumer<T> onResult) {
|
||||
synchronized (lock) {
|
||||
if (callbacksSet) throw new IllegalStateException("callbacks already set up");
|
||||
this.onResult = onResult;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public Sec<T> doOnError(Consumer<Throwable> onError) {
|
||||
synchronized (lock) {
|
||||
if (callbacksSet) throw new IllegalStateException("callbacks already set up");
|
||||
this.onError = onError;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static <T, U> Consumer<T> applyMap(Function<T, U> mapper, Consumer<U> downstreamResult, Consumer<Throwable> downstreamError) {
|
||||
return r -> {
|
||||
U mapped;
|
||||
try {
|
||||
mapped = mapper.apply(r);
|
||||
} catch (Throwable t) {
|
||||
downstreamError.accept(t);
|
||||
return;
|
||||
}
|
||||
downstreamResult.accept(mapped);
|
||||
};
|
||||
}
|
||||
|
||||
public <U> Sec<U> map(Function<T, U> map) {
|
||||
// for now just use the callbacks. this may change in the future
|
||||
SecAdapter.SecWithAdapter<U> secWithAdapter;
|
||||
synchronized (lock) {
|
||||
if (callbacksSet) throw new IllegalStateException("callbacks already set up");
|
||||
callbacksSet = true;
|
||||
|
||||
secWithAdapter = createThreadless();
|
||||
|
||||
SecAdapter<U> adapter = secWithAdapter.secAdapter();
|
||||
this.onResult = applyMap(map, adapter::provideResult, adapter::throwError);
|
||||
this.onError = adapter::throwError;
|
||||
}
|
||||
|
||||
if (isFinished()) callCallbacks();
|
||||
|
||||
return secWithAdapter.sec();
|
||||
}
|
||||
|
||||
public Sec<T> mapError(Function<Throwable, Throwable> map) {
|
||||
// for now just use the callbacks. this may change in the future
|
||||
SecAdapter.SecWithAdapter<T> secWithAdapter;
|
||||
synchronized (lock) {
|
||||
if (callbacksSet) throw new IllegalStateException("callbacks already set up");
|
||||
callbacksSet = true;
|
||||
|
||||
secWithAdapter = createThreadless();
|
||||
|
||||
SecAdapter<T> adapter = secWithAdapter.secAdapter();
|
||||
this.onResult = adapter::provideResult;
|
||||
this.onError = applyMap(map, adapter::throwError, adapter::throwError);
|
||||
}
|
||||
|
||||
if (isFinished()) callCallbacks();
|
||||
|
||||
return secWithAdapter.sec();
|
||||
}
|
||||
|
||||
public <U> Sec<U> flatMap(Function<T, Sec<U>> map) {
|
||||
// for now just use the callbacks. this may change in the future
|
||||
SecAdapter.SecWithAdapter<U> secWithAdapter;
|
||||
synchronized (lock) {
|
||||
if (callbacksSet) throw new IllegalStateException("callbacks already set up");
|
||||
callbacksSet = true;
|
||||
|
||||
secWithAdapter = createThreadless();
|
||||
|
||||
SecAdapter<U> adapter = secWithAdapter.secAdapter();
|
||||
this.onResult = applyMap(
|
||||
map,
|
||||
nextSec -> nextSec
|
||||
.doOnResult(adapter::provideResult)
|
||||
.doOnError(adapter::throwError)
|
||||
.callMeWhenDone(),
|
||||
adapter::throwError);
|
||||
this.onError = adapter::throwError;
|
||||
}
|
||||
|
||||
if (isFinished()) callCallbacks();
|
||||
|
||||
return secWithAdapter.sec();
|
||||
|
||||
}
|
||||
|
||||
public void callMeWhenDone() {
|
||||
synchronized (lock) {
|
||||
if (callbacksSet) throw new IllegalStateException("callMeWhenDone() cannot be called twice");
|
||||
callbacksSet = true;
|
||||
}
|
||||
|
||||
if (isFinished()) callCallbacks();
|
||||
}
|
||||
|
||||
private void callCallbacks() {
|
||||
synchronized (lock) {
|
||||
assert finished;
|
||||
if (!callbacksSet) return; // callbacks aren't ready
|
||||
if (callbacksCalled) return; // don't call back twice
|
||||
callbacksCalled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (error == null && onResult != null)
|
||||
onResult.accept(result);
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "error during onResult callback", t);
|
||||
}
|
||||
|
||||
try {
|
||||
if (error != null && onError != null)
|
||||
onError.accept(error);
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "error during onError callback", t);
|
||||
}
|
||||
}
|
||||
|
||||
private class Adapter implements SecAdapter<T> {
|
||||
@Override
|
||||
public void provideResult(T r) {
|
||||
synchronized (lock) {
|
||||
if (finished) throw new IllegalStateException("a result or error has already been provided");
|
||||
finished = true;
|
||||
|
||||
result = r;
|
||||
}
|
||||
|
||||
callCallbacks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void throwError(Throwable t) {
|
||||
if (t == null) throw new IllegalArgumentException("throwable cannot be null");
|
||||
synchronized (lock) {
|
||||
if (finished) throw new IllegalStateException("a result or error has already been provided");
|
||||
finished = true;
|
||||
|
||||
error = t;
|
||||
}
|
||||
|
||||
callCallbacks();
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> Sec<T> premeditatedError(Throwable t) {
|
||||
SecAdapter.SecWithAdapter<T> secWithAdapter = createThreadless();
|
||||
secWithAdapter.secAdapter().throwError(t);
|
||||
return secWithAdapter.sec();
|
||||
}
|
||||
|
||||
public static <T> Sec<T> premeditated(T r) {
|
||||
SecAdapter.SecWithAdapter<T> secWithAdapter = createThreadless();
|
||||
secWithAdapter.secAdapter().provideResult(r);
|
||||
return secWithAdapter.sec();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package io.benwiegand.projection.libprivd.sec;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public interface SecAdapter<T> {
|
||||
String TAG = SecAdapter.class.getSimpleName();
|
||||
|
||||
void provideResult(T result);
|
||||
void throwError(Throwable t);
|
||||
|
||||
final class SecWithAdapter<T> {
|
||||
private final Sec<T> sec;
|
||||
private final SecAdapter<T> secAdapter;
|
||||
|
||||
public SecWithAdapter(Sec<T> sec, SecAdapter<T> secAdapter) {
|
||||
this.sec = sec;
|
||||
this.secAdapter = secAdapter;
|
||||
}
|
||||
|
||||
public Sec<T> sec() {
|
||||
return sec;
|
||||
}
|
||||
|
||||
public SecAdapter<T> secAdapter() {
|
||||
return secAdapter;
|
||||
}
|
||||
}
|
||||
|
||||
static <T> SecWithAdapter<T> createThreadless() {
|
||||
|
||||
Sec<T> sec = new Sec<>();
|
||||
SecAdapter<T> adapter = sec.createAdapter();
|
||||
|
||||
return new SecWithAdapter<>(sec, adapter);
|
||||
|
||||
}
|
||||
|
||||
private static Executor handlerAsExecutor(Handler handler) {
|
||||
return runnable -> {
|
||||
if (!handler.post(runnable))
|
||||
throw new RejectedExecutionException("handler is dead");
|
||||
};
|
||||
}
|
||||
|
||||
static <T> PendingSec<T> create(Executor executor, Consumer<SecAdapter<T>> deferredResult) {
|
||||
return new PendingSec<>(() -> {
|
||||
Sec<T> sec = new Sec<>();
|
||||
|
||||
try {
|
||||
executor.execute(() -> {
|
||||
SecAdapter<T> adapter = sec.createAdapter();
|
||||
try {
|
||||
deferredResult.accept(adapter);
|
||||
} catch (Throwable t) {
|
||||
// this situation should generally be avoided
|
||||
Log.wtf("SecAdapter", "deferred sec handler threw!", t);
|
||||
try {
|
||||
// ensure sec at least gets finished
|
||||
if (!sec.isFinished()) adapter.throwError(t);
|
||||
} catch (Throwable ignored) {}
|
||||
|
||||
// this will crash the app anyway
|
||||
throw t;
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
return Sec.premeditatedError(t);
|
||||
}
|
||||
|
||||
return sec;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
static <T> PendingSec<T> create(Handler handler, Consumer<SecAdapter<T>> deferredResult) {
|
||||
return create(handlerAsExecutor(handler), deferredResult);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package io.benwiegand.projection.libprivd.util;
|
||||
|
||||
public class ByteUtil {
|
||||
|
||||
public static int unsignByte(byte b) {
|
||||
return Byte.toUnsignedInt(b);
|
||||
}
|
||||
|
||||
public static int writeUInt16(int value, byte[] buffer, int offset) {
|
||||
assert value >= 0;
|
||||
assert value <= 0xFFFF;
|
||||
buffer[offset] = (byte) (value >> 8);
|
||||
buffer[offset+1] = (byte) (value & 0xff);
|
||||
return 2;
|
||||
}
|
||||
|
||||
public static int readUInt16(byte[] buffer, int offset) {
|
||||
return (unsignByte(buffer[offset]) << 8) + unsignByte(buffer[offset + 1]);
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -26,21 +26,30 @@ androidComponents {
|
||||
group = "build"
|
||||
description = "privileged daemon jar asset"
|
||||
dependsOn(tasks.build)
|
||||
dependsOn(":libprivd:build")
|
||||
|
||||
val outputJar = rootProject.projectDir.resolve("app/src/main/assets/privd.jar")
|
||||
val androidJar = "${android.sdkDirectory.path}/platforms/android-${android.defaultConfig.targetSdk}/android.jar"
|
||||
val classesDir = layout.buildDirectory.dir("intermediates/javac/${buildType}/compile${buildTypeUpper}JavaWithJavac/classes")
|
||||
val libClassesDir = rootProject.projectDir.resolve("libprivd/build/intermediates/javac/${buildType}/compile${buildTypeUpper}JavaWithJavac/classes")
|
||||
val classFiles = Files.walk(file(classesDir).toPath())
|
||||
.filter { it.toFile().isFile() }
|
||||
.toArray()
|
||||
val libClassFiles = Files.walk(file(libClassesDir).toPath())
|
||||
.filter { it.toFile().isFile() }
|
||||
.toArray()
|
||||
|
||||
commandLine(
|
||||
"${android.sdkDirectory}/build-tools/${android.buildToolsVersion}/d8",
|
||||
"--release",
|
||||
"--output", outputJar,
|
||||
"--classpath", androidJar,
|
||||
*classFiles
|
||||
*classFiles, *libClassFiles
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":libprivd"))
|
||||
}
|
||||
|
||||
@@ -1,28 +1,98 @@
|
||||
package io.benwiegand.projection.geargrinder.privd;
|
||||
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.COMMAND_PING;
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_PORT;
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_TOKEN_A;
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_TOKEN_B;
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.PING_INTERVAL;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.privd.reflection.ReflectionException;
|
||||
import io.benwiegand.projection.geargrinder.privd.ipc.IPCClient;
|
||||
import io.benwiegand.projection.geargrinder.privd.reflection.reflected.ReflectedActivityThread;
|
||||
import io.benwiegand.projection.libprivd.ipc.IPCConnection;
|
||||
|
||||
public class Main {
|
||||
private static final String TAG = "privd-" + Main.class.getSimpleName();
|
||||
|
||||
private static final long IPC_INIT_TIMEOUT = 5000;
|
||||
|
||||
private static void pingTask(Handler handler, IPCConnection connection) {
|
||||
connection.send(COMMAND_PING)
|
||||
.doOnResult(reply -> Log.i(TAG, "pong: " + reply.status))
|
||||
.doOnError(t -> {
|
||||
Log.e(TAG, "failed to ping", t);
|
||||
System.exit(1);
|
||||
})
|
||||
.callMeWhenDone();
|
||||
|
||||
if (!handler.postDelayed(() -> pingTask(handler, connection), PING_INTERVAL)) {
|
||||
Log.w(TAG, "failed to post next ping task, bailing");
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Log.wtf("Testing123", "hello from privd");
|
||||
Log.i(TAG, "Geargrinder privd");
|
||||
|
||||
int port;
|
||||
byte[] tokenA;
|
||||
byte[] tokenB;
|
||||
try {
|
||||
String portString = System.getenv(ENV_PORT);
|
||||
String tokenAString = System.getenv(ENV_TOKEN_A);
|
||||
String tokenBString = System.getenv(ENV_TOKEN_B);
|
||||
|
||||
if (portString == null || tokenAString == null || tokenBString == null)
|
||||
throw new AssertionError("missing required environment variable(s)");
|
||||
|
||||
port = Integer.parseInt(portString);
|
||||
tokenA = Base64.decode(tokenAString, 0);
|
||||
tokenB = Base64.decode(tokenBString, 0);
|
||||
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "failed to parse environment", t);
|
||||
System.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
IPCClient ipcClient;
|
||||
IPCConnection connection;
|
||||
try {
|
||||
ipcClient = new IPCClient(port, tokenA, tokenB);
|
||||
connection = ipcClient.connect();
|
||||
connection.waitForInit(IPC_INIT_TIMEOUT);
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "failed to open IPC connection", t);
|
||||
System.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
Looper.prepareMainLooper();
|
||||
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
if (!handler.post(() -> pingTask(handler, connection))) {
|
||||
Log.e(TAG, "failed to post ping task");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
ReflectedActivityThread activityThread = new ReflectedActivityThread();
|
||||
|
||||
Context context = activityThread.getSystemContext();
|
||||
System.out.println("got a system context: " + context);
|
||||
Log.i(TAG, "got a system context: " + context);
|
||||
|
||||
} catch (ReflectionException e) {
|
||||
throw new RuntimeException(e);
|
||||
Log.e(TAG, "failed to get system context", e);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
Looper.loop();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package io.benwiegand.projection.geargrinder.privd.ipc;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
|
||||
import io.benwiegand.projection.libprivd.ipc.IPCConnection;
|
||||
import io.benwiegand.projection.libprivd.ipc.IPCConstants;
|
||||
|
||||
public class AppIPCConnection extends IPCConnection {
|
||||
private static final String TAG = AppIPCConnection.class.getSimpleName();
|
||||
|
||||
public AppIPCConnection(Socket socket, byte[] tokenA, byte[] tokenB) throws IOException {
|
||||
super(socket, tokenA, tokenB);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Reply onCommand(int command, byte[] data, int offset, int length) {
|
||||
return switch (command) {
|
||||
case IPCConstants.COMMAND_PING -> {
|
||||
Log.d(TAG, "ping");
|
||||
yield new Reply(IPCConstants.REPLY_SUCCESS);
|
||||
}
|
||||
default -> {
|
||||
Log.wtf(TAG, "unhandled command: " + command);
|
||||
yield new Reply(IPCConstants.REPLY_FAILURE);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClose() {
|
||||
// daemon has no reason to run without an app
|
||||
Log.e(TAG, "connection closed");
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package io.benwiegand.projection.geargrinder.privd.ipc;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
|
||||
import io.benwiegand.projection.libprivd.ipc.IPCConnection;
|
||||
|
||||
public class IPCClient {
|
||||
private static final String TAG = IPCClient.class.getSimpleName();
|
||||
|
||||
private static final InetAddress SERVER_ADDRESS = InetAddress.getLoopbackAddress();
|
||||
|
||||
private final Socket socket;
|
||||
private final int port;
|
||||
|
||||
private final byte[] tokenA;
|
||||
private final byte[] tokenB;
|
||||
|
||||
public IPCClient(int port, byte[] tokenA, byte[] tokenB) throws IOException {
|
||||
// TODO
|
||||
// socket = SSLSocketFactory.getDefault().createSocket();
|
||||
socket = new Socket();
|
||||
this.port = port;
|
||||
this.tokenA = tokenA;
|
||||
this.tokenB = tokenB;
|
||||
}
|
||||
|
||||
public IPCConnection connect() throws IOException {
|
||||
InetSocketAddress socketAddress = new InetSocketAddress(SERVER_ADDRESS, port);
|
||||
Log.i(TAG, "connecting to " + socketAddress);
|
||||
socket.connect(socketAddress);
|
||||
return new AppIPCConnection(socket, tokenA, tokenB);
|
||||
}
|
||||
}
|
||||
@@ -22,3 +22,4 @@ dependencyResolutionManagement {
|
||||
rootProject.name = "Geargrinder"
|
||||
include(":app")
|
||||
include(":privd")
|
||||
include(":libprivd")
|
||||
|
||||
Reference in New Issue
Block a user