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