KiCad PCB EDA Suite
Loading...
Searching...
No Matches
remote_provider_client.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation; either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
21
22#include <curl/curl.h>
24#include <oauth/oauth_session.h>
26#include <wx/datetime.h>
27#include <wx/intl.h>
28
29
30namespace
31{
32wxString joinUrl( const wxString& aBaseUrl, const wxString& aPath )
33{
34 if( aPath.StartsWith( wxS( "https://" ) ) || aPath.StartsWith( wxS( "http://" ) ) )
35 return aPath;
36
37 wxString base = aBaseUrl;
38
39 while( base.EndsWith( wxS( "/" ) ) )
40 base.RemoveLast();
41
42 if( aPath.StartsWith( wxS( "/" ) ) )
43 return base + aPath;
44
45 return base + wxS( "/" ) + aPath;
46}
47
48
49wxString buildPartsUrl( const REMOTE_PROVIDER_METADATA& aProvider, const wxString& aPartId )
50{
51 wxString endpoint = aProvider.parts_endpoint_template;
52 endpoint.Replace( wxS( "{part_id}" ), UrlEncode( aPartId ) );
53 return joinUrl( aProvider.api_base_url, endpoint );
54}
55
56
57bool defaultHttpHandler( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, REMOTE_PROVIDER_HTTP_RESPONSE& aResponse,
58 wxString& aError )
59{
60 KICAD_CURL_EASY curl;
61 curl.SetUserAgent( "KiCad-RemoteProvider/1.0" );
62 curl.SetFollowRedirects( true );
63 curl.SetConnectTimeout( 10 );
64
65 if( !curl.SetURL( aRequest.url.ToStdString() ) )
66 {
67 aError = wxString::Format( wxS( "Unable to set URL '%s'." ), aRequest.url );
68 return false;
69 }
70
71 for( const REMOTE_PROVIDER_HTTP_HEADER& header : aRequest.headers )
72 curl.SetHeader( header.name.ToStdString(), header.value.ToStdString() );
73
75 curl.SetPostFields( aRequest.body.ToStdString() );
76
77 int result = curl.Perform();
78
79 if( result != CURLE_OK )
80 {
81 aError = wxString::FromUTF8( curl.GetErrorText( result ).c_str() );
82 return false;
83 }
84
85 aResponse.status_code = curl.GetResponseStatusCode();
86 aResponse.body = wxString::FromUTF8( curl.GetBuffer().c_str() );
87 return true;
88}
89} // namespace
90
91
93 m_handler( defaultHttpHandler )
94{
95}
96
97
102
103
104wxString REMOTE_PROVIDER_CLIENT::MetadataDiscoveryUrl( const wxString& aProviderUrl )
105{
106 static const wxString suffix = wxS( "/.well-known/kicad-remote-provider" );
107
108 if( aProviderUrl.EndsWith( suffix ) )
109 return aProviderUrl;
110
111 wxString base = aProviderUrl;
112
113 while( base.EndsWith( wxS( "/" ) ) )
114 base.RemoveLast();
115
116 return base + suffix;
117}
118
119
120bool REMOTE_PROVIDER_CLIENT::DiscoverProvider( const wxString& aProviderUrl, REMOTE_PROVIDER_METADATA& aMetadata,
121 REMOTE_PROVIDER_ERROR& aError ) const
122{
123 aError.Clear();
124
127 request.url = MetadataDiscoveryUrl( aProviderUrl );
128 request.headers.push_back( { wxS( "Accept" ), wxS( "application/json" ) } );
129
130 nlohmann::json responseJson;
131
132 if( !FetchJson( request, responseJson, aError ) )
133 return false;
134
135 wxString error;
136 std::optional<REMOTE_PROVIDER_METADATA> metadata = REMOTE_PROVIDER_METADATA::FromJson( responseJson, error );
137
138 if( !metadata )
139 {
141 aError.message = error;
142 return false;
143 }
144
145 aMetadata = *metadata;
146 return true;
147}
148
149
152 REMOTE_PROVIDER_ERROR& aError ) const
153{
154 aError.Clear();
155
157 {
159 return true;
160 }
161
164 request.url = aProvider.auth.metadata_url;
165 request.headers.push_back( { wxS( "Accept" ), wxS( "application/json" ) } );
166
167 nlohmann::json responseJson;
168
169 if( !FetchJson( request, responseJson, aError ) )
170 return false;
171
172 wxString error;
173 std::optional<REMOTE_PROVIDER_OAUTH_SERVER_METADATA> metadata =
175
176 if( !metadata )
177 {
179 aError.message = error;
180 return false;
181 }
182
183 aMetadata = *metadata;
184 return true;
185}
186
187
189 const REMOTE_PROVIDER_OAUTH_SERVER_METADATA& aMetadata, const OAUTH_SESSION& aSession,
190 const wxString& aCode, OAUTH_TOKEN_SET& aTokens, REMOTE_PROVIDER_ERROR& aError ) const
191{
192 aError.Clear();
193
196 request.url = aMetadata.token_endpoint;
197 request.body = wxString( wxS( "grant_type=authorization_code" ) )
198 + wxS( "&code=" ) + UrlEncode( aCode )
199 + wxS( "&client_id=" ) + UrlEncode( aSession.client_id )
200 + wxS( "&redirect_uri=" ) + UrlEncode( aSession.redirect_uri )
201 + wxS( "&code_verifier=" ) + UrlEncode( aSession.code_verifier );
202 request.headers.push_back( { wxS( "Accept" ), wxS( "application/json" ) } );
203 request.headers.push_back(
204 { wxS( "Content-Type" ), wxS( "application/x-www-form-urlencoded" ) } );
205 request.headers.push_back( { wxS( "Cache-Control" ), wxS( "no-store" ) } );
206
208
209 if( !SendRequest( request, response, aError ) )
210 return false;
211
212 return ParseTokenResponse( response, aTokens, aError );
213}
214
215
217 const REMOTE_PROVIDER_OAUTH_SERVER_METADATA& aMetadata, const wxString& aClientId,
218 const wxString& aRefreshToken, OAUTH_TOKEN_SET& aTokens, REMOTE_PROVIDER_ERROR& aError ) const
219{
220 aError.Clear();
221
224 request.url = aMetadata.token_endpoint;
225 request.body = wxString( wxS( "grant_type=refresh_token" ) )
226 + wxS( "&refresh_token=" ) + UrlEncode( aRefreshToken )
227 + wxS( "&client_id=" ) + UrlEncode( aClientId );
228 request.headers.push_back( { wxS( "Accept" ), wxS( "application/json" ) } );
229 request.headers.push_back(
230 { wxS( "Content-Type" ), wxS( "application/x-www-form-urlencoded" ) } );
231 request.headers.push_back( { wxS( "Cache-Control" ), wxS( "no-store" ) } );
232
234
235 if( !SendRequest( request, response, aError ) )
236 return false;
237
238 return ParseTokenResponse( response, aTokens, aError );
239}
240
241
243 const wxString& aClientId, const wxString& aToken,
244 REMOTE_PROVIDER_ERROR& aError ) const
245{
246 aError.Clear();
247
248 if( aMetadata.revocation_endpoint.IsEmpty() )
249 return true;
250
253 request.url = aMetadata.revocation_endpoint;
254 request.body = wxString( wxS( "token=" ) ) + UrlEncode( aToken )
255 + wxS( "&client_id=" ) + UrlEncode( aClientId );
256 request.headers.push_back( { wxS( "Accept" ), wxS( "application/json" ) } );
257 request.headers.push_back(
258 { wxS( "Content-Type" ), wxS( "application/x-www-form-urlencoded" ) } );
259
261 return SendRequest( request, response, aError );
262}
263
264
266 const wxString& aAccessToken ) const
267{
270
271 if( aAccessToken.IsEmpty() )
273
275}
276
277
278bool REMOTE_PROVIDER_CLIENT::FetchManifest( const REMOTE_PROVIDER_METADATA& aProvider, const wxString& aPartId,
279 const wxString& aAccessToken, REMOTE_PROVIDER_PART_MANIFEST& aManifest,
280 REMOTE_PROVIDER_ERROR& aError ) const
281{
282 aError.Clear();
283
284 if( GetSignInState( aProvider, aAccessToken ) == REMOTE_PROVIDER_SIGNIN_STATE::REQUIRED )
285 {
287 aError.message = _( "Sign in required for this provider." );
288 return false;
289 }
290
293 request.url = buildPartsUrl( aProvider, aPartId );
294 request.headers.push_back( { wxS( "Accept" ), wxS( "application/json" ) } );
295
296 if( !aAccessToken.IsEmpty() )
297 request.headers.push_back( { wxS( "Authorization" ), wxS( "Bearer " ) + aAccessToken } );
298
299 nlohmann::json responseJson;
300
301 if( !FetchJson( request, responseJson, aError ) )
302 return false;
303
304 wxString error;
305 std::optional<REMOTE_PROVIDER_PART_MANIFEST> manifest =
307
308 if( !manifest )
309 {
311 aError.message = error;
312 return false;
313 }
314
315 aManifest = *manifest;
316 return true;
317}
318
319
321 const wxString& aAccessToken, wxString& aNonceUrl,
322 REMOTE_PROVIDER_ERROR& aError ) const
323{
324 aError.Clear();
325
326 if( aMetadata.session_bootstrap_url.IsEmpty() )
327 {
329 aError.message = _( "Provider metadata does not include a session bootstrap URL." );
330 return false;
331 }
332
333 nlohmann::json body;
334 body["access_token"] = aAccessToken.ToStdString();
335 body["next_url"] = aMetadata.panel_url.ToStdString();
336
339 request.url = aMetadata.session_bootstrap_url;
340 request.body = wxString::FromUTF8( body.dump().c_str() );
341 request.headers.push_back( { wxS( "Accept" ), wxS( "application/json" ) } );
342 request.headers.push_back( { wxS( "Content-Type" ), wxS( "application/json" ) } );
343
344 nlohmann::json responseJson;
345
346 if( !FetchJson( request, responseJson, aError ) )
347 return false;
348
349 if( !responseJson.contains( "nonce_url" ) || !responseJson["nonce_url"].is_string() )
350 {
352 aError.message = _( "Bootstrap response did not include a nonce URL." );
353 return false;
354 }
355
356 aNonceUrl = wxString::FromUTF8( responseJson["nonce_url"].get_ref<const std::string&>().c_str() );
357
358 wxString securityError;
359
360 if( !ValidateRemoteUrlSecurity( aNonceUrl, aMetadata.allow_insecure_localhost, securityError,
361 wxS( "Bootstrap nonce URL" ) ) )
362 {
364 aError.message = securityError;
365 return false;
366 }
367
368 if( NormalizedUrlOrigin( aNonceUrl ) != NormalizedUrlOrigin( aMetadata.session_bootstrap_url ) )
369 {
371 aError.message = _( "Bootstrap nonce URL origin does not match the bootstrap endpoint." );
372 return false;
373 }
374
375 return true;
376}
377
378
381 REMOTE_PROVIDER_ERROR& aError ) const
382{
383 wxString transportError;
384
385 if( !m_handler( aRequest, aResponse, transportError ) )
386 {
388 aError.message = transportError;
389 return false;
390 }
391
392 aError.http_status = aResponse.status_code;
393
394 if( aResponse.status_code < 200 || aResponse.status_code >= 300 )
395 {
396 if( aResponse.status_code == 401 )
398 else if( aResponse.status_code == 403 )
400 else if( aResponse.status_code == 404 )
402 else if( aResponse.status_code >= 500 )
404 else
406
407 aError.message = wxString::Format( _( "Remote provider request failed with HTTP %d." ),
408 aResponse.status_code );
409 return false;
410 }
411
412 return true;
413}
414
415
416bool REMOTE_PROVIDER_CLIENT::FetchJson( const REMOTE_PROVIDER_HTTP_REQUEST& aRequest, nlohmann::json& aJson,
417 REMOTE_PROVIDER_ERROR& aError ) const
418{
420
421 if( !SendRequest( aRequest, response, aError ) )
422 return false;
423
424 try
425 {
426 aJson = nlohmann::json::parse( response.body.ToStdString() );
427 }
428 catch( const std::exception& e )
429 {
431 aError.message =
432 wxString::Format( _( "Remote provider returned invalid JSON: %s" ), wxString::FromUTF8( e.what() ) );
433 return false;
434 }
435
436 return true;
437}
438
439
441 OAUTH_TOKEN_SET& aTokens,
442 REMOTE_PROVIDER_ERROR& aError ) const
443{
444 nlohmann::json json;
445
446 try
447 {
448 json = nlohmann::json::parse( aResponse.body.ToStdString() );
449 }
450 catch( const std::exception& e )
451 {
453 aError.message = wxString::Format( _( "Remote provider returned invalid token JSON: %s" ),
454 wxString::FromUTF8( e.what() ) );
455 return false;
456 }
457
458 try
459 {
460 aTokens = OAUTH_TOKEN_SET();
461 aTokens.access_token =
462 wxString::FromUTF8( json.at( "access_token" ).get_ref<const std::string&>().c_str() );
463 aTokens.refresh_token = json.contains( "refresh_token" )
464 ? wxString::FromUTF8(
465 json.at( "refresh_token" ).get_ref<const std::string&>().c_str() )
466 : wxString();
467 aTokens.id_token = json.contains( "id_token" )
468 ? wxString::FromUTF8(
469 json.at( "id_token" ).get_ref<const std::string&>().c_str() )
470 : wxString();
471 aTokens.token_type = json.contains( "token_type" )
472 ? wxString::FromUTF8(
473 json.at( "token_type" ).get_ref<const std::string&>().c_str() )
474 : wxString( "Bearer" );
475 aTokens.scope = json.contains( "scope" )
476 ? wxString::FromUTF8( json.at( "scope" ).get_ref<const std::string&>().c_str() )
477 : wxString();
478
479 const long long expiresIn = json.contains( "expires_in" ) ? json.at( "expires_in" ).get<long long>() : 0;
480 aTokens.expires_at = static_cast<long long>( wxDateTime::Now().GetTicks() ) + expiresIn;
481 }
482 catch( const std::exception& e )
483 {
485 aError.message = wxString::Format( _( "Remote provider token response was incomplete: %s" ),
486 wxString::FromUTF8( e.what() ) );
487 return false;
488 }
489
490 return true;
491}
int Perform()
Equivalent to curl_easy_perform.
bool SetPostFields(const std::vector< std::pair< std::string, std::string > > &aFields)
Set fields for application/x-www-form-urlencoded POST request.
bool SetUserAgent(const std::string &aAgent)
Set the request user agent.
void SetHeader(const std::string &aName, const std::string &aValue)
Set an arbitrary header for the HTTP(s) request.
const std::string & GetBuffer()
Return a reference to the received data buffer.
bool SetURL(const std::string &aURL)
Set the request URL.
bool SetFollowRedirects(bool aFollow)
Enable the following of HTTP(s) and other redirects, by default curl does not follow redirects.
bool SetConnectTimeout(long aTimeoutSecs)
Set the connection timeout in seconds.
const std::string GetErrorText(int aCode)
Fetch CURL's "friendly" error string for a given error code.
REMOTE_PROVIDER_HTTP_HANDLER m_handler
bool RevokeToken(const REMOTE_PROVIDER_OAUTH_SERVER_METADATA &aMetadata, const wxString &aClientId, const wxString &aToken, REMOTE_PROVIDER_ERROR &aError) const
static wxString MetadataDiscoveryUrl(const wxString &aProviderUrl)
REMOTE_PROVIDER_SIGNIN_STATE GetSignInState(const REMOTE_PROVIDER_METADATA &aProvider, const wxString &aAccessToken) const
bool ExchangeBootstrapNonce(const REMOTE_PROVIDER_METADATA &aMetadata, const wxString &aAccessToken, wxString &aNonceUrl, REMOTE_PROVIDER_ERROR &aError) const
bool SendRequest(const REMOTE_PROVIDER_HTTP_REQUEST &aRequest, REMOTE_PROVIDER_HTTP_RESPONSE &aResponse, REMOTE_PROVIDER_ERROR &aError) const
bool FetchOAuthServerMetadata(const REMOTE_PROVIDER_METADATA &aProvider, REMOTE_PROVIDER_OAUTH_SERVER_METADATA &aMetadata, REMOTE_PROVIDER_ERROR &aError) const
bool FetchManifest(const REMOTE_PROVIDER_METADATA &aProvider, const wxString &aPartId, const wxString &aAccessToken, REMOTE_PROVIDER_PART_MANIFEST &aManifest, REMOTE_PROVIDER_ERROR &aError) const
bool ExchangeAuthorizationCode(const REMOTE_PROVIDER_OAUTH_SERVER_METADATA &aMetadata, const OAUTH_SESSION &aSession, const wxString &aCode, OAUTH_TOKEN_SET &aTokens, REMOTE_PROVIDER_ERROR &aError) const
bool RefreshAccessToken(const REMOTE_PROVIDER_OAUTH_SERVER_METADATA &aMetadata, const wxString &aClientId, const wxString &aRefreshToken, OAUTH_TOKEN_SET &aTokens, REMOTE_PROVIDER_ERROR &aError) const
bool FetchJson(const REMOTE_PROVIDER_HTTP_REQUEST &aRequest, nlohmann::json &aJson, REMOTE_PROVIDER_ERROR &aError) const
bool ParseTokenResponse(const REMOTE_PROVIDER_HTTP_RESPONSE &aResponse, OAUTH_TOKEN_SET &aTokens, REMOTE_PROVIDER_ERROR &aError) const
bool DiscoverProvider(const wxString &aProviderUrl, REMOTE_PROVIDER_METADATA &aMetadata, REMOTE_PROVIDER_ERROR &aError) const
#define _(s)
nlohmann::json json
Definition gerbview.cpp:50
STL namespace.
std::function< bool(const REMOTE_PROVIDER_HTTP_REQUEST &, REMOTE_PROVIDER_HTTP_RESPONSE &, wxString &)> REMOTE_PROVIDER_HTTP_HANDLER
REMOTE_PROVIDER_SIGNIN_STATE
wxString NormalizedUrlOrigin(const wxString &aUrl)
Return a normalized scheme://host:port origin string for aUrl.
wxString UrlEncode(const wxString &aValue)
Percent-encode a string for use in URL query parameters (RFC 3986 unreserved characters are passed th...
bool ValidateRemoteUrlSecurity(const wxString &aUrl, bool aAllowInsecureLocalhost, wxString &aError, const wxString &aLabel)
Validate that aUrl uses HTTPS, or HTTP on a loopback address when aAllowInsecureLocalhost is true.
wxString client_id
wxString code_verifier
wxString redirect_uri
REMOTE_PROVIDER_ERROR_TYPE type
std::vector< REMOTE_PROVIDER_HTTP_HEADER > headers
REMOTE_PROVIDER_HTTP_METHOD method
static std::optional< REMOTE_PROVIDER_METADATA > FromJson(const nlohmann::json &aJson, wxString &aError)
REMOTE_PROVIDER_AUTH_METADATA auth
static std::optional< REMOTE_PROVIDER_OAUTH_SERVER_METADATA > FromJson(const nlohmann::json &aJson, bool aAllowInsecureLocalhost, wxString &aError)
static std::optional< REMOTE_PROVIDER_PART_MANIFEST > FromJson(const nlohmann::json &aJson, bool aAllowInsecureLocalhost, wxString &aError)
wxString result
Test unit parsing edge cases and error handling.