From 60cc63ed60f8bf401bf48655ae96123cd0b49511 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 22 Aug 2020 23:43:14 +0200 Subject: [PATCH] GCM: Add support for message acking, deliver to all receivers of package when working permissionless --- .../java/org/microg/gms/gcm/GcmConstants.java | 1 + .../java/org/microg/gms/gcm/McsConstants.java | 2 + .../java/org/microg/gms/gcm/McsService.java | 112 +++++++++++----- .../microg/gms/gcm/PushRegisterHandler.java | 126 ++++++++++++------ 4 files changed, 168 insertions(+), 73 deletions(-) diff --git a/play-services-basement/src/main/java/org/microg/gms/gcm/GcmConstants.java b/play-services-basement/src/main/java/org/microg/gms/gcm/GcmConstants.java index d33bbfde..3b6a55de 100644 --- a/play-services-basement/src/main/java/org/microg/gms/gcm/GcmConstants.java +++ b/play-services-basement/src/main/java/org/microg/gms/gcm/GcmConstants.java @@ -30,6 +30,7 @@ public final class GcmConstants { public static final String ACTION_INSTANCE_ID = "com.google.android.gms.iid.InstanceID"; public static final String EXTRA_APP = "app"; + public static final String EXTRA_APP_OVERRIDE = "org.microg.gms.gcm.APP_OVERRIDE"; public static final String EXTRA_APP_ID = "appid"; public static final String EXTRA_APP_VERSION_CODE = "app_ver"; public static final String EXTRA_APP_VERSION_NAME = "app_ver_name"; diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/McsConstants.java b/play-services-core/src/main/java/org/microg/gms/gcm/McsConstants.java index 300dac0c..0b3108dd 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/McsConstants.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/McsConstants.java @@ -36,10 +36,12 @@ public final class McsConstants { public static final int MSG_TEARDOWN = 30; public static final int MSG_CONNECT = 40; public static final int MSG_HEARTBEAT = 41; + public static final int MSG_ACK = 42; public static String ACTION_CONNECT = "org.microg.gms.gcm.mcs.CONNECT"; public static String ACTION_RECONNECT = "org.microg.gms.gcm.mcs.RECONNECT"; public static String ACTION_HEARTBEAT = "org.microg.gms.gcm.mcs.HEARTBEAT"; public static String ACTION_SEND = "org.microg.gms.gcm.mcs.SEND"; + public static String ACTION_ACK = "org.microg.gms.gcm.mcs.ACK"; public static String EXTRA_REASON = "org.microg.gms.gcm.mcs.REASON"; } diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java b/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java index 238c1c55..32a91736 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java @@ -26,6 +26,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.pm.PermissionInfo; import android.content.pm.ResolveInfo; import android.net.ConnectivityManager; import android.os.Build; @@ -51,8 +52,10 @@ import org.microg.gms.common.PackageUtils; import org.microg.gms.gcm.mcs.AppData; import org.microg.gms.gcm.mcs.Close; import org.microg.gms.gcm.mcs.DataMessageStanza; +import org.microg.gms.gcm.mcs.Extension; import org.microg.gms.gcm.mcs.HeartbeatAck; import org.microg.gms.gcm.mcs.HeartbeatPing; +import org.microg.gms.gcm.mcs.IqStanza; import org.microg.gms.gcm.mcs.LoginRequest; import org.microg.gms.gcm.mcs.LoginResponse; import org.microg.gms.gcm.mcs.Setting; @@ -64,6 +67,7 @@ import java.net.Socket; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import javax.net.ssl.SSLContext; @@ -74,6 +78,7 @@ import static android.os.Build.VERSION.SDK_INT; import static org.microg.gms.common.ForegroundServiceContext.EXTRA_FOREGROUND; import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_RECEIVE; import static org.microg.gms.gcm.GcmConstants.EXTRA_APP; +import static org.microg.gms.gcm.GcmConstants.EXTRA_APP_OVERRIDE; import static org.microg.gms.gcm.GcmConstants.EXTRA_COLLAPSE_KEY; import static org.microg.gms.gcm.GcmConstants.EXTRA_FROM; import static org.microg.gms.gcm.GcmConstants.EXTRA_MESSAGE_ID; @@ -82,6 +87,7 @@ import static org.microg.gms.gcm.GcmConstants.EXTRA_REGISTRATION_ID; import static org.microg.gms.gcm.GcmConstants.EXTRA_SEND_FROM; import static org.microg.gms.gcm.GcmConstants.EXTRA_SEND_TO; import static org.microg.gms.gcm.GcmConstants.EXTRA_TTL; +import static org.microg.gms.gcm.McsConstants.ACTION_ACK; import static org.microg.gms.gcm.McsConstants.ACTION_CONNECT; import static org.microg.gms.gcm.McsConstants.ACTION_HEARTBEAT; import static org.microg.gms.gcm.McsConstants.ACTION_RECONNECT; @@ -91,8 +97,10 @@ import static org.microg.gms.gcm.McsConstants.MCS_CLOSE_TAG; import static org.microg.gms.gcm.McsConstants.MCS_DATA_MESSAGE_STANZA_TAG; import static org.microg.gms.gcm.McsConstants.MCS_HEARTBEAT_ACK_TAG; import static org.microg.gms.gcm.McsConstants.MCS_HEARTBEAT_PING_TAG; +import static org.microg.gms.gcm.McsConstants.MCS_IQ_STANZA_TAG; import static org.microg.gms.gcm.McsConstants.MCS_LOGIN_REQUEST_TAG; import static org.microg.gms.gcm.McsConstants.MCS_LOGIN_RESPONSE_TAG; +import static org.microg.gms.gcm.McsConstants.MSG_ACK; import static org.microg.gms.gcm.McsConstants.MSG_CONNECT; import static org.microg.gms.gcm.McsConstants.MSG_HEARTBEAT; import static org.microg.gms.gcm.McsConstants.MSG_INPUT; @@ -119,6 +127,7 @@ public class McsService extends Service implements Handler.Callback { private static long lastIncomingNetworkRealtime = 0; private static long startTimestamp = 0; public static String activeNetworkPref = null; + private AtomicInteger nextMessageId = new AtomicInteger(0x1000000); private static Socket sslSocket; private static McsInputStream inputStream; @@ -307,6 +316,8 @@ public class McsService extends Service implements Handler.Callback { rootHandler.sendMessage(rootHandler.obtainMessage(MSG_HEARTBEAT, reason)); } else if (ACTION_SEND.equals(intent.getAction())) { handleSendMessage(intent); + } else if (ACTION_ACK.equals(intent.getAction())) { + rootHandler.sendMessage(rootHandler.obtainMessage(MSG_ACK, reason)); } WakefulBroadcastReceiver.completeWakefulIntent(intent); } else if (connectIntent == null) { @@ -347,12 +358,20 @@ public class McsService extends Service implements Handler.Callback { Log.w(TAG, "Failed to send message, missing package name"); return; } + if (packageName.equals(getPackageName()) && intent.hasExtra(EXTRA_APP_OVERRIDE)) { + packageName = intent.getStringExtra(EXTRA_APP_OVERRIDE); + intent.removeExtra(EXTRA_APP_OVERRIDE); + } intent.removeExtra(EXTRA_APP); int ttl; try { - ttl = Integer.parseInt(intent.getStringExtra(EXTRA_TTL)); - if (ttl < 0 || ttl > maxTtl) { + if (intent.hasExtra(EXTRA_TTL)) { + ttl = Integer.parseInt(intent.getStringExtra(EXTRA_TTL)); + if (ttl < 0 || ttl > maxTtl) { + ttl = maxTtl; + } + } else { ttl = maxTtl; } } catch (NumberFormatException e) { @@ -403,7 +422,8 @@ public class McsService extends Service implements Handler.Callback { try { DataMessageStanza msg = new DataMessageStanza.Builder() .sent(System.currentTimeMillis() / 1000L) - .id(messageId) + .id(Integer.toHexString(nextMessageId.incrementAndGet())) + .persistent_id(messageId) .token(collapseKey) .from(from) .reg_id(registrationId) @@ -513,9 +533,11 @@ public class McsService extends Service implements Handler.Callback { Intent intent = new Intent(); intent.setAction(ACTION_C2DM_RECEIVE); - intent.setPackage(packageName); intent.putExtra(EXTRA_FROM, msg.from); intent.putExtra(EXTRA_MESSAGE_ID, msg.id); + if (msg.persistent_id != null) { + intent.putExtra(EXTRA_MESSAGE_ID, msg.persistent_id); + } if (app.wakeForDelivery) { intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); } else { @@ -526,41 +548,52 @@ public class McsService extends Service implements Handler.Callback { intent.putExtra(appData.key, appData.value); } - String receiverPermission; + String receiverPermission = null; try { String name = packageName + ".permission.C2D_MESSAGE"; - getPackageManager().getPermissionInfo(name, 0); - receiverPermission = name; - } catch (PackageManager.NameNotFoundException e) { - receiverPermission = null; + PermissionInfo info = getPackageManager().getPermissionInfo(name, 0); + if (info.packageName.equals(packageName)) { + receiverPermission = name; + } + } catch (Exception ignored) { + // Keep null, no valid permission found } - List infos = getPackageManager().queryBroadcastReceivers(intent, PackageManager.GET_RESOLVED_FILTER); - if (infos == null || infos.isEmpty()) { - logd("No target for message, wut?"); + if (receiverPermission == null) { + // Without receiver permission, we only restrict by package name + logd("Deliver message to all receivers in package " + packageName); + intent.setPackage(packageName); + sendOrderedBroadcast(intent, null); } else { - for (ResolveInfo resolveInfo : infos) { - logd("Target: " + resolveInfo); - Intent targetIntent = new Intent(intent); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && app.wakeForDelivery) { - try { - if (getUserIdMethod != null && addPowerSaveTempWhitelistAppMethod != null && deviceIdleController != null) { - int userId = (int) getUserIdMethod.invoke(null, getPackageManager().getApplicationInfo(packageName, 0).uid); - logd("Adding app " + packageName + " for userId " + userId + " to the temp whitelist"); - addPowerSaveTempWhitelistAppMethod.invoke(deviceIdleController, packageName, 10000, userId, "GCM Push"); + List infos = getPackageManager().queryBroadcastReceivers(intent, PackageManager.GET_RESOLVED_FILTER); + if (infos == null || infos.isEmpty()) { + logd("No target for message, wut?"); + } else { + for (ResolveInfo resolveInfo : infos) { + Intent targetIntent = new Intent(intent); + targetIntent.setComponent(new ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name)); + if (resolveInfo.activityInfo.packageName.equals(packageName)) { + // Wake up the package itself + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && app.wakeForDelivery) { + try { + if (getUserIdMethod != null && addPowerSaveTempWhitelistAppMethod != null && deviceIdleController != null) { + int userId = (int) getUserIdMethod.invoke(null, getPackageManager().getApplicationInfo(packageName, 0).uid); + logd("Adding app " + packageName + " for userId " + userId + " to the temp whitelist"); + addPowerSaveTempWhitelistAppMethod.invoke(deviceIdleController, packageName, 10000, userId, "GCM Push"); + } + } catch (Exception e) { + Log.w(TAG, e); + } } - } catch (Exception e) { - Log.w(TAG, e); + // We don't need receiver permission for our own package + logd("Deliver message to own receiver " + resolveInfo); + sendOrderedBroadcast(targetIntent, null); + } else if (resolveInfo.filter.hasCategory(packageName)) { + // Permission required + logd("Deliver message to third-party receiver (with permission check)" + resolveInfo); + sendOrderedBroadcast(targetIntent, receiverPermission); } } - targetIntent.setComponent(new ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name)); - if (resolveInfo.activityInfo.packageName.equals(packageName)) { - sendOrderedBroadcast(targetIntent, null); - } else if (receiverPermission != null) { - sendOrderedBroadcast(targetIntent, receiverPermission); - } else { - Log.w(TAG, resolveInfo.activityInfo.packageName + "/" + resolveInfo.activityInfo.name + " matches for C2D message to " + packageName + " but corresponding permission was not declared"); - } } } } @@ -569,6 +602,7 @@ public class McsService extends Service implements Handler.Callback { for (AppData appData : msg.app_data) { if (IDLE_NOTIFICATION.equals(appData.key)) { DataMessageStanza.Builder msgResponse = new DataMessageStanza.Builder() + .id(Integer.toHexString(nextMessageId.incrementAndGet())) .from(FROM_FIELD) .sent(System.currentTimeMillis() / 1000) .ttl(0) @@ -633,6 +667,22 @@ public class McsService extends Service implements Handler.Callback { scheduleReconnect(this); } return true; + case MSG_ACK: + logd("Ack initiated, reason: " + msg.obj); + if (isConnected()) { + IqStanza.Builder iq = new IqStanza.Builder() + .type(IqStanza.IqType.SET) + .id("") + .extension(new Extension.Builder().id(13).data(ByteString.EMPTY).build()) // StreamAck + .status(0L); + if (inputStream.newStreamIdAvailable()) { + iq.last_stream_id_received(inputStream.getStreamId()); + } + send(MCS_IQ_STANZA_TAG, iq.build()); + } else { + logd("Ignoring ack, not connected!"); + } + return true; case MSG_OUTPUT_READY: logd("Sending login request..."); send(MCS_LOGIN_REQUEST_TAG, buildLoginRequest()); diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/PushRegisterHandler.java b/play-services-core/src/main/java/org/microg/gms/gcm/PushRegisterHandler.java index e95c98f0..15aaecc1 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/PushRegisterHandler.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/PushRegisterHandler.java @@ -28,13 +28,17 @@ import android.os.RemoteException; import android.util.Log; import org.microg.gms.checkin.LastCheckinInfo; +import org.microg.gms.common.ForegroundServiceContext; import org.microg.gms.common.PackageUtils; import org.microg.gms.common.Utils; import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTRATION; import static org.microg.gms.gcm.GcmConstants.ERROR_SERVICE_NOT_AVAILABLE; import static org.microg.gms.gcm.GcmConstants.EXTRA_APP; +import static org.microg.gms.gcm.GcmConstants.EXTRA_APP_OVERRIDE; import static org.microg.gms.gcm.GcmConstants.EXTRA_ERROR; +import static org.microg.gms.gcm.McsConstants.ACTION_ACK; +import static org.microg.gms.gcm.McsConstants.ACTION_SEND; class PushRegisterHandler extends Handler { private static final String TAG = "GmsGcmRegisterHdl"; @@ -54,40 +58,54 @@ class PushRegisterHandler extends Handler { return super.sendMessageAtTime(msg, uptimeMillis); } - private void sendReply(int what, int id, Messenger replyTo, Bundle data) { - if (what == 0) { - Intent outIntent = new Intent(ACTION_C2DM_REGISTRATION); - outIntent.putExtras(data); - Message message = Message.obtain(); - message.obj = outIntent; - try { - replyTo.send(message); - } catch (RemoteException e) { - Log.w(TAG, e); - } - } else { - Bundle messageData = new Bundle(); - messageData.putBundle("data", data); - Message response = Message.obtain(); - response.what = what; - response.arg1 = id; - response.setData(messageData); - try { - replyTo.send(response); - } catch (RemoteException e) { - Log.w(TAG, e); - } + private void sendReplyViaMessage(int what, int id, Messenger replyTo, Bundle messageData) { + Message response = Message.obtain(); + response.what = what; + response.arg1 = id; + response.setData(messageData); + try { + replyTo.send(response); + } catch (RemoteException e) { + Log.w(TAG, e); } } - private void replyError(int what, int id, Messenger replyTo, String errorMessage) { + private void sendReplyViaIntent(Intent outIntent, Messenger replyTo) { + Message message = Message.obtain(); + message.obj = outIntent; + try { + replyTo.send(message); + } catch (RemoteException e) { + Log.w(TAG, e); + } + } + + private void sendReply(int what, int id, Messenger replyTo, Bundle data, boolean oneWay) { + if (what == 0) { + Intent outIntent = new Intent(ACTION_C2DM_REGISTRATION); + outIntent.putExtras(data); + sendReplyViaIntent(outIntent, replyTo); + return; + } + Bundle messageData = new Bundle(); + messageData.putBundle("data", data); + sendReplyViaMessage(what, id, replyTo, messageData); + } + + private void replyError(int what, int id, Messenger replyTo, String errorMessage, boolean oneWay) { Bundle bundle = new Bundle(); bundle.putString(EXTRA_ERROR, errorMessage); - sendReply(what, id, replyTo, bundle); + sendReply(what, id, replyTo, bundle, oneWay); } private void replyNotAvailable(int what, int id, Messenger replyTo) { - replyError(what, id, replyTo, ERROR_SERVICE_NOT_AVAILABLE); + replyError(what, id, replyTo, ERROR_SERVICE_NOT_AVAILABLE, false); + } + + private PendingIntent getSelfAuthIntent() { + Intent intent = new Intent(); + intent.setPackage("com.google.example.invalidpackage"); + return PendingIntent.getBroadcast(context, 0, intent, 0); } @Override @@ -119,15 +137,9 @@ class PushRegisterHandler extends Handler { return; } Bundle data = msg.getData(); - if (data.getBoolean("oneWay", false)) { - Log.w(TAG, "oneWay requested"); - return; - } String packageName = data.getString("pkg"); Bundle subdata = data.getBundle("data"); - String sender = subdata.getString("sender"); - boolean delete = subdata.get("delete") != null; try { PackageUtils.checkPackageUid(context, packageName, callingUid); @@ -136,16 +148,46 @@ class PushRegisterHandler extends Handler { return; } - // TODO: We should checkin and/or ask for permission here. + Log.d(TAG, "handleMessage: package=" + packageName + " what=" + what + " id=" + id); - PushRegisterManager.completeRegisterRequest(context, database, - new RegisterRequest() - .build(Utils.getBuild(context)) - .sender(sender) - .checkin(LastCheckinInfo.read(context)) - .app(packageName) - .delete(delete) - .extraParams(subdata), - bundle -> sendReply(what, id, replyTo, bundle)); + boolean oneWay = data.getBoolean("oneWay", false); + + switch (what) { + case 0: + case 1: + // TODO: We should checkin and/or ask for permission here. + String sender = subdata.getString("sender"); + boolean delete = subdata.get("delete") != null; + + PushRegisterManager.completeRegisterRequest(context, database, + new RegisterRequest() + .build(Utils.getBuild(context)) + .sender(sender) + .checkin(LastCheckinInfo.read(context)) + .app(packageName) + .delete(delete) + .extraParams(subdata), + bundle -> sendReply(what, id, replyTo, bundle, oneWay)); + break; + case 2: + String messageId = subdata.getString("google.message_id"); + Log.d(TAG, "Ack " + messageId + " for " + packageName); + Intent i = new Intent(context, McsService.class); + i.setAction(ACTION_ACK); + i.putExtra(EXTRA_APP, getSelfAuthIntent()); + new ForegroundServiceContext(context).startService(i); + break; + default: + Bundle bundle = new Bundle(); + bundle.putBoolean("unsupported", true); + sendReplyViaMessage(what, id, replyTo, bundle); + return; + } + + if (oneWay) { + Bundle bundle = new Bundle(); + bundle.putBoolean("ack", true); + sendReplyViaMessage(what, id, replyTo, bundle); + } } }