Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions libpromises/evalfunction.c
Original file line number Diff line number Diff line change
Expand Up @@ -6571,6 +6571,48 @@ static FnCallResult FnCallIsIpInSubnet(ARG_UNUSED EvalContext *ctx,
Log(LOG_LEVEL_DEBUG, "%s(%s): no match", fp->name, range);
return FnReturnContext(false);
}

/*********************************************************************/

static FnCallResult FnCallIsValidIP(ARG_UNUSED EvalContext *ctx,
ARG_UNUSED const Policy *policy,
const FnCall *fp, const Rlist *finalargs)
{
assert(fp != NULL);

if (finalargs == NULL ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we write/use a function that makes this expression more obvious to it's meaning? I know the message after is pretty clear but reading ->next and ->next->next is kind of non-obvious at a glance.

(finalargs->next != NULL && finalargs->next->next != NULL))
{
Log(LOG_LEVEL_ERR, "Function '%s' requires one or two arguments", fp->name);
return FnFailure();
}

const char *address = RlistScalarValue(finalargs);
const char *version = (finalargs->next != NULL)
? RlistScalarValue(finalargs->next)
: NULL;

if (version != NULL &&
!StringEqual(version, "ipv4") && !StringEqual(version, "ipv6"))
{
Log(LOG_LEVEL_ERR,
"Function '%s': invalid IP version '%s', expected 'ipv4' or 'ipv6'",
fp->name, version);
return FnFailure();
}

const bool check_ipv4 = (version == NULL || StringEqual(version, "ipv4"));
const bool check_ipv6 = (version == NULL || StringEqual(version, "ipv6"));

unsigned char binary[sizeof(struct in6_addr)];
const bool valid =
(check_ipv4 && inet_pton(AF_INET, address, binary) == 1) ||
(check_ipv6 && inet_pton(AF_INET6, address, binary) == 1);

Log(LOG_LEVEL_DEBUG, "%s(%s): %s", fp->name, address, valid ? "valid" : "invalid");
return FnReturnContext(valid);
}

/*********************************************************************/

static FnCallResult FnCallHostRange(ARG_UNUSED EvalContext *ctx, ARG_UNUSED const Policy *policy, ARG_UNUSED const FnCall *fp, const Rlist *finalargs)
Expand Down Expand Up @@ -11098,6 +11140,13 @@ static const FnCallArg IPRANGE_ARGS[] =
{NULL, CF_DATA_TYPE_NONE, NULL}
};

static const FnCallArg ISVALIDIP_ARGS[] =
{
{CF_ANYSTRING, CF_DATA_TYPE_STRING, "String to validate as an IP address"},
{"ipv4,ipv6", CF_DATA_TYPE_OPTION, "Only accept this IP version"},
{NULL, CF_DATA_TYPE_NONE, NULL}
};

