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 */
512632static 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 */
769893static 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