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-2025, 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
33 0 : usage(char *argv[])
34 : {
35 0 : printf("usage: %s [flags] CONNINFO\n\n", argv[0]);
36 :
37 0 : printf("recognized flags:\n");
38 0 : printf(" -h, --help show this message\n");
39 0 : printf(" --expected-scope SCOPE fail if received scopes do not match SCOPE\n");
40 0 : printf(" --expected-uri URI fail if received configuration link does not match URI\n");
41 0 : printf(" --misbehave=MODE have the hook fail required postconditions\n"
42 : " (MODEs: no-hook, fail-async, no-token, no-socket)\n");
43 0 : printf(" --no-hook don't install OAuth hooks\n");
44 0 : printf(" --hang-forever don't ever return a token (combine with connect_timeout)\n");
45 0 : printf(" --token TOKEN use the provided TOKEN value\n");
46 0 : printf(" --stress-async busy-loop on PQconnectPoll rather than polling\n");
47 0 : }
48 :
49 : /* --options */
50 : static bool no_hook = false;
51 : static bool hang_forever = false;
52 : static bool stress_async = false;
53 : static const char *expected_uri = NULL;
54 : static const char *expected_scope = NULL;
55 : static const char *misbehave_mode = NULL;
56 : static char *token = NULL;
57 :
58 : int
59 0 : main(int argc, char *argv[])
60 : {
61 : static const struct option long_options[] = {
62 : {"help", no_argument, NULL, 'h'},
63 :
64 : {"expected-scope", required_argument, NULL, 1000},
65 : {"expected-uri", required_argument, NULL, 1001},
66 : {"no-hook", no_argument, NULL, 1002},
67 : {"token", required_argument, NULL, 1003},
68 : {"hang-forever", no_argument, NULL, 1004},
69 : {"misbehave", required_argument, NULL, 1005},
70 : {"stress-async", no_argument, NULL, 1006},
71 : {0}
72 : };
73 :
74 : const char *conninfo;
75 : PGconn *conn;
76 : int c;
77 :
78 0 : while ((c = getopt_long(argc, argv, "h", long_options, NULL)) != -1)
79 : {
80 0 : switch (c)
81 : {
82 0 : case 'h':
83 0 : usage(argv);
84 0 : return 0;
85 :
86 0 : case 1000: /* --expected-scope */
87 0 : expected_scope = optarg;
88 0 : break;
89 :
90 0 : case 1001: /* --expected-uri */
91 0 : expected_uri = optarg;
92 0 : break;
93 :
94 0 : case 1002: /* --no-hook */
95 0 : no_hook = true;
96 0 : break;
97 :
98 0 : case 1003: /* --token */
99 0 : token = optarg;
100 0 : break;
101 :
102 0 : case 1004: /* --hang-forever */
103 0 : hang_forever = true;
104 0 : break;
105 :
106 0 : case 1005: /* --misbehave */
107 0 : misbehave_mode = optarg;
108 0 : break;
109 :
110 0 : case 1006: /* --stress-async */
111 0 : stress_async = true;
112 0 : break;
113 :
114 0 : default:
115 0 : usage(argv);
116 0 : return 1;
117 : }
118 : }
119 :
120 0 : if (argc != optind + 1)
121 : {
122 0 : usage(argv);
123 0 : return 1;
124 : }
125 :
126 0 : conninfo = argv[optind];
127 :
128 : /* Set up our OAuth hooks. */
129 0 : PQsetAuthDataHook(handle_auth_data);
130 :
131 : /* Connect. (All the actual work is in the hook.) */
132 0 : if (stress_async)
133 : {
134 : /*
135 : * Perform an asynchronous connection, busy-looping on PQconnectPoll()
136 : * without actually waiting on socket events. This stresses code paths
137 : * that rely on asynchronous work to be done before continuing with
138 : * the next step in the flow.
139 : */
140 : PostgresPollingStatusType res;
141 :
142 0 : conn = PQconnectStart(conninfo);
143 :
144 : do
145 : {
146 0 : res = PQconnectPoll(conn);
147 0 : } while (res != PGRES_POLLING_FAILED && res != PGRES_POLLING_OK);
148 : }
149 : else
150 : {
151 : /* Perform a standard synchronous connection. */
152 0 : conn = PQconnectdb(conninfo);
153 : }
154 :
155 0 : if (PQstatus(conn) != CONNECTION_OK)
156 : {
157 0 : fprintf(stderr, "connection to database failed: %s\n",
158 : PQerrorMessage(conn));
159 0 : PQfinish(conn);
160 0 : return 1;
161 : }
162 :
163 0 : printf("connection succeeded\n");
164 0 : PQfinish(conn);
165 0 : return 0;
166 : }
167 :
168 : /*
169 : * PQauthDataHook implementation. Replaces the default client flow by handling
170 : * PQAUTHDATA_OAUTH_BEARER_TOKEN.
171 : */
172 : static int
173 0 : handle_auth_data(PGauthData type, PGconn *conn, void *data)
174 : {
175 0 : PGoauthBearerRequest *req = data;
176 :
177 0 : if (no_hook || (type != PQAUTHDATA_OAUTH_BEARER_TOKEN))
178 0 : return 0;
179 :
180 0 : if (hang_forever)
181 : {
182 : /* Start asynchronous processing. */
183 0 : req->async = async_cb;
184 0 : return 1;
185 : }
186 :
187 0 : if (misbehave_mode)
188 : {
189 0 : if (strcmp(misbehave_mode, "no-hook") != 0)
190 0 : req->async = misbehave_cb;
191 0 : return 1;
192 : }
193 :
194 0 : if (expected_uri)
195 : {
196 0 : if (!req->openid_configuration)
197 : {
198 0 : fprintf(stderr, "expected URI \"%s\", got NULL\n", expected_uri);
199 0 : return -1;
200 : }
201 :
202 0 : if (strcmp(expected_uri, req->openid_configuration) != 0)
203 : {
204 0 : fprintf(stderr, "expected URI \"%s\", got \"%s\"\n", expected_uri, req->openid_configuration);
205 0 : return -1;
206 : }
207 : }
208 :
209 0 : if (expected_scope)
210 : {
211 0 : if (!req->scope)
212 : {
213 0 : fprintf(stderr, "expected scope \"%s\", got NULL\n", expected_scope);
214 0 : return -1;
215 : }
216 :
217 0 : if (strcmp(expected_scope, req->scope) != 0)
218 : {
219 0 : fprintf(stderr, "expected scope \"%s\", got \"%s\"\n", expected_scope, req->scope);
220 0 : return -1;
221 : }
222 : }
223 :
224 0 : req->token = token;
225 0 : return 1;
226 : }
227 :
228 : static PostgresPollingStatusType
229 0 : async_cb(PGconn *conn, PGoauthBearerRequest *req, pgsocket *altsock)
230 : {
231 0 : if (hang_forever)
232 : {
233 : /*
234 : * This code tests that nothing is interfering with libpq's handling
235 : * of connect_timeout.
236 : */
237 : static pgsocket sock = PGINVALID_SOCKET;
238 :
239 0 : if (sock == PGINVALID_SOCKET)
240 : {
241 : /* First call. Create an unbound socket to wait on. */
242 : #ifdef WIN32
243 : WSADATA wsaData;
244 : int err;
245 :
246 : err = WSAStartup(MAKEWORD(2, 2), &wsaData);
247 : if (err)
248 : {
249 : perror("WSAStartup failed");
250 : return PGRES_POLLING_FAILED;
251 : }
252 : #endif
253 0 : sock = socket(AF_INET, SOCK_DGRAM, 0);
254 0 : if (sock == PGINVALID_SOCKET)
255 : {
256 0 : perror("failed to create datagram socket");
257 0 : return PGRES_POLLING_FAILED;
258 : }
259 : }
260 :
261 : /* Make libpq wait on the (unreadable) socket. */
262 0 : *altsock = sock;
263 0 : return PGRES_POLLING_READING;
264 : }
265 :
266 0 : req->token = token;
267 0 : return PGRES_POLLING_OK;
268 : }
269 :
270 : static PostgresPollingStatusType
271 0 : misbehave_cb(PGconn *conn, PGoauthBearerRequest *req, pgsocket *altsock)
272 : {
273 0 : if (strcmp(misbehave_mode, "fail-async") == 0)
274 : {
275 : /* Just fail "normally". */
276 0 : return PGRES_POLLING_FAILED;
277 : }
278 0 : else if (strcmp(misbehave_mode, "no-token") == 0)
279 : {
280 : /* Callbacks must assign req->token before returning OK. */
281 0 : return PGRES_POLLING_OK;
282 : }
283 0 : else if (strcmp(misbehave_mode, "no-socket") == 0)
284 : {
285 : /* Callbacks must assign *altsock before asking for polling. */
286 0 : return PGRES_POLLING_READING;
287 : }
288 : else
289 : {
290 0 : fprintf(stderr, "unrecognized --misbehave mode: %s\n", misbehave_mode);
291 0 : exit(1);
292 : }
293 : }
|