static const FnCallArg IRANGE_ARGS[] =
{
{CF_INTRANGE, CF_DATA_TYPE_INT, "Integer start of range"},
Expand Down Expand Up @@ -12004,6 +12053,8 @@ const FnCallType CF_FNCALL_TYPES[] =
FNCALL_OPTION_VARARG, FNCALL_CATEGORY_COMM, SYNTAX_STATUS_NORMAL, ARGC(1, -1)),
FnCallTypeNew("isipinsubnet", CF_DATA_TYPE_CONTEXT, IPRANGE_ARGS, &FnCallIsIpInSubnet, "True if an IP address specified in arg2, arg3, ... lies in the range of IP addresses specified in arg1",
FNCALL_OPTION_VARARG, FNCALL_CATEGORY_COMM, SYNTAX_STATUS_NORMAL, ARGC(1, -1)),
FnCallTypeNew("isvalidip", CF_DATA_TYPE_CONTEXT, ISVALIDIP_ARGS, &FnCallIsValidIP, "True if arg1 is a valid IPv4 or IPv6 address (only the IP version arg2, if given)",
FNCALL_OPTION_VARARG, FNCALL_CATEGORY_COMM, SYNTAX_STATUS_NORMAL, ARGC(1, 2)),
FnCallTypeNew("irange", CF_DATA_TYPE_INT_RANGE, IRANGE_ARGS, &FnCallIRange, "Define a range of integer values for cfengine internal use",
FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC),
FnCallTypeNew("isconnectable", CF_DATA_TYPE_CONTEXT, ISCONNECTABLE_ARGS, &FnCallIsConnectable, "Check if a port is connectable",
Expand Down
109 changes: 109 additions & 0 deletions tests/acceptance/02_classes/02_functions/isvalidip.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#######################################################
#
# Test isvalidip()
#
#######################################################
body common control
{
inputs => { "../../default.sub.cf" };
bundlesequence => { default("$(this.promise_filename)") };
version => "1.0";
}

#######################################################
bundle agent test
{
meta:
"description" -> { "CFE-3478" }
string => "Test that isvalidip() validates IPv4 and IPv6 addresses";

vars:
"valid"
slist => {
"1.2.3.4",
"127.0.0.1",
"5.9.10.22",
"0.0.0.0",
"255.255.255.255",
"FE80:CD00:0000:0CDE:1257:0000:211E:729C",
"fe80:cd00:0:cde:1257:0:211e:729c",
"2001:db8::1",
"::1",
"::",
"::ffff:192.0.2.128",
};

"invalid"
slist => {
"",
"10510.17.10",
"100",
"1.2.3",
"1.2.3.4.5",
"233.444.23.1",
"300.123.123.1",
"1.2.3.4/24",
"1.2.3.4:80",
" 1.2.3.4",
"1.2.3.4 ",
"localhost",
"[::1]",
"[::1]:80",
"1::2::3",
"12345::1",
"g::1",
"1:2:3:4:5:6:7:8:9",
"2001:db8::/32",
};

"valid_ipv4" slist => { "1.2.3.4", "255.255.255.255" };

"invalid_ipv4"
slist => {
"::1",
"::ffff:192.0.2.128",
"FE80:CD00:0000:0CDE:1257:0000:211E:729C",
"300.123.123.1",
};

"valid_ipv6"
slist => {
"::1", "::ffff:192.0.2.128", "FE80:CD00:0000:0CDE:1257:0000:211E:729C",
};

"invalid_ipv6" slist => { "1.2.3.4", "255.255.255.255", "12345::1" };
}

#######################################################
bundle agent check
{
classes:
"fail" not => isvalidip("$(test.valid)");
"fail" expression => isvalidip("$(test.invalid)");
"fail" not => isvalidip("$(test.valid_ipv4)", "ipv4");
"fail" expression => isvalidip("$(test.invalid_ipv4)", "ipv4");
"fail" not => isvalidip("$(test.valid_ipv6)", "ipv6");
"fail" expression => isvalidip("$(test.invalid_ipv6)", "ipv6");

reports:
"Wrongly rejected '$(test.valid)'" unless => isvalidip("$(test.valid)");
"Wrongly accepted '$(test.invalid)'" if => isvalidip("$(test.invalid)");

"Wrongly rejected '$(test.valid_ipv4)' as IPv4"
unless => isvalidip("$(test.valid_ipv4)", "ipv4");

"Wrongly accepted '$(test.invalid_ipv4)' as IPv4"
if => isvalidip("$(test.invalid_ipv4)", "ipv4");

"Wrongly rejected '$(test.valid_ipv6)' as IPv6"
unless => isvalidip("$(test.valid_ipv6)", "ipv6");

"Wrongly accepted '$(test.invalid_ipv6)' as IPv6"
if => isvalidip("$(test.invalid_ipv6)", "ipv6");

!fail::
"$(this.promise_filename) Pass";

fail::
"$(this.promise_filename) FAIL";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#######################################################
#
# Test that isvalidip() rejects an unknown IP version
#
#######################################################
body common control
{
inputs => { "../../default.sub.cf" };
bundlesequence => { default("$(this.promise_filename)") };
version => "1.0";
}

#######################################################
bundle agent test
{
meta:
"description" -> { "CFE-3478" }
string => "Test that isvalidip() fails on an IP version other than ipv4 or ipv6";

classes:
"valid" expression => isvalidip("1.2.3.4", "ipv5");
}

#######################################################
bundle agent check
{
reports:
"$(this.promise_filename) Pass";
}
Loading