GCM: Add support for message acking, deliver to all receivers of package when working permissionless

This commit is contained in:
Marvin W 2020-08-22 23:43:14 +02:00
parent 74c0e28e27
commit 60cc63ed60
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
4 changed files with 168 additions and 73 deletions

View File

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

View File

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

View File

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

View File

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