Skip to content

Commit 80faa06

Browse files
authored
Add Garmin server selection and debug logging (#4235)
* Add Garmin server selection and debug logging Introduces a ComboBox in settings to select between global and China Garmin servers, prompting for app restart when changed. Adds debug logging in garminconnect.cpp to trace domain and API URLs, and logs the loaded domain from settings. * Add verbose debug logging for GarminConnect responses Introduces a DEBUG_GARMIN_VERBOSE flag to enable detailed logging of HTTP responses and ticket extraction attempts in the GarminConnect authentication flow. This aids in troubleshooting login and MFA issues by providing more insight into response contents and extraction logic. * Detect MFA via page title and handle CSRF Instead of scanning the entire response body for "MFA", detect MFA by parsing the HTML <title> (matching the Python garth approach) to avoid false positives from bodies that contain "MFA" text. Extract the page title early, check for "MFA" case-insensitively, and if detected update m_lastError, refresh cookies, extract a new CSRF token using two regex patterns, emit mfaRequired (unless suppressed), and abort the login flow. Also adjust the success check to rely on the title == "Success" and remove the legacy body-based MFA detection block. Added debugging logs for the title, CSRF token, and MFA signal paths. * Update garminconnect.h * popup not needed
1 parent 51808cc commit 80faa06

4 files changed

Lines changed: 112 additions & 35 deletions

File tree

.claude/settings.local.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(git log:*)"
5+
]
6+
}
7+
}

src/garminconnect.cpp

