mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-10 09:01:55 +01:00
Garmin: Allow fake OAuth
This commit is contained in:
parent
db10fd66f2
commit
9e3b5a1b2e
@ -176,6 +176,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
|||||||
return new GarminHeartRateRestingSampleProvider(device, session);
|
return new GarminHeartRateRestingSampleProvider(device, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getSupportedDeviceSpecificAuthenticationSettings() {
|
||||||
|
return new int[]{R.xml.devicesettings_garmin_fake_oauth};
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
|
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
|
||||||
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
|
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
|
||||||
|
@ -11,6 +11,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|||||||
import com.google.protobuf.InvalidProtocolBufferException;
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||||
@ -32,6 +34,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRealtimeSetting
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
|
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
|
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiAuthenticationService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiCalendarService;
|
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiCalendarService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiCore;
|
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiCore;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiDataTransferService;
|
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiDataTransferService;
|
||||||
@ -52,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent;
|
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager;
|
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
|
||||||
|
|
||||||
public class ProtocolBufferHandler implements MessageHandler {
|
public class ProtocolBufferHandler implements MessageHandler {
|
||||||
|
|
||||||
@ -136,6 +140,27 @@ public class ProtocolBufferHandler implements MessageHandler {
|
|||||||
processed = true;
|
processed = true;
|
||||||
processProtobufSettingsService(smart.getSettingsService());
|
processProtobufSettingsService(smart.getSettingsService());
|
||||||
}
|
}
|
||||||
|
if (smart.hasAuthenticationService() && smart.getAuthenticationService().hasOauthRequest()) {
|
||||||
|
LOG.debug("Got OAuth request");
|
||||||
|
final DevicePrefs devicePrefs = deviceSupport.getDevicePrefs();
|
||||||
|
if (!devicePrefs.getBoolean("garmin_fake_oauth_enabled", false)) {
|
||||||
|
LOG.warn("Got OAuth request, but fake OAuth is disabled");
|
||||||
|
} else {
|
||||||
|
final GdiAuthenticationService.OAuthResponse oauthResponse = GdiAuthenticationService.OAuthResponse.newBuilder()
|
||||||
|
.setKeys(GdiAuthenticationService.OAuthKeys.newBuilder()
|
||||||
|
.setConsumerKey(UUID.randomUUID().toString())
|
||||||
|
.setConsumerSecret(RandomStringUtils.insecure().next(35, true, true))
|
||||||
|
.setOauthToken(UUID.randomUUID().toString())
|
||||||
|
.setOauthSecret(RandomStringUtils.insecure().next(35, true, true))
|
||||||
|
.build()
|
||||||
|
).setUnk2(0).build();
|
||||||
|
|
||||||
|
return prepareProtobufResponse(GdiSmartProto.Smart.newBuilder().setAuthenticationService(
|
||||||
|
GdiAuthenticationService.AuthenticationService.newBuilder()
|
||||||
|
.setOauthResponse(oauthResponse)
|
||||||
|
).build(), message.getRequestId());
|
||||||
|
}
|
||||||
|
}
|
||||||
if (smart.hasNotificationsService()) {
|
if (smart.hasNotificationsService()) {
|
||||||
return prepareProtobufResponse(processProtobufNotificationsServiceMessage(smart.getNotificationsService()), message.getRequestId());
|
return prepareProtobufResponse(processProtobufNotificationsServiceMessage(smart.getNotificationsService()), message.getRequestId());
|
||||||
}
|
}
|
||||||
|
@ -20,10 +20,12 @@ public class HttpHandler {
|
|||||||
|
|
||||||
private final AgpsHandler agpsHandler;
|
private final AgpsHandler agpsHandler;
|
||||||
private final ContactsHandler contactsHandler;
|
private final ContactsHandler contactsHandler;
|
||||||
|
private final OauthHandler oauthHandler;
|
||||||
|
|
||||||
public HttpHandler(GarminSupport deviceSupport) {
|
public HttpHandler(GarminSupport deviceSupport) {
|
||||||
agpsHandler = new AgpsHandler(deviceSupport);
|
agpsHandler = new AgpsHandler(deviceSupport);
|
||||||
contactsHandler = new ContactsHandler(deviceSupport);
|
contactsHandler = new ContactsHandler(deviceSupport);
|
||||||
|
oauthHandler = new OauthHandler(deviceSupport);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GdiHttpService.HttpService handle(final GdiHttpService.HttpService httpService) {
|
public GdiHttpService.HttpService handle(final GdiHttpService.HttpService httpService) {
|
||||||
@ -57,6 +59,9 @@ public class HttpHandler {
|
|||||||
} else if (request.getPath().startsWith("/device-gateway/usercontact/")) {
|
} else if (request.getPath().startsWith("/device-gateway/usercontact/")) {
|
||||||
LOG.info("Got contacts request for {}", request.getPath());
|
LOG.info("Got contacts request for {}", request.getPath());
|
||||||
response = contactsHandler.handleRequest(request);
|
response = contactsHandler.handleRequest(request);
|
||||||
|
} else if (request.getPath().startsWith("/api/oauth") || request.getPath().startsWith("/oauthTokenExchangeService")) {
|
||||||
|
LOG.info("Got OAuth request for {}", request.getPath());
|
||||||
|
response = oauthHandler.handleRequest(request);
|
||||||
} else {
|
} else {
|
||||||
LOG.warn("Unhandled path {}", request.getPath());
|
LOG.warn("Unhandled path {}", request.getPath());
|
||||||
response = null;
|
response = null;
|
||||||
|
@ -0,0 +1,137 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiHttpService;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
|
||||||
|
|
||||||
|
public class OauthHandler {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(OauthHandler.class);
|
||||||
|
|
||||||
|
private static final Gson GSON = new GsonBuilder()
|
||||||
|
//.serializeNulls()
|
||||||
|
.create();
|
||||||
|
|
||||||
|
private final GarminSupport deviceSupport;
|
||||||
|
|
||||||
|
public OauthHandler(final GarminSupport deviceSupport) {
|
||||||
|
this.deviceSupport = deviceSupport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GarminHttpResponse handleRequest(final GarminHttpRequest request) {
|
||||||
|
if (request.getRawRequest().getMethod() != GdiHttpService.HttpService.Method.POST) {
|
||||||
|
LOG.warn("Known OAuth requests should be POST");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DevicePrefs devicePrefs = deviceSupport.getDevicePrefs();
|
||||||
|
if (!devicePrefs.getBoolean("garmin_fake_oauth_enabled", false)) {
|
||||||
|
LOG.warn("Got OAuth HTTP request, but fake OAuth is disabled");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> scopes = Arrays.asList(
|
||||||
|
// Swim 2
|
||||||
|
"GCS_EPHEMERIS_SONY_READ",
|
||||||
|
// Venu 3
|
||||||
|
"GCS_CIQ_APPSTORE_MOBILE_READ",
|
||||||
|
"GCS_EMERGENCY_ASSISTANCE_CREATE",
|
||||||
|
"GCS_GEOLOCATION_ELEVATION_READ",
|
||||||
|
"GCS_IMAGE_READ",
|
||||||
|
"GCS_LIVETRACK_FIT_CREATE",
|
||||||
|
"GCS_LIVETRACK_FIT_READ",
|
||||||
|
"GCS_LIVETRACK_FIT_UPDATE",
|
||||||
|
"OMT_GOLF_SUBSCRIPTION_READ",
|
||||||
|
"OMT_SUBSCRIPTION_READ"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (request.getPath().equals("/oauthTokenExchangeService/connectToIT")) {
|
||||||
|
final AuthorizationResponse authorizationResponse = new AuthorizationResponse();
|
||||||
|
authorizationResponse.accessToken = UUID.randomUUID().toString();
|
||||||
|
authorizationResponse.tokenType = "Bearer";
|
||||||
|
authorizationResponse.refreshToken = UUID.randomUUID().toString();
|
||||||
|
authorizationResponse.expiresIn = 7776000;
|
||||||
|
authorizationResponse.scope = String.join(" ", scopes);
|
||||||
|
authorizationResponse.refreshTokenExpiresIn = "31536000";
|
||||||
|
authorizationResponse.customerId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
final GarminHttpResponse response = new GarminHttpResponse();
|
||||||
|
response.setStatus(200);
|
||||||
|
response.setBody(GSON.toJson(authorizationResponse).getBytes(StandardCharsets.UTF_8));
|
||||||
|
response.getHeaders().put("Content-Type", "application/json");
|
||||||
|
return response;
|
||||||
|
} else if (request.getPath().equals("/api/oauth/token")) {
|
||||||
|
// Attempt to keep the same refresh token
|
||||||
|
final String refreshToken;
|
||||||
|
if (request.getRawRequest().hasRawBody()) {
|
||||||
|
// grant_type=refresh_token&refresh_token=xxxxxxx&client_id=yyyyyyyy
|
||||||
|
final String body = request.getRawRequest().getRawBody().toStringUtf8();
|
||||||
|
final String[] args = body.split("&");
|
||||||
|
final Map<String, String> queryParameters = Arrays.stream(args)
|
||||||
|
.map(a -> a.split("="))
|
||||||
|
.filter(a -> a.length == 2)
|
||||||
|
.collect(Collectors.toMap(a -> a[0], a -> a[1]));
|
||||||
|
if (queryParameters.containsKey("refresh_token")) {
|
||||||
|
refreshToken = queryParameters.get("refresh_token");
|
||||||
|
} else {
|
||||||
|
LOG.warn("Failed to find refresh_token in parameters");
|
||||||
|
refreshToken = UUID.randomUUID().toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG.warn("Oauth refresh request has no body");
|
||||||
|
refreshToken = UUID.randomUUID().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
final RefreshResponse refreshResponse = new RefreshResponse();
|
||||||
|
refreshResponse.access_token = UUID.randomUUID().toString();
|
||||||
|
refreshResponse.token_type = "Bearer";
|
||||||
|
refreshResponse.expires_in = 7776000;
|
||||||
|
refreshResponse.scope = String.join(" ", scopes);
|
||||||
|
refreshResponse.refresh_token = refreshToken;
|
||||||
|
refreshResponse.refresh_token_expires_in = "31536000";
|
||||||
|
refreshResponse.customerId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
final GarminHttpResponse response = new GarminHttpResponse();
|
||||||
|
response.setStatus(200);
|
||||||
|
response.setBody(GSON.toJson(refreshResponse).getBytes(StandardCharsets.UTF_8));
|
||||||
|
response.getHeaders().put("Content-Type", "application/json");
|
||||||
|
return response;
|
||||||
|
} else {
|
||||||
|
LOG.warn("Unknown OAuth path {}", request.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AuthorizationResponse {
|
||||||
|
public String accessToken;
|
||||||
|
public String tokenType;
|
||||||
|
public String refreshToken;
|
||||||
|
public int expiresIn;
|
||||||
|
public String scope;
|
||||||
|
public String refreshTokenExpiresIn;
|
||||||
|
public String customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RefreshResponse {
|
||||||
|
public String access_token;
|
||||||
|
public String token_type;
|
||||||
|
public int expires_in;
|
||||||
|
public String scope;
|
||||||
|
public String refresh_token;
|
||||||
|
public String refresh_token_expires_in;
|
||||||
|
public String customerId;
|
||||||
|
}
|
||||||
|
}
|
26
app/src/main/proto/garmin/gdi_authentication_service.proto
Normal file
26
app/src/main/proto/garmin/gdi_authentication_service.proto
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package garmin_vivomovehr;
|
||||||
|
|
||||||
|
option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.garmin";
|
||||||
|
|
||||||
|
message AuthenticationService {
|
||||||
|
optional OAuthRequest oauthRequest = 1;
|
||||||
|
optional OAuthResponse oauthResponse = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OAuthRequest {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
message OAuthResponse {
|
||||||
|
optional OAuthKeys keys = 1;
|
||||||
|
optional uint32 unk2 = 2; // 0
|
||||||
|
}
|
||||||
|
|
||||||
|
message OAuthKeys {
|
||||||
|
optional string consumerKey = 1;
|
||||||
|
optional string consumerSecret = 2;
|
||||||
|
optional string oauthToken = 3;
|
||||||
|
optional string oauthSecret = 4;
|
||||||
|
}
|
@ -31,6 +31,7 @@ message HttpService {
|
|||||||
optional Method method = 3;
|
optional Method method = 3;
|
||||||
repeated Header header = 5;
|
repeated Header header = 5;
|
||||||
optional bool useDataXfer = 6;
|
optional bool useDataXfer = 6;
|
||||||
|
optional bytes rawBody = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RawResponse {
|
message RawResponse {
|
||||||
|
@ -4,6 +4,7 @@ package garmin_vivomovehr;
|
|||||||
|
|
||||||
option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.garmin";
|
option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.garmin";
|
||||||
|
|
||||||
|
import "garmin/gdi_authentication_service.proto";
|
||||||
import "garmin/gdi_device_status.proto";
|
import "garmin/gdi_device_status.proto";
|
||||||
import "garmin/gdi_find_my_watch.proto";
|
import "garmin/gdi_find_my_watch.proto";
|
||||||
import "garmin/gdi_core.proto";
|
import "garmin/gdi_core.proto";
|
||||||
@ -22,6 +23,7 @@ message Smart {
|
|||||||
optional FindMyWatchService find_my_watch_service = 12;
|
optional FindMyWatchService find_my_watch_service = 12;
|
||||||
optional CoreService core_service = 13;
|
optional CoreService core_service = 13;
|
||||||
optional SmsNotificationService sms_notification_service = 16;
|
optional SmsNotificationService sms_notification_service = 16;
|
||||||
|
optional AuthenticationService authenticationService = 27;
|
||||||
optional SettingsService settings_service = 42;
|
optional SettingsService settings_service = 42;
|
||||||
optional NotificationsService notifications_service = 49;
|
optional NotificationsService notifications_service = 49;
|
||||||
}
|
}
|
||||||
|
@ -3578,4 +3578,7 @@
|
|||||||
<string name="battery_minimum_charge">Minimum battery charge in %</string>
|
<string name="battery_minimum_charge">Minimum battery charge in %</string>
|
||||||
<string name="battery_allow_pass_though_summary">When enabled, the battery can be charged while discharging</string>
|
<string name="battery_allow_pass_though_summary">When enabled, the battery can be charged while discharging</string>
|
||||||
<string name="battery_allow_pass_through">Allow battery pass-through</string>
|
<string name="battery_allow_pass_through">Allow battery pass-through</string>
|
||||||
|
<string name="pref_garmin_fake_oauth_warning">Do not enable this if you plan to use the official app or connect the watch to the internet. After enabling this setting, you may need to factory reset the watch to authenticate again.</string>
|
||||||
|
<string name="pref_garmin_fake_oauth_title">Send fake OAuth responses</string>
|
||||||
|
<string name="pref_garmin_fake_oauth_summary">Fixes some functions such as weather and AGPS updates without connecting to the official app.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
16
app/src/main/res/xml/devicesettings_garmin_fake_oauth.xml
Normal file
16
app/src/main/res/xml/devicesettings_garmin_fake_oauth.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<Preference
|
||||||
|
android:icon="@drawable/ic_warning"
|
||||||
|
android:key="garmin_fake_oauth_warning"
|
||||||
|
android:summary="@string/pref_garmin_fake_oauth_warning"
|
||||||
|
android:title="@string/warning" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:icon="@drawable/ic_vpn_key"
|
||||||
|
android:key="garmin_fake_oauth_enabled"
|
||||||
|
android:layout="@layout/preference_checkbox"
|
||||||
|
android:summary="@string/pref_garmin_fake_oauth_summary"
|
||||||
|
android:title="@string/pref_garmin_fake_oauth_title" />
|
||||||
|
</androidx.preference.PreferenceScreen>
|
Loading…
Reference in New Issue
Block a user