Skip to content

Commit 7989a72

Browse files
committed
GUACAMOLE-2210: Fetch Chrome major version at runtime from Google VersionHistory API for AAD User-Agent string.
1 parent de377cf commit 7989a72

1 file changed

Lines changed: 146 additions & 10 deletions

File tree

src/protocols/rdp/aad.c

Lines changed: 146 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,31 @@
6363
#define GUAC_AAD_HTTP_TIMEOUT_SECONDS 30
6464

6565
/**
66-
* User-Agent string sent with all HTTP requests to Microsoft login endpoints.
67-
* A browser-like UA is required to avoid "unsupported browser" responses.
66+
* Format string for the User-Agent sent with HTTP requests to Microsoft login
67+
* endpoints. A browser-like UA is required to avoid "unsupported browser"
68+
* responses. The %s placeholder is replaced with the Chrome major version,
69+
* which is fetched at runtime from Google's VersionHistory API.
6870
*/
69-
#define GUAC_AAD_USER_AGENT \
71+
#define GUAC_AAD_USER_AGENT_FORMAT \
7072
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " \
71-
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
73+
"(KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36"
74+
75+
/**
76+
* Default Chrome major version used if the runtime version fetch fails.
77+
*/
78+
#define GUAC_AAD_DEFAULT_CHROME_VERSION "146"
79+
80+
/**
81+
* URL for Google's VersionHistory API to retrieve the latest stable Chrome
82+
* version for Linux.
83+
*/
84+
#define GUAC_AAD_CHROME_VERSION_URL \
85+
"https://versionhistory.googleapis.com/v1/chrome/platforms/linux/channels/stable/versions"
86+
87+
/**
88+
* Timeout in seconds for the Chrome version fetch request.
89+
*/
90+
#define GUAC_AAD_VERSION_FETCH_TIMEOUT 5
7291

7392
/**
7493
* HTTP response structure for AAD requests.
@@ -87,6 +106,104 @@ typedef struct guac_rdp_aad_response {
87106

88107
} guac_rdp_aad_response;
89108

109+
/**
110+
* Maximum length of a Chrome major version string (e.g. "130").
111+
*/
112+
#define GUAC_AAD_VERSION_BUFFER_SIZE 16
113+
114+
/**
115+
* Callback for the Chrome version fetch request. Writes response data into
116+
* a simple buffer.
117+
*/
118+
static size_t guac_rdp_aad_version_write_callback(void* contents, size_t size,
119+
size_t nmemb, void* userp) {
120+
121+
size_t total_size = size * nmemb;
122+
guac_rdp_aad_response* response = (guac_rdp_aad_response*) userp;
123+
124+
if (response->size + total_size > GUAC_AAD_LOGIN_PAGE_MAX_SIZE)
125+
return 0;
126+
127+
memcpy(response->data + response->size, contents, total_size);
128+
response->size += total_size;
129+
response->data[response->size] = '\0';
130+
131+
return total_size;
132+
}
133+
134+
/**
135+
* Fetches the latest stable Chrome major version from Google's VersionHistory
136+
* API and returns a User-Agent string using that version. Falls back to
137+
* GUAC_AAD_DEFAULT_CHROME_VERSION if the fetch fails.
138+
*
139+
* @param client
140+
* The guac_client for logging.
141+
*
142+
* @return
143+
* A newly allocated User-Agent string. The caller must free this with
144+
* guac_mem_free().
145+
*/
146+
static char* guac_rdp_aad_get_user_agent(guac_client* client) {
147+
148+
const char* version = GUAC_AAD_DEFAULT_CHROME_VERSION;
149+
char version_buf[GUAC_AAD_VERSION_BUFFER_SIZE];
150+
151+
CURL* curl = curl_easy_init();
152+
if (curl != NULL) {
153+
154+
guac_rdp_aad_response response = { .data = NULL, .size = 0 };
155+
response.data = guac_mem_alloc(GUAC_AAD_LOGIN_PAGE_MAX_SIZE + 1);
156+
157+
if (response.data != NULL) {
158+
159+
response.data[0] = '\0';
160+
161+
curl_easy_setopt(curl, CURLOPT_URL,
162+
GUAC_AAD_CHROME_VERSION_URL);
163+
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION,
164+
guac_rdp_aad_version_write_callback);
165+
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
166+
curl_easy_setopt(curl, CURLOPT_TIMEOUT,
167+
(long) GUAC_AAD_VERSION_FETCH_TIMEOUT);
168+
169+
CURLcode res = curl_easy_perform(curl);
170+
171+
if (res == CURLE_OK && response.data != NULL) {
172+
173+
/* Extract the major version from the first "version"
174+
* value in the JSON response (e.g. "146.0.7680.177").
175+
* The API returns versions sorted newest-first. */
176+
int major = 0;
177+
const char* ver_key = strstr(response.data, "\"version\":");
178+
if (ver_key != NULL)
179+
ver_key = strchr(ver_key + strlen("\"version\":"), '"');
180+
if (ver_key != NULL
181+
&& sscanf(ver_key, "\"%d.", &major) == 1
182+
&& major > 0) {
183+
snprintf(version_buf, sizeof(version_buf), "%d", major);
184+
version = version_buf;
185+
}
186+
}
187+
188+
guac_mem_free(response.data);
189+
}
190+
191+
curl_easy_cleanup(curl);
192+
}
193+
194+
guac_client_log(client, GUAC_LOG_DEBUG,
195+
"AAD: Using Chrome major version %s for User-Agent", version);
196+
197+
/* Build the full User-Agent string */
198+
size_t ua_len = strlen(GUAC_AAD_USER_AGENT_FORMAT) + strlen(version) + 1;
199+
char* user_agent = guac_mem_alloc(ua_len);
200+
if (user_agent == NULL)
201+
return NULL;
202+
203+
snprintf(user_agent, ua_len, GUAC_AAD_USER_AGENT_FORMAT, version);
204+
return user_agent;
205+
}
206+
90207
/**
91208
* Callback function for libcurl to write received HTTP data into a
92209
* guac_rdp_aad_response buffer.
@@ -504,13 +621,17 @@ static char* guac_rdp_aad_extract_auth_code(guac_client* client,
504621
* @param auth_code
505622
* The authorization code obtained from the login redirect.
506623
*
624+
* @param user_agent
625+
* The User-Agent string to use for HTTP requests.
626+
*
507627
* @return
508628
* A newly allocated string containing the access token, or NULL if
509629
* the exchange failed. The caller must free the returned string with
510630
* guac_mem_free().
511631
*/
512632
static char* guac_rdp_aad_exchange_code_for_token(guac_client* client,
513-
guac_rdp_aad_params* params, const char* auth_code) {
633+
guac_rdp_aad_params* params, const char* auth_code,
634+
const char* user_agent) {
514635

515636
CURL* curl = NULL;
516637
char* token = NULL;
@@ -584,7 +705,7 @@ static char* guac_rdp_aad_exchange_code_for_token(guac_client* client,
584705
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION,
585706
guac_rdp_aad_write_callback);
586707
curl_easy_setopt(curl, CURLOPT_WRITEDATA, response);
587-
curl_easy_setopt(curl, CURLOPT_USERAGENT, GUAC_AAD_USER_AGENT);
708+
curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent);
588709
curl_easy_setopt(curl, CURLOPT_TIMEOUT,
589710
(long) GUAC_AAD_HTTP_TIMEOUT_SECONDS);
590711

