From: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
To: pdm-devel@lists.proxmox.com
Cc: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
Subject: [PATCH proxmox 3/4] api-types: add a scheme generic URL regex
Date: Thu, 13 Aug 2026 17:05:50 +0200 [thread overview]
Message-ID: <20260813150551.415237-4-t.ellmenreich@proxmox.com> (raw)
In-Reply-To: <20260813150551.415237-1-t.ellmenreich@proxmox.com>
The current URL regex, HTTP_URL_REGEX, is specific to the HTTP and HTTPS
schemes. This patch creates a new regex that is scheme-generic and thus
allows any other valid scheme.
It also adds tests that cover some basic URL edge cases.
Signed-off-by: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
---
pbs-api-types/src/lib.rs | 1 +
proxmox-schema/src/api_types.rs | 40 ++++++++++++++++++++++++++++++++-
2 files changed, 40 insertions(+), 1 deletion(-)
diff --git a/pbs-api-types/src/lib.rs b/pbs-api-types/src/lib.rs
index 21ef733d..924f356b 100644
--- a/pbs-api-types/src/lib.rs
+++ b/pbs-api-types/src/lib.rs
@@ -44,6 +44,7 @@ pub use proxmox_schema::api_types::HTTP_URL_SCHEMA;
pub use proxmox_schema::api_types::MULTI_LINE_COMMENT_SCHEMA;
pub use proxmox_schema::api_types::NODE_SCHEMA;
pub use proxmox_schema::api_types::SINGLE_LINE_COMMENT_FORMAT;
+pub use proxmox_schema::api_types::URL_SCHEMA;
pub use proxmox_schema::api_types::{
BLOCKDEVICE_DISK_AND_PARTITION_NAME_SCHEMA, BLOCKDEVICE_NAME_SCHEMA,
};
diff --git a/proxmox-schema/src/api_types.rs b/proxmox-schema/src/api_types.rs
index 81878dbe..05e50f26 100644
--- a/proxmox-schema/src/api_types.rs
+++ b/proxmox-schema/src/api_types.rs
@@ -50,6 +50,10 @@ pub const CIDR_V6_REGEX_STR: &str = concatcp!(r"(?:", IPV6RE_STR, r"/\d{1,3})$")
#[rustfmt::skip]
pub const SAFE_ID_REGEX_STR: &str = r"(?:[A-Za-z0-9_][A-Za-z0-9._\-]*)";
+/// Regular expression string to match allowed URL schemes.
+#[rustfmt::skip]
+pub const URL_SCHEME_REGEX_STR: &str = r"(?:[a-zA-Z][a-zA-Z0-9\+.\-]*)";
+
/// Regular expression string to match a host, optional port and optionally a
/// path, although the path remains completely unvalidated except a few
/// prohibited characters.
@@ -113,6 +117,12 @@ const_regex! {
pub DNS_NAME_OR_IP_REGEX = concatcp!(r"^(?:", DNS_NAME_STR, "|", IPRE_STR, r")$");
pub HOST_PORT_REGEX = concatcp!(r"^(?:", DNS_NAME_STR, "|", IPRE_BRACKET_STR, "):", PORT_REGEX_STR ,"$");
+ /// Matches URL constructed out of a scheme, host, port, and path. With
+ /// the host possibly being an IPv[4|6] address and the port being
+ /// optional. The path remains completely unvalidated except for a few
+ /// prohibited characters.
+ pub URL_REGEX = concatcp!(r"^", URL_SCHEME_REGEX_STR, "://", URL_HOST_PORT_PATH_REGEX_STR, "$");
+
/// A specialisation of [`URL_REGEX`] regex that only allows http and
/// https as the URL scheme
pub HTTP_URL_REGEX = concatcp!(r"^https?://", URL_HOST_PORT_PATH_REGEX_STR, "$");
@@ -166,6 +176,7 @@ pub const SYSTEMD_DATETIME_FORMAT: ApiStringFormat =
pub const HOSTNAME_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&HOSTNAME_REGEX);
pub const HOST_PORT_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&HOST_PORT_REGEX);
pub const HTTP_URL_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&HTTP_URL_REGEX);
+pub const URL_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&URL_REGEX);
pub const DNS_ALIAS_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&DNS_ALIAS_REGEX);
pub const DNS_NAME_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&DNS_NAME_REGEX);
@@ -243,6 +254,10 @@ pub const PORT_SCHEMA: Schema = IntegerSchema::new("Node port")
.maximum(65535)
.schema();
+pub const URL_SCHEMA: Schema = StringSchema::new("Url with optional port.")
+ .format(&URL_FORMAT)
+ .schema();
+
pub const HTTP_URL_SCHEMA: Schema = StringSchema::new("HTTP(s) url with optional port.")
.format(&HTTP_URL_FORMAT)
.schema();
@@ -286,7 +301,7 @@ pub const DISK_LIST_SCHEMA: Schema = StringSchema::new("A list of disk names, co
mod tests {
use super::{
CIDR_REGEX, CIDR_V4_REGEX, CIDR_V6_REGEX, ED25519_BASE64_KEY_REGEX, IP_BRACKET_REGEX,
- IP_REGEX, IP_V4_REGEX, IP_V6_REGEX,
+ IP_REGEX, IP_V4_REGEX, IP_V6_REGEX, URL_REGEX,
};
#[test]
@@ -327,4 +342,27 @@ mod tests {
// 33 bytes of data
assert!(!ED25519_BASE64_KEY_REGEX.is_match("IiC3Nkh4Fn2ukUZUNmdK5K5CWO53Zmk/eGlKO4m6aCD/"));
}
+
+ #[test]
+ fn test_url_regex_on_valid_urls() {
+ assert!(URL_REGEX.is_match("https://www.example.com/index.html"));
+ assert!(URL_REGEX.is_match("https://[2001:db8::1]/docs/%E2%9C%93.html?lang=en"));
+ assert!(URL_REGEX.is_match("ftp://ftp.example.com/pub/archive/file.zip"));
+ assert!(URL_REGEX.is_match("file://test.com/home/alice/documents/report.pdf"));
+ assert!(URL_REGEX.is_match("https://example.com:443/a%20file.html?name=John%20Doe#top"));
+ }
+
+ #[test]
+ fn test_url_regex_on_invalid_urls() {
+ // is missing the ':' after the scheme
+ assert!(!URL_REGEX.is_match("https//www.example.com/index.html"));
+ // has two port numbers
+ assert!(!URL_REGEX.is_match("http://example.com:80:90/page"));
+ // only has one '/' after the scheme
+ assert!(!URL_REGEX.is_match("ftp:/ftp.example.com/file.txt"));
+ // has userinfo (technically correct, but not accepted by the regex)
+ assert!(!URL_REGEX.is_match("mailto:alice@example.com"));
+ // missing closing angle ']' brachet
+ assert!(!URL_REGEX.is_match("https://[2001:db8::1/path"));
+ }
}
--
2.47.3
next prev parent reply other threads:[~2026-08-13 15:06 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-13 15:05 [PATCH datacenter-manager/proxmox 0/4] fix #7747: OpenID: allow non HTTP scheme in redirect URL Thomas Ellmenreich
2026-08-13 15:05 ` [PATCH proxmox 1/4] api-types: reorganise unit tests Thomas Ellmenreich
2026-08-13 15:05 ` [PATCH proxmox 2/4] api-types: refactor HTTP_URL_REGEX construction Thomas Ellmenreich
2026-08-13 15:05 ` Thomas Ellmenreich [this message]
2026-08-13 15:05 ` [PATCH datacenter-manager 4/4] fix #7747: openid: allow non HTTP schemes in redirect URL Thomas Ellmenreich
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260813150551.415237-4-t.ellmenreich@proxmox.com \
--to=t.ellmenreich@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox