diff --git a/CMakeLists.txt b/CMakeLists.txt index 640e642bc2..109665b33b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -701,7 +701,7 @@ if (APPLE) set_target_properties(libqore PROPERTIES SOVERSION 12) set_target_properties(libqore PROPERTIES INSTALL_NAME_DIR ${CMAKE_INSTALL_FULL_LIBDIR}) else (APPLE) - set_target_properties(libqore PROPERTIES VERSION 12.1.2) + set_target_properties(libqore PROPERTIES VERSION 12.2.0) set_target_properties(libqore PROPERTIES SOVERSION 12) endif (APPLE) diff --git a/doxygen/lang/900_release_notes.dox.tmpl b/doxygen/lang/900_release_notes.dox.tmpl index 5593130f9e..c59d5ef02e 100644 --- a/doxygen/lang/900_release_notes.dox.tmpl +++ b/doxygen/lang/900_release_notes.dox.tmpl @@ -26,6 +26,8 @@ - added a \c path_params element to the \c UriQueryInfo typed hash to allow for reporting path arguments in calls supported by REST schemas (issue 4661) + - added the @ref Qore::get_safe_url() "get_safe_url()" function to avoid exposing passwords in URLs + (issue 4671) @subsection qore_1_13_bug_fixes Bug Fixes in Qore - RestHandler module diff --git a/examples/test/qlib/ConnectionProvider/ConnectionProviderModule.qtest b/examples/test/qlib/ConnectionProvider/ConnectionProviderModule.qtest index 711642f889..cc5b68a3ab 100755 --- a/examples/test/qlib/ConnectionProvider/ConnectionProviderModule.qtest +++ b/examples/test/qlib/ConnectionProvider/ConnectionProviderModule.qtest @@ -43,7 +43,7 @@ public class ConnectionProviderModuleTest inherits QUnit::Test { p.parse("hash sub get_hash() { return get_connection_hash(); }", ""); any obj = p.callFunction("get_obj", "X"); assertEq(Type::Object, obj.type()); - assertEq("test://user@x", obj.getInfo().url); + assertEq("test://user:@x", obj.getInfo().url); assertEq(NOTHING, obj.getInfo().url_hash.password); assertEq("test://user:pass@x", obj.getInfo(True).url); assertEq("pass", obj.getInfo(True).url_hash.password); diff --git a/examples/test/qore/functions/get_qore_option_list.qtest b/examples/test/qore/functions/get_qore_option_list.qtest new file mode 100755 index 0000000000..199af2d15e --- /dev/null +++ b/examples/test/qore/functions/get_qore_option_list.qtest @@ -0,0 +1,25 @@ +#!/usr/bin/env qore +# -*- mode: qore; indent-tabs-mode: nil -*- + +%new-style +%enable-all-warnings +%require-types +%strict-args + +%requires ../../../../qlib/QUnit.qm + +%exec-class GetQoreOptionListTest + +public class GetQoreOptionListTest inherits QUnit::Test { + constructor() : Test("GetQoreOptionListTest", "1.0") { + addTestCase("get_qore_option_list() test", \getQoreOptionListTest()); + + # Return for compatibility with test harness that checks return value. + set_return_value(main()); + } + + getQoreOptionListTest() { + list l = get_qore_option_list(); + assertEq("list>", l.fullType()); + } +} diff --git a/examples/test/qore/functions/get_safe_url.qtest b/examples/test/qore/functions/get_safe_url.qtest new file mode 100755 index 0000000000..068c748762 --- /dev/null +++ b/examples/test/qore/functions/get_safe_url.qtest @@ -0,0 +1,29 @@ +#!/usr/bin/env qore +# -*- mode: qore; indent-tabs-mode: nil -*- + +%new-style +%enable-all-warnings +%require-types +%strict-args + +%requires ../../../../qlib/QUnit.qm + +%exec-class getSafeUrlTest + +public class getSafeUrlTest inherits QUnit::Test { + constructor() : Test("get_safe_url test", "1.0") { + addTestCase("main test", \mainTest()); + + # Return for compatibility with test harnesses that check the return value + set_return_value(main()); + } + + mainTest() { + assertEq("https://user:@site:8001/path", get_safe_url("https://user:password@site:8001/path")); + assertEq("https://user:@site:8001", get_safe_url("https://user:password@site:8001")); + + assertEq("user:@site:8001/path", get_safe_url("user:password@site:8001/path")); + assertEq("user:@:8001/path", get_safe_url("user:pass@:8001/path")); + assertEq(":@:8001/path", get_safe_url(":pass@:8001/path")); + } +} diff --git a/examples/test/qore/functions/parse_url.qtest b/examples/test/qore/functions/parse_url.qtest index 1212122e9a..7f780db511 100755 --- a/examples/test/qore/functions/parse_url.qtest +++ b/examples/test/qore/functions/parse_url.qtest @@ -73,6 +73,23 @@ public class parseUrlTest inherits QUnit::Test { "port": 587, }, h); + h = parse_url("user:pass@site:1001/path"); + assertEq({ + "username": "user", + "password": "pass", + "host": "site", + "port": 1001, + "path": "/path", + }, h); + + h = parse_url("user:pass@:1001/path"); + assertEq({ + "username": "user", + "password": "pass", + "port": 1001, + "path": "/path", + }, h); + # "standard" test assertEq(("protocol": "http", "path": "/path", "username": "user", "password": "pass", "host": "host", "port": 80), parse_url("http://user:pass@host:80/path")); diff --git a/include/qore/intern/QoreHttpClientObjectIntern.h b/include/qore/intern/QoreHttpClientObjectIntern.h index 4fc5e2583b..30ac314097 100644 --- a/include/qore/intern/QoreHttpClientObjectIntern.h +++ b/include/qore/intern/QoreHttpClientObjectIntern.h @@ -117,20 +117,32 @@ struct con_info { return 0; } - DLLLOCAL QoreStringNode* get_url(bool suppress_password = false) const { + DLLLOCAL QoreStringNode* get_url(bool mask_password = false) const { QoreStringNode *pstr = new QoreStringNode("http"); if (ssl) { pstr->concat("s://"); } else { pstr->concat("://"); } + bool has_username_or_password = false; if (!username.empty()) { - if (suppress_password) { - pstr->sprintf("%s@", username.c_str()); + pstr->concat(username); + has_username_or_password = true; + } + if (!password.empty()) { + pstr->concat(':'); + if (mask_password) { + pstr->concat(""); } else { - pstr->sprintf("%s:%s@", username.c_str(), password.c_str()); + pstr->concat(password); + } + if (!has_username_or_password) { + has_username_or_password = true; } } + if (has_username_or_password) { + pstr->concat('@'); + } if (!port) { // concat and encode "host" when using a UNIX domain socket diff --git a/lib/Makefile.am b/lib/Makefile.am index 33e06b1e82..987adc4036 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -11,7 +11,7 @@ dummy: echo "Build started!" EXTRA_INCLUDES = -I$(top_srcdir)/include -I$(top_builddir)/include -I$(top_builddir)/lib -libqore_la_LDFLAGS = -version-info 13:2:1 -no-undefined ${QORE_LIB_LDFLAGS} +libqore_la_LDFLAGS = -version-info 14:0:2 -no-undefined ${QORE_LIB_LDFLAGS} AM_CPPFLAGS = $(EXTRA_INCLUDES) ${QORE_LIB_CPPFLAGS} AM_CXXFLAGS = ${QORE_LIB_CXXFLAGS} AM_YFLAGS = -d diff --git a/lib/ql_misc.qpp b/lib/ql_misc.qpp index 025f7aeab6..bb50af53ea 100644 --- a/lib/ql_misc.qpp +++ b/lib/ql_misc.qpp @@ -1167,6 +1167,67 @@ hash parse_url(string url, *int options) { return qurl.isValid() ? qurl.getHash() : QoreValue(); } +//! Returns the URL string passed without any password information +/** @param url the URL to process + + @return the URL string passed without any password information + + @since %Qore 1.13 +*/ +string get_safe_url(string url) { + QoreURL qurl(xsink, url, QURL_KEEP_BRACKETS); + if (!qurl.isValid()) { + assert(*xsink); + return QoreValue(); + } + if (!qurl.getPassword()) { + url->ref(); + return url; + } + + SimpleRefHolder rv(new QoreStringNode(QCS_UTF8)); + + const QoreString* str = qurl.getProtocol(); + if (str && !str->empty()) { + rv->concat(str, xsink); + rv->concat("://"); + } + + str = qurl.getUserName(); + bool has_user_or_pass = false; + if (str && !str->empty()) { + rv->concat(str, xsink); + has_user_or_pass = true; + } + + str = qurl.getPassword(); + if (str && !str->empty()) { + rv->concat(":"); + if (!has_user_or_pass) { + has_user_or_pass = true; + } + } + if (has_user_or_pass) { + rv->concat('@'); + } + + str = qurl.getHost(); + if (str && !str->empty()) { + rv->concat(str, xsink); + } + + if (qurl.getPort()) { + rv->sprintf(":%d", qurl.getPort()); + } + + str = qurl.getPath(); + if (str && !str->empty()) { + rv->concat(str, xsink); + } + + return rv.release(); +} + //! This function variant does nothing at all; it is only included for backwards-compatibility with qore prior to version 0.8.0 for functions that would ignore type errors in arguments /** */ diff --git a/qlib/AwsRestClient.qm b/qlib/AwsRestClient.qm index a98845f469..b45dc79a56 100644 --- a/qlib/AwsRestClient.qm +++ b/qlib/AwsRestClient.qm @@ -228,7 +228,8 @@ AwsRestClient rest({ # AWS does not support basic authentication hash url_info = parse_url(opts.url); if (url_info{"username", "password"}) { - throw "AWSRESTCLIENT-ERROR", sprintf("AWS URL must not contain a username or password; URL: %y", opts.url); + throw "AWSRESTCLIENT-ERROR", sprintf("AWS URL must not contain a username or password; URL: %y", + get_safe_url(opts.url)); } # set the region automatically from the URL if not provided in an option @@ -269,7 +270,8 @@ AwsRestClient rest({ } } - hash sendAndDecodeResponse(*data body, string m, string path, hash hdr, *reference> info, *softbool decode_errors) { + hash sendAndDecodeResponse(*data body, string m, string path, hash hdr, *reference> info, + *softbool decode_errors) { # get request time date gmtime = Qore::gmtime(); # get credential scope @@ -311,8 +313,8 @@ AwsRestClient rest({ return sig; } - private string getRequestString(string http_method, string path, reference> hdr, *data body, date gmtime, - string scope, reference signed_headers) { + private string getRequestString(string http_method, string path, reference> hdr, *data body, + date gmtime, string scope, reference signed_headers) { # get AWS date value string aws_date = gmtime.format("YYYYMMDDTHHmmSS") + "Z"; hdr."X-Amz-Date" = aws_date; @@ -359,7 +361,8 @@ AwsRestClient rest({ cstr += ( foldl $1 + "&" + $2, ( map - sprintf("%s=%s", encode_url($1, True), uri_info.params{$1} === True ? "" : encode_url(uri_info.params{$1}, True)), + sprintf("%s=%s", encode_url($1, True), + uri_info.params{$1} === True ? "" : encode_url(uri_info.params{$1}, True)), sort(keys uri_info.params) ) ); diff --git a/qlib/CdsRestDataProvider/CdsRestDataProvider.qc b/qlib/CdsRestDataProvider/CdsRestDataProvider.qc index 74a1b5657b..ebf9e98b97 100644 --- a/qlib/CdsRestDataProvider/CdsRestDataProvider.qc +++ b/qlib/CdsRestDataProvider/CdsRestDataProvider.qc @@ -161,7 +161,7 @@ public class CdsRestDataProvider inherits DataProvider::AbstractDataProvider { } catch (hash ex) { # ensure that any error response body is included in the exception hash ex_arg = info{"request-uri", "response-code", "response-body"}; - throw ex.err, ex.desc, ex.arg + ex_arg; + rethrow ex.err, ex.desc, ex.arg + ex_arg; } #printf("%N\n", resp.EntityType); diff --git a/qlib/ConnectionProvider/AbstractConnection.qc b/qlib/ConnectionProvider/AbstractConnection.qc index c43f7c7937..eb1af14564 100644 --- a/qlib/ConnectionProvider/AbstractConnection.qc +++ b/qlib/ConnectionProvider/AbstractConnection.qc @@ -469,8 +469,20 @@ scheme://user:pass@hostname:port/path */ private string getSafeUrl(hash urlh) { string url = urlh.protocol + "://"; - if (urlh.username) - url += urlh.username + "@"; + bool has_user_or_pass; + if (urlh.username) { + url += urlh.username; + has_user_or_pass = True; + } + if (urlh.password) { + url += ":"; + if (!has_user_or_pass) { + has_user_or_pass = True; + } + } + if (has_user_or_pass) { + url += "@"; + } url += urlh.host; if (urlh.port) url += ":" + urlh.port; diff --git a/qlib/Pop3Client.qm b/qlib/Pop3Client.qm index 43d23556cb..805d8771e7 100644 --- a/qlib/Pop3Client.qm +++ b/qlib/Pop3Client.qm @@ -257,7 +257,8 @@ Pop3Client pop3("pop3s://user@gmail.com:password@pop.gmail.com"); else { *hash conf = Protocols.(hurl.protocol.lwr()); if (!exists conf) { - throw "POP3-URL-ERROR", sprintf("unknown scheme %y in %y; known schemes: %y", hurl.protocol, url, keys Protocols); + throw "POP3-URL-ERROR", sprintf("unknown scheme %y in %y; known schemes: %y", hurl.protocol, + get_safe_url(url), keys Protocols); } tls = conf.tls; if (!hurl.port && hurl.host[0] != "/") { @@ -266,7 +267,7 @@ Pop3Client pop3("pop3s://user@gmail.com:password@pop.gmail.com"); } if (!hurl.host.val()) - throw "POP3-URL-ERROR", sprintf("no hostname was given in URL %y", url); + throw "POP3-URL-ERROR", sprintf("no hostname was given in URL %y", get_safe_url(url)); # if the hostname is an integer, then assume the given port on localhost if (hurl.host.val()) { @@ -285,7 +286,7 @@ Pop3Client pop3("pop3s://user@gmail.com:password@pop.gmail.com"); } if (!exists hurl.username) { - throw "POP3-URL-ERROR", sprintf("missing username in POP3 URL: %y", url); + throw "POP3-URL-ERROR", sprintf("missing username in POP3 URL: %y", get_safe_url(url)); } if (!exists hurl.password) { @@ -1043,7 +1044,7 @@ public class Pop3Connection inherits ConnectionProvider::AbstractConnectionWithI @throw CONNECTION-OPTION-ERROR missing or invalid connection option */ constructor(string name, string description, string url, hash attributes = {}, hash options = {}) - : AbstractConnectionWithInfo(name, description, url, attributes, options) { + : AbstractConnectionWithInfo(name, description, url, attributes, options) { } #! returns \c "pop3" diff --git a/qlib/RestClient.qm b/qlib/RestClient.qm index 8118274b86..c523393a02 100644 --- a/qlib/RestClient.qm +++ b/qlib/RestClient.qm @@ -1043,6 +1043,8 @@ hash ans = rest.doRequest("DELETE", "/orders/1"); # prepare path preparePath(\path); + on_error rethrow $1.err, sprintf("%s (REST URL %y)", $1.desc, getSafeURL()); + return sendAndDecodeResponse(body, m, path, hdr, \info, decode_errors); } diff --git a/qlib/Swagger.qm b/qlib/Swagger.qm index 94a170dff3..a9eadc88ad 100644 --- a/qlib/Swagger.qm +++ b/qlib/Swagger.qm @@ -896,6 +896,9 @@ public class SwaggerLoader { json = True; } } + on_error { + rethrow $1.err, sprintf("%s (from URL %y)", $1.desc, get_safe_url(url)); + } return SwaggerLoader::fromString(FileLocationHandler::getTextFileFromLocation(url), json); } @@ -930,7 +933,7 @@ public class SwaggerLoader { #! parses the source encoding from the given string static hash parseSchemaSource(string str, string ser) { - hash rv; + auto rv; switch (ser) { case "json": { %ifdef NoJson @@ -957,6 +960,10 @@ public class SwaggerLoader { %endif } } + if (rv.typeCode() != NT_HASH) { + throw "SWAGGER-SCHEMA-ERROR", sprintf("data of type %y provided for OpenAPI / Swagger schema; expecting " + "\"string\"", rv.fullType()); + } return rv; } diff --git a/qlib/Util.qm b/qlib/Util.qm index acc00e6523..632853af52 100644 --- a/qlib/Util.qm +++ b/qlib/Util.qm @@ -1762,7 +1762,7 @@ check_ip_address(str, True); public data sub get_file_from_sftp(string url, string path, *hash options) { string file = basename(path); if (!file) { - throw "GET-FILE-FROM-URL-ERROR", sprintf("missing file name in URL %y", url); + throw "GET-FILE-FROM-URL-ERROR", sprintf("missing file name in URL %y", get_safe_url(url)); } # dynamically load the ssh2 module try { @@ -1794,7 +1794,7 @@ check_ip_address(str, True); public data sub get_file_from_ftp(string url, string path, *hash options) { string file = basename(path); if (!file) { - throw "GET-FILE-FROM-URL-ERROR", sprintf("missing file name in URL %y", url); + throw "GET-FILE-FROM-URL-ERROR", sprintf("missing file name in URL %y", get_safe_url(url)); } FtpClient f(url); f.connect(); @@ -1864,21 +1864,21 @@ check_ip_address(str, True); case /^ftp(s)?$/: { if (!url_info.path.val()) { - throw "GET-FILE-FROM-URL-ERROR", sprintf("missing path in URL %y", url); + throw "GET-FILE-FROM-URL-ERROR", sprintf("missing path in URL %y", get_safe_url(url)); } return get_file_from_ftp(url, url_info.path, options); } case "sftp": { if (!url_info.path.val()) { - throw "GET-FILE-FROM-URL-ERROR", sprintf("missing path in URL %y", url); + throw "GET-FILE-FROM-URL-ERROR", sprintf("missing path in URL %y", get_safe_url(url)); } return get_file_from_sftp(url, url_info.path, options); } default: throw "GET-FILE-FROM-URL-ERROR", sprintf("do not know how to retrieve data with scheme %y given in " - "URL %y", url_info.protocol, url); + "URL %y", url_info.protocol, get_safe_url(url)); } } diff --git a/qlib/WebSocketClient.qm b/qlib/WebSocketClient.qm index 9a92cd8123..d206773576 100644 --- a/qlib/WebSocketClient.qm +++ b/qlib/WebSocketClient.qm @@ -939,7 +939,7 @@ public class WebSocketConnectionObject inherits ConnectionProvider::AbstractConn @throw CONNECTION-OPTION-ERROR missing or invalid connection option */ constructor(string name, string description, string url, hash attributes = {}, hash options = {}) - : AbstractConnectionWithInfo(name, description, url, attributes, options) { + : AbstractConnectionWithInfo(name, description, url, attributes, options) { } #! returns \c "ws" diff --git a/qore.spec-fedora b/qore.spec-fedora index 686f28f86b..8f23357d62 100644 --- a/qore.spec-fedora +++ b/qore.spec-fedora @@ -43,7 +43,7 @@ This package provides the qore library required for all clients using qore functionality. %files -n libqore -%{_libdir}/libqore.so.12.1.2 +%{_libdir}/libqore.so.12.2.0 %{_libdir}/libqore.so.12 %license COPYING.LGPL COPYING.GPL COPYING.MIT README-LICENSE %doc README.md README-MODULES RELEASE-NOTES AUTHORS ABOUT @@ -182,6 +182,7 @@ export QORE_MODULE_DIR=qlib %changelog * Mon Jan 2 2023 David Nichols 1.13.0-1 - updated version to 1.13.0-1 +- updated libqore version to 12.2.0 * Mon Dec 12 2022 David Nichols 1.12.4-1 - updated version to 1.12.4-1 diff --git a/qore.spec-multi b/qore.spec-multi index 916a5df541..597a933b81 100644 --- a/qore.spec-multi +++ b/qore.spec-multi @@ -112,7 +112,7 @@ functionality. %files -n %{libname} %defattr(-,root,root,-) -%{_libdir}/libqore.so.12.1.2 +%{_libdir}/libqore.so.12.2.0 %{_libdir}/libqore.so.12 %doc COPYING.LGPL COPYING.GPL COPYING.MIT README.md README-LICENSE README-MODULES RELEASE-NOTES AUTHORS ABOUT @@ -285,6 +285,7 @@ rm -rf $RPM_BUILD_ROOT %changelog * Mon Jan 2 2023 David Nichols 1.13.0 - updated version to 1.13.0 +- updated libqore version to 12.2.0 * Mon Dec 12 2022 David Nichols 1.12.4 - updated version to 1.12.4 diff --git a/qore.spec-opensuse b/qore.spec-opensuse index d06881aeef..43354186b1 100644 --- a/qore.spec-opensuse +++ b/qore.spec-opensuse @@ -68,7 +68,7 @@ functionality. %files -n libqore12 %defattr(-,root,root,-) -%{_libdir}/libqore.so.12.1.2 +%{_libdir}/libqore.so.12.2.0 %{_libdir}/libqore.so.12 %doc COPYING.LGPL COPYING.GPL COPYING.MIT README-LICENSE