LCOV - code coverage report
Current view: top level - clients/mapilib - parseurl.c (source / functions) Hit Total Coverage
Test: coverage.info Lines: 227 265 85.7 %
Date: 2024-12-20 21:24:02 Functions: 21 23 91.3 %

          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         404 : deinitialize(scanner *sc)
      51             : {
      52         404 :         free(sc->buffer);
      53             : }
      54             : 
      55             : static bool
      56       18518 : has_failed(const scanner *sc)
      57             : {
      58       18518 :         return sc->error_message[0] != '\0';
      59             : }
      60             : 
      61             : static char
      62       16703 : advance(scanner *sc)
      63             : {
      64       16703 :         assert(!has_failed(sc));
      65       16703 :         sc->p++;
      66       16703 :         sc->c = *sc->p;
      67       16703 :         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         567 : consume(scanner *sc, const char *text)
     105             : {
     106        3259 :         for (const char *c = text; *c; c++) {
     107        2695 :                 if (sc->c == *c) {
     108        2687 :                         advance(sc);
     109        2692 :                         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        9984 : classify(char c)
     173             : {
     174        9984 :         switch (c) {
     175             :                 case '\0':
     176             :                 case '#':
     177             :                 case '&':
     178             :                 case '=':
     179             :                         return very_special;
     180         959 :                 case ':':
     181             :                 case '/':
     182             :                 case '?':
     183             :                 case '[':
     184             :                 case ']':
     185             :                 case '@':
     186         959 :                         return generic_special;
     187        8616 :                 case '%':  // % is NOT special!
     188             :                 default:
     189        8616 :                         return not_special;
     190             :         }
     191             : }
     192             : 
     193             : static char *
     194        1370 : scan(scanner *sc, enum character_class level)
     195             : {
     196        1370 :         assert(!has_failed(sc));
     197        1370 :         char *token = sc->p;
     198             : 
     199             :         // scan the token
     200        9987 :         while (classify(sc->c) < level)
     201        8616 :                 advance(sc);
     202             : 
     203             :         // the current character is a delimiter.
     204             :         // overwrite it with \0 to terminate the scanned string.
     205        1371 :         assert(sc->c == *sc->p);
     206        1371 :         *sc->p = '\0';
     207             : 
     208        1371 :         return token;
     209             : }
     210             : 
     211             : static char *
     212         381 : find(scanner *sc, const char *delims)
     213             : {
     214         381 :         assert(!has_failed(sc));
     215         381 :         char *token = sc->p;
     216             : 
     217        4575 :         while (sc->c) {
     218       14545 :                 for (const char *d = delims; *d; d++)
     219       10352 :                         if (sc->c == *d) {
     220         235 :                                 *sc->p = '\0';
     221         235 :                                 return token;
     222             :                         }
     223        4193 :                 advance(sc);
     224             :         }
     225             :         return token;
     226             : }
     227             : 
     228             : static bool
     229         771 : store(msettings *mp, scanner *sc, mparm parm, const char *value)
     230             : {
     231         771 :         msettings_error msg = msetting_parse(mp, parm, value);
     232         773 :         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         388 : parse_port(msettings *mp, scanner *sc) {
     254         388 :         if (sc->c == ':') {
     255         244 :                 advance(sc);
     256         245 :                 char *portstr = scan(sc, generic_special);
     257         243 :                 char *end;
     258         243 :                 long port = strtol(portstr, &end, 10);
     259         244 :                 if (portstr[0] == '\0' || *end != '\0' || port < 1 || port > 65535)
     260           5 :                         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         379 : parse_path(msettings *mp, scanner *sc, bool percent)
     270             : {
     271             :         // parse the database name
     272         379 :         if (sc->c != '/')
     273             :                 return true;
     274         374 :         advance(sc);
     275         376 :         char *database = scan(sc, generic_special);
     276         376 :         if (percent && !percent_decode(sc, "database", database))
     277             :                 return false;
     278         375 :         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         207 : parse_classic_tcp(msettings *mp, scanner *sc)
     404             : {
     405         207 :         assert(sc->c != '/');
     406             : 
     407             :         // parse the host
     408         207 :         char *host = find(sc, ":?/");
     409         207 :         if (strchr(host, '@') != NULL)
     410           1 :                 return complain(sc, "host@user syntax is not allowed");
     411         206 :         if (!store(mp, sc, MP_HOST, host))
     412             :                 return false;
     413             : 
     414         207 :         if (!parse_port(mp, sc))
     415             :                 return false;
     416             : 
     417         207 :         if (!parse_path(mp, sc, false))
     418             :                 return false;
     419             : 
     420         206 :         if (sc->c == '?') {
     421           8 :                 if (!parse_classic_query_parameters(mp, sc))
     422             :                         return false;
     423             :         }
     424             : 
     425             :         // should have consumed everything
     426         206 :         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         213 :         if (sc->c == '/')
     494           6 :                 return parse_classic_unix(mp, sc);
     495             :         else
     496         207 :                 return parse_classic_tcp(mp, sc);
     497             : }
     498             : 
     499             : static bool
     500         404 : parse_by_scheme(msettings *mp, scanner *sc)
     501             : {
     502             :         // process the scheme
     503         404 :         char *scheme = scan(sc, generic_special);
     504         404 :         if (sc->c == ':')
     505         404 :                 advance(sc);
     506             :         else
     507           0 :                 return complain(sc, "expected URL starting with monetdb:, monetdbs: or mapi:monetdb:");
     508         404 :         if (strcmp(scheme, "monetdb") == 0) {
     509         114 :                 msetting_set_bool(mp, MP_TLS, false);
     510         114 :                 return parse_modern(mp, sc);
     511         290 :         } else if (strcmp(scheme, "monetdbs") == 0) {
     512          70 :                 msetting_set_bool(mp, MP_TLS, true);
     513          70 :                 return parse_modern(mp, sc);
     514         220 :         } else if (strcmp(scheme, "mapi") == 0) {
     515         220 :                 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         404 : 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         404 :         bool is_mero = (strncmp(sc->p, "mapi:merovingian:", 16) == 0);
     529             : 
     530         404 :         if (!is_mero) {
     531             :                 // clear existing core values
     532         403 :                 msetting_set_bool(mp, MP_TLS, false);
     533         404 :                 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         406 :         long user_gen = msettings_user_generation(mp);
     539         404 :         long password_gen = msettings_password_generation(mp);
     540             : 
     541         404 :         if (is_mero) {
     542           0 :                 if (!parse_classic_merovingian(mp, sc))
     543             :                         return false;
     544             :         } else {
     545         404 :                 if (!parse_by_scheme(mp, sc))
     546             :                         return false;
     547             :         }
     548             : 
     549         372 :         bool user_changed = (msettings_user_generation(mp) != user_gen);
     550         373 :         bool password_changed = (msettings_password_generation(mp) != password_gen);
     551             : 
     552         372 :         if (is_mero && (user_changed || password_changed))
     553           0 :                 return complain(sc, "MAPI redirect must not change user or password");
     554             : 
     555         372 :         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         404 :         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         404 :         deinitialize(&sc);
     592         404 :         return ok;
     593             : }

Generated by: LCOV version 1.14