summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/doc-docbook/spec.xfpt21
-rw-r--r--doc/doc-txt/NewStuff7
-rw-r--r--src/src/expand.c37
-rw-r--r--src/src/functions.h2
-rw-r--r--src/src/host.c67
-rw-r--r--test/scripts/0000-Basic/000211
-rw-r--r--test/stdout/000211
7 files changed, 148 insertions, 8 deletions
diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt
index 101df6b90..54480ee8d 100644
--- a/doc/doc-docbook/spec.xfpt
+++ b/doc/doc-docbook/spec.xfpt
@@ -10107,6 +10107,27 @@ as is, and other byte values are converted to &`\xNN`&, for example a
byte value 127 is converted to &`\x7f`&.
+.new
+.vitem &*${ipv6denorm:*&<&'string'&>&*}*&
+.cindex "&%ipv6denorm%& expansion item"
+.cindex "IP address" normalisation
+This expands an IPv6 address to a full eight-element colon-separated set
+of hex digits including leading zeroes.
+A trailing ipv4-style dotted-decimal set is converted to hex.
+Pure IPv4 addresses are converted to IPv4-mapped IPv6.
+
+.vitem &*${ipv6norm:*&<&'string'&>&*}*&
+.cindex "&%ipv6norm%& expansion item"
+.cindex "IP address" normalisation
+.cindex "IP address" "canonical form"
+This converts an IPv6 address to canonical form.
+Leading zeroes of groups are omitted, and the longest
+set of zero-valued groups is replaced with a double colon.
+A trailing ipv4-style dotted-decimal set is converted to hex.
+Pure IPv4 addresses are converted to IPv4-mapped IPv6.
+.wen
+
+
.vitem &*${lc:*&<&'string'&>&*}*&
.cindex "case forcing in strings"
.cindex "string" "case forcing"
diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff
index a7185a4e2..ca65c98ea 100644
--- a/doc/doc-txt/NewStuff
+++ b/doc/doc-txt/NewStuff
@@ -17,6 +17,13 @@ Version 4.87
3. Transports now take a "max_parallel" option, to limit concurrency.
+ 4. Expansion operators ${ipv6norm:<string>} and ${ipv6denorm:<string>}.
+ The latter expands to a 8-element colon-sep set of hex digits including
+ leading zeroes. A trailing ipv4-style dotted-decimal set is converted
+ to hex. Pure ipv4 addresses are converted to IPv4-mapped IPv6.
+ The former operator strips leading zeroes and collapses the longest
+ set of 0-groups to a double-colon.
+
Version 4.86
------------
diff --git a/src/src/expand.c b/src/src/expand.c
index fbbc68154..90ffe78c0 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -210,6 +210,8 @@ static uschar *op_table_main[] = {
US"hash",
US"hex2b64",
US"hexquote",
+ US"ipv6denorm",
+ US"ipv6norm",
US"l",
US"lc",
US"length",
@@ -248,6 +250,8 @@ enum {
EOP_HASH,
EOP_HEX2B64,
EOP_HEXQUOTE,
+ EOP_IPV6DENORM,
+ EOP_IPV6NORM,
EOP_L,
EOP_LC,
EOP_LENGTH,
@@ -6406,6 +6410,39 @@ while (*s != 0)
continue;
}
+ case EOP_IPV6NORM:
+ case EOP_IPV6DENORM:
+ {
+ int type = string_is_ip_address(sub, NULL);
+ int binary[4];
+ uschar buffer[44];
+
+ switch (type)
+ {
+ case 6:
+ (void) host_aton(sub, binary);
+ break;
+
+ case 4: /* convert to IPv4-mapped IPv6 */
+ binary[0] = binary[1] = 0;
+ binary[2] = 0x0000ffff;
+ (void) host_aton(sub, binary+3);
+ break;
+
+ case 0:
+ expand_string_message =
+ string_sprintf("\"%s\" is not an IP address", sub);
+ goto EXPAND_FAILED;
+ }
+
+ yield = string_cat(yield, &size, &ptr, buffer,
+ c == EOP_IPV6NORM
+ ? ipv6_nmtoa(binary, buffer)
+ : host_nmtoa(4, binary, -1, buffer, ':')
+ );
+ continue;
+ }
+
case EOP_ADDRESS:
case EOP_LOCAL_PART:
case EOP_DOMAIN:
diff --git a/src/src/functions.h b/src/src/functions.h
index 2fe681394..9df8030f5 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -233,6 +233,8 @@ extern int ip_tcpsocket(const uschar *, uschar **, int);
extern int ip_unixsocket(const uschar *, uschar **);
extern int ip_streamsocket(const uschar *, uschar **, int);
+extern int ipv6_nmtoa(int *, uschar *);
+
extern uschar *local_part_quote(uschar *);
extern int log_create(uschar *);
extern int log_create_as_exim(uschar *);
diff --git a/src/src/host.c b/src/src/host.c
index 429b6352c..67d33a9b8 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -1146,20 +1146,14 @@ if (count == 1)
{
j = binary[0];
for (i = 24; i >= 0; i -= 8)
- {
- sprintf(CS tt, "%d.", (j >> i) & 255);
- while (*tt) tt++;
- }
+ tt += sprintf(CS tt, "%d.", (j >> i) & 255);
}
else
- {
for (i = 0; i < 4; i++)
{
j = binary[i];
- sprintf(CS tt, "%04x%c%04x%c", (j >> 16) & 0xffff, sep, j & 0xffff, sep);
- while (*tt) tt++;
+ tt += sprintf(CS tt, "%04x%c%04x%c", (j >> 16) & 0xffff, sep, j & 0xffff, sep);
}
- }
tt--; /* lose final separator */
@@ -1175,6 +1169,63 @@ return tt - buffer;
}
+/* Like host_nmtoa() but: ipv6-only, canonical output, no mask
+
+Arguments:
+ binary points to the ints
+ buffer big enough to hold the result
+
+Returns: the number of characters placed in buffer, not counting
+ the final nul.
+*/
+
+int
+ipv6_nmtoa(int * binary, uschar * buffer)
+{
+int i, j, k;
+uschar * c = buffer;
+uschar * d;
+
+for (i = 0; i < 4; i++)
+ { /* expand to text */
+ j = binary[i];
+ c += sprintf(CS c, "%x:%x:", (j >> 16) & 0xffff, j & 0xffff);
+ }
+
+for (c = buffer, k = -1, i = 0; i < 8; i++)
+ { /* find longest 0-group sequence */
+ if (*c == '0') /* must be "0:" */
+ {
+ uschar * s = c;
+ j = i;
+ while (c[2] == '0') i++, c += 2;
+ if (i-j > k)
+ {
+ k = i-j; /* length of sequence */
+ d = s; /* start of sequence */
+ }
+ }
+ while (*++c != ':') ;
+ c++;
+ }
+
+c[-1] = '\0'; /* drop trailing colon */
+
+/* debug_printf("%s: D k %d <%s> <%s>\n", __FUNCTION__, k, d, d + 2*(k+1)); */
+if (k >= 0)
+ { /* collapse */
+ c = d + 2*(k+1);
+ if (d == buffer) c--; /* need extra colon */
+ *d++ = ':'; /* 1st 0 */
+ while (*d++ = *c++) ;
+ }
+else
+ d = c;
+
+return d - buffer;
+}
+
+
/*************************************************
* Check port for tls_on_connect *
diff --git a/test/scripts/0000-Basic/0002 b/test/scripts/0000-Basic/0002
index 9afb556a3..bf2c1068c 100644
--- a/test/scripts/0000-Basic/0002
+++ b/test/scripts/0000-Basic/0002
@@ -208,6 +208,17 @@ mask: ${mask:192.168.10.206/33}
mask: ${mask:192.168.10.206/0}
mask: ${mask:192.168.10.206}
mask: ${mask:a.b.c.d}
+ipv6denorm: ${ipv6denorm:::1}
+ipv6denorm: ${ipv6denorm:fe00::1}
+ipv6denorm: ${ipv6denorm:192.168.0.1}
+ipv6denorm: ${ipv6denorm:fe80::192.168.0.1}
+ipv6norm: ${ipv6norm:0:0:0::1}
+ipv6norm: ${ipv6norm:2a00::0}
+ipv6norm: ${ipv6norm:2a00::1}
+ipv6norm: ${ipv6norm:2a00:eadf:0000:0000:0000:0000:0001:0000}
+ipv6norm: ${ipv6norm:2a00:eadf:0000:0001:0000:0000:0000:0000}
+ipv6norm: ${ipv6norm:2a00:0:0:0::}
+ipv6norm: ${ipv6norm:2a00:2:3:4:5:6:7:8}
nhash: ${nhash_24:monty} ${nhash_8_63:monty python}
lc/uc: ${lc:The Quick} ${uc: Brown Fox}
length: ${length_10:The quick brown fox} ${l_10:abc}
diff --git a/test/stdout/0002 b/test/stdout/0002
index 9a3219d59..3018dce1f 100644
--- a/test/stdout/0002
+++ b/test/stdout/0002
@@ -189,6 +189,17 @@
> mask: 0.0.0.0/0
> Failed: missing mask value in "192.168.10.206"
> Failed: "a.b.c.d" is not an IP address
+> ipv6denorm: 0000:0000:0000:0000:0000:0000:0000:0001
+> ipv6denorm: fe00:0000:0000:0000:0000:0000:0000:0001
+> ipv6denorm: 0000:0000:0000:0000:0000:ffff:c0a8:0001
+> ipv6denorm: fe80:0000:0000:0000:0000:0000:c0a8:0001
+> ipv6norm: ::1
+> ipv6norm: 2a00::
+> ipv6norm: 2a00::1
+> ipv6norm: 2a00:eadf::1:0
+> ipv6norm: 2a00:eadf:0:1::
+> ipv6norm: 2a00::
+> ipv6norm: 2a00:2:3:4:5:6:7:8
> nhash: 19 0/61
> lc/uc: the quick BROWN FOX
> length: The quick abc