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