Lines changed: 81 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,9 @@ bool GarminConnect::fetchCsrfToken()
391391
bool GarminConnect::performLogin(const QString &email, const QString &password, bool suppressMfaSignal)
392392
{
393393
qDebug() << "GarminConnect: Performing login...";
394+
qDebug() << "GarminConnect: Using domain:" << m_domain;
395+
qDebug() << "GarminConnect: SSO URL:" << ssoUrl();
396+
qDebug() << "GarminConnect: Connect API URL:" << connectApiUrl();
394397

395398
QString ssoEmbedUrl = ssoUrl() + SSO_EMBED_PATH;
396399

@@ -452,15 +455,54 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
452455
qDebug() << "GarminConnect: Login response length:" << response.length();
453456
qDebug() << "GarminConnect: Response snippet:" << response.left(300);
454457

455-
// Check for success title (like Python garth library)
458+
// Check page title (like Python garth library)
459+
// garth checks ONLY the title for MFA detection, not the body
460+
// This is important because some servers (like garmin.cn) may have "MFA" text
461+
// in their Success page HTML body, which would cause false positives
462+
QString pageTitle;
456463
QRegularExpression titleRegex("<title>(.+?)</title>");
457464
QRegularExpressionMatch titleMatch = titleRegex.match(response);
458465
if (titleMatch.hasMatch()) {
459-
QString title = titleMatch.captured(1);
460-
qDebug() << "GarminConnect: Page title:" << title;
461-
if (title == "Success") {
462-
qDebug() << "GarminConnect: Login successful (Success page detected)";
466+
pageTitle = titleMatch.captured(1);
467+
qDebug() << "GarminConnect: Page title:" << pageTitle;
468+
}
469+
470+
// Check if MFA is required by looking at the TITLE (garth approach)
471+
// This is more reliable than checking the body which may contain "MFA" in scripts/URLs
472+
if (pageTitle.contains("MFA", Qt::CaseInsensitive)) {
473+
m_lastError = "MFA Required";
474+
qDebug() << "GarminConnect: MFA detected in page title";
475+
476+
// Extract new CSRF token from MFA page - try multiple patterns
477+
QRegularExpression csrfRegex1("name=\"_csrf\"[^>]*value=\"([^\"]+)\"");
478+
QRegularExpression csrfRegex2("value=\"([^\"]+)\"[^>]*name=\"_csrf\"");
479+
480+
QRegularExpressionMatch match = csrfRegex1.match(response);
481+
if (!match.hasMatch()) {
482+
match = csrfRegex2.match(response);
483+
}
484+
if (match.hasMatch()) {
485+
m_csrfToken = match.captured(1);
486+
qDebug() << "GarminConnect: CSRF token from MFA page:" << m_csrfToken.left(20) << "...";
487+
}
488+
489+
// Update cookies
490+
m_cookies = m_manager->cookieJar()->cookiesForUrl(url);
491+
492+
if (!suppressMfaSignal) {
493+
qDebug() << "GarminConnect: Emitting mfaRequired signal";
494+
emit mfaRequired();
495+
} else {
496+
qDebug() << "GarminConnect: MFA required but signal suppressed (retrying with MFA code)";
463497
}
498+
reply->deleteLater();
499+
return false;
500+
}
501+
502+
// Check if login was successful (title is "Success")
503+
if (pageTitle == "Success") {
504+
qDebug() << "GarminConnect: Login successful (Success page detected)";
505+
// Continue to extract ticket below
464506
}
465507

466508
// Check for error messages in response
@@ -549,39 +591,17 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
549591
return false;
550592
}
551593

552-
// Check if MFA is required (legacy check for non-redirect MFA)
553-
if (response.contains("MFA", Qt::CaseInsensitive) ||
554-
response.contains("Enter MFA Code", Qt::CaseInsensitive)) {
555-
m_lastError = "MFA Required";
556-
qDebug() << "GarminConnect: MFA content detected in response";
557-
558-
// Extract new CSRF token from MFA page - try multiple patterns
559-
QRegularExpression csrfRegex1("name=\"_csrf\"[^>]*value=\"([^\"]+)\"");
560-
QRegularExpression csrfRegex2("value=\"([^\"]+)\"[^>]*name=\"_csrf\"");
561-
562-
QRegularExpressionMatch match = csrfRegex1.match(response);
563-
if (!match.hasMatch()) {
564-
match = csrfRegex2.match(response);
565-
}
566-
if (match.hasMatch()) {
567-
m_csrfToken = match.captured(1);
568-
}
569-
570-
// Update cookies
571-
m_cookies = m_manager->cookieJar()->cookiesForUrl(url);
572-
573-
if (!suppressMfaSignal) {
574-
emit mfaRequired();
575-
}
576-
reply->deleteLater();
577-
return false;
578-
}
579-
580594
// Extract ticket from response URL (already declared above)
581595
if (responseUrl.isEmpty()) {
582596
responseUrl = reply->url();
583597
}
584598

599+
if (DEBUG_GARMIN_VERBOSE) {
600+
qDebug() << "GarminConnect: Response URL:" << responseUrl.toString();
601+
qDebug() << "GarminConnect: Response length:" << response.length();
602+
qDebug() << "GarminConnect: Full response body:" << response;
603+
}
604+
585605
QUrlQuery responseQuery(responseUrl);
586606
QString ticket = responseQuery.queryItemValue("ticket");
587607

@@ -599,6 +619,8 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
599619
if (match.hasMatch()) {
600620
ticket = match.captured(1);
601621
qDebug() << "GarminConnect: Found ticket with fallback pattern:" << ticket.left(20) << "...";
622+
} else if (DEBUG_GARMIN_VERBOSE) {
623+
qDebug() << "GarminConnect: No ticket patterns matched in response body";
602624
}
603625
}
604626
}
@@ -608,6 +630,9 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
608630
if (ticket.isEmpty()) {
609631
m_lastError = "Failed to extract ticket from login response";
610632
qDebug() << "GarminConnect:" << m_lastError;
633+
if (DEBUG_GARMIN_VERBOSE) {
634+
qDebug() << "GarminConnect: Response snippet:" << response.left(1000);
635+
}
611636
return false;
612637
}
613638

@@ -708,8 +733,12 @@ void GarminConnect::handleMfaReplyFinished()
708733
qDebug() << "GarminConnect: MFA response status code:" << statusCode;
709734
qDebug() << "GarminConnect: MFA response redirect URL:" << responseUrl.toString();
710735

