Line data Source code
1 : /*
2 : * SPDX-License-Identifier: MPL-2.0
3 : *
4 : * This Source Code Form is subject to the terms of the Mozilla Public
5 : * License, v. 2.0. If a copy of the MPL was not distributed with this
6 : * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 : *
8 : * Copyright 2024, 2025 MonetDB Foundation;
9 : * Copyright August 2008 - 2023 MonetDB B.V.;
10 : * Copyright 1997 - July 2008 CWI.
11 : */
12 :
13 : #include "monetdb_config.h"
14 :
15 : #include "msettings.h"
16 : #include "msettings_internal.h"
17 :
18 : #include <assert.h>
19 : #include <ctype.h>
20 : #include <stdarg.h>
21 : #include <stdio.h>
22 : #include <stdlib.h>
23 : #include <string.h>
24 :
25 : // Scanner state.
26 : // Most scanner-related functions return 'false' on failure, 'true' on success.
27 : // Some return a character pointer, NULL on failure, non-NULL on success.
28 : typedef struct scanner {
29 : char *buffer; // owned buffer with the scanned text in it
30 : char c; // character we're currently looking at
31 : char *p; // pointer to where we found c (may have been updated since)
32 : msettings *mp; // this is where we leave the error messages
33 : } scanner;
34 :
35 :
36 :
37 :
38 : static bool
39 2561 : initialize(scanner *sc, msettings *mp, const char *url)
40 : {
41 2561 : sc->buffer = strdup(url);
42 2561 : if (!sc->buffer)
43 : return false;
44 2561 : sc->p = &sc->buffer[0];
45 2561 : sc->c = *sc->p;
46 2561 : sc->mp = mp;
47 2561 : return true;
48 : }
49 :
50 : static void
51 2560 : deinitialize(scanner *sc)
52 : {
53 2560 : free(sc->buffer);
54 : }
55 :
56 : static bool
57 119777 : has_failed(const scanner *sc)
58 : {
59 119777 : return sc->mp->error_message[0] != '\0';
60 : }
61 :
62 : static char
63 107578 : advance(scanner *sc)
64 : {
65 107578 : assert(!has_failed(sc));
66 107578 : sc->p++;
67 107578 : sc->c = *sc->p;
68 107578 : return sc->c;
69 : }
70 :
71 : static bool complain(scanner *sc, const char *fmt, ...)
72 : __attribute__((__format__(printf, 2, 3)));
73 :
74 : static bool
75 64 : complain(scanner *sc, const char *fmt, ...)
76 : {
77 : // do not overwrite existing error message,
78 : // the first one is usually the most informative.
79 64 : if (!has_failed(sc)) {
80 34 : va_list ap;
81 34 : va_start(ap, fmt);
82 34 : vsnprintf(sc->mp->error_message, sizeof(sc->mp->error_message), fmt, ap);
83 34 : va_end(ap);
84 34 : if (!has_failed(sc)) {
85 : // error message was empty, need non-empty so we know an error has occurred
86 0 : strcpy(sc->mp->error_message, "?");
87 : }
88 : }
89 :
90 64 : return false;
91 : }
92 :
93 : static bool
94 0 : unexpected(scanner *sc)
95 : {
96 0 : if (sc->c == 0) {
97 0 : return complain(sc, "URL ended unexpectedly");
98 : } else {
99 0 : size_t pos = sc->p - sc->buffer;
100 0 : return complain(sc, "unexpected character '%c' at position %zu", sc->c, pos);
101 : }
102 : }
103 :
104 : static bool
105 3170 : consume(scanner *sc, const char *text)
106 : {
107 10883 : for (const char *c = text; *c; c++) {
108 7728 : if (sc->c == *c) {
109 7712 : advance(sc);
110 7713 : continue;
111 : }
112 16 : size_t pos = sc->p - sc->buffer;
113 16 : if (sc->c == '\0') {
114 16 : return complain(sc, "unexpected end at position %zu, expected '%s'", pos, c);
115 : }
116 0 : return complain(sc, "unexpected character '%c' at position %zu, expected '%s'", sc->c, pos, c);
117 : }
118 : return true;
119 : }
120 :
121 :
122 : static int
123 504 : percent_decode_digit(char c)
124 : {
125 504 : if (c >= '0' && c <= '9')
126 272 : return c - '0';
127 232 : if (c >= 'A' && c <= 'F')
128 4 : return c - 'A' + 10;
129 228 : if (c >= 'a' && c <= 'f')
130 224 : return c - 'a' + 10;
131 : // return something so negative that it will still
132 : // be negative after we combine it with another digit
133 : return -1000;
134 : }
135 :
136 : static bool
137 7067 : percent_decode(scanner *sc, const char *context, char *string)
138 : {
139 7067 : char *r = string;
140 7067 : char *w = string;
141 65746 : while (*r != '\0') {
142 :
143 58681 : if (*r != '%') {
144 58429 : *w++ = *r++;
145 58429 : continue;
146 : }
147 252 : char x = r[1];
148 252 : if (x == '\0')
149 0 : return complain(sc, "percent escape in %s ends after one digit", context);
150 252 : char y = r[2];
151 252 : int n = 16 * percent_decode_digit(x) + percent_decode_digit(y);
152 252 : if (n < 0) {
153 2 : return complain(sc, "invalid percent escape in %s", context);
154 : }
155 250 : *w++ = (char)n;
156 250 : r += 3;
157 :
158 : }
159 7065 : *w = '\0';
160 7065 : return true;
161 : }
162 :
163 : enum character_class {
164 : // regular characters
165 : not_special,
166 : // special characters in the sense of RFC 3986 Section 2.2, plus '&' and '='
167 : generic_special,
168 : // very special, special even in query parameter values
169 : very_special,
170 : };
171 :
172 : static enum character_class
173 93188 : classify(char c)
174 : {
175 93188 : switch (c) {
176 : case '\0':
177 : case '#':
178 : case '&':
179 : case '=':
180 : return very_special;
181 8616 : case ':':
182 : case '/':
183 : case '?':
184 : case '[':
185 : case ']':
186 : case '@':
187 8616 : return generic_special;
188 81937 : case '%': // % is NOT special!
189 : default:
190 81937 : return not_special;
191 : }
192 : }
193 :
194 : static char *
195 11253 : scan(scanner *sc, enum character_class level)
196 : {
197 11253 : assert(!has_failed(sc));
198 11253 : char *token = sc->p;
199 :
200 : // scan the token
201 93196 : while (classify(sc->c) < level)
202 81937 : advance(sc);
203 :
204 : // the current character is a delimiter.
205 : // overwrite it with \0 to terminate the scanned string.
206 11259 : assert(sc->c == *sc->p);
207 11259 : *sc->p = '\0';
208 :
209 11259 : return token;
210 : }
211 :
212 : static char *
213 848 : find(scanner *sc, const char *delims)
214 : {
215 848 : assert(!has_failed(sc));
216 848 : char *token = sc->p;
217 :
218 9651 : while (sc->c) {
219 28760 : for (const char *d = delims; *d; d++)
220 19957 : if (sc->c == *d) {
221 352 : *sc->p = '\0';
222 352 : return token;
223 : }
224 8803 : advance(sc);
225 : }
226 : return token;
227 : }
228 :
229 : static bool
230 6503 : store(msettings *mp, scanner *sc, mparm parm, const char *value)
231 : {
232 6503 : msettings_error msg = msetting_parse(mp, parm, value);
233 6505 : if (msg)
234 0 : return complain(sc, "%s: %s", msg, value);
235 : else
236 : return true;
237 : }
238 :
239 : static bool
240 603 : scan_query_parameters(scanner *sc, char **key, char **value)
241 : {
242 603 : *key = scan(sc, very_special);
243 603 : if (strlen(*key) == 0)
244 0 : return complain(sc, "parameter name must not be empty");
245 :
246 603 : if (!consume(sc, "="))
247 : return false;
248 603 : *value = find(sc, "&#");
249 :
250 603 : return true;
251 : }
252 :
253 : static bool
254 2531 : parse_port(msettings *mp, scanner *sc) {
255 2531 : if (sc->c == ':') {
256 1838 : advance(sc);
257 1839 : char *portstr = scan(sc, generic_special);
258 1840 : char *end;
259 1840 : long port = strtol(portstr, &end, 10);
260 1839 : if (portstr[0] == '\0' || *end != '\0' || port < 1 || port > 65535)
261 13 : return complain(sc, "invalid port: '%s'", portstr);
262 1826 : msettings_error msg = msetting_set_long(mp, MP_PORT, port);
263 1826 : if (msg != NULL)
264 0 : return complain(sc, "could not set port: %s\n", msg);
265 : }
266 : return true;
267 : }
268 :
269 : static bool
270 2517 : parse_path(msettings *mp, scanner *sc, bool percent)
271 : {
272 : // parse the database name
273 2517 : if (sc->c != '/')
274 : return true;
275 2205 : advance(sc);
276 2205 : char *database = scan(sc, generic_special);
277 2207 : if (percent && !percent_decode(sc, "database", database))
278 : return false;
279 2205 : if (!store(mp, sc, MP_DATABASE, database))
280 : return false;
281 :
282 : // parse the schema name
283 2205 : if (sc->c != '/')
284 : return true;
285 889 : advance(sc);
286 889 : char *schema = scan(sc, generic_special);
287 889 : if (percent && !percent_decode(sc, "schema", schema))
288 : return false;
289 889 : if (!store(mp, sc, MP_TABLESCHEMA, schema))
290 : return false;
291 :
292 : // parse the table name
293 889 : if (sc->c != '/')
294 : return true;
295 867 : advance(sc);
296 867 : char *table = scan(sc, generic_special);
297 867 : if (percent && !percent_decode(sc, "table", table))
298 : return false;
299 867 : if (!store(mp, sc, MP_TABLE, table))
300 : return false;
301 :
302 : return true;
303 : }
304 :
305 : static bool
306 2307 : parse_modern(msettings *mp, scanner *sc)
307 : {
308 2307 : if (!consume(sc, "//"))
309 : return false;
310 :
311 : // parse the host
312 2300 : if (sc->c == '[') {
313 8 : advance(sc);
314 8 : char *host = sc->p;
315 176 : while (sc->c == ':' || isxdigit(sc->c))
316 168 : advance(sc);
317 8 : *sc->p = '\0';
318 8 : if (!consume(sc, "]"))
319 : return false;
320 8 : if (!store(mp, sc, MP_HOST, host))
321 : return false;
322 : } else {
323 2292 : char *host = scan(sc, generic_special);
324 2291 : if (!percent_decode(sc, "host name", host))
325 : return false;
326 2292 : if (strcmp(host, "localhost") == 0)
327 : host = "";
328 1856 : else if (strcmp(host, "localhost.") == 0)
329 : host = "localhost";
330 370 : else if (sc->c == ':' && strlen(host) == 0) {
331 : // cannot port number without host, so this is not allowed: monetdb://:50000
332 0 : return unexpected(sc);
333 : }
334 2292 : if (!store(mp, sc, MP_HOST, host))
335 : return false;
336 : }
337 :
338 2301 : if (!parse_port(mp, sc))
339 : return false;
340 :
341 2292 : if (!parse_path(mp, sc, true))
342 : return false;
343 :
344 : // parse query parameters
345 2291 : if (sc->c == '?') {
346 578 : do {
347 578 : advance(sc); // skip ? or &
348 :
349 578 : char *key = NULL;
350 578 : char *value = NULL;
351 578 : if (!scan_query_parameters(sc, &key, &value))
352 30 : return false;
353 578 : assert(key && value);
354 :
355 578 : if (!percent_decode(sc, "parameter name", key))
356 : return false;
357 578 : if (!percent_decode(sc, key, value))
358 : return false;
359 :
360 578 : msettings_error msg = msetting_set_named(mp, false, key, value);
361 578 : if (msg)
362 30 : return complain(sc, "%s", msg);
363 548 : } while (sc->c == '&');
364 : }
365 :
366 : // should have consumed everything
367 2261 : if (sc->c != '\0' && sc-> c != '#')
368 0 : return unexpected(sc);
369 :
370 : return true;
371 : }
372 :
373 : static bool
374 20 : parse_classic_query_parameters(msettings *mp, scanner *sc)
375 : {
376 20 : assert(sc->c == '?');
377 25 : do {
378 25 : advance(sc); // skip & or ?
379 :
380 25 : char *key = NULL;
381 25 : char *value = NULL;
382 25 : if (!scan_query_parameters(sc, &key, &value))
383 0 : return false;
384 25 : assert(key && value);
385 25 : mparm parm = mparm_parse(key);
386 25 : msettings_error msg;
387 25 : switch (parm) {
388 15 : case MP_DATABASE:
389 : case MP_LANGUAGE:
390 15 : msg = msetting_set_string(mp, parm, value);
391 15 : if (msg)
392 0 : return complain(sc, "%s", msg);
393 : break;
394 : default:
395 : // ignore
396 : break;
397 : }
398 25 : } while (sc->c == '&');
399 :
400 : return true;
401 : }
402 :
403 : static bool
404 234 : parse_classic_tcp(msettings *mp, scanner *sc)
405 : {
406 234 : assert(sc->c != '/');
407 :
408 : // parse the host
409 234 : char *host = find(sc, ":?/");
410 234 : if (strchr(host, '@') != NULL)
411 2 : return complain(sc, "host@user syntax is not allowed");
412 232 : if (!store(mp, sc, MP_HOST, host))
413 : return false;
414 :
415 232 : if (!parse_port(mp, sc))
416 : return false;
417 :
418 226 : if (!parse_path(mp, sc, false))
419 : return false;
420 :
421 226 : if (sc->c == '?') {
422 15 : if (!parse_classic_query_parameters(mp, sc))
423 : return false;
424 : }
425 :
426 : // should have consumed everything
427 226 : if (sc->c != '\0' && sc-> c != '#')
428 0 : return unexpected(sc);
429 :
430 : return true;
431 : }
432 :
433 : static bool
434 11 : parse_classic_unix(msettings *mp, scanner *sc)
435 : {
436 11 : assert(sc->c == '/');
437 11 : char *sock = find(sc, "?");
438 :
439 11 : if (!store(mp, sc, MP_SOCK, sock))
440 : return false;
441 :
442 11 : if (sc->c == '?') {
443 5 : if (!parse_classic_query_parameters(mp, sc))
444 : return false;
445 : }
446 :
447 : // should have consumed everything
448 11 : if (sc->c != '\0' && sc-> c != '#')
449 0 : return unexpected(sc);
450 :
451 : return true;
452 : }
453 :
454 : static bool
455 0 : parse_classic_merovingian(msettings *mp, scanner *sc)
456 : {
457 0 : if (!consume(sc, "mapi:merovingian://proxy"))
458 : return false;
459 :
460 0 : long user_gen = msettings_user_generation(mp);
461 0 : long password_gen = msettings_password_generation(mp);
462 :
463 0 : if (sc->c == '?') {
464 0 : if (!parse_classic_query_parameters(mp, sc))
465 : return false;
466 : }
467 :
468 : // should have consumed everything
469 0 : if (sc->c != '\0' && sc-> c != '#')
470 0 : return unexpected(sc);
471 :
472 0 : long new_user_gen = msettings_user_generation(mp);
473 0 : long new_password_gen = msettings_password_generation(mp);
474 0 : if (new_user_gen > user_gen || new_password_gen > password_gen)
475 0 : return complain(sc, "MAPI redirect is not allowed to set user or password");
476 :
477 : return true;
478 : }
479 :
480 : static bool
481 253 : parse_classic(msettings *mp, scanner *sc)
482 : {
483 : // we accept mapi:merovingian but we don't want to
484 : // expose that we do
485 253 : if (sc->p[0] == 'm' && sc->p[1] == 'e') {
486 0 : if (!consume(sc, "merovingian://proxy"))
487 : return false;
488 0 : return parse_classic_merovingian(mp, sc);
489 : }
490 :
491 253 : if (!consume(sc, "monetdb://"))
492 : return false;
493 :
494 245 : if (sc->c == '/')
495 11 : return parse_classic_unix(mp, sc);
496 : else
497 234 : return parse_classic_tcp(mp, sc);
498 : }
499 :
500 : static bool
501 2562 : parse_by_scheme(msettings *mp, scanner *sc)
502 : {
503 : // process the scheme
504 2562 : char *scheme = scan(sc, generic_special);
505 2562 : if (sc->c == ':')
506 2562 : advance(sc);
507 : else
508 0 : return complain(sc, "expected URL starting with monetdb:, monetdbs: or mapi:monetdb:");
509 2561 : if (strcmp(scheme, "monetdb") == 0) {
510 2047 : msetting_set_bool(mp, MP_TLS, false);
511 2047 : return parse_modern(mp, sc);
512 514 : } else if (strcmp(scheme, "monetdbs") == 0) {
513 261 : msetting_set_bool(mp, MP_TLS, true);
514 261 : return parse_modern(mp, sc);
515 253 : } else if (strcmp(scheme, "mapi") == 0) {
516 253 : msetting_set_bool(mp, MP_TLS, false);
517 253 : return parse_classic(mp, sc);
518 : } else {
519 0 : return complain(sc, "unknown URL scheme '%s'", scheme);
520 : }
521 : }
522 :
523 : static bool
524 2561 : parse(msettings *mp, scanner *sc)
525 : {
526 : // mapi:merovingian:://proxy is not like other URLs,
527 : // it designates the existing connection so the core properties
528 : // must not be cleared and user and password cannot be changed.
529 2561 : bool is_mero = (strncmp(sc->p, "mapi:merovingian:", 16) == 0);
530 :
531 2561 : if (!is_mero) {
532 : // clear existing core values
533 2561 : msetting_set_bool(mp, MP_TLS, false);
534 2561 : msetting_set_string(mp, MP_HOST, "");
535 2562 : msetting_set_long(mp, MP_PORT, -1);
536 2562 : msetting_set_string(mp, MP_DATABASE, "");
537 : }
538 :
539 2562 : long user_gen = msettings_user_generation(mp);
540 2562 : long password_gen = msettings_password_generation(mp);
541 :
542 2562 : if (is_mero) {
543 0 : if (!parse_classic_merovingian(mp, sc))
544 : return false;
545 : } else {
546 2562 : if (!parse_by_scheme(mp, sc))
547 : return false;
548 : }
549 :
550 2497 : bool user_changed = (msettings_user_generation(mp) != user_gen);
551 2498 : bool password_changed = (msettings_password_generation(mp) != password_gen);
552 :
553 2496 : if (is_mero && (user_changed || password_changed))
554 0 : return complain(sc, "MAPI redirect must not change user or password");
555 :
556 2496 : if (user_changed && !password_changed) {
557 : // clear password
558 30 : msettings_error msg = msetting_set_string(mp, MP_PASSWORD, "");
559 30 : if (msg) {
560 : // failed, report
561 0 : return complain(sc, "%s", msg);
562 : }
563 : }
564 :
565 : return true;
566 : }
567 :
568 : /* update the msettings from the URL. */
569 2561 : msettings_error msettings_parse_url(msettings *mp, const char *url)
570 : {
571 2561 : bool ok;
572 2561 : scanner sc;
573 :
574 : // This function is all about setting up the scanner and copying
575 : // error messages out of it.
576 :
577 2561 : if (!initialize(&sc, mp, url))
578 0 : return format_error(mp, "%s", MALLOC_FAILED);
579 :
580 2561 : mp->error_message[0] = '\0';
581 2561 : ok = parse(mp, &sc);
582 2560 : if (!ok)
583 64 : assert(mp->error_message[0] != '\0');
584 :
585 2560 : deinitialize(&sc);
586 2560 : return ok ? NULL : mp->error_message;
587 : }
|