Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * oauth-curl.c
4 : : * The libcurl implementation of OAuth/OIDC authentication, using the
5 : : * OAuth Device Authorization Grant (RFC 8628).
6 : : *
7 : : * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
8 : : * Portions Copyright (c) 1994, Regents of the University of California
9 : : *
10 : : * IDENTIFICATION
11 : : * src/interfaces/libpq-oauth/oauth-curl.c
12 : : *
13 : : *-------------------------------------------------------------------------
14 : : */
15 : :
16 : : #include "postgres_fe.h"
17 : :
18 : : #include <curl/curl.h>
19 : : #include <math.h>
20 : : #include <unistd.h>
21 : :
22 : : #if defined(HAVE_SYS_EPOLL_H)
23 : : #include <sys/epoll.h>
24 : : #include <sys/timerfd.h>
25 : : #elif defined(HAVE_SYS_EVENT_H)
26 : : #include <sys/event.h>
27 : : #else
28 : : #error libpq-oauth is not supported on this platform
29 : : #endif
30 : :
31 : : #include "common/jsonapi.h"
32 : : #include "mb/pg_wchar.h"
33 : : #include "oauth-curl.h"
34 : :
35 : : #ifdef USE_DYNAMIC_OAUTH
36 : :
37 : : /*
38 : : * The module build is decoupled from libpq-int.h, to try to avoid inadvertent
39 : : * ABI breaks during minor version bumps. Replacements for the missing internals
40 : : * are provided by oauth-utils.
41 : : */
42 : : #include "oauth-utils.h"
43 : :
44 : : #else /* !USE_DYNAMIC_OAUTH */
45 : :
46 : : /* Static builds may make use of libpq internals directly. */
47 : : #include "fe-auth-oauth.h"
48 : : #include "libpq-int.h"
49 : :
50 : : #endif /* USE_DYNAMIC_OAUTH */
51 : :
52 : : /*
53 : : * oauth-debug.h needs the declaration of libpq_gettext(), from one of the above
54 : : * sources.
55 : : */
56 : : #include "oauth-debug.h"
57 : :
58 : : /* One final guardrail against accidental inclusion... */
59 : : #if defined(USE_DYNAMIC_OAUTH) && defined(LIBPQ_INT_H)
60 : : #error do not rely on libpq-int.h in dynamic builds of libpq-oauth
61 : : #endif
62 : :
63 : : /*
64 : : * It's generally prudent to set a maximum response size to buffer in memory,
65 : : * but it's less clear what size to choose. The biggest of our expected
66 : : * responses is the server metadata JSON, which will only continue to grow in
67 : : * size; the number of IANA-registered parameters in that document is up to 78
68 : : * as of February 2025.
69 : : *
70 : : * Even if every single parameter were to take up 2k on average (a previously
71 : : * common limit on the size of a URL), 256k gives us 128 parameter values before
72 : : * we give up. (That's almost certainly complete overkill in practice; 2-4k
73 : : * appears to be common among popular providers at the moment.)
74 : : */
75 : : #define MAX_OAUTH_RESPONSE_SIZE (256 * 1024)
76 : :
77 : : /*
78 : : * Similarly, a limit on the maximum JSON nesting level keeps a server from
79 : : * running us out of stack space. A common nesting level in practice is 2 (for a
80 : : * top-level object containing arrays of strings). As of May 2025, the maximum
81 : : * depth for standard server metadata appears to be 6, if the document contains
82 : : * a full JSON Web Key Set in its "jwks" parameter.
83 : : *
84 : : * Since it's easy to nest JSON, and the number of parameters and key types
85 : : * keeps growing, take a healthy buffer of 16. (If this ever proves to be a
86 : : * problem in practice, we may want to switch over to the incremental JSON
87 : : * parser instead of playing with this parameter.)
88 : : */
89 : : #define MAX_OAUTH_NESTING_LEVEL 16
90 : :
91 : : /*
92 : : * Parsed JSON Representations
93 : : *
94 : : * As a general rule, we parse and cache only the fields we're currently using.
95 : : * When adding new fields, ensure the corresponding free_*() function is updated
96 : : * too.
97 : : */
98 : :
99 : : /*
100 : : * The OpenID Provider configuration (alternatively named "authorization server
101 : : * metadata") jointly described by OpenID Connect Discovery 1.0 and RFC 8414:
102 : : *
103 : : * https://openid.net/specs/openid-connect-discovery-1_0.html
104 : : * https://www.rfc-editor.org/rfc/rfc8414#section-3.2
105 : : */
106 : : struct provider
107 : : {
108 : : char *issuer;
109 : : char *token_endpoint;
110 : : char *device_authorization_endpoint;
111 : : struct curl_slist *grant_types_supported;
112 : : };
113 : :
114 : : static void
439 dgustafsson@postgres 115 :CBC 57 : free_provider(struct provider *provider)
116 : : {
117 : 57 : free(provider->issuer);
118 : 57 : free(provider->token_endpoint);
119 : 57 : free(provider->device_authorization_endpoint);
120 : 57 : curl_slist_free_all(provider->grant_types_supported);
121 : 57 : }
122 : :
123 : : /*
124 : : * The Device Authorization response, described by RFC 8628:
125 : : *
126 : : * https://www.rfc-editor.org/rfc/rfc8628#section-3.2
127 : : */
128 : : struct device_authz
129 : : {
130 : : char *device_code;
131 : : char *user_code;
132 : : char *verification_uri;
133 : : char *verification_uri_complete;
134 : : char *expires_in_str;
135 : : char *interval_str;
136 : :
137 : : /* Fields below are parsed from the corresponding string above. */
138 : : int expires_in;
139 : : int interval;
140 : : };
141 : :
142 : : static void
143 : 57 : free_device_authz(struct device_authz *authz)
144 : : {
145 : 57 : free(authz->device_code);
146 : 57 : free(authz->user_code);
147 : 57 : free(authz->verification_uri);
148 : 57 : free(authz->verification_uri_complete);
149 : 57 : free(authz->expires_in_str);
150 : 57 : free(authz->interval_str);
151 : 57 : }
152 : :
153 : : /*
154 : : * The Token Endpoint error response, as described by RFC 6749:
155 : : *
156 : : * https://www.rfc-editor.org/rfc/rfc6749#section-5.2
157 : : *
158 : : * Note that this response type can also be returned from the Device
159 : : * Authorization Endpoint.
160 : : */
161 : : struct token_error
162 : : {
163 : : char *error;
164 : : char *error_description;
165 : : };
166 : :
167 : : static void
168 : 57 : free_token_error(struct token_error *err)
169 : : {
170 : 57 : free(err->error);
171 : 57 : free(err->error_description);
172 : 57 : }
173 : :
174 : : /*
175 : : * The Access Token response, as described by RFC 6749:
176 : : *
177 : : * https://www.rfc-editor.org/rfc/rfc6749#section-4.1.4
178 : : *
179 : : * During the Device Authorization flow, several temporary errors are expected
180 : : * as part of normal operation. To make it easy to handle these in the happy
181 : : * path, this contains an embedded token_error that is filled in if needed.
182 : : */
183 : : struct token
184 : : {
185 : : /* for successful responses */
186 : : char *access_token;
187 : : char *token_type;
188 : :
189 : : /* for error responses */
190 : : struct token_error err;
191 : : };
192 : :
193 : : static void
194 : 57 : free_token(struct token *tok)
195 : : {
196 : 57 : free(tok->access_token);
197 : 57 : free(tok->token_type);
198 : 57 : free_token_error(&tok->err);
199 : 57 : }
200 : :
201 : : /*
202 : : * Asynchronous State
203 : : */
204 : :
205 : : /* States for the overall async machine. */
206 : : enum OAuthStep
207 : : {
208 : : OAUTH_STEP_INIT = 0,
209 : : OAUTH_STEP_DISCOVERY,
210 : : OAUTH_STEP_DEVICE_AUTHORIZATION,
211 : : OAUTH_STEP_TOKEN_REQUEST,
212 : : OAUTH_STEP_WAIT_INTERVAL,
213 : : };
214 : :
215 : : /*
216 : : * The async_ctx holds onto state that needs to persist across multiple calls
217 : : * to pg_fe_run_oauth_flow(). Almost everything interacts with this in some
218 : : * way.
219 : : */
220 : : struct async_ctx
221 : : {
222 : : /* relevant connection options cached from the PGconn */
223 : : char *client_id; /* oauth_client_id */
224 : : char *client_secret; /* oauth_client_secret (may be NULL) */
225 : : char *ca_file; /* oauth_ca_file */
226 : :
227 : : /* options cached from the PGoauthBearerRequest (we don't own these) */
228 : : const char *discovery_uri;
229 : : const char *issuer_id;
230 : : const char *scope;
231 : :
232 : : enum OAuthStep step; /* where are we in the flow? */
233 : :
234 : : int timerfd; /* descriptor for signaling async timeouts */
235 : : pgsocket mux; /* the multiplexer socket containing all
236 : : * descriptors tracked by libcurl, plus the
237 : : * timerfd */
238 : : CURLM *curlm; /* top-level multi handle for libcurl
239 : : * operations */
240 : : CURL *curl; /* the (single) easy handle for serial
241 : : * requests */
242 : :
243 : : struct curl_slist *headers; /* common headers for all requests */
244 : : PQExpBufferData work_data; /* scratch buffer for general use (remember to
245 : : * clear out prior contents first!) */
246 : :
247 : : /*------
248 : : * Since a single logical operation may stretch across multiple calls to
249 : : * our entry point, errors have three parts:
250 : : *
251 : : * - errctx: an optional static string, describing the global operation
252 : : * currently in progress. Should be translated with
253 : : * libpq_gettext().
254 : : *
255 : : * - errbuf: contains the actual error message. Generally speaking, use
256 : : * actx_error[_str] to manipulate this. This must be filled
257 : : * with something useful on an error.
258 : : *
259 : : * - curl_err: an optional static error buffer used by libcurl to put
260 : : * detailed information about failures. Unfortunately
261 : : * untranslatable.
262 : : *
263 : : * These pieces will be combined into a single error message looking
264 : : * something like the following, with errctx and/or curl_err omitted when
265 : : * absent:
266 : : *
267 : : * connection to server ... failed: errctx: errbuf (libcurl: curl_err)
268 : : */
269 : : const char *errctx; /* not freed; must point to static allocation */
270 : : PQExpBufferData errbuf;
271 : : char curl_err[CURL_ERROR_SIZE];
272 : :
273 : : /*
274 : : * These documents need to survive over multiple calls, and are therefore
275 : : * cached directly in the async_ctx.
276 : : */
277 : : struct provider provider;
278 : : struct device_authz authz;
279 : :
280 : : int running; /* is asynchronous work in progress? */
281 : : bool user_prompted; /* have we already sent the authz prompt? */
282 : : bool used_basic_auth; /* did we send a client secret? */
283 : : uint32 debug_flags; /* can we give developer assistance? */
284 : : int dbg_num_calls; /* (debug mode) how many times were we called? */
285 : : };
286 : :
287 : : /*
288 : : * Tears down the Curl handles and frees the async_ctx.
289 : : */
290 : : static void
61 jchampion@postgresql 291 :GNC 57 : free_async_ctx(struct async_ctx *actx)
292 : : {
293 : : /*
294 : : * In general, none of the error cases below should ever happen if we have
295 : : * no bugs above. But if we do hit them, surfacing those errors somehow
296 : : * might be the only way to have a chance to debug them.
297 : : *
298 : : * Print them as warnings to stderr, following the example of similar
299 : : * situations in fe-secure-openssl.c and fe-connect.c.
300 : : */
301 : :
439 dgustafsson@postgres 302 [ + - + - ]:CBC 57 : if (actx->curlm && actx->curl)
303 : : {
304 : 57 : CURLMcode err = curl_multi_remove_handle(actx->curlm, actx->curl);
305 : :
306 [ - + ]: 57 : if (err)
61 jchampion@postgresql 307 :UNC 0 : fprintf(stderr,
308 : 0 : libpq_gettext("WARNING: libcurl easy handle removal failed: %s\n"),
309 : : curl_multi_strerror(err));
310 : : }
311 : :
439 dgustafsson@postgres 312 [ + - ]:CBC 57 : if (actx->curl)
313 : : {
314 : : /*
315 : : * curl_multi_cleanup() doesn't free any associated easy handles; we
316 : : * need to do that separately. We only ever have one easy handle per
317 : : * multi handle.
318 : : */
319 : 57 : curl_easy_cleanup(actx->curl);
320 : : }
321 : :
322 [ + - ]: 57 : if (actx->curlm)
323 : : {
324 : 57 : CURLMcode err = curl_multi_cleanup(actx->curlm);
325 : :
326 [ - + ]: 57 : if (err)
61 jchampion@postgresql 327 :UNC 0 : fprintf(stderr,
328 : 0 : libpq_gettext("WARNING: libcurl multi handle cleanup failed: %s\n"),
329 : : curl_multi_strerror(err));
330 : : }
331 : :
439 dgustafsson@postgres 332 :CBC 57 : free_provider(&actx->provider);
333 : 57 : free_device_authz(&actx->authz);
334 : :
335 : 57 : curl_slist_free_all(actx->headers);
336 : 57 : termPQExpBuffer(&actx->work_data);
337 : 57 : termPQExpBuffer(&actx->errbuf);
338 : :
339 [ + - ]: 57 : if (actx->mux != PGINVALID_SOCKET)
340 : 57 : close(actx->mux);
341 [ + - ]: 57 : if (actx->timerfd >= 0)
342 : 57 : close(actx->timerfd);
343 : :
53 jchampion@postgresql 344 :GNC 57 : free(actx->client_id);
345 : 57 : free(actx->client_secret);
36 346 : 57 : free(actx->ca_file);
347 : :
439 dgustafsson@postgres 348 :CBC 57 : free(actx);
349 : 57 : }
350 : :
351 : : /*
352 : : * Release resources used for the asynchronous exchange.
353 : : */
354 : : static void
53 jchampion@postgresql 355 :GNC 57 : pg_fe_cleanup_oauth_flow(PGconn *conn, PGoauthBearerRequest *request)
356 : : {
357 : 57 : struct async_ctx *actx = request->user;
358 : :
359 : : /* request->cleanup is only set after actx has been allocated. */
360 [ - + ]: 57 : Assert(actx);
361 : :
362 : 57 : free_async_ctx(actx);
363 : 57 : request->user = NULL;
364 : :
365 : : /* libpq has made its own copy of the token; clear ours now. */
366 [ + + ]: 57 : if (request->token)
367 : : {
368 : 40 : explicit_bzero(request->token, strlen(request->token));
369 : 40 : free(request->token);
370 : 40 : request->token = NULL;
371 : : }
372 : 57 : }
373 : :
374 : : /*
375 : : * Builds an error message from actx and stores it in req->error. The allocation
376 : : * is backed by actx->work_data (which will be reset first).
377 : : */
378 : : static void
379 : 15 : append_actx_error(PGoauthBearerRequestV2 *req, struct async_ctx *actx)
380 : : {
381 : 15 : PQExpBuffer errbuf = &actx->work_data;
382 : :
383 : 15 : resetPQExpBuffer(errbuf);
384 : :
385 : : /*
386 : : * Assemble the three parts of our error: context, body, and detail. See
387 : : * also the documentation for struct async_ctx.
388 : : */
389 [ + - ]: 15 : if (actx->errctx)
390 : 15 : appendPQExpBuffer(errbuf, "%s: ", actx->errctx);
391 : :
392 [ - + ]: 15 : if (PQExpBufferDataBroken(actx->errbuf))
53 jchampion@postgresql 393 :UNC 0 : appendPQExpBufferStr(errbuf, libpq_gettext("out of memory"));
394 : : else
53 jchampion@postgresql 395 :GNC 15 : appendPQExpBufferStr(errbuf, actx->errbuf.data);
396 : :
397 [ + + ]: 15 : if (actx->curl_err[0])
398 : : {
399 : 3 : appendPQExpBuffer(errbuf, " (libcurl: %s)", actx->curl_err);
400 : :
401 : : /* Sometimes libcurl adds a newline to the error buffer. :( */
402 [ + - - + ]: 3 : if (errbuf->len >= 2 && errbuf->data[errbuf->len - 2] == '\n')
403 : : {
53 jchampion@postgresql 404 :UNC 0 : errbuf->data[errbuf->len - 2] = ')';
405 : 0 : errbuf->data[errbuf->len - 1] = '\0';
406 : 0 : errbuf->len--;
407 : : }
408 : : }
409 : :
53 jchampion@postgresql 410 :GNC 15 : req->error = errbuf->data;
439 dgustafsson@postgres 411 :CBC 15 : }
412 : :
413 : : /*
414 : : * Macros for manipulating actx->errbuf. actx_error() translates and formats a
415 : : * string for you, actx_error_internal() is the untranslated equivalent, and
416 : : * actx_error_str() appends a string directly (also without translation).
417 : : */
418 : :
419 : : #define actx_error(ACTX, FMT, ...) \
420 : : appendPQExpBuffer(&(ACTX)->errbuf, libpq_gettext(FMT), ##__VA_ARGS__)
421 : :
422 : : #define actx_error_internal(ACTX, FMT, ...) \
423 : : appendPQExpBuffer(&(ACTX)->errbuf, FMT, ##__VA_ARGS__)
424 : :
425 : : #define actx_error_str(ACTX, S) \
426 : : appendPQExpBufferStr(&(ACTX)->errbuf, S)
427 : :
428 : : /*
429 : : * Macros for getting and setting state for the connection's two libcurl
430 : : * handles, so you don't have to write out the error handling every time.
431 : : */
432 : :
433 : : #define CHECK_MSETOPT(ACTX, OPT, VAL, FAILACTION) \
434 : : do { \
435 : : struct async_ctx *_actx = (ACTX); \
436 : : CURLMcode _setopterr = curl_multi_setopt(_actx->curlm, OPT, VAL); \
437 : : if (_setopterr) { \
438 : : actx_error(_actx, "failed to set %s on OAuth connection: %s",\
439 : : #OPT, curl_multi_strerror(_setopterr)); \
440 : : FAILACTION; \
441 : : } \
442 : : } while (0)
443 : :
444 : : #define CHECK_SETOPT(ACTX, OPT, VAL, FAILACTION) \
445 : : do { \
446 : : struct async_ctx *_actx = (ACTX); \
447 : : CURLcode _setopterr = curl_easy_setopt(_actx->curl, OPT, VAL); \
448 : : if (_setopterr) { \
449 : : actx_error(_actx, "failed to set %s on OAuth connection: %s",\
450 : : #OPT, curl_easy_strerror(_setopterr)); \
451 : : FAILACTION; \
452 : : } \
453 : : } while (0)
454 : :
455 : : #define CHECK_GETINFO(ACTX, INFO, OUT, FAILACTION) \
456 : : do { \
457 : : struct async_ctx *_actx = (ACTX); \
458 : : CURLcode _getinfoerr = curl_easy_getinfo(_actx->curl, INFO, OUT); \
459 : : if (_getinfoerr) { \
460 : : actx_error(_actx, "failed to get %s from OAuth response: %s",\
461 : : #INFO, curl_easy_strerror(_getinfoerr)); \
462 : : FAILACTION; \
463 : : } \
464 : : } while (0)
465 : :
466 : : /*
467 : : * General JSON Parsing for OAuth Responses
468 : : */
469 : :
470 : : /*
471 : : * Represents a single name/value pair in a JSON object. This is the primary
472 : : * interface to parse_oauth_json().
473 : : *
474 : : * All fields are stored internally as strings or lists of strings, so clients
475 : : * have to explicitly parse other scalar types (though they will have gone
476 : : * through basic lexical validation). Storing nested objects is not currently
477 : : * supported, nor is parsing arrays of anything other than strings.
478 : : */
479 : : struct json_field
480 : : {
481 : : const char *name; /* name (key) of the member */
482 : :
483 : : JsonTokenType type; /* currently supports JSON_TOKEN_STRING,
484 : : * JSON_TOKEN_NUMBER, and
485 : : * JSON_TOKEN_ARRAY_START */
486 : : union
487 : : {
488 : : char **scalar; /* for all scalar types */
489 : : struct curl_slist **array; /* for type == JSON_TOKEN_ARRAY_START */
490 : : };
491 : :
492 : : bool required; /* REQUIRED field, or just OPTIONAL? */
493 : : };
494 : :
495 : : /* Documentation macros for json_field.required. */
496 : : #define PG_OAUTH_REQUIRED true
497 : : #define PG_OAUTH_OPTIONAL false
498 : :
499 : : /* Parse state for parse_oauth_json(). */
500 : : struct oauth_parse
501 : : {
502 : : PQExpBuffer errbuf; /* detail message for JSON_SEM_ACTION_FAILED */
503 : : int nested; /* nesting level (zero is the top) */
504 : :
505 : : const struct json_field *fields; /* field definition array */
506 : : const struct json_field *active; /* points inside the fields array */
507 : : };
508 : :
509 : : #define oauth_parse_set_error(ctx, fmt, ...) \
510 : : appendPQExpBuffer((ctx)->errbuf, libpq_gettext(fmt), ##__VA_ARGS__)
511 : :
512 : : #define oauth_parse_set_error_internal(ctx, fmt, ...) \
513 : : appendPQExpBuffer((ctx)->errbuf, fmt, ##__VA_ARGS__)
514 : :
515 : : static void
439 dgustafsson@postgres 516 :UBC 0 : report_type_mismatch(struct oauth_parse *ctx)
517 : : {
518 : : char *msgfmt;
519 : :
520 [ # # ]: 0 : Assert(ctx->active);
521 : :
522 : : /*
523 : : * At the moment, the only fields we're interested in are strings,
524 : : * numbers, and arrays of strings.
525 : : */
526 [ # # # # ]: 0 : switch (ctx->active->type)
527 : : {
528 : 0 : case JSON_TOKEN_STRING:
141 jchampion@postgresql 529 : 0 : msgfmt = gettext_noop("field \"%s\" must be a string");
439 dgustafsson@postgres 530 : 0 : break;
531 : :
532 : 0 : case JSON_TOKEN_NUMBER:
141 jchampion@postgresql 533 : 0 : msgfmt = gettext_noop("field \"%s\" must be a number");
439 dgustafsson@postgres 534 : 0 : break;
535 : :
536 : 0 : case JSON_TOKEN_ARRAY_START:
141 jchampion@postgresql 537 : 0 : msgfmt = gettext_noop("field \"%s\" must be an array of strings");
439 dgustafsson@postgres 538 : 0 : break;
539 : :
540 : 0 : default:
541 : 0 : Assert(false);
542 : : msgfmt = gettext_noop("field \"%s\" has unexpected type");
543 : : }
544 : :
545 : 0 : oauth_parse_set_error(ctx, msgfmt, ctx->active->name);
546 : 0 : }
547 : :
548 : : static JsonParseErrorType
439 dgustafsson@postgres 549 :CBC 192 : oauth_json_object_start(void *state)
550 : : {
551 : 192 : struct oauth_parse *ctx = state;
552 : :
553 [ - + ]: 192 : if (ctx->active)
554 : : {
555 : : /*
556 : : * Currently, none of the fields we're interested in can be or contain
557 : : * objects, so we can reject this case outright.
558 : : */
439 dgustafsson@postgres 559 :UBC 0 : report_type_mismatch(ctx);
560 : 0 : return JSON_SEM_ACTION_FAILED;
561 : : }
562 : :
439 dgustafsson@postgres 563 :CBC 192 : ++ctx->nested;
347 jchampion@postgresql 564 [ + + ]: 192 : if (ctx->nested > MAX_OAUTH_NESTING_LEVEL)
565 : : {
566 : 1 : oauth_parse_set_error(ctx, "JSON is too deeply nested");
567 : 1 : return JSON_SEM_ACTION_FAILED;
568 : : }
569 : :
439 dgustafsson@postgres 570 : 191 : return JSON_SUCCESS;
571 : : }
572 : :
573 : : static JsonParseErrorType
574 : 767 : oauth_json_object_field_start(void *state, char *name, bool isnull)
575 : : {
576 : 767 : struct oauth_parse *ctx = state;
577 : :
578 : : /* We care only about the top-level fields. */
579 [ + + ]: 767 : if (ctx->nested == 1)
580 : : {
581 : 737 : const struct json_field *field = ctx->fields;
582 : :
583 : : /*
584 : : * We should never start parsing a new field while a previous one is
585 : : * still active.
586 : : */
587 [ - + ]: 737 : if (ctx->active)
588 : : {
439 dgustafsson@postgres 589 :UBC 0 : Assert(false);
590 : : oauth_parse_set_error_internal(ctx,
591 : : "internal error: started field \"%s\" before field \"%s\" was finished",
592 : : name, ctx->active->name);
593 : : return JSON_SEM_ACTION_FAILED;
594 : : }
595 : :
439 dgustafsson@postgres 596 [ + + ]:CBC 2388 : while (field->name)
597 : : {
598 [ + + ]: 2222 : if (strcmp(name, field->name) == 0)
599 : : {
600 : 571 : ctx->active = field;
601 : 571 : break;
602 : : }
603 : :
604 : 1651 : ++field;
605 : : }
606 : :
607 : : /*
608 : : * We don't allow duplicate field names; error out if the target has
609 : : * already been set.
610 : : */
611 [ + + ]: 737 : if (ctx->active)
612 : : {
613 : 571 : field = ctx->active;
614 : :
162 jchampion@postgresql 615 [ + + + - ]:GNC 571 : if ((field->type == JSON_TOKEN_ARRAY_START && *field->array)
616 [ + + - + ]: 571 : || (field->type != JSON_TOKEN_ARRAY_START && *field->scalar))
617 : : {
439 dgustafsson@postgres 618 :UBC 0 : oauth_parse_set_error(ctx, "field \"%s\" is duplicated",
619 : : field->name);
620 : 0 : return JSON_SEM_ACTION_FAILED;
621 : : }
622 : : }
623 : : }
624 : :
439 dgustafsson@postgres 625 :CBC 767 : return JSON_SUCCESS;
626 : : }
627 : :
628 : : static JsonParseErrorType
629 : 174 : oauth_json_object_end(void *state)
630 : : {
631 : 174 : struct oauth_parse *ctx = state;
632 : :
633 : 174 : --ctx->nested;
634 : :
635 : : /*
636 : : * All fields should be fully processed by the end of the top-level
637 : : * object.
638 : : */
639 [ + + - + ]: 174 : if (!ctx->nested && ctx->active)
640 : : {
439 dgustafsson@postgres 641 :UBC 0 : Assert(false);
642 : : oauth_parse_set_error_internal(ctx,
643 : : "internal error: field \"%s\" still active at end of object",
644 : : ctx->active->name);
645 : : return JSON_SEM_ACTION_FAILED;
646 : : }
647 : :
439 dgustafsson@postgres 648 :CBC 174 : return JSON_SUCCESS;
649 : : }
650 : :
651 : : static JsonParseErrorType
652 : 247 : oauth_json_array_start(void *state)
653 : : {
654 : 247 : struct oauth_parse *ctx = state;
655 : :
656 [ - + ]: 247 : if (!ctx->nested)
657 : : {
439 dgustafsson@postgres 658 :UBC 0 : oauth_parse_set_error(ctx, "top-level element must be an object");
659 : 0 : return JSON_SEM_ACTION_FAILED;
660 : : }
661 : :
439 dgustafsson@postgres 662 [ + + ]:CBC 247 : if (ctx->active)
663 : : {
664 [ + - ]: 54 : if (ctx->active->type != JSON_TOKEN_ARRAY_START
665 : : /* The arrays we care about must not have arrays as values. */
666 [ - + ]: 54 : || ctx->nested > 1)
667 : : {
439 dgustafsson@postgres 668 :UBC 0 : report_type_mismatch(ctx);
669 : 0 : return JSON_SEM_ACTION_FAILED;
670 : : }
671 : : }
672 : :
439 dgustafsson@postgres 673 :CBC 247 : ++ctx->nested;
347 jchampion@postgresql 674 [ + + ]: 247 : if (ctx->nested > MAX_OAUTH_NESTING_LEVEL)
675 : : {
676 : 1 : oauth_parse_set_error(ctx, "JSON is too deeply nested");
677 : 1 : return JSON_SEM_ACTION_FAILED;
678 : : }
679 : :
439 dgustafsson@postgres 680 : 246 : return JSON_SUCCESS;
681 : : }
682 : :
683 : : static JsonParseErrorType
684 : 231 : oauth_json_array_end(void *state)
685 : : {
686 : 231 : struct oauth_parse *ctx = state;
687 : :
688 [ + + ]: 231 : if (ctx->active)
689 : : {
690 : : /*
691 : : * Clear the target (which should be an array inside the top-level
692 : : * object). For this to be safe, no target arrays can contain other
693 : : * arrays; we check for that in the array_start callback.
694 : : */
695 [ + - - + ]: 54 : if (ctx->nested != 2 || ctx->active->type != JSON_TOKEN_ARRAY_START)
696 : : {
439 dgustafsson@postgres 697 :UBC 0 : Assert(false);
698 : : oauth_parse_set_error_internal(ctx,
699 : : "internal error: found unexpected array end while parsing field \"%s\"",
700 : : ctx->active->name);
701 : : return JSON_SEM_ACTION_FAILED;
702 : : }
703 : :
439 dgustafsson@postgres 704 :CBC 54 : ctx->active = NULL;
705 : : }
706 : :
707 : 231 : --ctx->nested;
708 : 231 : return JSON_SUCCESS;
709 : : }
710 : :
711 : : static JsonParseErrorType
712 : 789 : oauth_json_scalar(void *state, char *token, JsonTokenType type)
713 : : {
714 : 789 : struct oauth_parse *ctx = state;
715 : :
716 [ - + ]: 789 : if (!ctx->nested)
717 : : {
439 dgustafsson@postgres 718 :UBC 0 : oauth_parse_set_error(ctx, "top-level element must be an object");
719 : 0 : return JSON_SEM_ACTION_FAILED;
720 : : }
721 : :
439 dgustafsson@postgres 722 [ + + ]:CBC 789 : if (ctx->active)
723 : : {
724 : 625 : const struct json_field *field = ctx->active;
725 : 625 : JsonTokenType expected = field->type;
726 : :
727 : : /* Make sure this matches what the active field expects. */
728 [ + + ]: 625 : if (expected == JSON_TOKEN_ARRAY_START)
729 : : {
730 : : /* Are we actually inside an array? */
731 [ - + ]: 108 : if (ctx->nested < 2)
732 : : {
439 dgustafsson@postgres 733 :UBC 0 : report_type_mismatch(ctx);
734 : 0 : return JSON_SEM_ACTION_FAILED;
735 : : }
736 : :
737 : : /* Currently, arrays can only contain strings. */
439 dgustafsson@postgres 738 :CBC 108 : expected = JSON_TOKEN_STRING;
739 : : }
740 : :
741 [ - + ]: 625 : if (type != expected)
742 : : {
439 dgustafsson@postgres 743 :UBC 0 : report_type_mismatch(ctx);
744 : 0 : return JSON_SEM_ACTION_FAILED;
745 : : }
746 : :
439 dgustafsson@postgres 747 [ + + ]:CBC 625 : if (field->type != JSON_TOKEN_ARRAY_START)
748 : : {
749 : : /* Ensure that we're parsing the top-level keys... */
750 [ - + ]: 517 : if (ctx->nested != 1)
751 : : {
439 dgustafsson@postgres 752 :UBC 0 : Assert(false);
753 : : oauth_parse_set_error_internal(ctx,
754 : : "internal error: scalar target found at nesting level %d",
755 : : ctx->nested);
756 : : return JSON_SEM_ACTION_FAILED;
757 : : }
758 : :
759 : : /* ...and that a result has not already been set. */
162 jchampion@postgresql 760 [ - + ]:GNC 517 : if (*field->scalar)
761 : : {
439 dgustafsson@postgres 762 :UBC 0 : Assert(false);
763 : : oauth_parse_set_error_internal(ctx,
764 : : "internal error: scalar field \"%s\" would be assigned twice",
765 : : ctx->active->name);
766 : : return JSON_SEM_ACTION_FAILED;
767 : : }
768 : :
162 jchampion@postgresql 769 :GNC 517 : *field->scalar = strdup(token);
770 [ - + ]: 517 : if (!*field->scalar)
439 dgustafsson@postgres 771 :UBC 0 : return JSON_OUT_OF_MEMORY;
772 : :
439 dgustafsson@postgres 773 :CBC 517 : ctx->active = NULL;
774 : :
775 : 517 : return JSON_SUCCESS;
776 : : }
777 : : else
778 : : {
779 : : struct curl_slist *temp;
780 : :
781 : : /* The target array should be inside the top-level object. */
782 [ - + ]: 108 : if (ctx->nested != 2)
783 : : {
439 dgustafsson@postgres 784 :UBC 0 : Assert(false);
785 : : oauth_parse_set_error_internal(ctx,
786 : : "internal error: array member found at nesting level %d",
787 : : ctx->nested);
788 : : return JSON_SEM_ACTION_FAILED;
789 : : }
790 : :
791 : : /* Note that curl_slist_append() makes a copy of the token. */
162 jchampion@postgresql 792 :GNC 108 : temp = curl_slist_append(*field->array, token);
439 dgustafsson@postgres 793 [ - + ]:CBC 108 : if (!temp)
439 dgustafsson@postgres 794 :UBC 0 : return JSON_OUT_OF_MEMORY;
795 : :
162 jchampion@postgresql 796 :GNC 108 : *field->array = temp;
797 : : }
798 : : }
799 : : else
800 : : {
801 : : /* otherwise we just ignore it */
802 : : }
803 : :
439 dgustafsson@postgres 804 :CBC 272 : return JSON_SUCCESS;
805 : : }
806 : :
807 : : /*
808 : : * Checks the Content-Type header against the expected type. Parameters are
809 : : * allowed but ignored.
810 : : */
811 : : static bool
812 : 164 : check_content_type(struct async_ctx *actx, const char *type)
813 : : {
814 : 164 : const size_t type_len = strlen(type);
815 : : char *content_type;
816 : :
817 [ - + ]: 164 : CHECK_GETINFO(actx, CURLINFO_CONTENT_TYPE, &content_type, return false);
818 : :
819 [ - + ]: 164 : if (!content_type)
820 : : {
439 dgustafsson@postgres 821 :UBC 0 : actx_error(actx, "no content type was provided");
822 : 0 : return false;
823 : : }
824 : :
825 : : /*
826 : : * We need to perform a length limited comparison and not compare the
827 : : * whole string.
828 : : */
439 dgustafsson@postgres 829 [ + + ]:CBC 164 : if (pg_strncasecmp(content_type, type, type_len) != 0)
830 : 2 : goto fail;
831 : :
832 : : /* On an exact match, we're done. */
833 [ - + ]: 162 : Assert(strlen(content_type) >= type_len);
834 [ + + ]: 162 : if (content_type[type_len] == '\0')
835 : 157 : return true;
836 : :
837 : : /*
838 : : * Only a semicolon (optionally preceded by HTTP optional whitespace) is
839 : : * acceptable after the prefix we checked. This marks the start of media
840 : : * type parameters, which we currently have no use for.
841 : : */
842 [ + - ]: 9 : for (size_t i = type_len; content_type[i]; ++i)
843 : : {
844 [ + + + ]: 9 : switch (content_type[i])
845 : : {
846 : 4 : case ';':
847 : 4 : return true; /* success! */
848 : :
849 : 4 : case ' ':
850 : : case '\t':
851 : : /* HTTP optional whitespace allows only spaces and htabs. */
852 : 4 : break;
853 : :
854 : 1 : default:
855 : 1 : goto fail;
856 : : }
857 : : }
858 : :
439 dgustafsson@postgres 859 :UBC 0 : fail:
439 dgustafsson@postgres 860 :CBC 3 : actx_error(actx, "unexpected content type: \"%s\"", content_type);
861 : 3 : return false;
862 : : }
863 : :
864 : : /*
865 : : * A helper function for general JSON parsing. fields is the array of field
866 : : * definitions with their backing pointers. The response will be parsed from
867 : : * actx->curl and actx->work_data (as set up by start_request()), and any
868 : : * parsing errors will be placed into actx->errbuf.
869 : : */
870 : : static bool
871 : 164 : parse_oauth_json(struct async_ctx *actx, const struct json_field *fields)
872 : : {
873 : 164 : PQExpBuffer resp = &actx->work_data;
874 : 164 : JsonLexContext lex = {0};
875 : 164 : JsonSemAction sem = {0};
876 : : JsonParseErrorType err;
877 : 164 : struct oauth_parse ctx = {0};
878 : 164 : bool success = false;
879 : :
880 [ + + ]: 164 : if (!check_content_type(actx, "application/json"))
881 : 3 : return false;
882 : :
883 [ - + ]: 161 : if (strlen(resp->data) != resp->len)
884 : : {
439 dgustafsson@postgres 885 :UBC 0 : actx_error(actx, "response contains embedded NULLs");
886 : 0 : return false;
887 : : }
888 : :
889 : : /*
890 : : * pg_parse_json doesn't validate the incoming UTF-8, so we have to check
891 : : * that up front.
892 : : */
439 dgustafsson@postgres 893 [ - + ]:CBC 161 : if (pg_encoding_verifymbstr(PG_UTF8, resp->data, resp->len) != resp->len)
894 : : {
439 dgustafsson@postgres 895 :UBC 0 : actx_error(actx, "response is not valid UTF-8");
896 : 0 : return false;
897 : : }
898 : :
439 dgustafsson@postgres 899 :CBC 161 : makeJsonLexContextCstringLen(&lex, resp->data, resp->len, PG_UTF8, true);
900 : 161 : setJsonLexContextOwnsTokens(&lex, true); /* must not leak on error */
901 : :
902 : 161 : ctx.errbuf = &actx->errbuf;
903 : 161 : ctx.fields = fields;
904 : 161 : sem.semstate = &ctx;
905 : :
906 : 161 : sem.object_start = oauth_json_object_start;
907 : 161 : sem.object_field_start = oauth_json_object_field_start;
908 : 161 : sem.object_end = oauth_json_object_end;
909 : 161 : sem.array_start = oauth_json_array_start;
910 : 161 : sem.array_end = oauth_json_array_end;
911 : 161 : sem.scalar = oauth_json_scalar;
912 : :
913 : 161 : err = pg_parse_json(&lex, &sem);
914 : :
915 [ + + ]: 161 : if (err != JSON_SUCCESS)
916 : : {
917 : : /*
918 : : * For JSON_SEM_ACTION_FAILED, we've already written the error
919 : : * message. Other errors come directly from pg_parse_json(), already
920 : : * translated.
921 : : */
922 [ - + ]: 2 : if (err != JSON_SEM_ACTION_FAILED)
439 dgustafsson@postgres 923 :UBC 0 : actx_error_str(actx, json_errdetail(err, &lex));
924 : :
439 dgustafsson@postgres 925 :CBC 2 : goto cleanup;
926 : : }
927 : :
928 : : /* Check all required fields. */
929 [ + + ]: 835 : while (fields->name)
930 : : {
931 [ + + ]: 676 : if (fields->required
162 jchampion@postgresql 932 [ - + ]:GNC 453 : && !*fields->scalar
162 jchampion@postgresql 933 [ # # ]:UNC 0 : && !*fields->array)
934 : : {
439 dgustafsson@postgres 935 :UBC 0 : actx_error(actx, "field \"%s\" is missing", fields->name);
936 : 0 : goto cleanup;
937 : : }
938 : :
439 dgustafsson@postgres 939 :CBC 676 : fields++;
940 : : }
941 : :
942 : 159 : success = true;
943 : :
944 : 161 : cleanup:
945 : 161 : freeJsonLexContext(&lex);
946 : 161 : return success;
947 : : }
948 : :
949 : : /*
950 : : * JSON Parser Definitions
951 : : */
952 : :
953 : : /*
954 : : * Parses authorization server metadata. Fields are defined by OIDC Discovery
955 : : * 1.0 and RFC 8414.
956 : : */
957 : : static bool
958 : 54 : parse_provider(struct async_ctx *actx, struct provider *provider)
959 : : {
960 : 54 : struct json_field fields[] = {
435 961 : 54 : {"issuer", JSON_TOKEN_STRING, {&provider->issuer}, PG_OAUTH_REQUIRED},
962 : 54 : {"token_endpoint", JSON_TOKEN_STRING, {&provider->token_endpoint}, PG_OAUTH_REQUIRED},
963 : :
964 : : /*----
965 : : * The following fields are technically REQUIRED, but we don't use
966 : : * them anywhere yet:
967 : : *
968 : : * - jwks_uri
969 : : * - response_types_supported
970 : : * - subject_types_supported
971 : : * - id_token_signing_alg_values_supported
972 : : */
973 : :
974 : 54 : {"device_authorization_endpoint", JSON_TOKEN_STRING, {&provider->device_authorization_endpoint}, PG_OAUTH_OPTIONAL},
975 : 54 : {"grant_types_supported", JSON_TOKEN_ARRAY_START, {.array = &provider->grant_types_supported}, PG_OAUTH_OPTIONAL},
976 : :
977 : : {0},
978 : : };
979 : :
439 980 : 54 : return parse_oauth_json(actx, fields);
981 : : }
982 : :
983 : : /*
984 : : * Parses a valid JSON number into a double. The input must have come from
985 : : * pg_parse_json(), so that we know the lexer has validated it; there's no
986 : : * in-band signal for invalid formats.
987 : : */
988 : : static double
989 : 99 : parse_json_number(const char *s)
990 : : {
991 : : double parsed;
992 : : int cnt;
993 : :
994 : : /*
995 : : * The JSON lexer has already validated the number, which is stricter than
996 : : * the %f format, so we should be good to use sscanf().
997 : : */
998 : 99 : cnt = sscanf(s, "%lf", &parsed);
999 : :
1000 [ - + ]: 99 : if (cnt != 1)
1001 : : {
1002 : : /*
1003 : : * Either the lexer screwed up or our assumption above isn't true, and
1004 : : * either way a developer needs to take a look.
1005 : : */
439 dgustafsson@postgres 1006 :UBC 0 : Assert(false);
1007 : : return 0;
1008 : : }
1009 : :
439 dgustafsson@postgres 1010 :CBC 99 : return parsed;
1011 : : }
1012 : :
1013 : : /*
1014 : : * Parses the "interval" JSON number, corresponding to the number of seconds to
1015 : : * wait between token endpoint requests.
1016 : : *
1017 : : * RFC 8628 is pretty silent on sanity checks for the interval. As a matter of
1018 : : * practicality, round any fractional intervals up to the next second, and clamp
1019 : : * the result at a minimum of one. (Zero-second intervals would result in an
1020 : : * expensive network polling loop.) Tests may remove the lower bound with
1021 : : * PGOAUTHDEBUG, for improved performance.
1022 : : */
1023 : : static int
1024 : 49 : parse_interval(struct async_ctx *actx, const char *interval_str)
1025 : : {
1026 : : double parsed;
1027 : :
1028 : 49 : parsed = parse_json_number(interval_str);
1029 : 49 : parsed = ceil(parsed);
1030 : :
1031 [ + + ]: 49 : if (parsed < 1)
28 jchampion@postgresql 1032 :GNC 46 : return (actx->debug_flags & OAUTHDEBUG_UNSAFE_DOS_ENDPOINT) ? 0 : 1;
1033 : :
439 dgustafsson@postgres 1034 [ + + ]:CBC 3 : else if (parsed >= INT_MAX)
1035 : 1 : return INT_MAX;
1036 : :
1037 : 2 : return parsed;
1038 : : }
1039 : :
1040 : : /*
1041 : : * Parses the "expires_in" JSON number, corresponding to the number of seconds
1042 : : * remaining in the lifetime of the device code request.
1043 : : *
1044 : : * Similar to parse_interval, but we have even fewer requirements for reasonable
1045 : : * values since we don't use the expiration time directly (it's passed to the
1046 : : * PQAUTHDATA_PROMPT_OAUTH_DEVICE hook, in case the application wants to do
1047 : : * something with it). We simply round down and clamp to int range.
1048 : : */
1049 : : static int
1050 : 50 : parse_expires_in(struct async_ctx *actx, const char *expires_in_str)
1051 : : {
1052 : : double parsed;
1053 : :
1054 : 50 : parsed = parse_json_number(expires_in_str);
1055 : 50 : parsed = floor(parsed);
1056 : :
1057 [ - + ]: 50 : if (parsed >= INT_MAX)
439 dgustafsson@postgres 1058 :UBC 0 : return INT_MAX;
439 dgustafsson@postgres 1059 [ - + ]:CBC 50 : else if (parsed <= INT_MIN)
439 dgustafsson@postgres 1060 :UBC 0 : return INT_MIN;
1061 : :
439 dgustafsson@postgres 1062 :CBC 50 : return parsed;
1063 : : }
1064 : :
1065 : : /*
1066 : : * Parses the Device Authorization Response (RFC 8628, Sec. 3.2).
1067 : : */
1068 : : static bool
1069 : 53 : parse_device_authz(struct async_ctx *actx, struct device_authz *authz)
1070 : : {
1071 : 53 : struct json_field fields[] = {
435 1072 : 53 : {"device_code", JSON_TOKEN_STRING, {&authz->device_code}, PG_OAUTH_REQUIRED},
1073 : 53 : {"user_code", JSON_TOKEN_STRING, {&authz->user_code}, PG_OAUTH_REQUIRED},
1074 : 53 : {"verification_uri", JSON_TOKEN_STRING, {&authz->verification_uri}, PG_OAUTH_REQUIRED},
1075 : 53 : {"expires_in", JSON_TOKEN_NUMBER, {&authz->expires_in_str}, PG_OAUTH_REQUIRED},
1076 : :
1077 : : /*
1078 : : * Some services (Google, Azure) spell verification_uri differently.
1079 : : * We accept either.
1080 : : */
1081 : 53 : {"verification_url", JSON_TOKEN_STRING, {&authz->verification_uri}, PG_OAUTH_REQUIRED},
1082 : :
1083 : : /*
1084 : : * There is no evidence of verification_uri_complete being spelled
1085 : : * with "url" instead with any service provider, so only support
1086 : : * "uri".
1087 : : */
1088 : 53 : {"verification_uri_complete", JSON_TOKEN_STRING, {&authz->verification_uri_complete}, PG_OAUTH_OPTIONAL},
1089 : 53 : {"interval", JSON_TOKEN_NUMBER, {&authz->interval_str}, PG_OAUTH_OPTIONAL},
1090 : :
1091 : : {0},
1092 : : };
1093 : :
439 1094 [ + + ]: 53 : if (!parse_oauth_json(actx, fields))
1095 : 3 : return false;
1096 : :
1097 : : /*
1098 : : * Parse our numeric fields. Lexing has already completed by this time, so
1099 : : * we at least know they're valid JSON numbers.
1100 : : */
1101 [ + + ]: 50 : if (authz->interval_str)
1102 : 49 : authz->interval = parse_interval(actx, authz->interval_str);
1103 : : else
1104 : : {
1105 : : /*
1106 : : * RFC 8628 specifies 5 seconds as the default value if the server
1107 : : * doesn't provide an interval.
1108 : : */
1109 : 1 : authz->interval = 5;
1110 : : }
1111 : :
1112 [ - + ]: 50 : Assert(authz->expires_in_str); /* ensured by parse_oauth_json() */
1113 : 50 : authz->expires_in = parse_expires_in(actx, authz->expires_in_str);
1114 : :
1115 : 50 : return true;
1116 : : }
1117 : :
1118 : : /*
1119 : : * Parses the device access token error response (RFC 8628, Sec. 3.5, which
1120 : : * uses the error response defined in RFC 6749, Sec. 5.2).
1121 : : */
1122 : : static bool
1123 : 15 : parse_token_error(struct async_ctx *actx, struct token_error *err)
1124 : : {
1125 : : bool result;
1126 : 15 : struct json_field fields[] = {
435 1127 : 15 : {"error", JSON_TOKEN_STRING, {&err->error}, PG_OAUTH_REQUIRED},
1128 : :
1129 : 15 : {"error_description", JSON_TOKEN_STRING, {&err->error_description}, PG_OAUTH_OPTIONAL},
1130 : :
1131 : : {0},
1132 : : };
1133 : :
439 1134 : 15 : result = parse_oauth_json(actx, fields);
1135 : :
1136 : : /*
1137 : : * Since token errors are parsed during other active error paths, only
1138 : : * override the errctx if parsing explicitly fails.
1139 : : */
1140 [ - + ]: 15 : if (!result)
141 jchampion@postgresql 1141 :UBC 0 : actx->errctx = libpq_gettext("failed to parse token error response");
1142 : :
439 dgustafsson@postgres 1143 :CBC 15 : return result;
1144 : : }
1145 : :
1146 : : /*
1147 : : * Constructs a message from the token error response and puts it into
1148 : : * actx->errbuf.
1149 : : */
1150 : : static void
1151 : 6 : record_token_error(struct async_ctx *actx, const struct token_error *err)
1152 : : {
1153 [ + + ]: 6 : if (err->error_description)
1154 : 3 : appendPQExpBuffer(&actx->errbuf, "%s ", err->error_description);
1155 : : else
1156 : : {
1157 : : /*
1158 : : * Try to get some more helpful detail into the error string. A 401
1159 : : * status in particular implies that the oauth_client_secret is
1160 : : * missing or wrong.
1161 : : */
1162 : : long response_code;
1163 : :
1164 [ - + ]: 3 : CHECK_GETINFO(actx, CURLINFO_RESPONSE_CODE, &response_code, response_code = 0);
1165 : :
1166 [ + + ]: 3 : if (response_code == 401)
1167 : : {
1168 [ + + ]: 2 : actx_error(actx, actx->used_basic_auth
1169 : : ? gettext_noop("provider rejected the oauth_client_secret")
1170 : : : gettext_noop("provider requires client authentication, and no oauth_client_secret is set"));
1171 : 2 : actx_error_str(actx, " ");
1172 : : }
1173 : : }
1174 : :
1175 : 6 : appendPQExpBuffer(&actx->errbuf, "(%s)", err->error);
1176 : 6 : }
1177 : :
1178 : : /*
1179 : : * Parses the device access token response (RFC 8628, Sec. 3.5, which uses the
1180 : : * success response defined in RFC 6749, Sec. 5.1).
1181 : : */
1182 : : static bool
1183 : 42 : parse_access_token(struct async_ctx *actx, struct token *tok)
1184 : : {
1185 : 42 : struct json_field fields[] = {
435 1186 : 42 : {"access_token", JSON_TOKEN_STRING, {&tok->access_token}, PG_OAUTH_REQUIRED},
1187 : 42 : {"token_type", JSON_TOKEN_STRING, {&tok->token_type}, PG_OAUTH_REQUIRED},
1188 : :
1189 : : /*---
1190 : : * We currently have no use for the following OPTIONAL fields:
1191 : : *
1192 : : * - expires_in: This will be important for maintaining a token cache,
1193 : : * but we do not yet implement one.
1194 : : *
1195 : : * - refresh_token: Ditto.
1196 : : *
1197 : : * - scope: This is only sent when the authorization server sees fit to
1198 : : * change our scope request. It's not clear what we should do
1199 : : * about this; either it's been done as a matter of policy, or
1200 : : * the user has explicitly denied part of the authorization,
1201 : : * and either way the server-side validator is in a better
1202 : : * place to complain if the change isn't acceptable.
1203 : : */
1204 : :
1205 : : {0},
1206 : : };
1207 : :
439 1208 : 42 : return parse_oauth_json(actx, fields);
1209 : : }
1210 : :
1211 : : /*
1212 : : * libcurl Multi Setup/Callbacks
1213 : : */
1214 : :
1215 : : /*
1216 : : * Sets up the actx->mux, which is the altsock that PQconnectPoll clients will
1217 : : * select() on instead of the Postgres socket during OAuth negotiation.
1218 : : *
1219 : : * This is just an epoll set or kqueue abstracting multiple other descriptors.
1220 : : * For epoll, the timerfd is always part of the set; it's just disabled when
1221 : : * we're not using it. For kqueue, the "timerfd" is actually a second kqueue
1222 : : * instance which is only added to the set when needed.
1223 : : */
1224 : : static bool
1225 : 59 : setup_multiplexer(struct async_ctx *actx)
1226 : : {
1227 : : #if defined(HAVE_SYS_EPOLL_H)
1228 : 59 : struct epoll_event ev = {.events = EPOLLIN};
1229 : :
1230 : 59 : actx->mux = epoll_create1(EPOLL_CLOEXEC);
1231 [ - + ]: 59 : if (actx->mux < 0)
1232 : : {
141 jchampion@postgresql 1233 :UBC 0 : actx_error_internal(actx, "failed to create epoll set: %m");
439 dgustafsson@postgres 1234 : 0 : return false;
1235 : : }
1236 : :
439 dgustafsson@postgres 1237 :CBC 59 : actx->timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
1238 [ - + ]: 59 : if (actx->timerfd < 0)
1239 : : {
141 jchampion@postgresql 1240 :UBC 0 : actx_error_internal(actx, "failed to create timerfd: %m");
439 dgustafsson@postgres 1241 : 0 : return false;
1242 : : }
1243 : :
439 dgustafsson@postgres 1244 [ - + ]:CBC 59 : if (epoll_ctl(actx->mux, EPOLL_CTL_ADD, actx->timerfd, &ev) < 0)
1245 : : {
141 jchampion@postgresql 1246 :UBC 0 : actx_error_internal(actx, "failed to add timerfd to epoll set: %m");
439 dgustafsson@postgres 1247 : 0 : return false;
1248 : : }
1249 : :
439 dgustafsson@postgres 1250 :CBC 59 : return true;
1251 : : #elif defined(HAVE_SYS_EVENT_H)
1252 : : actx->mux = kqueue();
1253 : : if (actx->mux < 0)
1254 : : {
1255 : : actx_error_internal(actx, "failed to create kqueue: %m");
1256 : : return false;
1257 : : }
1258 : :
1259 : : /*
1260 : : * Originally, we set EVFILT_TIMER directly on the top-level multiplexer.
1261 : : * This makes it difficult to implement timer_expired(), though, so now we
1262 : : * set EVFILT_TIMER on a separate actx->timerfd, which is chained to
1263 : : * actx->mux while the timer is active.
1264 : : */
1265 : : actx->timerfd = kqueue();
1266 : : if (actx->timerfd < 0)
1267 : : {
1268 : : actx_error_internal(actx, "failed to create timer kqueue: %m");
1269 : : return false;
1270 : : }
1271 : :
1272 : : return true;
1273 : : #else
1274 : : #error setup_multiplexer is not implemented on this platform
1275 : : #endif
1276 : : }
1277 : :
1278 : : /*
1279 : : * Adds and removes sockets from the multiplexer set, as directed by the
1280 : : * libcurl multi handle.
1281 : : */
1282 : : static int
1283 : 523 : register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
1284 : : void *socketp)
1285 : : {
1286 : 523 : struct async_ctx *actx = ctx;
1287 : :
1288 : : #if defined(HAVE_SYS_EPOLL_H)
1289 : 523 : struct epoll_event ev = {0};
1290 : : int res;
1291 : 523 : int op = EPOLL_CTL_ADD;
1292 : :
1293 [ + + + + : 523 : switch (what)
- ]
1294 : : {
1295 : 165 : case CURL_POLL_IN:
1296 : 165 : ev.events = EPOLLIN;
1297 : 165 : break;
1298 : :
1299 : 171 : case CURL_POLL_OUT:
1300 : 171 : ev.events = EPOLLOUT;
1301 : 171 : break;
1302 : :
439 dgustafsson@postgres 1303 :GBC 8 : case CURL_POLL_INOUT:
1304 : 8 : ev.events = EPOLLIN | EPOLLOUT;
1305 : 8 : break;
1306 : :
439 dgustafsson@postgres 1307 :CBC 179 : case CURL_POLL_REMOVE:
1308 : 179 : op = EPOLL_CTL_DEL;
1309 : 179 : break;
1310 : :
439 dgustafsson@postgres 1311 :UBC 0 : default:
141 jchampion@postgresql 1312 : 0 : actx_error_internal(actx, "unknown libcurl socket operation: %d", what);
439 dgustafsson@postgres 1313 : 0 : return -1;
1314 : : }
1315 : :
439 dgustafsson@postgres 1316 :CBC 523 : res = epoll_ctl(actx->mux, op, socket, &ev);
1317 [ + + + - ]: 523 : if (res < 0 && errno == EEXIST)
1318 : : {
1319 : : /* We already had this socket in the poll set. */
1320 : 165 : op = EPOLL_CTL_MOD;
1321 : 165 : res = epoll_ctl(actx->mux, op, socket, &ev);
1322 : : }
1323 : :
1324 [ - + ]: 523 : if (res < 0)
1325 : : {
439 dgustafsson@postgres 1326 [ # # # ]:UBC 0 : switch (op)
1327 : : {
1328 : 0 : case EPOLL_CTL_ADD:
141 jchampion@postgresql 1329 : 0 : actx_error_internal(actx, "could not add to epoll set: %m");
439 dgustafsson@postgres 1330 : 0 : break;
1331 : :
1332 : 0 : case EPOLL_CTL_DEL:
141 jchampion@postgresql 1333 : 0 : actx_error_internal(actx, "could not delete from epoll set: %m");
439 dgustafsson@postgres 1334 : 0 : break;
1335 : :
1336 : 0 : default:
141 jchampion@postgresql 1337 : 0 : actx_error_internal(actx, "could not update epoll set: %m");
1338 : : }
1339 : :
439 dgustafsson@postgres 1340 : 0 : return -1;
1341 : : }
1342 : :
439 dgustafsson@postgres 1343 :CBC 523 : return 0;
1344 : : #elif defined(HAVE_SYS_EVENT_H)
1345 : : struct kevent ev[2];
1346 : : struct kevent ev_out[2];
1347 : : struct timespec timeout = {0};
1348 : : int nev = 0;
1349 : : int res;
1350 : :
1351 : : /*
1352 : : * We don't know which of the events is currently registered, perhaps
1353 : : * both, so we always try to remove unneeded events. This means we need to
1354 : : * tolerate ENOENT below.
1355 : : */
1356 : : switch (what)
1357 : : {
1358 : : case CURL_POLL_IN:
1359 : : EV_SET(&ev[nev], socket, EVFILT_READ, EV_ADD | EV_RECEIPT, 0, 0, 0);
1360 : : nev++;
1361 : : EV_SET(&ev[nev], socket, EVFILT_WRITE, EV_DELETE | EV_RECEIPT, 0, 0, 0);
1362 : : nev++;
1363 : : break;
1364 : :
1365 : : case CURL_POLL_OUT:
1366 : : EV_SET(&ev[nev], socket, EVFILT_WRITE, EV_ADD | EV_RECEIPT, 0, 0, 0);
1367 : : nev++;
1368 : : EV_SET(&ev[nev], socket, EVFILT_READ, EV_DELETE | EV_RECEIPT, 0, 0, 0);
1369 : : nev++;
1370 : : break;
1371 : :
1372 : : case CURL_POLL_INOUT:
1373 : : EV_SET(&ev[nev], socket, EVFILT_READ, EV_ADD | EV_RECEIPT, 0, 0, 0);
1374 : : nev++;
1375 : : EV_SET(&ev[nev], socket, EVFILT_WRITE, EV_ADD | EV_RECEIPT, 0, 0, 0);
1376 : : nev++;
1377 : : break;
1378 : :
1379 : : case CURL_POLL_REMOVE:
1380 : : EV_SET(&ev[nev], socket, EVFILT_READ, EV_DELETE | EV_RECEIPT, 0, 0, 0);
1381 : : nev++;
1382 : : EV_SET(&ev[nev], socket, EVFILT_WRITE, EV_DELETE | EV_RECEIPT, 0, 0, 0);
1383 : : nev++;
1384 : : break;
1385 : :
1386 : : default:
1387 : : actx_error_internal(actx, "unknown libcurl socket operation: %d", what);
1388 : : return -1;
1389 : : }
1390 : :
1391 : : Assert(nev <= lengthof(ev));
1392 : : Assert(nev <= lengthof(ev_out));
1393 : :
1394 : : res = kevent(actx->mux, ev, nev, ev_out, nev, &timeout);
1395 : : if (res < 0)
1396 : : {
1397 : : actx_error_internal(actx, "could not modify kqueue: %m");
1398 : : return -1;
1399 : : }
1400 : :
1401 : : /*
1402 : : * We can't use the simple errno version of kevent, because we need to
1403 : : * skip over ENOENT while still allowing a second change to be processed.
1404 : : * So we need a longer-form error checking loop.
1405 : : */
1406 : : for (int i = 0; i < res; ++i)
1407 : : {
1408 : : /*
1409 : : * EV_RECEIPT should guarantee one EV_ERROR result for every change,
1410 : : * whether successful or not. Failed entries contain a non-zero errno
1411 : : * in the data field.
1412 : : */
1413 : : Assert(ev_out[i].flags & EV_ERROR);
1414 : :
1415 : : errno = ev_out[i].data;
1416 : : if (errno && errno != ENOENT)
1417 : : {
1418 : : switch (what)
1419 : : {
1420 : : case CURL_POLL_REMOVE:
1421 : : actx_error_internal(actx, "could not delete from kqueue: %m");
1422 : : break;
1423 : : default:
1424 : : actx_error_internal(actx, "could not add to kqueue: %m");
1425 : : }
1426 : : return -1;
1427 : : }
1428 : : }
1429 : :
1430 : : return 0;
1431 : : #else
1432 : : #error register_socket is not implemented on this platform
1433 : : #endif
1434 : : }
1435 : :
1436 : : /*
1437 : : * If there is no work to do on any of the descriptors in the multiplexer, then
1438 : : * this function must ensure that the multiplexer is not readable.
1439 : : *
1440 : : * Unlike epoll descriptors, kqueue descriptors only transition from readable to
1441 : : * unreadable when kevent() is called and finds nothing, after removing
1442 : : * level-triggered conditions that have gone away. We therefore need a dummy
1443 : : * kevent() call after operations might have been performed on the monitored
1444 : : * sockets or timer_fd. Any event returned is ignored here, but it also remains
1445 : : * queued (being level-triggered) and leaves the descriptor readable. This is a
1446 : : * no-op for epoll descriptors.
1447 : : */
1448 : : static bool
270 jchampion@postgresql 1449 : 1000 : comb_multiplexer(struct async_ctx *actx)
1450 : : {
1451 : : #if defined(HAVE_SYS_EPOLL_H)
1452 : : /* The epoll implementation doesn't hold onto stale events. */
1453 : 1000 : return true;
1454 : : #elif defined(HAVE_SYS_EVENT_H)
1455 : : struct timespec timeout = {0};
1456 : : struct kevent ev;
1457 : :
1458 : : /*
1459 : : * Try to read a single pending event. We can actually ignore the result:
1460 : : * either we found an event to process, in which case the multiplexer is
1461 : : * correctly readable for that event at minimum, and it doesn't matter if
1462 : : * there are any stale events; or we didn't find any, in which case the
1463 : : * kernel will have discarded any stale events as it traveled to the end
1464 : : * of the queue.
1465 : : *
1466 : : * Note that this depends on our registrations being level-triggered --
1467 : : * even the timer, so we use a chained kqueue for that instead of an
1468 : : * EVFILT_TIMER on the top-level mux. If we used edge-triggered events,
1469 : : * this call would improperly discard them.
1470 : : */
1471 : : if (kevent(actx->mux, NULL, 0, &ev, 1, &timeout) < 0)
1472 : : {
1473 : : actx_error_internal(actx, "could not comb kqueue: %m");
1474 : : return false;
1475 : : }
1476 : :
1477 : : return true;
1478 : : #else
1479 : : #error comb_multiplexer is not implemented on this platform
1480 : : #endif
1481 : : }
1482 : :
1483 : : /*
1484 : : * Enables or disables the timer in the multiplexer set. The timeout value is
1485 : : * in milliseconds (negative values disable the timer).
1486 : : *
1487 : : * For epoll, rather than continually adding and removing the timer, we keep it
1488 : : * in the set at all times and just disarm it when it's not needed. For kqueue,
1489 : : * the timer is removed completely when disabled to prevent stale timeouts from
1490 : : * remaining in the queue.
1491 : : *
1492 : : * To meet Curl requirements for the CURLMOPT_TIMERFUNCTION, implementations of
1493 : : * set_timer must handle repeated calls by fully discarding any previous running
1494 : : * or expired timer.
1495 : : */
1496 : : static bool
439 dgustafsson@postgres 1497 : 360 : set_timer(struct async_ctx *actx, long timeout)
1498 : : {
1499 : : #if defined(HAVE_SYS_EPOLL_H)
1500 : 360 : struct itimerspec spec = {0};
1501 : :
1502 [ + + ]: 360 : if (timeout < 0)
1503 : : {
1504 : : /* the zero itimerspec will disarm the timer below */
1505 : : }
1506 [ + + ]: 181 : else if (timeout == 0)
1507 : : {
1508 : : /*
1509 : : * A zero timeout means libcurl wants us to call back immediately.
1510 : : * That's not technically an option for timerfd, but we can make the
1511 : : * timeout ridiculously short.
1512 : : */
1513 : 177 : spec.it_value.tv_nsec = 1;
1514 : : }
1515 : : else
1516 : : {
1517 : 4 : spec.it_value.tv_sec = timeout / 1000;
1518 : 4 : spec.it_value.tv_nsec = (timeout % 1000) * 1000000;
1519 : : }
1520 : :
1521 [ - + ]: 360 : if (timerfd_settime(actx->timerfd, 0 /* no flags */ , &spec, NULL) < 0)
1522 : : {
141 jchampion@postgresql 1523 :UBC 0 : actx_error_internal(actx, "setting timerfd to %ld: %m", timeout);
439 dgustafsson@postgres 1524 : 0 : return false;
1525 : : }
1526 : :
439 dgustafsson@postgres 1527 :CBC 360 : return true;
1528 : : #elif defined(HAVE_SYS_EVENT_H)
1529 : : struct kevent ev;
1530 : :
1531 : : #ifdef __NetBSD__
1532 : :
1533 : : /*
1534 : : * Work around NetBSD's rejection of zero timeouts (EINVAL), a bit like
1535 : : * timerfd above.
1536 : : */
1537 : : if (timeout == 0)
1538 : : timeout = 1;
1539 : : #endif
1540 : :
1541 : : /*
1542 : : * Always disable the timer, and remove it from the multiplexer, to clear
1543 : : * out any already-queued events. (On some BSDs, adding an EVFILT_TIMER to
1544 : : * a kqueue that already has one will clear stale events, but not on
1545 : : * macOS.)
1546 : : *
1547 : : * If there was no previous timer set, the kevent calls will result in
1548 : : * ENOENT, which is fine.
1549 : : */
1550 : : EV_SET(&ev, 1, EVFILT_TIMER, EV_DELETE, 0, 0, 0);
1551 : : if (kevent(actx->timerfd, &ev, 1, NULL, 0, NULL) < 0 && errno != ENOENT)
1552 : : {
1553 : : actx_error_internal(actx, "deleting kqueue timer: %m");
1554 : : return false;
1555 : : }
1556 : :
1557 : : EV_SET(&ev, actx->timerfd, EVFILT_READ, EV_DELETE, 0, 0, 0);
1558 : : if (kevent(actx->mux, &ev, 1, NULL, 0, NULL) < 0 && errno != ENOENT)
1559 : : {
1560 : : actx_error_internal(actx, "removing kqueue timer from multiplexer: %m");
1561 : : return false;
1562 : : }
1563 : :
1564 : : /* If we're not adding a timer, we're done. */
1565 : : if (timeout < 0)
1566 : : return true;
1567 : :
1568 : : EV_SET(&ev, 1, EVFILT_TIMER, (EV_ADD | EV_ONESHOT), 0, timeout, 0);
1569 : : if (kevent(actx->timerfd, &ev, 1, NULL, 0, NULL) < 0)
1570 : : {
1571 : : actx_error_internal(actx, "setting kqueue timer to %ld: %m", timeout);
1572 : : return false;
1573 : : }
1574 : :
1575 : : EV_SET(&ev, actx->timerfd, EVFILT_READ, EV_ADD, 0, 0, 0);
1576 : : if (kevent(actx->mux, &ev, 1, NULL, 0, NULL) < 0)
1577 : : {
1578 : : actx_error_internal(actx, "adding kqueue timer to multiplexer: %m");
1579 : : return false;
1580 : : }
1581 : :
1582 : : return true;
1583 : : #else
1584 : : #error set_timer is not implemented on this platform
1585 : : #endif
1586 : : }
1587 : :
1588 : : /*
1589 : : * Returns 1 if the timeout in the multiplexer set has expired since the last
1590 : : * call to set_timer(), 0 if the timer is either still running or disarmed, or
1591 : : * -1 (with an actx_error() report) if the timer cannot be queried.
1592 : : */
1593 : : static int
1594 : 340321 : timer_expired(struct async_ctx *actx)
1595 : : {
1596 : : #if defined(HAVE_SYS_EPOLL_H) || defined(HAVE_SYS_EVENT_H)
1597 : : int res;
1598 : :
1599 : : /* Is the timer ready? */
1600 : 340321 : res = PQsocketPoll(actx->timerfd, 1 /* forRead */ , 0, 0);
1601 [ - + ]: 340321 : if (res < 0)
1602 : : {
270 jchampion@postgresql 1603 :UBC 0 : actx_error(actx, "checking timer expiration: %m");
439 dgustafsson@postgres 1604 : 0 : return -1;
1605 : : }
1606 : :
439 dgustafsson@postgres 1607 :CBC 340321 : return (res > 0);
1608 : : #else
1609 : : #error timer_expired is not implemented on this platform
1610 : : #endif
1611 : : }
1612 : :
1613 : : /*
1614 : : * Adds or removes timeouts from the multiplexer set, as directed by the
1615 : : * libcurl multi handle.
1616 : : */
1617 : : static int
1618 : 167 : register_timer(CURLM *curlm, long timeout, void *ctx)
1619 : : {
1620 : 167 : struct async_ctx *actx = ctx;
1621 : :
1622 : : /*
1623 : : * There might be an optimization opportunity here: if timeout == 0, we
1624 : : * could signal drive_request to immediately call
1625 : : * curl_multi_socket_action, rather than returning all the way up the
1626 : : * stack only to come right back. But it's not clear that the additional
1627 : : * code complexity is worth it.
1628 : : */
1629 [ - + ]: 167 : if (!set_timer(actx, timeout))
439 dgustafsson@postgres 1630 :UBC 0 : return -1; /* actx_error already called */
1631 : :
439 dgustafsson@postgres 1632 :CBC 167 : return 0;
1633 : : }
1634 : :
1635 : : /*
1636 : : * Removes any expired-timer event from the multiplexer. If was_expired is not
1637 : : * NULL, it will contain whether or not the timer was expired at time of call.
1638 : : */
1639 : : static bool
270 jchampion@postgresql 1640 : 340311 : drain_timer_events(struct async_ctx *actx, bool *was_expired)
1641 : : {
1642 : : int res;
1643 : :
1644 : 340311 : res = timer_expired(actx);
1645 [ - + ]: 340311 : if (res < 0)
270 jchampion@postgresql 1646 :UBC 0 : return false;
1647 : :
270 jchampion@postgresql 1648 [ + + ]:CBC 340311 : if (res > 0)
1649 : : {
1650 : : /*
1651 : : * Timer is expired. We could drain the event manually from the
1652 : : * timerfd, but it's easier to simply disable it; that keeps the
1653 : : * platform-specific code in set_timer().
1654 : : */
1655 [ - + ]: 178 : if (!set_timer(actx, -1))
270 jchampion@postgresql 1656 :UBC 0 : return false;
1657 : : }
1658 : :
270 jchampion@postgresql 1659 [ + + ]:CBC 340311 : if (was_expired)
1660 : 339152 : *was_expired = (res > 0);
1661 : :
1662 : 340311 : return true;
1663 : : }
1664 : :
1665 : : /*
1666 : : * Prints Curl request debugging information to stderr.
1667 : : *
1668 : : * Note that this will expose a number of critical secrets, so users have to opt
1669 : : * into this (see PGOAUTHDEBUG).
1670 : : */
1671 : : static int
439 dgustafsson@postgres 1672 : 11113 : debug_callback(CURL *handle, curl_infotype type, char *data, size_t size,
1673 : : void *clientp)
1674 : : {
1675 : : const char *prefix;
1676 : 11113 : bool printed_prefix = false;
1677 : : PQExpBufferData buf;
1678 : :
1679 : : /* Prefixes are modeled off of the default libcurl debug output. */
1680 [ + + + + ]: 11113 : switch (type)
1681 : : {
1682 : 4664 : case CURLINFO_TEXT:
1683 : 4664 : prefix = "*";
1684 : 4664 : break;
1685 : :
1686 : 1152 : case CURLINFO_HEADER_IN: /* fall through */
1687 : : case CURLINFO_DATA_IN:
1688 : 1152 : prefix = "<";
1689 : 1152 : break;
1690 : :
1691 : 268 : case CURLINFO_HEADER_OUT: /* fall through */
1692 : : case CURLINFO_DATA_OUT:
1693 : 268 : prefix = ">";
1694 : 268 : break;
1695 : :
439 dgustafsson@postgres 1696 :GBC 5029 : default:
1697 : 5029 : return 0;
1698 : : }
1699 : :
439 dgustafsson@postgres 1700 :CBC 6084 : initPQExpBuffer(&buf);
1701 : :
1702 : : /*
1703 : : * Split the output into lines for readability; sometimes multiple headers
1704 : : * are included in a single call. We also don't allow unprintable ASCII
1705 : : * through without a basic <XX> escape.
1706 : : */
1707 [ + + ]: 865218 : for (int i = 0; i < size; i++)
1708 : : {
1709 : 859134 : char c = data[i];
1710 : :
1711 [ + + ]: 859134 : if (!printed_prefix)
1712 : : {
1713 : 6634 : appendPQExpBuffer(&buf, "[libcurl] %s ", prefix);
1714 : 6634 : printed_prefix = true;
1715 : : }
1716 : :
1717 [ + + + - ]: 859134 : if (c >= 0x20 && c <= 0x7E)
1718 : 851126 : appendPQExpBufferChar(&buf, c);
1719 [ + + ]: 8008 : else if ((type == CURLINFO_HEADER_IN
1720 [ + + ]: 6084 : || type == CURLINFO_HEADER_OUT
1721 [ + - ]: 4664 : || type == CURLINFO_TEXT)
1722 [ + + + + ]: 8008 : && (c == '\r' || c == '\n'))
1723 : : {
1724 : : /*
1725 : : * Don't bother emitting <0D><0A> for headers and text; it's not
1726 : : * helpful noise.
1727 : : */
1728 : : }
1729 : : else
1730 : 4 : appendPQExpBuffer(&buf, "<%02X>", c);
1731 : :
1732 [ + + ]: 859134 : if (c == '\n')
1733 : : {
1734 : 6334 : appendPQExpBufferChar(&buf, c);
1735 : 6334 : printed_prefix = false;
1736 : : }
1737 : : }
1738 : :
1739 [ + + ]: 6084 : if (printed_prefix)
1740 : 300 : appendPQExpBufferChar(&buf, '\n'); /* finish the line */
1741 : :
1742 : 6084 : fprintf(stderr, "%s", buf.data);
1743 : 6084 : termPQExpBuffer(&buf);
1744 : 6084 : return 0;
1745 : : }
1746 : :
1747 : : /*
1748 : : * Initializes the two libcurl handles in the async_ctx. The multi handle,
1749 : : * actx->curlm, is what drives the asynchronous engine and tells us what to do
1750 : : * next. The easy handle, actx->curl, encapsulates the state for a single
1751 : : * request/response. It's added to the multi handle as needed, during
1752 : : * start_request().
1753 : : */
1754 : : static bool
1755 : 57 : setup_curl_handles(struct async_ctx *actx)
1756 : : {
1757 : : /*
1758 : : * Create our multi handle. This encapsulates the entire conversation with
1759 : : * libcurl for this connection.
1760 : : */
1761 : 57 : actx->curlm = curl_multi_init();
1762 [ - + ]: 57 : if (!actx->curlm)
1763 : : {
1764 : : /* We don't get a lot of feedback on the failure reason. */
439 dgustafsson@postgres 1765 :UBC 0 : actx_error(actx, "failed to create libcurl multi handle");
1766 : 0 : return false;
1767 : : }
1768 : :
1769 : : /*
1770 : : * The multi handle tells us what to wait on using two callbacks. These
1771 : : * will manipulate actx->mux as needed.
1772 : : */
439 dgustafsson@postgres 1773 [ - + ]:CBC 57 : CHECK_MSETOPT(actx, CURLMOPT_SOCKETFUNCTION, register_socket, return false);
1774 [ - + ]: 57 : CHECK_MSETOPT(actx, CURLMOPT_SOCKETDATA, actx, return false);
1775 [ - + ]: 57 : CHECK_MSETOPT(actx, CURLMOPT_TIMERFUNCTION, register_timer, return false);
1776 [ - + ]: 57 : CHECK_MSETOPT(actx, CURLMOPT_TIMERDATA, actx, return false);
1777 : :
1778 : : /*
1779 : : * Set up an easy handle. All of our requests are made serially, so we
1780 : : * only ever need to keep track of one.
1781 : : */
1782 : 57 : actx->curl = curl_easy_init();
1783 [ - + ]: 57 : if (!actx->curl)
1784 : : {
439 dgustafsson@postgres 1785 :UBC 0 : actx_error(actx, "failed to create libcurl handle");
1786 : 0 : return false;
1787 : : }
1788 : :
1789 : : /*
1790 : : * Multi-threaded applications must set CURLOPT_NOSIGNAL. This requires us
1791 : : * to handle the possibility of SIGPIPE ourselves using pq_block_sigpipe;
1792 : : * see pg_fe_run_oauth_flow().
1793 : : *
1794 : : * NB: If libcurl is not built against a friendly DNS resolver (c-ares or
1795 : : * threaded), setting this option prevents DNS lookups from timing out
1796 : : * correctly. We warn about this situation at configure time.
1797 : : *
1798 : : * TODO: Perhaps there's a clever way to warn the user about synchronous
1799 : : * DNS at runtime too? It's not immediately clear how to do that in a
1800 : : * helpful way: for many standard single-threaded use cases, the user
1801 : : * might not care at all, so spraying warnings to stderr would probably do
1802 : : * more harm than good.
1803 : : */
439 dgustafsson@postgres 1804 [ - + ]:CBC 57 : CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false);
1805 : :
28 jchampion@postgresql 1806 [ + + ]:GNC 57 : if (actx->debug_flags & OAUTHDEBUG_UNSAFE_TRACE)
1807 : : {
1808 : : /*
1809 : : * Set a callback for retrieving error information from libcurl, the
1810 : : * function only takes effect when CURLOPT_VERBOSE has been set so
1811 : : * make sure the order is kept.
1812 : : */
439 dgustafsson@postgres 1813 [ - + ]:CBC 55 : CHECK_SETOPT(actx, CURLOPT_DEBUGFUNCTION, debug_callback, return false);
1814 [ - + ]: 55 : CHECK_SETOPT(actx, CURLOPT_VERBOSE, 1L, return false);
1815 : : }
1816 : :
1817 [ - + ]: 57 : CHECK_SETOPT(actx, CURLOPT_ERRORBUFFER, actx->curl_err, return false);
1818 : :
1819 : : /*
1820 : : * Only HTTPS is allowed. (Debug mode additionally allows HTTP; this is
1821 : : * intended for testing only.)
1822 : : *
1823 : : * There's a bit of unfortunate complexity around the choice of
1824 : : * CURLoption. CURLOPT_PROTOCOLS is deprecated in modern Curls, but its
1825 : : * replacement didn't show up until relatively recently.
1826 : : */
1827 : : {
1828 : : #if CURL_AT_LEAST_VERSION(7, 85, 0)
1829 : 57 : const CURLoption popt = CURLOPT_PROTOCOLS_STR;
1830 : 57 : const char *protos = "https";
1831 : 57 : const char *const unsafe = "https,http";
1832 : : #else
1833 : : const CURLoption popt = CURLOPT_PROTOCOLS;
1834 : : long protos = CURLPROTO_HTTPS;
1835 : : const long unsafe = CURLPROTO_HTTPS | CURLPROTO_HTTP;
1836 : : #endif
1837 : :
28 jchampion@postgresql 1838 [ + + ]:GNC 57 : if (actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP)
439 dgustafsson@postgres 1839 :CBC 1 : protos = unsafe;
1840 : :
1841 [ - + ]: 57 : CHECK_SETOPT(actx, popt, protos, return false);
1842 : : }
1843 : :
1844 : : /* Allow the user to change the trusted CA list. */
36 jchampion@postgresql 1845 [ + + ]:GNC 57 : if (actx->ca_file != NULL)
1846 [ - + ]: 56 : CHECK_SETOPT(actx, CURLOPT_CAINFO, actx->ca_file, return false);
1847 : :
1848 : : /*
1849 : : * Suppress the Accept header to make our request as minimal as possible.
1850 : : * (Ideally we would set it to "application/json" instead, but OpenID is
1851 : : * pretty strict when it comes to provider behavior, so we have to check
1852 : : * what comes back anyway.)
1853 : : */
439 dgustafsson@postgres 1854 :CBC 57 : actx->headers = curl_slist_append(actx->headers, "Accept:");
1855 [ - + ]: 57 : if (actx->headers == NULL)
1856 : : {
439 dgustafsson@postgres 1857 :UBC 0 : actx_error(actx, "out of memory");
1858 : 0 : return false;
1859 : : }
439 dgustafsson@postgres 1860 [ - + ]:CBC 57 : CHECK_SETOPT(actx, CURLOPT_HTTPHEADER, actx->headers, return false);
1861 : :
1862 : 57 : return true;
1863 : : }
1864 : :
1865 : : /*
1866 : : * Generic HTTP Request Handlers
1867 : : */
1868 : :
1869 : : /*
1870 : : * Response callback from libcurl which appends the response body into
1871 : : * actx->work_data (see start_request()). The maximum size of the data is
1872 : : * defined by CURL_MAX_WRITE_SIZE which by default is 16kb (and can only be
1873 : : * changed by recompiling libcurl).
1874 : : */
1875 : : static size_t
1876 : 198 : append_data(char *buf, size_t size, size_t nmemb, void *userdata)
1877 : : {
1878 : 198 : struct async_ctx *actx = userdata;
1879 : 198 : PQExpBuffer resp = &actx->work_data;
1880 : 198 : size_t len = size * nmemb;
1881 : :
1882 : : /* In case we receive data over the threshold, abort the transfer */
1883 [ + + ]: 198 : if ((resp->len + len) > MAX_OAUTH_RESPONSE_SIZE)
1884 : : {
1885 : 2 : actx_error(actx, "response is too large");
1886 : 2 : return 0;
1887 : : }
1888 : :
1889 : : /* The data passed from libcurl is not null-terminated */
1890 : 196 : appendBinaryPQExpBuffer(resp, buf, len);
1891 : :
1892 : : /*
1893 : : * Signal an error in order to abort the transfer in case we ran out of
1894 : : * memory in accepting the data.
1895 : : */
1896 [ + - - + ]: 196 : if (PQExpBufferBroken(resp))
1897 : : {
439 dgustafsson@postgres 1898 :UBC 0 : actx_error(actx, "out of memory");
1899 : 0 : return 0;
1900 : : }
1901 : :
439 dgustafsson@postgres 1902 :CBC 196 : return len;
1903 : : }
1904 : :
1905 : : /*
1906 : : * Begins an HTTP request on the multi handle. The caller should have set up all
1907 : : * request-specific options on actx->curl first. The server's response body will
1908 : : * be accumulated in actx->work_data (which will be reset, so don't store
1909 : : * anything important there across this call).
1910 : : *
1911 : : * Once a request is queued, it can be driven to completion via drive_request().
1912 : : * If actx->running is zero upon return, the request has already finished and
1913 : : * drive_request() can be called without returning control to the client.
1914 : : */
1915 : : static bool
1916 : 167 : start_request(struct async_ctx *actx)
1917 : : {
1918 : : CURLMcode err;
1919 : :
1920 : 167 : resetPQExpBuffer(&actx->work_data);
1921 [ - + ]: 167 : CHECK_SETOPT(actx, CURLOPT_WRITEFUNCTION, append_data, return false);
1922 [ - + ]: 167 : CHECK_SETOPT(actx, CURLOPT_WRITEDATA, actx, return false);
1923 : :
1924 : 167 : err = curl_multi_add_handle(actx->curlm, actx->curl);
1925 [ - + ]: 167 : if (err)
1926 : : {
439 dgustafsson@postgres 1927 :UBC 0 : actx_error(actx, "failed to queue HTTP request: %s",
1928 : : curl_multi_strerror(err));
1929 : 0 : return false;
1930 : : }
1931 : :
1932 : : /*
1933 : : * actx->running tracks the number of running handles, so we can
1934 : : * immediately call back if no waiting is needed.
1935 : : *
1936 : : * Even though this is nominally an asynchronous process, there are some
1937 : : * operations that can synchronously fail by this point (e.g. connections
1938 : : * to closed local ports) or even synchronously succeed if the stars align
1939 : : * (all the libcurl connection caches hit and the server is fast).
1940 : : */
439 dgustafsson@postgres 1941 :CBC 167 : err = curl_multi_socket_action(actx->curlm, CURL_SOCKET_TIMEOUT, 0, &actx->running);
1942 [ - + ]: 167 : if (err)
1943 : : {
439 dgustafsson@postgres 1944 :UBC 0 : actx_error(actx, "asynchronous HTTP request failed: %s",
1945 : : curl_multi_strerror(err));
1946 : 0 : return false;
1947 : : }
1948 : :
439 dgustafsson@postgres 1949 :CBC 167 : return true;
1950 : : }
1951 : :
1952 : : /*
1953 : : * CURL_IGNORE_DEPRECATION was added in 7.87.0. If it's not defined, we can make
1954 : : * it a no-op.
1955 : : */
1956 : : #ifndef CURL_IGNORE_DEPRECATION
1957 : : #define CURL_IGNORE_DEPRECATION(x) x
1958 : : #endif
1959 : :
1960 : : /*
1961 : : * Add another macro layer that inserts the needed semicolon, to avoid having
1962 : : * to write a literal semicolon in the call below, which would break pgindent.
1963 : : */
1964 : : #define PG_CURL_IGNORE_DEPRECATION(x) CURL_IGNORE_DEPRECATION(x;)
1965 : :
1966 : : /*
1967 : : * Drives the multi handle towards completion. The caller should have already
1968 : : * set up an asynchronous request via start_request().
1969 : : */
1970 : : static PostgresPollingStatusType
1971 : 1159 : drive_request(struct async_ctx *actx)
1972 : : {
1973 : : CURLMcode err;
1974 : : CURLMsg *msg;
1975 : : int msgs_left;
1976 : : bool done;
1977 : :
1978 [ + - ]: 1159 : if (actx->running)
1979 : : {
1980 : : /*---
1981 : : * There's an async request in progress. Pump the multi handle.
1982 : : *
1983 : : * curl_multi_socket_all() is officially deprecated, because it's
1984 : : * inefficient and pointless if your event loop has already handed you
1985 : : * the exact sockets that are ready. But that's not our use case --
1986 : : * our client has no way to tell us which sockets are ready. (They
1987 : : * don't even know there are sockets to begin with.)
1988 : : *
1989 : : * We can grab the list of triggered events from the multiplexer
1990 : : * ourselves, but that's effectively what curl_multi_socket_all() is
1991 : : * going to do. And there are currently no plans for the Curl project
1992 : : * to remove or break this API, so ignore the deprecation. See
1993 : : *
1994 : : * https://curl.se/mail/lib-2024-11/0028.html
1995 : : */
174 alvherre@kurilemu.de 1996 : 1159 : PG_CURL_IGNORE_DEPRECATION(err =
1997 : : curl_multi_socket_all(actx->curlm,
1998 : : &actx->running));
1999 : :
439 dgustafsson@postgres 2000 [ - + ]: 1159 : if (err)
2001 : : {
439 dgustafsson@postgres 2002 :UBC 0 : actx_error(actx, "asynchronous HTTP request failed: %s",
2003 : : curl_multi_strerror(err));
2004 : 0 : return PGRES_POLLING_FAILED;
2005 : : }
2006 : :
439 dgustafsson@postgres 2007 [ + + ]:CBC 1159 : if (actx->running)
2008 : : {
2009 : : /* We'll come back again. */
2010 : 992 : return PGRES_POLLING_READING;
2011 : : }
2012 : : }
2013 : :
2014 : 167 : done = false;
2015 [ + + ]: 331 : while ((msg = curl_multi_info_read(actx->curlm, &msgs_left)) != NULL)
2016 : : {
2017 [ - + ]: 167 : if (msg->msg != CURLMSG_DONE)
2018 : : {
2019 : : /*
2020 : : * Future libcurl versions may define new message types; we don't
2021 : : * know how to handle them, so we'll ignore them.
2022 : : */
439 dgustafsson@postgres 2023 :UBC 0 : continue;
2024 : : }
2025 : :
2026 : : /* First check the status of the request itself. */
439 dgustafsson@postgres 2027 [ + + ]:CBC 167 : if (msg->data.result != CURLE_OK)
2028 : : {
2029 : : /*
2030 : : * If a more specific error hasn't already been reported, use
2031 : : * libcurl's description.
2032 : : */
2033 [ + + ]: 3 : if (actx->errbuf.len == 0)
439 dgustafsson@postgres 2034 :GBC 1 : actx_error_str(actx, curl_easy_strerror(msg->data.result));
2035 : :
439 dgustafsson@postgres 2036 :CBC 3 : return PGRES_POLLING_FAILED;
2037 : : }
2038 : :
2039 : : /* Now remove the finished handle; we'll add it back later if needed. */
2040 : 164 : err = curl_multi_remove_handle(actx->curlm, msg->easy_handle);
2041 [ - + ]: 164 : if (err)
2042 : : {
439 dgustafsson@postgres 2043 :UBC 0 : actx_error(actx, "libcurl easy handle removal failed: %s",
2044 : : curl_multi_strerror(err));
2045 : 0 : return PGRES_POLLING_FAILED;
2046 : : }
2047 : :
439 dgustafsson@postgres 2048 :CBC 164 : done = true;
2049 : : }
2050 : :
2051 : : /* Sanity check. */
2052 [ - + ]: 164 : if (!done)
2053 : : {
439 dgustafsson@postgres 2054 :UBC 0 : actx_error(actx, "no result was retrieved for the finished handle");
2055 : 0 : return PGRES_POLLING_FAILED;
2056 : : }
2057 : :
439 dgustafsson@postgres 2058 :CBC 164 : return PGRES_POLLING_OK;
2059 : : }
2060 : :
2061 : : /*
2062 : : * URL-Encoding Helpers
2063 : : */
2064 : :
2065 : : /*
2066 : : * Encodes a string using the application/x-www-form-urlencoded format, and
2067 : : * appends it to the given buffer.
2068 : : */
2069 : : static void
2070 : 538 : append_urlencoded(PQExpBuffer buf, const char *s)
2071 : : {
2072 : : char *escaped;
2073 : : char *haystack;
2074 : : char *match;
2075 : :
2076 : : /* The first parameter to curl_easy_escape is deprecated by Curl */
2077 : 538 : escaped = curl_easy_escape(NULL, s, 0);
2078 [ - + ]: 538 : if (!escaped)
2079 : : {
439 dgustafsson@postgres 2080 :UBC 0 : termPQExpBuffer(buf); /* mark the buffer broken */
2081 : 0 : return;
2082 : : }
2083 : :
2084 : : /*
2085 : : * curl_easy_escape() almost does what we want, but we need the
2086 : : * query-specific flavor which uses '+' instead of '%20' for spaces. The
2087 : : * Curl command-line tool does this with a simple search-and-replace, so
2088 : : * follow its lead.
2089 : : */
439 dgustafsson@postgres 2090 :CBC 538 : haystack = escaped;
2091 : :
2092 [ + + ]: 596 : while ((match = strstr(haystack, "%20")) != NULL)
2093 : : {
2094 : : /* Append the unmatched portion, followed by the plus sign. */
2095 : 58 : appendBinaryPQExpBuffer(buf, haystack, match - haystack);
2096 : 58 : appendPQExpBufferChar(buf, '+');
2097 : :
2098 : : /* Keep searching after the match. */
2099 : 58 : haystack = match + 3 /* strlen("%20") */ ;
2100 : : }
2101 : :
2102 : : /* Push the remainder of the string onto the buffer. */
2103 : 538 : appendPQExpBufferStr(buf, haystack);
2104 : :
2105 : 538 : curl_free(escaped);
2106 : : }
2107 : :
2108 : : /*
2109 : : * Convenience wrapper for encoding a single string. Returns NULL on allocation
2110 : : * failure.
2111 : : */
2112 : : static char *
2113 : 28 : urlencode(const char *s)
2114 : : {
2115 : : PQExpBufferData buf;
2116 : :
2117 : 28 : initPQExpBuffer(&buf);
2118 : 28 : append_urlencoded(&buf, s);
2119 : :
2120 [ + - ]: 28 : return PQExpBufferDataBroken(buf) ? NULL : buf.data;
2121 : : }
2122 : :
2123 : : /*
2124 : : * Appends a key/value pair to the end of an application/x-www-form-urlencoded
2125 : : * list.
2126 : : */
2127 : : static void
2128 : 255 : build_urlencoded(PQExpBuffer buf, const char *key, const char *value)
2129 : : {
2130 [ + + ]: 255 : if (buf->len)
2131 : 143 : appendPQExpBufferChar(buf, '&');
2132 : :
2133 : 255 : append_urlencoded(buf, key);
2134 : 255 : appendPQExpBufferChar(buf, '=');
2135 : 255 : append_urlencoded(buf, value);
2136 : 255 : }
2137 : :
2138 : : /*
2139 : : * Specific HTTP Request Handlers
2140 : : *
2141 : : * This is finally the beginning of the actual application logic. Generally
2142 : : * speaking, a single request consists of a start_* and a finish_* step, with
2143 : : * drive_request() pumping the machine in between.
2144 : : */
2145 : :
2146 : : /*
2147 : : * Queue an OpenID Provider Configuration Request:
2148 : : *
2149 : : * https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest
2150 : : * https://www.rfc-editor.org/rfc/rfc8414#section-3.1
2151 : : *
2152 : : * This is done first to get the endpoint URIs we need to contact and to make
2153 : : * sure the provider provides a device authorization flow. finish_discovery()
2154 : : * will fill in actx->provider.
2155 : : */
2156 : : static bool
2157 : 55 : start_discovery(struct async_ctx *actx, const char *discovery_uri)
2158 : : {
2159 [ - + ]: 55 : CHECK_SETOPT(actx, CURLOPT_HTTPGET, 1L, return false);
2160 [ - + ]: 55 : CHECK_SETOPT(actx, CURLOPT_URL, discovery_uri, return false);
2161 : :
2162 : 55 : return start_request(actx);
2163 : : }
2164 : :
2165 : : static bool
2166 : 54 : finish_discovery(struct async_ctx *actx)
2167 : : {
2168 : : long response_code;
2169 : :
2170 : : /*----
2171 : : * Now check the response. OIDC Discovery 1.0 is pretty strict:
2172 : : *
2173 : : * A successful response MUST use the 200 OK HTTP status code and
2174 : : * return a JSON object using the application/json content type that
2175 : : * contains a set of Claims as its members that are a subset of the
2176 : : * Metadata values defined in Section 3.
2177 : : *
2178 : : * Compared to standard HTTP semantics, this makes life easy -- we don't
2179 : : * need to worry about redirections (which would call the Issuer host
2180 : : * validation into question), or non-authoritative responses, or any other
2181 : : * complications.
2182 : : */
2183 [ - + ]: 54 : CHECK_GETINFO(actx, CURLINFO_RESPONSE_CODE, &response_code, return false);
2184 : :
2185 [ - + ]: 54 : if (response_code != 200)
2186 : : {
439 dgustafsson@postgres 2187 :UBC 0 : actx_error(actx, "unexpected response code %ld", response_code);
2188 : 0 : return false;
2189 : : }
2190 : :
2191 : : /*
2192 : : * Pull the fields we care about from the document.
2193 : : */
141 jchampion@postgresql 2194 :CBC 54 : actx->errctx = libpq_gettext("failed to parse OpenID discovery document");
439 dgustafsson@postgres 2195 [ - + ]: 54 : if (!parse_provider(actx, &actx->provider))
439 dgustafsson@postgres 2196 :UBC 0 : return false; /* error message already set */
2197 : :
2198 : : /*
2199 : : * Fill in any defaults for OPTIONAL/RECOMMENDED fields we care about.
2200 : : */
439 dgustafsson@postgres 2201 [ - + ]:CBC 54 : if (!actx->provider.grant_types_supported)
2202 : : {
2203 : : /*
2204 : : * Per Section 3, the default is ["authorization_code", "implicit"].
2205 : : */
439 dgustafsson@postgres 2206 :UBC 0 : struct curl_slist *temp = actx->provider.grant_types_supported;
2207 : :
2208 : 0 : temp = curl_slist_append(temp, "authorization_code");
2209 [ # # ]: 0 : if (temp)
2210 : : {
2211 : 0 : temp = curl_slist_append(temp, "implicit");
2212 : : }
2213 : :
2214 [ # # ]: 0 : if (!temp)
2215 : : {
2216 : 0 : actx_error(actx, "out of memory");
2217 : 0 : return false;
2218 : : }
2219 : :
2220 : 0 : actx->provider.grant_types_supported = temp;
2221 : : }
2222 : :
439 dgustafsson@postgres 2223 :CBC 54 : return true;
2224 : : }
2225 : :
2226 : : /*
2227 : : * Ensure that the discovery document is provided by the expected issuer.
2228 : : * Currently, issuers are statically configured in the connection string.
2229 : : */
2230 : : static bool
2231 : 54 : check_issuer(struct async_ctx *actx, PGconn *conn)
2232 : : {
2233 : 54 : const struct provider *provider = &actx->provider;
53 jchampion@postgresql 2234 :GNC 54 : const char *oauth_issuer_id = actx->issuer_id;
2235 : :
369 jchampion@postgresql 2236 [ - + ]:CBC 54 : Assert(oauth_issuer_id); /* ensured by setup_oauth_parameters() */
439 dgustafsson@postgres 2237 [ - + ]: 54 : Assert(provider->issuer); /* ensured by parse_provider() */
2238 : :
2239 : : /*---
2240 : : * We require strict equality for issuer identifiers -- no path or case
2241 : : * normalization, no substitution of default ports and schemes, etc. This
2242 : : * is done to match the rules in OIDC Discovery Sec. 4.3 for config
2243 : : * validation:
2244 : : *
2245 : : * The issuer value returned MUST be identical to the Issuer URL that
2246 : : * was used as the prefix to /.well-known/openid-configuration to
2247 : : * retrieve the configuration information.
2248 : : *
2249 : : * as well as the rules set out in RFC 9207 for avoiding mix-up attacks:
2250 : : *
2251 : : * Clients MUST then [...] compare the result to the issuer identifier
2252 : : * of the authorization server where the authorization request was
2253 : : * sent to. This comparison MUST use simple string comparison as defined
2254 : : * in Section 6.2.1 of [RFC3986].
2255 : : */
369 jchampion@postgresql 2256 [ - + ]: 54 : if (strcmp(oauth_issuer_id, provider->issuer) != 0)
2257 : : {
439 dgustafsson@postgres 2258 :UBC 0 : actx_error(actx,
2259 : : "the issuer identifier (%s) does not match oauth_issuer (%s)",
2260 : : provider->issuer, oauth_issuer_id);
2261 : 0 : return false;
2262 : : }
2263 : :
439 dgustafsson@postgres 2264 :CBC 54 : return true;
2265 : : }
2266 : :
2267 : : #define HTTPS_SCHEME "https://"
2268 : : #define OAUTH_GRANT_TYPE_DEVICE_CODE "urn:ietf:params:oauth:grant-type:device_code"
2269 : :
2270 : : /*
2271 : : * Ensure that the provider supports the Device Authorization flow (i.e. it
2272 : : * provides an authorization endpoint, and both the token and authorization
2273 : : * endpoint URLs seem reasonable).
2274 : : */
2275 : : static bool
2276 : 54 : check_for_device_flow(struct async_ctx *actx)
2277 : : {
2278 : 54 : const struct provider *provider = &actx->provider;
2279 : :
2280 [ - + ]: 54 : Assert(provider->issuer); /* ensured by parse_provider() */
2281 [ - + ]: 54 : Assert(provider->token_endpoint); /* ensured by parse_provider() */
2282 : :
2283 [ - + ]: 54 : if (!provider->device_authorization_endpoint)
2284 : : {
439 dgustafsson@postgres 2285 :UBC 0 : actx_error(actx,
2286 : : "issuer \"%s\" does not provide a device authorization endpoint",
2287 : : provider->issuer);
2288 : 0 : return false;
2289 : : }
2290 : :
2291 : : /*
2292 : : * The original implementation checked that OAUTH_GRANT_TYPE_DEVICE_CODE
2293 : : * was present in the discovery document's grant_types_supported list. MS
2294 : : * Entra does not advertise this grant type, though, and since it doesn't
2295 : : * make sense to stand up a device_authorization_endpoint without also
2296 : : * accepting device codes at the token_endpoint, that's the only thing we
2297 : : * currently require.
2298 : : */
2299 : :
2300 : : /*
2301 : : * Although libcurl will fail later if the URL contains an unsupported
2302 : : * scheme, that error message is going to be a bit opaque. This is a
2303 : : * decent time to bail out if we're not using HTTPS for the endpoints
2304 : : * we'll use for the flow.
2305 : : */
28 jchampion@postgresql 2306 [ + - ]:GNC 54 : if ((actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP) == 0)
2307 : : {
439 dgustafsson@postgres 2308 [ - + ]:GBC 54 : if (pg_strncasecmp(provider->device_authorization_endpoint,
2309 : : HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0)
2310 : : {
439 dgustafsson@postgres 2311 :UBC 0 : actx_error(actx,
2312 : : "device authorization endpoint \"%s\" must use HTTPS",
2313 : : provider->device_authorization_endpoint);
2314 : 0 : return false;
2315 : : }
2316 : :
439 dgustafsson@postgres 2317 [ - + ]:GBC 54 : if (pg_strncasecmp(provider->token_endpoint,
2318 : : HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0)
2319 : : {
439 dgustafsson@postgres 2320 :UBC 0 : actx_error(actx,
2321 : : "token endpoint \"%s\" must use HTTPS",
2322 : : provider->token_endpoint);
2323 : 0 : return false;
2324 : : }
2325 : : }
2326 : :
439 dgustafsson@postgres 2327 :CBC 54 : return true;
2328 : : }
2329 : :
2330 : : /*
2331 : : * Adds the client ID (and secret, if provided) to the current request, using
2332 : : * either HTTP headers or the request body.
2333 : : */
2334 : : static bool
2335 : 112 : add_client_identification(struct async_ctx *actx, PQExpBuffer reqbody, PGconn *conn)
2336 : : {
53 jchampion@postgresql 2337 :GNC 112 : const char *oauth_client_id = actx->client_id;
2338 : 112 : const char *oauth_client_secret = actx->client_secret;
2339 : :
439 dgustafsson@postgres 2340 :CBC 112 : bool success = false;
2341 : 112 : char *username = NULL;
2342 : 112 : char *password = NULL;
2343 : :
369 jchampion@postgresql 2344 [ + + ]: 112 : if (oauth_client_secret) /* Zero-length secrets are permitted! */
2345 : : {
2346 : : /*----
2347 : : * Use HTTP Basic auth to send the client_id and secret. Per RFC 6749,
2348 : : * Sec. 2.3.1,
2349 : : *
2350 : : * Including the client credentials in the request-body using the
2351 : : * two parameters is NOT RECOMMENDED and SHOULD be limited to
2352 : : * clients unable to directly utilize the HTTP Basic authentication
2353 : : * scheme (or other password-based HTTP authentication schemes).
2354 : : *
2355 : : * Additionally:
2356 : : *
2357 : : * The client identifier is encoded using the
2358 : : * "application/x-www-form-urlencoded" encoding algorithm per Appendix
2359 : : * B, and the encoded value is used as the username; the client
2360 : : * password is encoded using the same algorithm and used as the
2361 : : * password.
2362 : : *
2363 : : * (Appendix B modifies application/x-www-form-urlencoded by requiring
2364 : : * an initial UTF-8 encoding step. Since the client ID and secret must
2365 : : * both be 7-bit ASCII -- RFC 6749 Appendix A -- we don't worry about
2366 : : * that in this function.)
2367 : : *
2368 : : * client_id is not added to the request body in this case. Not only
2369 : : * would it be redundant, but some providers in the wild (e.g. Okta)
2370 : : * refuse to accept it.
2371 : : */
2372 : 14 : username = urlencode(oauth_client_id);
2373 : 14 : password = urlencode(oauth_client_secret);
2374 : :
439 dgustafsson@postgres 2375 [ + - - + ]: 14 : if (!username || !password)
2376 : : {
439 dgustafsson@postgres 2377 :UBC 0 : actx_error(actx, "out of memory");
2378 : 0 : goto cleanup;
2379 : : }
2380 : :
439 dgustafsson@postgres 2381 [ - + ]:CBC 14 : CHECK_SETOPT(actx, CURLOPT_HTTPAUTH, CURLAUTH_BASIC, goto cleanup);
2382 [ - + ]: 14 : CHECK_SETOPT(actx, CURLOPT_USERNAME, username, goto cleanup);
2383 [ - + ]: 14 : CHECK_SETOPT(actx, CURLOPT_PASSWORD, password, goto cleanup);
2384 : :
2385 : 14 : actx->used_basic_auth = true;
2386 : : }
2387 : : else
2388 : : {
2389 : : /*
2390 : : * If we're not otherwise authenticating, client_id is REQUIRED in the
2391 : : * request body.
2392 : : */
369 jchampion@postgresql 2393 : 98 : build_urlencoded(reqbody, "client_id", oauth_client_id);
2394 : :
439 dgustafsson@postgres 2395 [ - + ]: 98 : CHECK_SETOPT(actx, CURLOPT_HTTPAUTH, CURLAUTH_NONE, goto cleanup);
2396 : 98 : actx->used_basic_auth = false;
2397 : : }
2398 : :
2399 : 112 : success = true;
2400 : :
2401 : 112 : cleanup:
2402 : 112 : free(username);
2403 : 112 : free(password);
2404 : :
2405 : 112 : return success;
2406 : : }
2407 : :
2408 : : /*
2409 : : * Queue a Device Authorization Request:
2410 : : *
2411 : : * https://www.rfc-editor.org/rfc/rfc8628#section-3.1
2412 : : *
2413 : : * This is the second step. We ask the provider to verify the end user out of
2414 : : * band and authorize us to act on their behalf; it will give us the required
2415 : : * nonces for us to later poll the request status, which we'll grab in
2416 : : * finish_device_authz().
2417 : : */
2418 : : static bool
2419 : 54 : start_device_authz(struct async_ctx *actx, PGconn *conn)
2420 : : {
53 jchampion@postgresql 2421 :GNC 54 : const char *oauth_scope = actx->scope;
439 dgustafsson@postgres 2422 :CBC 54 : const char *device_authz_uri = actx->provider.device_authorization_endpoint;
2423 : 54 : PQExpBuffer work_buffer = &actx->work_data;
2424 : :
2425 [ - + ]: 54 : Assert(device_authz_uri); /* ensured by check_for_device_flow() */
2426 : :
2427 : : /* Construct our request body. */
2428 : 54 : resetPQExpBuffer(work_buffer);
369 jchampion@postgresql 2429 [ + + + + ]: 54 : if (oauth_scope && oauth_scope[0])
2430 : 41 : build_urlencoded(work_buffer, "scope", oauth_scope);
2431 : :
439 dgustafsson@postgres 2432 [ - + ]: 54 : if (!add_client_identification(actx, work_buffer, conn))
439 dgustafsson@postgres 2433 :UBC 0 : return false;
2434 : :
439 dgustafsson@postgres 2435 [ + - - + ]:CBC 54 : if (PQExpBufferBroken(work_buffer))
2436 : : {
439 dgustafsson@postgres 2437 :UBC 0 : actx_error(actx, "out of memory");
2438 : 0 : return false;
2439 : : }
2440 : :
2441 : : /* Make our request. */
439 dgustafsson@postgres 2442 [ - + ]:CBC 54 : CHECK_SETOPT(actx, CURLOPT_URL, device_authz_uri, return false);
2443 [ - + ]: 54 : CHECK_SETOPT(actx, CURLOPT_COPYPOSTFIELDS, work_buffer->data, return false);
2444 : :
2445 : 54 : return start_request(actx);
2446 : : }
2447 : :
2448 : : static bool
2449 : 53 : finish_device_authz(struct async_ctx *actx)
2450 : : {
2451 : : long response_code;
2452 : :
2453 [ - + ]: 53 : CHECK_GETINFO(actx, CURLINFO_RESPONSE_CODE, &response_code, return false);
2454 : :
2455 : : /*
2456 : : * Per RFC 8628, Section 3, a successful device authorization response
2457 : : * uses 200 OK.
2458 : : */
2459 [ + - ]: 53 : if (response_code == 200)
2460 : : {
141 jchampion@postgresql 2461 : 53 : actx->errctx = libpq_gettext("failed to parse device authorization");
439 dgustafsson@postgres 2462 [ + + ]: 53 : if (!parse_device_authz(actx, &actx->authz))
2463 : 3 : return false; /* error message already set */
2464 : :
2465 : 50 : return true;
2466 : : }
2467 : :
2468 : : /*
2469 : : * The device authorization endpoint uses the same error response as the
2470 : : * token endpoint, so the error handling roughly follows
2471 : : * finish_token_request(). The key difference is that an error here is
2472 : : * immediately fatal.
2473 : : */
439 dgustafsson@postgres 2474 [ # # # # ]:UBC 0 : if (response_code == 400 || response_code == 401)
2475 : : {
2476 : 0 : struct token_error err = {0};
2477 : :
2478 [ # # ]: 0 : if (!parse_token_error(actx, &err))
2479 : : {
2480 : 0 : free_token_error(&err);
2481 : 0 : return false;
2482 : : }
2483 : :
2484 : : /* Copy the token error into the context error buffer */
2485 : 0 : record_token_error(actx, &err);
2486 : :
2487 : 0 : free_token_error(&err);
2488 : 0 : return false;
2489 : : }
2490 : :
2491 : : /* Any other response codes are considered invalid */
2492 : 0 : actx_error(actx, "unexpected response code %ld", response_code);
2493 : 0 : return false;
2494 : : }
2495 : :
2496 : : /*
2497 : : * Queue an Access Token Request:
2498 : : *
2499 : : * https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3
2500 : : *
2501 : : * This is the final step. We continually poll the token endpoint to see if the
2502 : : * user has authorized us yet. finish_token_request() will pull either the token
2503 : : * or a (ideally temporary) error status from the provider.
2504 : : */
2505 : : static bool
439 dgustafsson@postgres 2506 :CBC 58 : start_token_request(struct async_ctx *actx, PGconn *conn)
2507 : : {
2508 : 58 : const char *token_uri = actx->provider.token_endpoint;
2509 : 58 : const char *device_code = actx->authz.device_code;
2510 : 58 : PQExpBuffer work_buffer = &actx->work_data;
2511 : :
2512 [ - + ]: 58 : Assert(token_uri); /* ensured by parse_provider() */
2513 [ - + ]: 58 : Assert(device_code); /* ensured by parse_device_authz() */
2514 : :
2515 : : /* Construct our request body. */
2516 : 58 : resetPQExpBuffer(work_buffer);
2517 : 58 : build_urlencoded(work_buffer, "device_code", device_code);
2518 : 58 : build_urlencoded(work_buffer, "grant_type", OAUTH_GRANT_TYPE_DEVICE_CODE);
2519 : :
2520 [ - + ]: 58 : if (!add_client_identification(actx, work_buffer, conn))
439 dgustafsson@postgres 2521 :UBC 0 : return false;
2522 : :
439 dgustafsson@postgres 2523 [ + - - + ]:CBC 58 : if (PQExpBufferBroken(work_buffer))
2524 : : {
439 dgustafsson@postgres 2525 :UBC 0 : actx_error(actx, "out of memory");
2526 : 0 : return false;
2527 : : }
2528 : :
2529 : : /* Make our request. */
439 dgustafsson@postgres 2530 [ - + ]:CBC 58 : CHECK_SETOPT(actx, CURLOPT_URL, token_uri, return false);
2531 [ - + ]: 58 : CHECK_SETOPT(actx, CURLOPT_COPYPOSTFIELDS, work_buffer->data, return false);
2532 : :
2533 : 58 : return start_request(actx);
2534 : : }
2535 : :
2536 : : static bool
2537 : 57 : finish_token_request(struct async_ctx *actx, struct token *tok)
2538 : : {
2539 : : long response_code;
2540 : :
2541 [ - + ]: 57 : CHECK_GETINFO(actx, CURLINFO_RESPONSE_CODE, &response_code, return false);
2542 : :
2543 : : /*
2544 : : * Per RFC 6749, Section 5, a successful response uses 200 OK.
2545 : : */
2546 [ + + ]: 57 : if (response_code == 200)
2547 : : {
141 jchampion@postgresql 2548 : 42 : actx->errctx = libpq_gettext("failed to parse access token response");
439 dgustafsson@postgres 2549 [ + + ]: 42 : if (!parse_access_token(actx, tok))
2550 : 2 : return false; /* error message already set */
2551 : :
2552 : 40 : return true;
2553 : : }
2554 : :
2555 : : /*
2556 : : * An error response uses either 400 Bad Request or 401 Unauthorized.
2557 : : * There are references online to implementations using 403 for error
2558 : : * return which would violate the specification. For now we stick to the
2559 : : * specification but we might have to revisit this.
2560 : : */
2561 [ + + + - ]: 15 : if (response_code == 400 || response_code == 401)
2562 : : {
2563 [ - + ]: 15 : if (!parse_token_error(actx, &tok->err))
439 dgustafsson@postgres 2564 :UBC 0 : return false;
2565 : :
439 dgustafsson@postgres 2566 :CBC 15 : return true;
2567 : : }
2568 : :
2569 : : /* Any other response codes are considered invalid */
439 dgustafsson@postgres 2570 :UBC 0 : actx_error(actx, "unexpected response code %ld", response_code);
2571 : 0 : return false;
2572 : : }
2573 : :
2574 : : /*
2575 : : * Finishes the token request and examines the response. If the flow has
2576 : : * completed, a valid token will be returned via the parameter list. Otherwise,
2577 : : * the token parameter remains unchanged, and the caller needs to wait for
2578 : : * another interval (which will have been increased in response to a slow_down
2579 : : * message from the server) before starting a new token request.
2580 : : *
2581 : : * False is returned only for permanent error conditions.
2582 : : */
2583 : : static bool
439 dgustafsson@postgres 2584 :CBC 57 : handle_token_response(struct async_ctx *actx, char **token)
2585 : : {
2586 : 57 : bool success = false;
2587 : 57 : struct token tok = {0};
2588 : : const struct token_error *err;
2589 : :
2590 [ + + ]: 57 : if (!finish_token_request(actx, &tok))
2591 : 2 : goto token_cleanup;
2592 : :
2593 : : /* A successful token request gives either a token or an in-band error. */
2594 [ + + - + ]: 55 : Assert(tok.access_token || tok.err.error);
2595 : :
2596 [ + + ]: 55 : if (tok.access_token)
2597 : : {
2598 : 40 : *token = tok.access_token;
2599 : 40 : tok.access_token = NULL;
2600 : :
2601 : 40 : success = true;
2602 : 40 : goto token_cleanup;
2603 : : }
2604 : :
2605 : : /*
2606 : : * authorization_pending and slow_down are the only acceptable errors;
2607 : : * anything else and we bail. These are defined in RFC 8628, Sec. 3.5.
2608 : : */
2609 : 15 : err = &tok.err;
2610 [ + + ]: 15 : if (strcmp(err->error, "authorization_pending") != 0 &&
2611 [ + + ]: 7 : strcmp(err->error, "slow_down") != 0)
2612 : : {
2613 : 6 : record_token_error(actx, err);
2614 : 6 : goto token_cleanup;
2615 : : }
2616 : :
2617 : : /*
2618 : : * A slow_down error requires us to permanently increase our retry
2619 : : * interval by five seconds.
2620 : : */
2621 [ + + ]: 9 : if (strcmp(err->error, "slow_down") == 0)
2622 : : {
2623 : 1 : int prev_interval = actx->authz.interval;
2624 : :
2625 : 1 : actx->authz.interval += 5;
2626 [ + - ]: 1 : if (actx->authz.interval < prev_interval)
2627 : : {
2628 : 1 : actx_error(actx, "slow_down interval overflow");
2629 : 1 : goto token_cleanup;
2630 : : }
2631 : : }
2632 : :
2633 : 8 : success = true;
2634 : :
2635 : 57 : token_cleanup:
2636 : 57 : free_token(&tok);
2637 : 57 : return success;
2638 : : }
2639 : :
2640 : : /*
2641 : : * Displays a device authorization prompt for action by the end user, either via
2642 : : * the PQauthDataHook, or by a message on standard error if no hook is set.
2643 : : */
2644 : : static bool
2645 : 40 : prompt_user(struct async_ctx *actx, PGconn *conn)
2646 : : {
2647 : : int res;
2648 : 40 : PGpromptOAuthDevice prompt = {
2649 : 40 : .verification_uri = actx->authz.verification_uri,
2650 : 40 : .user_code = actx->authz.user_code,
2651 : 40 : .verification_uri_complete = actx->authz.verification_uri_complete,
2652 : 40 : .expires_in = actx->authz.expires_in,
2653 : : };
369 jchampion@postgresql 2654 : 40 : PQauthDataHook_type hook = PQgetAuthDataHook();
2655 : :
2656 : 40 : res = hook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt);
2657 : :
439 dgustafsson@postgres 2658 [ + - ]: 40 : if (!res)
2659 : : {
2660 : : /*
2661 : : * translator: The first %s is a URL for the user to visit in a
2662 : : * browser, and the second %s is a code to be copy-pasted there.
2663 : : */
2664 : 40 : fprintf(stderr, libpq_gettext("Visit %s and enter the code: %s\n"),
2665 : : prompt.verification_uri, prompt.user_code);
2666 : : }
439 dgustafsson@postgres 2667 [ # # ]:UBC 0 : else if (res < 0)
2668 : : {
2669 : 0 : actx_error(actx, "device prompt failed");
2670 : 0 : return false;
2671 : : }
2672 : :
439 dgustafsson@postgres 2673 :CBC 40 : return true;
2674 : : }
2675 : :
2676 : : /*
2677 : : * Calls curl_global_init() in a thread-safe way.
2678 : : *
2679 : : * libcurl has stringent requirements for the thread context in which you call
2680 : : * curl_global_init(), because it's going to try initializing a bunch of other
2681 : : * libraries (OpenSSL, Winsock, etc). Recent versions of libcurl have improved
2682 : : * the thread-safety situation, but there's a chicken-and-egg problem at
2683 : : * runtime: you can't check the thread safety until you've initialized libcurl,
2684 : : * which you can't do from within a thread unless you know it's thread-safe...
2685 : : *
2686 : : * Returns true if initialization was successful. Successful or not, this
2687 : : * function will not try to reinitialize Curl on successive calls.
2688 : : */
2689 : : static bool
53 jchampion@postgresql 2690 :GNC 57 : initialize_curl(PGoauthBearerRequestV2 *req)
2691 : : {
2692 : : /*
2693 : : * Don't let the compiler play tricks with this variable. In the
2694 : : * HAVE_THREADSAFE_CURL_GLOBAL_INIT case, we don't care if two threads
2695 : : * enter simultaneously, but we do care if this gets set transiently to
2696 : : * PG_BOOL_YES/NO in cases where that's not the final answer.
2697 : : */
2698 : : static volatile PGTernaryBool init_successful = PG_BOOL_UNKNOWN;
2699 : : #if HAVE_THREADSAFE_CURL_GLOBAL_INIT
2700 : : curl_version_info_data *info;
2701 : : #endif
2702 : :
2703 : : #if !HAVE_THREADSAFE_CURL_GLOBAL_INIT
2704 : :
2705 : : /*
2706 : : * Lock around the whole function. If a libpq client performs its own work
2707 : : * with libcurl, it must either ensure that Curl is initialized safely
2708 : : * before calling us (in which case our call will be a no-op), or else it
2709 : : * must guard its own calls to curl_global_init() with a registered
2710 : : * threadlock handler. See PQregisterThreadLock().
2711 : : */
2712 : : pglock_thread();
2713 : : #endif
2714 : :
2715 : : /*
2716 : : * Skip initialization if we've already done it. (Curl tracks the number
2717 : : * of calls; there's no point in incrementing the counter every time we
2718 : : * connect.)
2719 : : */
439 dgustafsson@postgres 2720 [ - + ]:CBC 57 : if (init_successful == PG_BOOL_YES)
439 dgustafsson@postgres 2721 :LBC (429635) : goto done;
439 dgustafsson@postgres 2722 [ - + ]:CBC 57 : else if (init_successful == PG_BOOL_NO)
2723 : : {
53 jchampion@postgresql 2724 :UNC 0 : req->error = libpq_gettext("curl_global_init previously failed during OAuth setup");
439 dgustafsson@postgres 2725 :UBC 0 : goto done;
2726 : : }
2727 : :
2728 : : /*
2729 : : * We know we've already initialized Winsock by this point (see
2730 : : * pqMakeEmptyPGconn()), so we should be able to safely skip that bit. But
2731 : : * we have to tell libcurl to initialize everything else, because other
2732 : : * pieces of our client executable may already be using libcurl for their
2733 : : * own purposes. If we initialize libcurl with only a subset of its
2734 : : * features, we could break those other clients nondeterministically, and
2735 : : * that would probably be a nightmare to debug.
2736 : : *
2737 : : * If some other part of the program has already called this, it's a
2738 : : * no-op.
2739 : : */
439 dgustafsson@postgres 2740 [ - + ]:CBC 57 : if (curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32) != CURLE_OK)
2741 : : {
53 jchampion@postgresql 2742 :UNC 0 : req->error = libpq_gettext("curl_global_init failed during OAuth setup");
439 dgustafsson@postgres 2743 :UBC 0 : init_successful = PG_BOOL_NO;
2744 : 0 : goto done;
2745 : : }
2746 : :
2747 : : #if HAVE_THREADSAFE_CURL_GLOBAL_INIT
2748 : :
2749 : : /*
2750 : : * If we determined at configure time that the Curl installation is
2751 : : * thread-safe, our job here is much easier. We simply initialize above
2752 : : * without any locking (concurrent or duplicated calls are fine in that
2753 : : * situation), then double-check to make sure the runtime setting agrees,
2754 : : * to try to catch silent downgrades.
2755 : : */
439 dgustafsson@postgres 2756 :CBC 57 : info = curl_version_info(CURLVERSION_NOW);
2757 [ - + ]: 57 : if (!(info->features & CURL_VERSION_THREADSAFE))
2758 : : {
2759 : : /*
2760 : : * In a downgrade situation, the damage is already done. Curl global
2761 : : * state may be corrupted. Be noisy.
2762 : : */
53 jchampion@postgresql 2763 :UNC 0 : req->error = libpq_gettext("libcurl is no longer thread-safe\n"
2764 : : "\tCurl initialization was reported thread-safe when libpq\n"
2765 : : "\twas compiled, but the currently installed version of\n"
2766 : : "\tlibcurl reports that it is not. Recompile libpq against\n"
2767 : : "\tthe installed version of libcurl.");
439 dgustafsson@postgres 2768 :UBC 0 : init_successful = PG_BOOL_NO;
2769 : 0 : goto done;
2770 : : }
2771 : : #endif
2772 : :
439 dgustafsson@postgres 2773 :CBC 57 : init_successful = PG_BOOL_YES;
2774 : :
2775 : 57 : done:
2776 : : #if !HAVE_THREADSAFE_CURL_GLOBAL_INIT
2777 : : pgunlock_thread();
2778 : : #endif
2779 : 57 : return (init_successful == PG_BOOL_YES);
2780 : : }
2781 : :
2782 : : /*
2783 : : * The core nonblocking libcurl implementation. This will be called several
2784 : : * times to pump the async engine.
2785 : : *
2786 : : * The architecture is based on PQconnectPoll(). The first half drives the
2787 : : * connection state forward as necessary, returning if we're not ready to
2788 : : * proceed to the next step yet. The second half performs the actual transition
2789 : : * between states.
2790 : : *
2791 : : * You can trace the overall OAuth flow through the second half. It's linear
2792 : : * until we get to the end, where we flip back and forth between
2793 : : * OAUTH_STEP_TOKEN_REQUEST and OAUTH_STEP_WAIT_INTERVAL to regularly ping the
2794 : : * provider.
2795 : : */
2796 : : static PostgresPollingStatusType
53 jchampion@postgresql 2797 :GNC 340362 : pg_fe_run_oauth_flow_impl(PGconn *conn, PGoauthBearerRequestV2 *request,
2798 : : int *altsock)
2799 : : {
2800 : 340362 : struct async_ctx *actx = request->v1.user;
369 jchampion@postgresql 2801 :CBC 340362 : char *oauth_token = NULL;
2802 : :
2803 : : do
2804 : : {
2805 : : /* By default, the multiplexer is the altsock. Reassign as desired. */
53 jchampion@postgresql 2806 :GNC 340362 : *altsock = actx->mux;
2807 : :
439 dgustafsson@postgres 2808 [ + + + - ]:CBC 340362 : switch (actx->step)
2809 : : {
2810 : 55 : case OAUTH_STEP_INIT:
2811 : 55 : break;
2812 : :
2813 : 1159 : case OAUTH_STEP_DISCOVERY:
2814 : : case OAUTH_STEP_DEVICE_AUTHORIZATION:
2815 : : case OAUTH_STEP_TOKEN_REQUEST:
2816 : : {
2817 : : PostgresPollingStatusType status;
2818 : :
2819 : : /*
2820 : : * Clear any expired timeout before calling back into
2821 : : * Curl. Curl is not guaranteed to do this for us, because
2822 : : * its API expects us to use single-shot (i.e.
2823 : : * edge-triggered) timeouts, and ours are level-triggered
2824 : : * via the mux.
2825 : : *
2826 : : * This can't be combined with the comb_multiplexer() call
2827 : : * below: we might accidentally clear a short timeout that
2828 : : * was both set and expired during the call to
2829 : : * drive_request().
2830 : : */
270 jchampion@postgresql 2831 [ - + ]: 1159 : if (!drain_timer_events(actx, NULL))
270 jchampion@postgresql 2832 :UBC 0 : goto error_return;
2833 : :
2834 : : /* Move the request forward. */
439 dgustafsson@postgres 2835 :CBC 1159 : status = drive_request(actx);
2836 : :
2837 [ + + ]: 1159 : if (status == PGRES_POLLING_FAILED)
2838 : 3 : goto error_return;
270 jchampion@postgresql 2839 [ + + ]: 1156 : else if (status == PGRES_POLLING_OK)
2840 : 164 : break; /* done! */
2841 : :
2842 : : /*
2843 : : * This request is still running.
2844 : : *
2845 : : * Make sure that stale events don't cause us to come back
2846 : : * early. (Currently, this can occur only with kqueue.) If
2847 : : * this is forgotten, the multiplexer can get stuck in a
2848 : : * signaled state and we'll burn CPU cycles pointlessly.
2849 : : */
2850 [ - + ]: 992 : if (!comb_multiplexer(actx))
270 jchampion@postgresql 2851 :UBC 0 : goto error_return;
2852 : :
270 jchampion@postgresql 2853 :CBC 992 : return status;
2854 : : }
2855 : :
439 dgustafsson@postgres 2856 : 339148 : case OAUTH_STEP_WAIT_INTERVAL:
2857 : : {
2858 : : bool expired;
2859 : :
2860 : : /*
2861 : : * The client application is supposed to wait until our
2862 : : * timer expires before calling PQconnectPoll() again, but
2863 : : * that might not happen. To avoid sending a token request
2864 : : * early, check the timer before continuing.
2865 : : */
270 jchampion@postgresql 2866 [ - + ]: 339148 : if (!drain_timer_events(actx, &expired))
270 jchampion@postgresql 2867 :UBC 0 : goto error_return;
2868 : :
270 jchampion@postgresql 2869 [ + + ]:CBC 339148 : if (!expired)
2870 : : {
53 jchampion@postgresql 2871 :GNC 339140 : *altsock = actx->timerfd;
270 jchampion@postgresql 2872 :CBC 339140 : return PGRES_POLLING_READING;
2873 : : }
2874 : :
2875 : 8 : break;
2876 : : }
2877 : : }
2878 : :
2879 : : /*
2880 : : * Each case here must ensure that actx->running is set while we're
2881 : : * waiting on some asynchronous work. Most cases rely on
2882 : : * start_request() to do that for them.
2883 : : */
439 dgustafsson@postgres 2884 [ + + + + : 227 : switch (actx->step)
+ - ]
2885 : : {
2886 : 55 : case OAUTH_STEP_INIT:
141 jchampion@postgresql 2887 : 55 : actx->errctx = libpq_gettext("failed to fetch OpenID discovery document");
53 jchampion@postgresql 2888 [ - + ]:GNC 55 : if (!start_discovery(actx, actx->discovery_uri))
439 dgustafsson@postgres 2889 :UBC 0 : goto error_return;
2890 : :
439 dgustafsson@postgres 2891 :CBC 55 : actx->step = OAUTH_STEP_DISCOVERY;
2892 : 55 : break;
2893 : :
2894 : 54 : case OAUTH_STEP_DISCOVERY:
2895 [ - + ]: 54 : if (!finish_discovery(actx))
439 dgustafsson@postgres 2896 :UBC 0 : goto error_return;
2897 : :
439 dgustafsson@postgres 2898 [ - + ]:CBC 54 : if (!check_issuer(actx, conn))
439 dgustafsson@postgres 2899 :UBC 0 : goto error_return;
2900 : :
141 jchampion@postgresql 2901 :CBC 54 : actx->errctx = libpq_gettext("cannot run OAuth device authorization");
439 dgustafsson@postgres 2902 [ - + ]: 54 : if (!check_for_device_flow(actx))
439 dgustafsson@postgres 2903 :UBC 0 : goto error_return;
2904 : :
141 jchampion@postgresql 2905 :CBC 54 : actx->errctx = libpq_gettext("failed to obtain device authorization");
439 dgustafsson@postgres 2906 [ - + ]: 54 : if (!start_device_authz(actx, conn))
439 dgustafsson@postgres 2907 :UBC 0 : goto error_return;
2908 : :
439 dgustafsson@postgres 2909 :CBC 54 : actx->step = OAUTH_STEP_DEVICE_AUTHORIZATION;
2910 : 54 : break;
2911 : :
2912 : 53 : case OAUTH_STEP_DEVICE_AUTHORIZATION:
2913 [ + + ]: 53 : if (!finish_device_authz(actx))
2914 : 3 : goto error_return;
2915 : :
141 jchampion@postgresql 2916 : 50 : actx->errctx = libpq_gettext("failed to obtain access token");
439 dgustafsson@postgres 2917 [ - + ]: 50 : if (!start_token_request(actx, conn))
439 dgustafsson@postgres 2918 :UBC 0 : goto error_return;
2919 : :
439 dgustafsson@postgres 2920 :CBC 50 : actx->step = OAUTH_STEP_TOKEN_REQUEST;
2921 : 50 : break;
2922 : :
2923 : 57 : case OAUTH_STEP_TOKEN_REQUEST:
369 jchampion@postgresql 2924 [ + + ]: 57 : if (!handle_token_response(actx, &oauth_token))
439 dgustafsson@postgres 2925 : 9 : goto error_return;
2926 : :
2927 : : /*
2928 : : * Hook any oauth_token into the request struct immediately so
2929 : : * that the allocation isn't lost in case of an error.
2930 : : */
53 jchampion@postgresql 2931 :GNC 48 : request->v1.token = oauth_token;
2932 : :
439 dgustafsson@postgres 2933 [ + + ]:CBC 48 : if (!actx->user_prompted)
2934 : : {
2935 : : /*
2936 : : * Now that we know the token endpoint isn't broken, give
2937 : : * the user the login instructions.
2938 : : */
2939 [ - + ]: 40 : if (!prompt_user(actx, conn))
439 dgustafsson@postgres 2940 :UBC 0 : goto error_return;
2941 : :
439 dgustafsson@postgres 2942 :CBC 40 : actx->user_prompted = true;
2943 : : }
2944 : :
369 jchampion@postgresql 2945 [ + + ]: 48 : if (oauth_token)
439 dgustafsson@postgres 2946 : 40 : break; /* done! */
2947 : :
2948 : : /*
2949 : : * Wait for the required interval before issuing the next
2950 : : * request.
2951 : : */
2952 [ - + ]: 8 : if (!set_timer(actx, actx->authz.interval * 1000))
439 dgustafsson@postgres 2953 :UBC 0 : goto error_return;
2954 : :
2955 : : /*
2956 : : * No Curl requests are running, so we can simplify by having
2957 : : * the client wait directly on the timerfd rather than the
2958 : : * multiplexer.
2959 : : */
53 jchampion@postgresql 2960 :GNC 8 : *altsock = actx->timerfd;
2961 : :
439 dgustafsson@postgres 2962 :CBC 8 : actx->step = OAUTH_STEP_WAIT_INTERVAL;
2963 : 8 : actx->running = 1;
2964 : 8 : break;
2965 : :
2966 : 8 : case OAUTH_STEP_WAIT_INTERVAL:
141 jchampion@postgresql 2967 : 8 : actx->errctx = libpq_gettext("failed to obtain access token");
439 dgustafsson@postgres 2968 [ - + ]: 8 : if (!start_token_request(actx, conn))
439 dgustafsson@postgres 2969 :UBC 0 : goto error_return;
2970 : :
439 dgustafsson@postgres 2971 :CBC 8 : actx->step = OAUTH_STEP_TOKEN_REQUEST;
2972 : 8 : break;
2973 : : }
2974 : :
2975 : : /*
2976 : : * The vast majority of the time, if we don't have a token at this
2977 : : * point, actx->running will be set. But there are some corner cases
2978 : : * where we can immediately loop back around; see start_request().
2979 : : */
369 jchampion@postgresql 2980 [ + + - + ]: 215 : } while (!oauth_token && !actx->running);
2981 : :
2982 : : /* If we've stored a token, we're done. Otherwise come back later. */
2983 [ + + ]: 215 : return oauth_token ? PGRES_POLLING_OK : PGRES_POLLING_READING;
2984 : :
439 dgustafsson@postgres 2985 : 15 : error_return:
53 jchampion@postgresql 2986 :GNC 15 : append_actx_error(request, actx);
2987 : :
439 dgustafsson@postgres 2988 :CBC 15 : return PGRES_POLLING_FAILED;
2989 : : }
2990 : :
2991 : : /*
2992 : : * The top-level entry point for the flow. This is a convenient place to put
2993 : : * necessary wrapper logic before handing off to the true implementation, above.
2994 : : */
2995 : : static PostgresPollingStatusType
53 jchampion@postgresql 2996 :GNC 340362 : pg_fe_run_oauth_flow(PGconn *conn, struct PGoauthBearerRequest *request,
2997 : : int *altsock)
2998 : : {
2999 : : PostgresPollingStatusType result;
3000 : 340362 : struct async_ctx *actx = request->user;
3001 : : #ifndef WIN32
3002 : : sigset_t osigset;
3003 : : bool sigpipe_pending;
3004 : : bool masked;
3005 : :
3006 : : /*---
3007 : : * Ignore SIGPIPE on this thread during all Curl processing.
3008 : : *
3009 : : * Because we support multiple threads, we have to set up libcurl with
3010 : : * CURLOPT_NOSIGNAL, which disables its default global handling of
3011 : : * SIGPIPE. From the Curl docs:
3012 : : *
3013 : : * libcurl makes an effort to never cause such SIGPIPE signals to
3014 : : * trigger, but some operating systems have no way to avoid them and
3015 : : * even on those that have there are some corner cases when they may
3016 : : * still happen, contrary to our desire.
3017 : : *
3018 : : * Note that libcurl is also at the mercy of its DNS resolution and SSL
3019 : : * libraries; if any of them forget a MSG_NOSIGNAL then we're in trouble.
3020 : : * Modern platforms and libraries seem to get it right, so this is a
3021 : : * difficult corner case to exercise in practice, and unfortunately it's
3022 : : * not really clear whether it's necessary in all cases.
3023 : : */
439 dgustafsson@postgres 3024 :CBC 340362 : masked = (pq_block_sigpipe(&osigset, &sigpipe_pending) == 0);
3025 : : #endif
3026 : :
53 jchampion@postgresql 3027 :GNC 340362 : result = pg_fe_run_oauth_flow_impl(conn,
3028 : : (PGoauthBearerRequestV2 *) request,
3029 : : altsock);
3030 : :
3031 : : /*
3032 : : * To assist with finding bugs in comb_multiplexer() and
3033 : : * drain_timer_events(), when we're in debug mode, track the total number
3034 : : * of calls to this function and print that at the end of the flow.
3035 : : */
28 3036 [ + + ]: 340362 : if (actx->debug_flags & OAUTHDEBUG_CALL_COUNT)
3037 : : {
270 jchampion@postgresql 3038 :CBC 340344 : actx->dbg_num_calls++;
3039 [ + + + + ]: 340344 : if (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED)
3040 : 53 : fprintf(stderr, "[libpq] total number of polls: %d\n",
3041 : : actx->dbg_num_calls);
3042 : : }
3043 : :
3044 : : #ifndef WIN32
439 dgustafsson@postgres 3045 [ + - ]: 340362 : if (masked)
3046 : : {
3047 : : /*
3048 : : * Undo the SIGPIPE mask. Assume we may have gotten EPIPE (we have no
3049 : : * way of knowing at this level).
3050 : : */
3051 : 340362 : pq_reset_sigpipe(&osigset, sigpipe_pending, true /* EPIPE, maybe */ );
3052 : : }
3053 : : #endif
3054 : :
3055 : 340362 : return result;
3056 : : }
3057 : :
3058 : : /*
3059 : : * Callback registration for OAUTHBEARER. libpq calls this once per OAuth
3060 : : * connection.
3061 : : */
3062 : : int
53 jchampion@postgresql 3063 :GNC 57 : pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request)
3064 : : {
3065 : : struct async_ctx *actx;
3066 : 57 : PQconninfoOption *conninfo = NULL;
3067 : :
3068 [ - + ]: 57 : if (!initialize_curl(request))
53 jchampion@postgresql 3069 :UNC 0 : return -1;
3070 : :
3071 : : /*
3072 : : * Create our asynchronous state, and hook it into the upper-level OAuth
3073 : : * state immediately, so any failures below won't leak the context
3074 : : * allocation.
3075 : : */
53 jchampion@postgresql 3076 :GNC 57 : actx = calloc(1, sizeof(*actx));
3077 [ - + ]: 57 : if (!actx)
53 jchampion@postgresql 3078 :UNC 0 : goto oom;
3079 : :
53 jchampion@postgresql 3080 :GNC 57 : actx->mux = PGINVALID_SOCKET;
3081 : 57 : actx->timerfd = -1;
3082 : :
3083 : : /*
3084 : : * Now we have a valid (but still useless) actx, so we can fill in the
3085 : : * request object. From this point onward, failures will result in a call
3086 : : * to pg_fe_cleanup_oauth_flow(). Further cleanup logic belongs there.
3087 : : */
3088 : 57 : request->v1.async = pg_fe_run_oauth_flow;
3089 : 57 : request->v1.cleanup = pg_fe_cleanup_oauth_flow;
3090 : 57 : request->v1.user = actx;
3091 : :
3092 : : /*
3093 : : * Now finish filling in the actx.
3094 : : */
3095 : :
3096 : : /* Parse debug flags from the environment. */
28 3097 : 57 : actx->debug_flags = oauth_parse_debug_flags();
3098 : :
53 3099 : 57 : initPQExpBuffer(&actx->work_data);
3100 : 57 : initPQExpBuffer(&actx->errbuf);
3101 : :
3102 : : /* Pull relevant connection options. */
3103 : 57 : conninfo = PQconninfo(conn);
3104 [ - + ]: 57 : if (!conninfo)
53 jchampion@postgresql 3105 :UNC 0 : goto oom;
3106 : :
53 jchampion@postgresql 3107 [ + + ]:GNC 3021 : for (PQconninfoOption *opt = conninfo; opt->keyword; opt++)
3108 : : {
3109 [ + + ]: 2964 : if (!opt->val)
3110 : 1588 : continue; /* simplifies the strdup logic below */
3111 : :
3112 [ + + ]: 1376 : if (strcmp(opt->keyword, "oauth_client_id") == 0)
3113 : : {
3114 : 57 : actx->client_id = strdup(opt->val);
3115 [ - + ]: 57 : if (!actx->client_id)
53 jchampion@postgresql 3116 :UNC 0 : goto oom;
3117 : : }
53 jchampion@postgresql 3118 [ + + ]:GNC 1319 : else if (strcmp(opt->keyword, "oauth_client_secret") == 0)
3119 : : {
3120 : 6 : actx->client_secret = strdup(opt->val);
3121 [ - + ]: 6 : if (!actx->client_secret)
53 jchampion@postgresql 3122 :UNC 0 : goto oom;
3123 : : }
36 jchampion@postgresql 3124 [ + + ]:GNC 1313 : else if (strcmp(opt->keyword, "oauth_ca_file") == 0)
3125 : : {
3126 : 56 : actx->ca_file = strdup(opt->val);
3127 [ - + ]: 56 : if (!actx->ca_file)
36 jchampion@postgresql 3128 :UNC 0 : goto oom;
3129 : : }
3130 : : }
3131 : :
53 jchampion@postgresql 3132 :GNC 57 : PQconninfoFree(conninfo);
3133 : 57 : conninfo = NULL; /* keeps `goto oom` safe */
3134 : :
3135 : 57 : actx->discovery_uri = request->v1.openid_configuration;
3136 : 57 : actx->issuer_id = request->issuer;
3137 : 57 : actx->scope = request->v1.scope;
3138 : :
3139 [ - + ]: 57 : Assert(actx->client_id); /* ensured by setup_oauth_parameters() */
3140 [ - + ]: 57 : Assert(actx->issuer_id); /* ensured by setup_oauth_parameters() */
3141 [ - + ]: 57 : Assert(actx->discovery_uri); /* ensured by oauth_exchange() */
3142 : :
3143 [ - + ]: 57 : if (!setup_multiplexer(actx))
3144 : : {
53 jchampion@postgresql 3145 :UNC 0 : append_actx_error(request, actx);
3146 : 0 : return -1;
3147 : : }
3148 : :
53 jchampion@postgresql 3149 [ - + ]:GNC 57 : if (!setup_curl_handles(actx))
3150 : : {
53 jchampion@postgresql 3151 :UNC 0 : append_actx_error(request, actx);
3152 : 0 : return -1;
3153 : : }
3154 : :
53 jchampion@postgresql 3155 :GNC 57 : return 0;
3156 : :
53 jchampion@postgresql 3157 :UNC 0 : oom:
3158 [ # # ]: 0 : if (conninfo)
3159 : 0 : PQconninfoFree(conninfo);
3160 : :
3161 : 0 : request->error = libpq_gettext("out of memory");
3162 : 0 : return -1;
3163 : : }
|