711-
// If no redirect, log response body to understand what happened
712-
if (responseUrl.isEmpty()) {
736+
// Log detailed response information
737+
if (DEBUG_GARMIN_VERBOSE) {
738+
qDebug() << "GarminConnect: MFA response length:" << response.length();
739+
qDebug() << "GarminConnect: Full MFA response body:" << response;
740+
} else if (responseUrl.isEmpty()) {
741+
// If no redirect, log response body to understand what happened (non-verbose)
713742
qDebug() << "GarminConnect: MFA response body (first 500 chars):" << response.left(500);
714743
}
715744

@@ -748,6 +777,9 @@ void GarminConnect::handleMfaReplyFinished()
748777

749778
// If not found in redirect URL, try response body
750779
if (ticket.isEmpty() && !response.isEmpty()) {
780+
if (DEBUG_GARMIN_VERBOSE) {
781+
qDebug() << "GarminConnect: Attempting to extract ticket from MFA response body";
782+
}
751783
// Try multiple patterns for ticket extraction
752784
QRegularExpression ticketRegex1("embed\\?ticket=([^\"]+)\"");
753785
QRegularExpression ticketRegex2("ticket=([^&\"']+)");
@@ -761,6 +793,16 @@ void GarminConnect::handleMfaReplyFinished()
761793
if (match.hasMatch()) {
762794
ticket = match.captured(1);
763795
qDebug() << "GarminConnect: Found ticket in response body (pattern 2):" << ticket.left(20) << "...";
796+
} else if (DEBUG_GARMIN_VERBOSE) {
797+
qDebug() << "GarminConnect: No MFA ticket patterns matched. Checking for other patterns...";
798+
// Check for JSON format
799+
if (response.contains("ticket")) {
800+
qDebug() << "GarminConnect: Response contains 'ticket' keyword, may be JSON or different format";
801+
}
802+
// Check for common response patterns
803+
if (response.contains("\"")) {
804+
qDebug() << "GarminConnect: Response contains quoted strings (may be JSON)";
805+
}
764806
}
765807
}
766808
}
@@ -770,6 +812,9 @@ void GarminConnect::handleMfaReplyFinished()
770812
if (ticket.isEmpty()) {
771813
m_lastError = "Failed to extract ticket after MFA";
772814
qDebug() << "GarminConnect:" << m_lastError;
815+
if (DEBUG_GARMIN_VERBOSE) {
816+
qDebug() << "GarminConnect: Response snippet:" << response.left(1000);
817+
}
773818
emit authenticationFailed(m_lastError);
774819
return;
775820
}
@@ -1401,6 +1446,7 @@ void GarminConnect::loadTokensFromSettings()
14011446
m_oauth1Token.oauth_token = settings.value(QZSettings::garmin_oauth1_token, QZSettings::default_garmin_oauth1_token).toString();
14021447
m_oauth1Token.oauth_token_secret = settings.value(QZSettings::garmin_oauth1_token_secret, QZSettings::default_garmin_oauth1_token_secret).toString();
14031448
m_domain = settings.value(QZSettings::garmin_domain, QZSettings::default_garmin_domain).toString();
1449+
qDebug() << "GarminConnect: Loaded Garmin domain from settings:" << m_domain;
14041450

14051451
if (!m_oauth2Token.access_token.isEmpty()) {
14061452
qDebug() << "GarminConnect: Loaded tokens from settings (OAuth1 + OAuth2)";

src/garminconnect.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ class GarminConnect : public QObject
176176
static constexpr const char* SSO_URL_PATH = "/sso/signin";
177177
static constexpr const char* SSO_EMBED_PATH = "/sso/embed";
178178
static constexpr const char* OAUTH_CONSUMER_URL = "https://thegarth.s3.amazonaws.com/oauth_consumer.json";
179+
static constexpr bool DEBUG_GARMIN_VERBOSE = false; // Set to true for detailed response logging (may contain sensitive data)
179180

180181
// Private methods
181182
QString ssoUrl() const { return QString("https://sso.%1").arg(m_domain); }

src/settings.qml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6726,6 +6726,29 @@ import Qt.labs.platform 1.1
67266726
}
67276727
}
67286728

6729+
RowLayout {
6730+
spacing: 10
6731+
Label {
6732+
text: qsTr("Garmin Server:")
6733+
Layout.fillWidth: true
6734+
}
6735+
ComboBox {
6736+
id: garminServerComboBox
6737+
Layout.fillHeight: false
6738+
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
6739+
model: ["Global (garmin.com)", "China (garmin.cn)"]
6740+
currentIndex: settings.garmin_domain === "garmin.cn" ? 1 : 0
6741+
onCurrentIndexChanged: {
6742+
var newDomain = currentIndex === 1 ? "garmin.cn" : "garmin.com";
6743+
if (newDomain !== settings.garmin_domain) {
6744+
rootItem.garmin_connect_logout();
6745+
settings.garmin_domain = newDomain;
6746+
window.settings_restart_to_apply = true;
6747+
}
6748+
}
6749+
}
6750+
}
6751+
67296752
Button {
67306753
text: "Test Garmin Login"
67316754
Layout.alignment: Qt.AlignHCenter

0 commit comments

Comments
 (0)