LCOV - code coverage report
Current view: top level - clients/mapilib - parseurl.c (source / functions) Hit Total Coverage
Test: coverage.info Lines: 226 263 85.9 %
Date: 2025-03-24 23:16:36 Functions: 20 22 90.9 %

          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             : }

Generated by: LCOV version 1.14