From 515361df662027d63368b4426b3d8b7635ddb5eb Mon Sep 17 00:00:00 2001 From: Georg Richter Date: Fri, 5 Nov 2021 06:31:58 +0100 Subject: [PATCH] CONC-274: connection string support A connection string contains key/value pairs, separated by a semicolon as used in ODBC. Supported keys are all configuration options which can be used in MariaDB configuration files. For a complete list check https://github.com/mariadb-corporation/mariadb-connector-c/wiki/config_files#configuration-options. The connection string must contain at least one semicolon, otherwise it wil be interpreted as hostname. Unknown or invalid keys will be ignored. To connect via connection string, the following methods might be used: - by specifing connection option in configuration file: connection=host=localhost;ssl_enforce=1; - by using mariadb_connect() macro mariadb_connect(mysql, "host=localhost;ssl_enforce=1") - by passing connection string in host parameter to mysql_real_connect mysql_real_connect(mysql, "host=localhost;ssl_enforce=1", NULL, NULL, NULL, 0, NULL, 0) --- include/errmsg.h | 3 +- include/mysql.h | 1 + libmariadb/ma_errmsg.c | 1 + libmariadb/mariadb_lib.c | 254 ++++++++++++++++++++++++------- unittest/libmariadb/connection.c | 72 +++++++++ 5 files changed, 276 insertions(+), 55 deletions(-) diff --git a/include/errmsg.h b/include/errmsg.h index 555cde19..47cb9df8 100644 --- a/include/errmsg.h +++ b/include/errmsg.h @@ -103,7 +103,8 @@ extern const char *mariadb_client_errors[]; /* Error messages */ #define CR_VERSION_MISMATCH 5008 #define CR_INVALID_PARAMETER 5009 #define CR_PLUGIN_NOT_ALLOWED 5010 +#define CR_CONNSTR_PARSE_ERROR 5011 /* Always last, if you add new error codes please update the value for CR_MARIADB_LAST_ERROR */ -#define CR_MARIADB_LAST_ERROR CR_PLUGIN_NOT_ALLOWED +#define CR_MARIADB_LAST_ERROR CR_CONNSTR_PARSE_ERROR #endif diff --git a/include/mysql.h b/include/mysql.h index a36d86e1..5e74b7e7 100644 --- a/include/mysql.h +++ b/include/mysql.h @@ -882,6 +882,7 @@ struct st_mariadb_methods { #define mysql_reload(mysql) mysql_refresh((mysql),REFRESH_GRANT) #define mysql_library_init mysql_server_init #define mysql_library_end mysql_server_end +#define mariadb_connect(hdl, conn_str) mysql_real_connect((hdl),(conn_str), NULL, NULL, NULL, 0, NULL, 0) /* new api functions */ diff --git a/libmariadb/ma_errmsg.c b/libmariadb/ma_errmsg.c index 46769935..cca26459 100644 --- a/libmariadb/ma_errmsg.c +++ b/libmariadb/ma_errmsg.c @@ -105,6 +105,7 @@ const char *mariadb_client_errors[] = /* 5008 */ "Unsupported version %d. Supported versions are in the range %d - %d", /* 5009 */ "Invalid or missing parameter '%s'.", /* 5010 */ "Authentication plugin '%s' couldn't be found in restricted_auth plugin list.", + /* 5011 */ "Parse error in connection string (offset %d)", "" }; diff --git a/libmariadb/mariadb_lib.c b/libmariadb/mariadb_lib.c index ff23f44b..6b66ddcb 100644 --- a/libmariadb/mariadb_lib.c +++ b/libmariadb/mariadb_lib.c @@ -92,6 +92,7 @@ static MYSQL_PARAMETERS mariadb_internal_parameters= {&max_allowed_packet, &net_ static my_bool mysql_client_init=0; static void mysql_close_options(MYSQL *mysql); static void ma_clear_session_state(MYSQL *mysql); +static int parse_connection_string(MYSQL *mysql, const char *unused, const char *conn_str, ssize_t len); extern void release_configuration_dirs(); extern char **get_default_configuration_dirs(); extern my_bool ma_init_done; @@ -610,64 +611,86 @@ enum enum_option_type { MARIADB_OPTION_INT, MARIADB_OPTION_SIZET, MARIADB_OPTION_STR, + MARIADB_OPTION_FUNC, }; struct st_default_options { - enum mysql_option option; + union { + enum mysql_option option; + int (*option_func)(MYSQL *mysql, const char *key, const char *value, ssize_t len); + } u; enum enum_option_type type; const char *conf_key; }; struct st_default_options mariadb_defaults[] = { - {MARIADB_OPT_PORT, MARIADB_OPTION_INT,"port"}, - {MARIADB_OPT_UNIXSOCKET, MARIADB_OPTION_STR, "socket"}, - {MYSQL_OPT_COMPRESS, MARIADB_OPTION_BOOL, "compress"}, - {MARIADB_OPT_PASSWORD, MARIADB_OPTION_STR, "password"}, - {MYSQL_OPT_NAMED_PIPE, MARIADB_OPTION_BOOL, "pipe"}, - {MYSQL_OPT_CONNECT_TIMEOUT, MARIADB_OPTION_INT, "timeout"}, - {MARIADB_OPT_USER, MARIADB_OPTION_STR, "user"}, - {MYSQL_INIT_COMMAND, MARIADB_OPTION_STR, "init-command"}, - {MARIADB_OPT_HOST, MARIADB_OPTION_STR, "host"}, - {MARIADB_OPT_SCHEMA, MARIADB_OPTION_STR, "database"}, - {MARIADB_OPT_DEBUG, MARIADB_OPTION_STR, "debug"}, - {MARIADB_OPT_FOUND_ROWS, MARIADB_OPTION_NONE, "return-found-rows"}, - {MYSQL_OPT_SSL_KEY, MARIADB_OPTION_STR, "ssl-key"}, - {MYSQL_OPT_SSL_CERT, MARIADB_OPTION_STR,"ssl-cert"}, - {MYSQL_OPT_SSL_CA, MARIADB_OPTION_STR,"ssl-ca"}, - {MYSQL_OPT_SSL_CAPATH, MARIADB_OPTION_STR,"ssl-capath"}, - {MYSQL_OPT_SSL_CRL, MARIADB_OPTION_STR,"ssl-crl"}, - {MYSQL_OPT_SSL_CRLPATH, MARIADB_OPTION_STR,"ssl-crlpath"}, - {MYSQL_OPT_SSL_VERIFY_SERVER_CERT, MARIADB_OPTION_BOOL,"ssl-verify-server-cert"}, - {MYSQL_SET_CHARSET_DIR, MARIADB_OPTION_STR, "character-sets-dir"}, - {MYSQL_SET_CHARSET_NAME, MARIADB_OPTION_STR, "default-character-set"}, - {MARIADB_OPT_INTERACTIVE, MARIADB_OPTION_NONE, "interactive-timeout"}, - {MYSQL_OPT_CONNECT_TIMEOUT, MARIADB_OPTION_INT, "connect-timeout"}, - {MYSQL_OPT_LOCAL_INFILE, MARIADB_OPTION_BOOL, "local-infile"}, - {0, 0 ,"disable-local-infile",}, - {MYSQL_OPT_SSL_CIPHER, MARIADB_OPTION_STR, "ssl-cipher"}, - {MYSQL_OPT_MAX_ALLOWED_PACKET, MARIADB_OPTION_SIZET, "max-allowed-packet"}, - {MYSQL_OPT_NET_BUFFER_LENGTH, MARIADB_OPTION_SIZET, "net-buffer-length"}, - {MYSQL_OPT_PROTOCOL, MARIADB_OPTION_INT, "protocol"}, - {MYSQL_SHARED_MEMORY_BASE_NAME, MARIADB_OPTION_STR,"shared-memory-base-name"}, - {MARIADB_OPT_MULTI_RESULTS, MARIADB_OPTION_NONE, "multi-results"}, - {MARIADB_OPT_MULTI_STATEMENTS, MARIADB_OPTION_STR, "multi-statements"}, - {MARIADB_OPT_MULTI_STATEMENTS, MARIADB_OPTION_STR, "multi-queries"}, - {MYSQL_SECURE_AUTH, MARIADB_OPTION_BOOL, "secure-auth"}, - {MYSQL_REPORT_DATA_TRUNCATION, MARIADB_OPTION_BOOL, "report-data-truncation"}, - {MYSQL_OPT_RECONNECT, MARIADB_OPTION_BOOL, "reconnect"}, - {MYSQL_PLUGIN_DIR, MARIADB_OPTION_STR, "plugin-dir"}, - {MYSQL_DEFAULT_AUTH, MARIADB_OPTION_STR, "default-auth"}, - {MARIADB_OPT_SSL_FP, MARIADB_OPTION_STR, "ssl-fp"}, - {MARIADB_OPT_SSL_FP_LIST, MARIADB_OPTION_STR, "ssl-fp-list"}, - {MARIADB_OPT_SSL_FP_LIST, MARIADB_OPTION_STR, "ssl-fplist"}, - {MARIADB_OPT_TLS_PASSPHRASE, MARIADB_OPTION_STR, "ssl-passphrase"}, - {MARIADB_OPT_TLS_VERSION, MARIADB_OPTION_STR, "tls-version"}, - {MYSQL_SERVER_PUBLIC_KEY, MARIADB_OPTION_STR, "server-public-key"}, - {MYSQL_OPT_BIND, MARIADB_OPTION_STR, "bind-address"}, - {MYSQL_OPT_SSL_ENFORCE, MARIADB_OPTION_BOOL, "ssl-enforce"}, - {MARIADB_OPT_RESTRICTED_AUTH, MARIADB_OPTION_STR, "restricted-auth"}, - {0, 0, NULL} + {{MARIADB_OPT_PORT}, MARIADB_OPTION_INT,"port"}, + {{MARIADB_OPT_UNIXSOCKET}, MARIADB_OPTION_STR, "socket"}, + {{MYSQL_OPT_COMPRESS}, MARIADB_OPTION_BOOL, "compress"}, + {{MARIADB_OPT_PASSWORD}, MARIADB_OPTION_STR, "password"}, + {{MYSQL_OPT_NAMED_PIPE}, MARIADB_OPTION_BOOL, "pipe"}, + {{MYSQL_OPT_CONNECT_TIMEOUT}, MARIADB_OPTION_INT, "timeout"}, + {{MARIADB_OPT_USER}, MARIADB_OPTION_STR, "user"}, + {{MYSQL_INIT_COMMAND}, MARIADB_OPTION_STR, "init-command"}, + {{MARIADB_OPT_HOST}, MARIADB_OPTION_STR, "host"}, + {{MARIADB_OPT_SCHEMA}, MARIADB_OPTION_STR, "database"}, + {{MARIADB_OPT_DEBUG}, MARIADB_OPTION_STR, "debug"}, + {{MARIADB_OPT_FOUND_ROWS}, MARIADB_OPTION_NONE, "return-found-rows"}, + {{MYSQL_OPT_SSL_KEY}, MARIADB_OPTION_STR, "ssl-key"}, + {{MYSQL_OPT_SSL_CERT}, MARIADB_OPTION_STR,"ssl-cert"}, + {{MYSQL_OPT_SSL_CA}, MARIADB_OPTION_STR,"ssl-ca"}, + {{MYSQL_OPT_SSL_CAPATH}, MARIADB_OPTION_STR,"ssl-capath"}, + {{MYSQL_OPT_SSL_CRL}, MARIADB_OPTION_STR,"ssl-crl"}, + {{MYSQL_OPT_SSL_CRLPATH}, MARIADB_OPTION_STR,"ssl-crlpath"}, + {{MYSQL_OPT_SSL_VERIFY_SERVER_CERT}, MARIADB_OPTION_BOOL,"ssl-verify-server-cert"}, + {{MYSQL_SET_CHARSET_DIR}, MARIADB_OPTION_STR, "character-sets-dir"}, + {{MYSQL_SET_CHARSET_NAME}, MARIADB_OPTION_STR, "default-character-set"}, + {{MARIADB_OPT_INTERACTIVE}, MARIADB_OPTION_NONE, "interactive-timeout"}, + {{MYSQL_OPT_CONNECT_TIMEOUT}, MARIADB_OPTION_INT, "connect-timeout"}, + {{MYSQL_OPT_LOCAL_INFILE}, MARIADB_OPTION_BOOL, "local-infile"}, + {{0}, 0 ,"disable-local-infile",}, + {{MYSQL_OPT_SSL_CIPHER}, MARIADB_OPTION_STR, "ssl-cipher"}, + {{MYSQL_OPT_MAX_ALLOWED_PACKET}, MARIADB_OPTION_SIZET, "max-allowed-packet"}, + {{MYSQL_OPT_NET_BUFFER_LENGTH}, MARIADB_OPTION_SIZET, "net-buffer-length"}, + {{MYSQL_OPT_PROTOCOL}, MARIADB_OPTION_INT, "protocol"}, + {{MYSQL_SHARED_MEMORY_BASE_NAME}, MARIADB_OPTION_STR,"shared-memory-base-name"}, + {{MARIADB_OPT_MULTI_RESULTS}, MARIADB_OPTION_BOOL, "multi-results"}, + {{MARIADB_OPT_MULTI_STATEMENTS}, MARIADB_OPTION_BOOL, "multi-statements"}, + {{MARIADB_OPT_MULTI_STATEMENTS}, MARIADB_OPTION_BOOL, "multi-queries"}, + {{MYSQL_SECURE_AUTH}, MARIADB_OPTION_BOOL, "secure-auth"}, + {{MYSQL_REPORT_DATA_TRUNCATION}, MARIADB_OPTION_BOOL, "report-data-truncation"}, + {{MYSQL_OPT_RECONNECT}, MARIADB_OPTION_BOOL, "reconnect"}, + {{MYSQL_PLUGIN_DIR}, MARIADB_OPTION_STR, "plugin-dir"}, + {{MYSQL_DEFAULT_AUTH}, MARIADB_OPTION_STR, "default-auth"}, + {{MARIADB_OPT_SSL_FP}, MARIADB_OPTION_STR, "ssl-fp"}, + {{MARIADB_OPT_SSL_FP_LIST}, MARIADB_OPTION_STR, "ssl-fp-list"}, + {{MARIADB_OPT_SSL_FP_LIST}, MARIADB_OPTION_STR, "ssl-fplist"}, + {{MARIADB_OPT_TLS_PASSPHRASE}, MARIADB_OPTION_STR, "ssl-passphrase"}, + {{MARIADB_OPT_TLS_VERSION}, MARIADB_OPTION_STR, "tls-version"}, + {{MYSQL_SERVER_PUBLIC_KEY}, MARIADB_OPTION_STR, "server-public-key"}, + {{MYSQL_OPT_BIND}, MARIADB_OPTION_STR, "bind-address"}, + {{MYSQL_OPT_SSL_ENFORCE}, MARIADB_OPTION_BOOL, "ssl-enforce"}, + {{MARIADB_OPT_RESTRICTED_AUTH}, MARIADB_OPTION_STR, "restricted-auth"}, + {{.option_func=parse_connection_string}, MARIADB_OPTION_FUNC, "connection"}, + /* Aliases */ + {{MARIADB_OPT_SCHEMA}, MARIADB_OPTION_STR, "db"}, + {{MARIADB_OPT_UNIXSOCKET}, MARIADB_OPTION_STR, "unix_socket"}, + {{MARIADB_OPT_HOST}, MARIADB_OPTION_STR, "servername"}, + {{MARIADB_OPT_PASSWORD}, MARIADB_OPTION_STR, "passwd"}, + {{MARIADB_OPT_SSL_FP}, MARIADB_OPTION_STR, "tls-fp"}, + {{MARIADB_OPT_SSL_FP_LIST}, MARIADB_OPTION_STR, "tls-fplist"}, + {{MYSQL_OPT_SSL_KEY}, MARIADB_OPTION_STR, "tls-key"}, + {{MYSQL_OPT_SSL_CERT}, MARIADB_OPTION_STR,"tls-cert"}, + {{MYSQL_OPT_SSL_CA}, MARIADB_OPTION_STR,"tls-ca"}, + {{MYSQL_OPT_SSL_CAPATH}, MARIADB_OPTION_STR,"tls-capath"}, + {{MYSQL_OPT_SSL_CRL}, MARIADB_OPTION_STR,"tls-crl"}, + {{MYSQL_OPT_SSL_CRLPATH}, MARIADB_OPTION_STR,"tls-crlpath"}, + {{MYSQL_OPT_SSL_CIPHER}, MARIADB_OPTION_STR, "tls-cipher"}, + {{MARIADB_OPT_TLS_PASSPHRASE}, MARIADB_OPTION_STR, "tls-passphrase"}, + {{MYSQL_OPT_SSL_ENFORCE}, MARIADB_OPTION_BOOL, "tls-enforce"}, + {{MYSQL_OPT_SSL_VERIFY_SERVER_CERT}, MARIADB_OPTION_BOOL,"tls-verify-peer"}, + {{0}, 0, NULL} }; #define CHECK_OPT_EXTENSION_SET(OPTS)\ @@ -733,6 +756,9 @@ my_bool _mariadb_set_conf_option(MYSQL *mysql, const char *config_option, const int rc; void *option_val= NULL; switch (mariadb_defaults[i].type) { + case MARIADB_OPTION_FUNC: + return mariadb_defaults[i].u.option_func(mysql, config_option, config_value, -1); + break; case MARIADB_OPTION_BOOL: val_bool= 0; if (config_value) @@ -757,7 +783,7 @@ my_bool _mariadb_set_conf_option(MYSQL *mysql, const char *config_option, const case MARIADB_OPTION_NONE: break; } - rc= mysql_optionsv(mysql, mariadb_defaults[i].option, option_val); + rc= mysql_optionsv(mysql, mariadb_defaults[i].u.option, option_val); return(test(rc)); } } @@ -766,6 +792,121 @@ my_bool _mariadb_set_conf_option(MYSQL *mysql, const char *config_option, const return 1; } +/** + * @brief: simple connection string parser + * + * key/value pairs (or key only) are separated by semicolons. + * If a semicolon is part of a value, it must be enclosed in + * curly braces. + * + * Unknown keys will be ignored. + */ +static int parse_connection_string(MYSQL *mysql, const char *unused __attribute__((unused)), + const char *conn_str, ssize_t len) +{ + char *conn_save, + *end, *pos, + *key= NULL, *val= NULL; + my_bool in_curly_brace= 0; + + if (len == -1) + len= strlen(conn_str); + + /* don't modify original dsn */ + conn_save= (char *)malloc(len + 1); + memcpy(conn_save, conn_str, len); + conn_save[len]= 0; + + /* start and end */ + pos= conn_save; + end= conn_save + len; + + while (pos <= end) + { + /* ignore white space, unless it is between curly braces */ + if (isspace(*pos) && !in_curly_brace) + { + pos++; + continue; + } + + switch (*pos) { + case '{': + if (!key) + goto error; + if (!in_curly_brace) + { + in_curly_brace= 1; + if (pos < end) + { + pos++; + val= pos; + } + } + break; + case '}': + if (in_curly_brace) + { + if (!key) + goto error; + if (pos < end && *(pos + 1) == '}') + { + memmove(pos, pos + 1, end - pos - 1); + end--; + pos += 2; + continue; + } + if (in_curly_brace) + in_curly_brace= 0; + else + goto error; + *pos++= 0; + continue; + } + break; + case '=': + if (in_curly_brace) + { + pos++; + continue; + } + if (!key) + goto error; + *pos++= 0; + if (pos < end) + val= pos; + continue; + break; + case ';': + if (in_curly_brace) + { + pos++; + continue; + } + if (!key) + goto error; + *pos++= 0; + if (key && strcasecmp(key, "connection") != 0) + _mariadb_set_conf_option(mysql, key, val); + key= val= NULL; + continue; + break; + } + if (!key && *pos) + key= pos; + pos++; + } + if (key && strcasecmp(key, "connection") != 0) + _mariadb_set_conf_option(mysql, key, val); + free(conn_save); + return 0; + +error: + my_set_error(mysql, CR_CONNSTR_PARSE_ERROR, SQLSTATE_UNKNOWN, 0, pos - conn_save); + free(conn_save); + return 1; +} + static MARIADB_CONST_STRING null_const_string= {0,0}; @@ -1258,7 +1399,15 @@ mysql_real_connect(MYSQL *mysql, const char *host, const char *user, if (!mysql->methods) mysql->methods= &MARIADB_DEFAULT_METHODS; - if (connection_handler || + /* if host contains a semicolon, we need to parse connection string */ + if (host && strchr(host, ';')) + { + if (parse_connection_string(mysql, NULL, host, strlen(host))) + return NULL; + /* if host was passed in connection string, it is now + stored in mysql->options.host */ + host= NULL; + } else if (connection_handler || (host && (end= strstr(host, "://")))) { MARIADB_CONNECTION_PLUGIN *plugin; @@ -1371,9 +1520,6 @@ MYSQL *mthd_my_real_connect(MYSQL *mysql, const char *host, const char *user, mysql->options.my_cnf_file=mysql->options.my_cnf_group=0; } - if (!host || !host[0]) - host = mysql->options.host; - ma_set_connect_attrs(mysql, host); #ifndef WIN32 diff --git a/unittest/libmariadb/connection.c b/unittest/libmariadb/connection.c index 53c81260..bee6bae0 100644 --- a/unittest/libmariadb/connection.c +++ b/unittest/libmariadb/connection.c @@ -1982,7 +1982,79 @@ static int test_conc544(MYSQL *mysql) return OK; } +static int test_conn_str(MYSQL *my __attribute__((unused))) +{ + MYSQL *mysql= mysql_init(NULL); + char conn_str[1024]; + int rc=OK; + + snprintf(conn_str, sizeof(conn_str)-1, "host=%s;user=%s;password={%s};db=%s;port=%d", + hostname ? hostname : "localhost", username ? username : "", + password ? password : "", + schema ? schema : "", port); + + /* SkySQL requires secure connection */ + if (IS_SKYSQL(hostname)) + { + strcat(conn_str, ";ssl_enforce=1"); + } + + diag("connection string: %s", conn_str); + + if (mariadb_connect(mysql, conn_str)) + { + diag("host: %s", mysql->host); + diag("user: %s", mysql->user); + diag("cipher: %s", mysql_get_ssl_cipher(mysql)); + } else + { + diag("error: %s", mysql_error(mysql)); + rc= FAIL; + } + mysql_close(mysql); + return rc; +} + +static int test_conn_str_1(MYSQL *my __attribute__((unused))) +{ + MYSQL *mysql; + FILE *fp; + int rc; + mysql= mysql_init(NULL); + if (!(fp= fopen("./conc274.cnf", "w"))) + return FAIL; + + fprintf(fp, "[client]\n"); + fprintf(fp, "connection=host=%s;user=%s;password=%s;port=%d;ssl_enforce=1\n", hostname, username, password, port); + + fclose(fp); + + rc= mysql_options(mysql, MYSQL_READ_DEFAULT_FILE, "./conc274.cnf"); + check_mysql_rc(rc, mysql); + rc= mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, ""); + check_mysql_rc(rc, mysql); + + if (!my_test_connect(mysql, NULL, NULL, NULL, NULL, 0, NULL, 0)) + { + diag("Error: %s", mysql_error(mysql)); + remove("./conc274.cnf"); + return FAIL; + } + remove("./conc274.cnf"); + + if (!mysql_get_ssl_cipher(mysql)) + { + diag("Error: No TLS connection"); + return FAIL; + } + diag("Cipher in use: %s", mysql_get_ssl_cipher(mysql)); + mysql_close(mysql); + return OK; +} + struct my_tests_st my_tests[] = { + {"test_conn_str", test_conn_str, TEST_CONNECTION_NONE, 0, NULL, NULL}, + {"test_conn_str_1", test_conn_str_1, TEST_CONNECTION_NONE, 0, NULL, NULL}, {"test_conc544", test_conc544, TEST_CONNECTION_DEFAULT, 0, NULL, NULL}, {"test_conc490", test_conc490, TEST_CONNECTION_NONE, 0, NULL, NULL}, {"test_gtid", test_gtid, TEST_CONNECTION_DEFAULT, 0, NULL, NULL},