Skip to content

Commit a0dca44

Browse files
committed
merge to master
2 parents 893d29c + 80faa06 commit a0dca44

5 files changed

Lines changed: 108 additions & 37 deletions

File tree

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"allow": [
44
"Bash(tshark:*)",
55
"Bash(find:*)",
6-
"Bash(python3:*)"
6+
"Bash(python3:*)",
7+
"Bash(git log:*)"
78
]
89
}
910
}

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/qfit.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
814814
lapMesg.SetMessageIndex(lap_index++);
815815
lapMesg.SetLapTrigger(FIT_LAP_TRIGGER_DISTANCE);
816816
if (type == JUMPROPE)
817-
lapMesg.SetRepetitionNum(session.at(i - 1).inclination);
817+
lapMesg.SetRepetitionNum(lap_index);
818818
lastLapTimer = sl.elapsedTime;
819819
lastLapOdometer = sl.distance;
820820

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)