Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * oauth_hook_client.c
4 : : * Test driver for t/002_client.pl, which verifies OAuth hook
5 : : * functionality in libpq.
6 : : *
7 : : * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
8 : : * Portions Copyright (c) 1994, Regents of the University of California
9 : : *
10 : : *
11 : : * IDENTIFICATION
12 : : * src/test/modules/oauth_validator/oauth_hook_client.c
13 : : *
14 : : *-------------------------------------------------------------------------
15 : : */
16 : :
17 : : #include "postgres_fe.h"
18 : :
19 : : #include <sys/socket.h>
20 : :
21 : : #include "getopt_long.h"
22 : : #include "libpq-fe.h"
23 : :
24 : : static int handle_auth_data(PGauthData type, PGconn *conn, void *data);
25 : : static PostgresPollingStatusType async_cb(PGconn *conn,
26 : : PGoauthBearerRequest *req,
27 : : pgsocket *altsock);
28 : : static PostgresPollingStatusType misbehave_cb(PGconn *conn,
29 : : PGoauthBearerRequest *req,
30 : : pgsocket *altsock);
31 : :
32 : : static void
388 dgustafsson@postgres 33 :UBC 0 : usage(char *argv[])
34 : : {
35 : 0 : printf("usage: %s [flags] CONNINFO\n\n", argv[0]);
36 : :
37 : 0 : printf("recognized flags:\n");
315 peter@eisentraut.org 38 : 0 : printf(" -h, --help show this message\n");
9 jchampion@postgresql 39 :UNC 0 : printf(" -v VERSION select the hook API version (default 2)\n");
315 peter@eisentraut.org 40 :UBC 0 : printf(" --expected-scope SCOPE fail if received scopes do not match SCOPE\n");
41 : 0 : printf(" --expected-uri URI fail if received configuration link does not match URI\n");
9 jchampion@postgresql 42 :UNC 0 : printf(" --expected-issuer ISS fail if received issuer does not match ISS (v2 only)\n");
315 peter@eisentraut.org 43 :UBC 0 : printf(" --misbehave=MODE have the hook fail required postconditions\n"
44 : : " (MODEs: no-hook, fail-async, no-token, no-socket)\n");
45 : 0 : printf(" --no-hook don't install OAuth hooks\n");
46 : 0 : printf(" --hang-forever don't ever return a token (combine with connect_timeout)\n");
47 : 0 : printf(" --token TOKEN use the provided TOKEN value\n");
9 jchampion@postgresql 48 :UNC 0 : printf(" --error ERRMSG fail instead, with the given ERRMSG (v2 only)\n");
315 peter@eisentraut.org 49 :UBC 0 : printf(" --stress-async busy-loop on PQconnectPoll rather than polling\n");
388 dgustafsson@postgres 50 : 0 : }
51 : :
52 : : /* --options */
53 : : static bool no_hook = false;
54 : : static bool hang_forever = false;
55 : : static bool stress_async = false;
56 : : static const char *expected_uri = NULL;
57 : : static const char *expected_issuer = NULL;
58 : : static const char *expected_scope = NULL;
59 : : static const char *misbehave_mode = NULL;
60 : : static char *token = NULL;
61 : : static char *errmsg = NULL;
62 : : static int hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN_V2;
63 : :
64 : : int
388 dgustafsson@postgres 65 :CBC 15 : main(int argc, char *argv[])
66 : : {
67 : : static const struct option long_options[] = {
68 : : {"help", no_argument, NULL, 'h'},
69 : :
70 : : {"expected-scope", required_argument, NULL, 1000},
71 : : {"expected-uri", required_argument, NULL, 1001},
72 : : {"no-hook", no_argument, NULL, 1002},
73 : : {"token", required_argument, NULL, 1003},
74 : : {"hang-forever", no_argument, NULL, 1004},
75 : : {"misbehave", required_argument, NULL, 1005},
76 : : {"stress-async", no_argument, NULL, 1006},
77 : : {"expected-issuer", required_argument, NULL, 1007},
78 : : {"error", required_argument, NULL, 1008},
79 : : {0}
80 : : };
81 : :
82 : : const char *conninfo;
83 : : PGconn *conn;
84 : : int c;
85 : :
9 jchampion@postgresql 86 [ + + ]:GNC 45 : while ((c = getopt_long(argc, argv, "hv:", long_options, NULL)) != -1)
87 : : {
388 dgustafsson@postgres 88 [ - + + + :CBC 30 : switch (c)
+ + + + +
+ + - ]
89 : : {
388 dgustafsson@postgres 90 :UBC 0 : case 'h':
91 : 0 : usage(argv);
92 : 0 : return 0;
93 : :
9 jchampion@postgresql 94 :GNC 5 : case 'v':
95 [ + - ]: 5 : if (strcmp(optarg, "1") == 0)
96 : 5 : hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN;
9 jchampion@postgresql 97 [ # # ]:UNC 0 : else if (strcmp(optarg, "2") == 0)
98 : 0 : hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN_V2;
99 : : else
100 : : {
101 : 0 : usage(argv);
102 : 0 : return 1;
103 : : }
9 jchampion@postgresql 104 :GNC 5 : break;
105 : :
388 dgustafsson@postgres 106 :CBC 3 : case 1000: /* --expected-scope */
107 : 3 : expected_scope = optarg;
108 : 3 : break;
109 : :
110 : 3 : case 1001: /* --expected-uri */
111 : 3 : expected_uri = optarg;
112 : 3 : break;
113 : :
114 : 1 : case 1002: /* --no-hook */
115 : 1 : no_hook = true;
116 : 1 : break;
117 : :
118 : 3 : case 1003: /* --token */
119 : 3 : token = optarg;
120 : 3 : break;
121 : :
122 : 1 : case 1004: /* --hang-forever */
123 : 1 : hang_forever = true;
124 : 1 : break;
125 : :
126 : 9 : case 1005: /* --misbehave */
127 : 9 : misbehave_mode = optarg;
128 : 9 : break;
129 : :
130 : 1 : case 1006: /* --stress-async */
131 : 1 : stress_async = true;
132 : 1 : break;
133 : :
9 jchampion@postgresql 134 :GNC 2 : case 1007: /* --expected-issuer */
135 : 2 : expected_issuer = optarg;
136 : 2 : break;
137 : :
138 : 2 : case 1008: /* --error */
139 : 2 : errmsg = optarg;
140 : 2 : break;
141 : :
388 dgustafsson@postgres 142 :UBC 0 : default:
143 : 0 : usage(argv);
144 : 0 : return 1;
145 : : }
146 : : }
147 : :
388 dgustafsson@postgres 148 [ - + ]:CBC 15 : if (argc != optind + 1)
149 : : {
388 dgustafsson@postgres 150 :UBC 0 : usage(argv);
151 : 0 : return 1;
152 : : }
153 : :
388 dgustafsson@postgres 154 :CBC 15 : conninfo = argv[optind];
155 : :
156 : : /* Set up our OAuth hooks. */
157 : 15 : PQsetAuthDataHook(handle_auth_data);
158 : :
159 : : /* Connect. (All the actual work is in the hook.) */
160 [ + + ]: 15 : if (stress_async)
161 : : {
162 : : /*
163 : : * Perform an asynchronous connection, busy-looping on PQconnectPoll()
164 : : * without actually waiting on socket events. This stresses code paths
165 : : * that rely on asynchronous work to be done before continuing with
166 : : * the next step in the flow.
167 : : */
168 : : PostgresPollingStatusType res;
169 : :
170 : 1 : conn = PQconnectStart(conninfo);
171 : :
172 : : do
173 : : {
174 : 259296 : res = PQconnectPoll(conn);
175 [ + - + + ]: 259296 : } while (res != PGRES_POLLING_FAILED && res != PGRES_POLLING_OK);
176 : : }
177 : : else
178 : : {
179 : : /* Perform a standard synchronous connection. */
180 : 14 : conn = PQconnectdb(conninfo);
181 : : }
182 : :
183 [ + + ]: 15 : if (PQstatus(conn) != CONNECTION_OK)
184 : : {
185 : 11 : fprintf(stderr, "connection to database failed: %s\n",
186 : : PQerrorMessage(conn));
187 : 11 : PQfinish(conn);
188 : 11 : return 1;
189 : : }
190 : :
191 : 4 : printf("connection succeeded\n");
192 : 4 : PQfinish(conn);
193 : 4 : return 0;
194 : : }
195 : :
196 : : /*
197 : : * PQauthDataHook implementation. Replaces the default client flow by handling
198 : : * PQAUTHDATA_OAUTH_BEARER_TOKEN[_V2].
199 : : */
200 : : static int
201 : 22 : handle_auth_data(PGauthData type, PGconn *conn, void *data)
202 : : {
203 : : PGoauthBearerRequest *req;
9 jchampion@postgresql 204 :GNC 22 : PGoauthBearerRequestV2 *req2 = NULL;
205 : :
206 [ + + - + ]: 22 : Assert(hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN ||
207 : : hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN_V2);
208 : :
209 [ + + + + ]: 22 : if (no_hook || type != hook_version)
388 dgustafsson@postgres 210 :CBC 8 : return 0;
211 : :
9 jchampion@postgresql 212 :GNC 14 : req = data;
213 [ + + ]: 14 : if (type == PQAUTHDATA_OAUTH_BEARER_TOKEN_V2)
214 : 9 : req2 = data;
215 : :
388 dgustafsson@postgres 216 [ + + ]:CBC 14 : if (hang_forever)
217 : : {
218 : : /* Start asynchronous processing. */
219 : 1 : req->async = async_cb;
220 : 1 : return 1;
221 : : }
222 : :
223 [ + + ]: 13 : if (misbehave_mode)
224 : : {
225 [ + + ]: 9 : if (strcmp(misbehave_mode, "no-hook") != 0)
226 : 7 : req->async = misbehave_cb;
227 : 9 : return 1;
228 : : }
229 : :
230 [ + + ]: 4 : if (expected_uri)
231 : : {
232 [ - + ]: 3 : if (!req->openid_configuration)
233 : : {
388 dgustafsson@postgres 234 :UBC 0 : fprintf(stderr, "expected URI \"%s\", got NULL\n", expected_uri);
235 : 0 : return -1;
236 : : }
237 : :
388 dgustafsson@postgres 238 [ - + ]:CBC 3 : if (strcmp(expected_uri, req->openid_configuration) != 0)
239 : : {
388 dgustafsson@postgres 240 :UBC 0 : fprintf(stderr, "expected URI \"%s\", got \"%s\"\n", expected_uri, req->openid_configuration);
241 : 0 : return -1;
242 : : }
243 : : }
244 : :
388 dgustafsson@postgres 245 [ + + ]:CBC 4 : if (expected_scope)
246 : : {
247 [ - + ]: 3 : if (!req->scope)
248 : : {
388 dgustafsson@postgres 249 :UBC 0 : fprintf(stderr, "expected scope \"%s\", got NULL\n", expected_scope);
250 : 0 : return -1;
251 : : }
252 : :
388 dgustafsson@postgres 253 [ - + ]:CBC 3 : if (strcmp(expected_scope, req->scope) != 0)
254 : : {
388 dgustafsson@postgres 255 :UBC 0 : fprintf(stderr, "expected scope \"%s\", got \"%s\"\n", expected_scope, req->scope);
256 : 0 : return -1;
257 : : }
258 : : }
259 : :
9 jchampion@postgresql 260 [ + + ]:GNC 4 : if (expected_issuer)
261 : : {
262 [ - + ]: 2 : if (!req2)
263 : : {
9 jchampion@postgresql 264 :UNC 0 : fprintf(stderr, "--expected-issuer cannot be combined with -v1\n");
265 : 0 : return -1;
266 : : }
267 : :
9 jchampion@postgresql 268 [ - + ]:GNC 2 : if (!req2->issuer)
269 : : {
9 jchampion@postgresql 270 :UNC 0 : fprintf(stderr, "expected issuer \"%s\", got NULL\n", expected_issuer);
271 : 0 : return -1;
272 : : }
273 : :
9 jchampion@postgresql 274 [ - + ]:GNC 2 : if (strcmp(expected_issuer, req2->issuer) != 0)
275 : : {
9 jchampion@postgresql 276 :UNC 0 : fprintf(stderr, "expected issuer \"%s\", got \"%s\"\n", expected_issuer, req2->issuer);
277 : 0 : return -1;
278 : : }
279 : : }
280 : :
9 jchampion@postgresql 281 [ + + ]:GNC 4 : if (errmsg)
282 : : {
283 [ - + ]: 1 : if (token)
284 : : {
9 jchampion@postgresql 285 :UNC 0 : fprintf(stderr, "--error cannot be combined with --token\n");
286 : 0 : return -1;
287 : : }
9 jchampion@postgresql 288 [ - + ]:GNC 1 : else if (!req2)
289 : : {
9 jchampion@postgresql 290 :UNC 0 : fprintf(stderr, "--error cannot be combined with -v1\n");
291 : 0 : return -1;
292 : : }
293 : :
9 jchampion@postgresql 294 :GNC 1 : req2->error = errmsg;
295 : 1 : return -1;
296 : : }
297 : :
388 dgustafsson@postgres 298 :CBC 3 : req->token = token;
299 : 3 : return 1;
300 : : }
301 : :
302 : : static PostgresPollingStatusType
303 : 1 : async_cb(PGconn *conn, PGoauthBearerRequest *req, pgsocket *altsock)
304 : : {
305 [ + - ]: 1 : if (hang_forever)
306 : : {
307 : : /*
308 : : * This code tests that nothing is interfering with libpq's handling
309 : : * of connect_timeout.
310 : : */
311 : : static pgsocket sock = PGINVALID_SOCKET;
312 : :
313 [ + - ]: 1 : if (sock == PGINVALID_SOCKET)
314 : : {
315 : : /* First call. Create an unbound socket to wait on. */
316 : : #ifdef WIN32
317 : : WSADATA wsaData;
318 : : int err;
319 : :
320 : : err = WSAStartup(MAKEWORD(2, 2), &wsaData);
321 : : if (err)
322 : : {
323 : : perror("WSAStartup failed");
324 : : return PGRES_POLLING_FAILED;
325 : : }
326 : : #endif
327 : 1 : sock = socket(AF_INET, SOCK_DGRAM, 0);
328 [ - + ]: 1 : if (sock == PGINVALID_SOCKET)
329 : : {
388 dgustafsson@postgres 330 :UBC 0 : perror("failed to create datagram socket");
331 : 0 : return PGRES_POLLING_FAILED;
332 : : }
333 : : }
334 : :
335 : : /* Make libpq wait on the (unreadable) socket. */
388 dgustafsson@postgres 336 :CBC 1 : *altsock = sock;
337 : 1 : return PGRES_POLLING_READING;
338 : : }
339 : :
388 dgustafsson@postgres 340 :UBC 0 : req->token = token;
341 : 0 : return PGRES_POLLING_OK;
342 : : }
343 : :
344 : : static PostgresPollingStatusType
388 dgustafsson@postgres 345 :CBC 7 : misbehave_cb(PGconn *conn, PGoauthBearerRequest *req, pgsocket *altsock)
346 : : {
347 [ + + ]: 7 : if (strcmp(misbehave_mode, "fail-async") == 0)
348 : : {
349 : : /* Just fail "normally". */
9 jchampion@postgresql 350 [ + + ]:GNC 3 : if (errmsg)
351 : : {
352 : : PGoauthBearerRequestV2 *req2;
353 : :
354 [ - + ]: 1 : if (hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN)
355 : : {
9 jchampion@postgresql 356 :UNC 0 : fprintf(stderr, "--error cannot be combined with -v1\n");
357 : 0 : exit(1);
358 : : }
359 : :
9 jchampion@postgresql 360 :GNC 1 : req2 = (PGoauthBearerRequestV2 *) req;
361 : 1 : req2->error = errmsg;
362 : : }
363 : :
388 dgustafsson@postgres 364 :CBC 3 : return PGRES_POLLING_FAILED;
365 : : }
366 [ + + ]: 4 : else if (strcmp(misbehave_mode, "no-token") == 0)
367 : : {
368 : : /* Callbacks must assign req->token before returning OK. */
369 : 2 : return PGRES_POLLING_OK;
370 : : }
371 [ + - ]: 2 : else if (strcmp(misbehave_mode, "no-socket") == 0)
372 : : {
373 : : /* Callbacks must assign *altsock before asking for polling. */
374 : 2 : return PGRES_POLLING_READING;
375 : : }
376 : : else
377 : : {
388 dgustafsson@postgres 378 :UBC 0 : fprintf(stderr, "unrecognized --misbehave mode: %s\n", misbehave_mode);
379 : 0 : exit(1);
380 : : }
381 : : }
|