@@ -761,13 +882,17 @@ static void guac_rdp_aad_get_credential_type(guac_client* client,
761882
* @param params
762883
* The AAD authentication parameters including username and password.
763884
*
885+
* @param user_agent
886+
* The User-Agent string to use for HTTP requests.
887+
*
764888
* @return
765889
* A newly allocated string containing the authorization code, or NULL
766890
* if login failed. The caller must free the returned string with
767891
* guac_mem_free().
768892
*/
769893
static char* guac_rdp_aad_automated_login(guac_client* client,
770-
const char* auth_url, guac_rdp_aad_params* params) {
894+
const char* auth_url, guac_rdp_aad_params* params,
895+
const char* user_agent) {
771896

772897
char* auth_code = NULL;
773898
CURL* curl = NULL;
@@ -785,7 +910,7 @@ static char* guac_rdp_aad_automated_login(guac_client* client,
785910
}
786911

787912
curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");
788-
curl_easy_setopt(curl, CURLOPT_USERAGENT, GUAC_AAD_USER_AGENT);
913+
curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent);
789914
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
790915
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 10L);
791916
curl_easy_setopt(curl, CURLOPT_TIMEOUT,
@@ -1047,24 +1172,35 @@ char* guac_rdp_aad_get_token_authcode(guac_client* client,
10471172
return NULL;
10481173
}
10491174

1175+
/* Fetch User-Agent string once for the entire auth flow */
1176+
char* user_agent = guac_rdp_aad_get_user_agent(client);
1177+
if (user_agent == NULL) {
1178+
guac_client_log(client, GUAC_LOG_ERROR,
1179+
"AAD: Failed to allocate User-Agent string");
1180+
return NULL;
1181+
}
1182+
10501183
/* Step 2: Automated login to get the authorization code */
10511184
guac_client_log(client, GUAC_LOG_INFO,
10521185
"AAD: Starting automated authorization code flow "
10531186
"for user: %s", params->username);
10541187

1055-
char* auth_code = guac_rdp_aad_automated_login(client, auth_url, params);
1188+
char* auth_code = guac_rdp_aad_automated_login(client, auth_url, params,
1189+
user_agent);
10561190

10571191
if (auth_code == NULL) {
10581192
guac_client_log(client, GUAC_LOG_ERROR,
10591193
"AAD: Failed to obtain authorization code");
1194+
guac_mem_free(user_agent);
10601195
return NULL;
10611196
}
10621197

10631198
/* Step 3: Exchange the code for an access token */
10641199
char* token = guac_rdp_aad_exchange_code_for_token(client, params,
1065-
auth_code);
1200+
auth_code, user_agent);
10661201

10671202
guac_mem_free(auth_code);
1203+
guac_mem_free(user_agent);
10681204

10691205
return token;
10701206
}

0 commit comments

Comments
 (0)