Copilot commented on code in PR #414:
URL: https://github.com/apache/commons-jexl/pull/414#discussion_r3894352803
##########
src/main/java/org/apache/commons/jexl3/internal/Interpreter.java:
##########
@@ -1377,12 +1378,47 @@ protected Object visit(final ASTEQSNode node, final
Object data) {
@Override
protected Object visit(final ASTERNode node, final Object data) {
final Object left = node.jjtGetChild(0).jjtAccept(this, data);
- final Object right = node.jjtGetChild(1).jjtAccept(this, data);
+ final JexlNode rightNode = node.jjtGetChild(1);
+ final Object right = resolvePattern(rightNode,
rightNode.jjtAccept(this, data));
// note the arguments inversion between 'in'/'matches' and 'contains'
// if x in y then y contains x
return operators.contains(node, JexlOperator.CONTAINS, right, left);
}
+ /**
+ * If the right operand of {@code =~} / {@code !~} is a string literal,
compile it to a Pattern once and
+ * cache the result in the node's value slot (same mechanism as negated
numeric literals).
+ * Dynamic string values (from variables) are returned unchanged.
+ * The regex string length is validated before compilation (JEXL-security
f012).
+ * Uses double-check locking: first check without lock, then synchronized
recheck-and-set to avoid
+ * holding the lock during expensive Pattern.compile() when the same
compiled script runs concurrently.
+ */
+ private static Object resolvePattern(final JexlNode rightNode, final
Object right) {
+ if (right instanceof CharSequence && rightNode instanceof
JexlNode.Constant) {
+ // First check (volatile read, no lock)
+ Object cached = rightNode.jjtGetValue();
+ if (cached instanceof Pattern) {
+ return cached;
+ }
+ // Compile without holding lock
+ final String regex = right.toString();
+ if (regex.length() > JexlArithmetic.REGEX_PATTERN_MAX_LENGTH) {
+ throw new ArithmeticException("regular expression too long: "
+ regex.length()
+ + " > " + JexlArithmetic.REGEX_PATTERN_MAX_LENGTH);
+ }
+ final Pattern compiled = Pattern.compile(regex);
+ // Double-check and set under lock
+ synchronized (rightNode) {
+ cached = rightNode.jjtGetValue();
+ if (!(cached instanceof Pattern)) {
+ rightNode.jjtSetValue(compiled);
+ }
+ return cached instanceof Pattern ? cached : compiled;
+ }
+ }
+ return right;
+ }
Review Comment:
The comment claims a “volatile read” but `rightNode.jjtGetValue()` is not
guaranteed to provide volatile/visibility semantics. Since the cached `Pattern`
is written under `synchronized (rightNode)` but read outside any
synchronization, this is not a correct double-check pattern and can lead to
repeated compilation and (depending on JexlNode implementation) unsafe
publication concerns. Consider using a thread-safe publication mechanism for
the cached value (e.g., store an `AtomicReference<Pattern>` in the node value,
or ensure both read+write are guarded consistently by the same lock, or use a
dedicated concurrent cache keyed by node identity).
##########
src/main/java/org/apache/commons/jexl3/JexlArithmetic.java:
##########
@@ -2435,4 +2493,42 @@ public Object xor(final Object left, final Object right)
{
final long r = toLong(right);
return l ^ r;
}
+
+ /**
+ * A CharSequence wrapper that throws ArithmeticException if the current
thread is interrupted.
+ * Used as the input to {@code Pattern.matcher()} so that
catastrophic-backtracking regex matches
+ * remain responsive to JEXL cancellation (which sets the thread interrupt
flag).
+ * The interrupt flag is checked every 256 {@code charAt} calls to limit
overhead.
+ */
+ private static final class InterruptibleCharSequence implements
CharSequence {
+ private final String seq;
+ private int count;
+
+ private InterruptibleCharSequence(final String s) {
+ this.seq = s;
+ }
+
+ @Override
+ public char charAt(final int index) {
+ if ((++count & 0xff) == 0 &&
Thread.currentThread().isInterrupted()) {
+ throw new ArithmeticException("Operation interrupted");
+ }
+ return seq.charAt(index);
+ }
+
+ @Override
+ public int length() {
+ return seq.length();
+ }
+
+ @Override
+ public CharSequence subSequence(final int start, final int end) {
+ return seq.subSequence(start, end);
Review Comment:
`InterruptibleCharSequence.subSequence()` returns the underlying `String`
subsequence, which does not perform the interruption checks. If the regex
engine ends up operating on a subsequence (directly or indirectly),
interruption responsiveness can be bypassed. To preserve the guarantee, return
an interruptible view for subsequences as well (e.g., a new wrapper over the
substring / a view that offsets indices) so that `charAt()` always funnels
through the interrupt check.
##########
src/main/java/org/apache/commons/jexl3/JexlArithmetic.java:
##########
@@ -908,6 +934,28 @@ public boolean equals(final Object left, final Object
right) {
return compare(left, right, EQ) == 0;
}
+ /**
+ * Guards a BigInteger result against exceeding the arithmetic context's
precision (JEXL-security f013).
+ * <p>When {@link MathContext#getPrecision()} is zero (unlimited), no
limit is enforced.
+ * Otherwise, the BigInteger must fit within approximately that many
significant decimal digits.</p>
+ *
+ * @param big the value to check
+ * @return big unchanged if within the limit
+ * @throws ArithmeticException when the limit is exceeded
+ */
+ protected BigInteger checkBigIntegerPrecision(final BigInteger big) {
+ final int precision = getMathContext().getPrecision();
+ if (precision > 0) {
+ // precision 0 means unlimited; otherwise, one decimal digit ≈
log2(10) ≈ 10/3 bits
+ final int maxBits = precision * 10 / 3 + 1;
+ if (big.bitLength() > maxBits) {
+ throw new ArithmeticException(
+ "BigInteger precision exceeded: " + big.bitLength() + " bits
for " + precision + "-digit context");
+ }
Review Comment:
`precision * 10 / 3 + 1` is computed in `int` and can overflow for large
`MathContext` precision values, producing an incorrect (possibly negative) bit
limit and breaking the guard. Use `long` (or `Math.multiplyExact`) for the
calculation and clamp to a sensible upper bound before comparing against
`bitLength()`; apply the same treatment anywhere this formula is repeated
(including multiply pre-check and NumberParser digit cap).
##########
src/test/java/org/apache/commons/jexl3/ArithmeticTest.java:
##########
@@ -2409,4 +2409,101 @@ void setOptions(final JexlOptions options) {
assertEquals("zero", jexl.createExpression("array.0").evaluate(jc));
assertEquals("one", jexl.createExpression("array.1").evaluate(jc));
}
+
+ // ----- security fixes f012 / f013 / f014 -----
+
+ /**
+ * f014: BigInteger literal with more digits than MAX_BIGINTEGER_DIGITS
must be rejected at parse time.
+ */
+ @Test
+ void testBigIntegerLiteralTooLong() {
+ // normal H-literal still works
+ assertNotNull(JEXL.createScript("42H"));
+ // a literal just over the cap (256 + 1 digits + 'H') must fail to
parse
+ final char[] digits = new char[256 + 1];
+ java.util.Arrays.fill(digits, '1');
+ final String huge = new String(digits) + "H";
+ assertThrows(JexlException.Parsing.class, () ->
JEXL.createScript(huge));
+ }
+
+ /**
+ * f013: BigInteger arithmetic results that exceed the MathContext
precision must throw.
+ */
+ @Test
+ void testBigIntegerArithmeticPrecisionCap() {
+ // precision=3 caps at ~11 bits (formula: 3 * 10 / 3 + 1 = 11), so
results > 2047 are rejected
+ final JexlArithmetic bounded = new JexlArithmetic(true, new
MathContext(3), JexlArithmetic.BIGD_SCALE);
+ final JexlEngine jexl = new JexlBuilder().arithmetic(bounded).create();
+ // small values are fine
+ assertEquals(new BigInteger("3"), jexl.createScript("a + b", "a", "b")
+ .execute(null, BigInteger.ONE, BigInteger.valueOf(2L)));
+ // result > 2047 is rejected: 1500 + 1000 = 2500, bitLength=12 > 11
+ assertThrows(JexlException.class, () ->
+ jexl.createScript("a + b", "a", "b")
+ .execute(null, BigInteger.valueOf(1500L),
BigInteger.valueOf(1000L)));
+ // multiply pre-check: 64 (7 bits) * 64 (7 bits), sum of operand bits
= 14 > 11
+ assertThrows(JexlException.class, () ->
+ jexl.createScript("a * b", "a", "b")
+ .execute(null, BigInteger.valueOf(64L),
BigInteger.valueOf(64L)));
+ }
+
+ /**
+ * f012: a regex pattern string longer than REGEX_PATTERN_MAX_LENGTH must
throw.
+ */
+ @Test
+ void testRegexPatternTooLong() {
+ final JexlEngine jexl = new JexlBuilder().strict(true).create();
+ final JexlScript script = jexl.createScript("x =~ y", "x", "y");
+ final char[] chars = new char[JexlArithmetic.REGEX_PATTERN_MAX_LENGTH
+ 1];
+ java.util.Arrays.fill(chars, 'a');
+ final String longPattern = new String(chars);
+ assertThrows(JexlException.class, () -> script.execute(null, "abc",
longPattern));
+ }
+
+ /**
+ * f012: regex matching must respond to thread interruption so a
catastrophic-backtracking
+ * pattern does not hang a cancellable engine indefinitely.
+ */
+ @Test
+ void testRegexMatchingInterruptible() throws InterruptedException {
+ final JexlEngine jexl = new JexlBuilder().cancellable(true).create();
+ final JexlScript script = jexl.createScript("x =~ y", "x", "y");
+ // Catastrophic backtracking pattern on non-matching input to force
long character scanning
+ final String evilPattern = "(a+)+b";
+ // Use a larger set of 'a's to extend matching time
+ final char[] chars = new char[50];
+ java.util.Arrays.fill(chars, 'a');
+ final String evilValue = new String(chars) + "c";
+
+ final java.util.concurrent.atomic.AtomicReference<Exception> caught =
+ new java.util.concurrent.atomic.AtomicReference<>();
+ final java.util.concurrent.CountDownLatch started = new
java.util.concurrent.CountDownLatch(1);
+
+ final Thread t = new Thread(() -> {
+ try {
+ started.countDown();
+ script.execute(null, evilValue, evilPattern);
+ } catch (final Exception e) {
+ caught.set(e);
+ }
+ });
+
+ t.start();
+ // Wait for thread to actually start executing
+ started.await();
+ // Give regex matching time to engage (50 'a's with (a+)+b pattern
causes backtracking)
+ Thread.sleep(300);
+ // Interrupt the matching thread
+ t.interrupt();
+ // Wait for thread to complete (should exit promptly if
InterruptibleCharSequence is working)
+ t.join(5000);
+
+ assertFalse(t.isAlive(), "Thread should have completed after
interruption (regex should be interruptible)");
+ // The thread may complete without exception if the regex finishes
faster than interruption catches it,
+ // or it may throw Cancel if interrupted during charset access. Both
are acceptable here.
+ if (caught.get() != null) {
+ assertTrue(caught.get() instanceof JexlException.Cancel,
+ "If interrupted during matching, expected
JexlException.Cancel, got " + caught.get().getClass().getSimpleName());
+ }
Review Comment:
This test is timing-dependent and can pass even if interruptibility is
broken (e.g., if the regex finishes before the interrupt, `caught` remains null
and the test still succeeds). To make this deterministic, prefer asserting that
an interrupt during matching reliably triggers `JexlException.Cancel` (or at
least that an interrupt results in a failure outcome), and avoid fixed sleeps
by using a stronger synchronization signal (e.g., larger input + polling until
the worker thread is inside execution, or repeat until you observe the
cancellation path within a bounded overall timeout).
##########
src/main/java/org/apache/commons/jexl3/parser/Parser.jjt:
##########
@@ -103,6 +103,10 @@ public final class Parser extends JexlParser
JexlInfo ji = et == null ? info : info.at(et.beginLine,
et.beginColumn);
String msg = et == null ? xparse.getMessage() : et.image;
throw new JexlException.Parsing(ji, msg).clean();
+ } catch (NumberFormatException xnfe) {
+ Token et = errorToken(jj_lastpos, jj_scanpos, token.next, token);
+ JexlInfo ji = et == null ? info : info.at(et.beginLine,
et.beginColumn);
+ throw new JexlException.Parsing(ji, xnfe.getMessage()).clean();
Review Comment:
`NumberFormatException.getMessage()` can be null or overly
low-level/implementation-specific. For parse errors, it’s better to ensure a
stable, user-facing message (e.g., using the token image when available, and
falling back to a generic 'invalid number literal' message when the exception
message is null).
##########
src/test/java/org/apache/commons/jexl3/ArithmeticTest.java:
##########
@@ -2409,4 +2409,101 @@ void setOptions(final JexlOptions options) {
assertEquals("zero", jexl.createExpression("array.0").evaluate(jc));
assertEquals("one", jexl.createExpression("array.1").evaluate(jc));
}
+
+ // ----- security fixes f012 / f013 / f014 -----
+
+ /**
+ * f014: BigInteger literal with more digits than MAX_BIGINTEGER_DIGITS
must be rejected at parse time.
+ */
+ @Test
+ void testBigIntegerLiteralTooLong() {
+ // normal H-literal still works
+ assertNotNull(JEXL.createScript("42H"));
+ // a literal just over the cap (256 + 1 digits + 'H') must fail to
parse
+ final char[] digits = new char[256 + 1];
+ java.util.Arrays.fill(digits, '1');
+ final String huge = new String(digits) + "H";
+ assertThrows(JexlException.Parsing.class, () ->
JEXL.createScript(huge));
+ }
+
+ /**
+ * f013: BigInteger arithmetic results that exceed the MathContext
precision must throw.
+ */
+ @Test
+ void testBigIntegerArithmeticPrecisionCap() {
+ // precision=3 caps at ~11 bits (formula: 3 * 10 / 3 + 1 = 11), so
results > 2047 are rejected
+ final JexlArithmetic bounded = new JexlArithmetic(true, new
MathContext(3), JexlArithmetic.BIGD_SCALE);
+ final JexlEngine jexl = new JexlBuilder().arithmetic(bounded).create();
+ // small values are fine
+ assertEquals(new BigInteger("3"), jexl.createScript("a + b", "a", "b")
+ .execute(null, BigInteger.ONE, BigInteger.valueOf(2L)));
+ // result > 2047 is rejected: 1500 + 1000 = 2500, bitLength=12 > 11
+ assertThrows(JexlException.class, () ->
+ jexl.createScript("a + b", "a", "b")
+ .execute(null, BigInteger.valueOf(1500L),
BigInteger.valueOf(1000L)));
+ // multiply pre-check: 64 (7 bits) * 64 (7 bits), sum of operand bits
= 14 > 11
+ assertThrows(JexlException.class, () ->
+ jexl.createScript("a * b", "a", "b")
+ .execute(null, BigInteger.valueOf(64L),
BigInteger.valueOf(64L)));
+ }
+
+ /**
+ * f012: a regex pattern string longer than REGEX_PATTERN_MAX_LENGTH must
throw.
+ */
+ @Test
+ void testRegexPatternTooLong() {
+ final JexlEngine jexl = new JexlBuilder().strict(true).create();
+ final JexlScript script = jexl.createScript("x =~ y", "x", "y");
+ final char[] chars = new char[JexlArithmetic.REGEX_PATTERN_MAX_LENGTH
+ 1];
+ java.util.Arrays.fill(chars, 'a');
+ final String longPattern = new String(chars);
+ assertThrows(JexlException.class, () -> script.execute(null, "abc",
longPattern));
+ }
+
+ /**
+ * f012: regex matching must respond to thread interruption so a
catastrophic-backtracking
+ * pattern does not hang a cancellable engine indefinitely.
+ */
+ @Test
+ void testRegexMatchingInterruptible() throws InterruptedException {
+ final JexlEngine jexl = new JexlBuilder().cancellable(true).create();
+ final JexlScript script = jexl.createScript("x =~ y", "x", "y");
+ // Catastrophic backtracking pattern on non-matching input to force
long character scanning
+ final String evilPattern = "(a+)+b";
+ // Use a larger set of 'a's to extend matching time
+ final char[] chars = new char[50];
+ java.util.Arrays.fill(chars, 'a');
+ final String evilValue = new String(chars) + "c";
+
+ final java.util.concurrent.atomic.AtomicReference<Exception> caught =
+ new java.util.concurrent.atomic.AtomicReference<>();
+ final java.util.concurrent.CountDownLatch started = new
java.util.concurrent.CountDownLatch(1);
+
+ final Thread t = new Thread(() -> {
+ try {
+ started.countDown();
+ script.execute(null, evilValue, evilPattern);
+ } catch (final Exception e) {
+ caught.set(e);
+ }
+ });
+
+ t.start();
+ // Wait for thread to actually start executing
+ started.await();
+ // Give regex matching time to engage (50 'a's with (a+)+b pattern
causes backtracking)
+ Thread.sleep(300);
+ // Interrupt the matching thread
+ t.interrupt();
+ // Wait for thread to complete (should exit promptly if
InterruptibleCharSequence is working)
+ t.join(5000);
+
+ assertFalse(t.isAlive(), "Thread should have completed after
interruption (regex should be interruptible)");
+ // The thread may complete without exception if the regex finishes
faster than interruption catches it,
+ // or it may throw Cancel if interrupted during charset access. Both
are acceptable here.
+ if (caught.get() != null) {
+ assertTrue(caught.get() instanceof JexlException.Cancel,
+ "If interrupted during matching, expected
JexlException.Cancel, got " + caught.get().getClass().getSimpleName());
+ }
Review Comment:
This test is timing-dependent and can pass even if interruptibility is
broken (e.g., if the regex finishes before the interrupt, `caught` remains null
and the test still succeeds). To make this deterministic, prefer asserting that
an interrupt during matching reliably triggers `JexlException.Cancel` (or at
least that an interrupt results in a failure outcome), and avoid fixed sleeps
by using a stronger synchronization signal (e.g., larger input + polling until
the worker thread is inside execution, or repeat until you observe the
cancellation path within a bounded overall timeout).
##########
src/test/java/org/apache/commons/jexl3/ArithmeticTest.java:
##########
@@ -2409,4 +2409,101 @@ void setOptions(final JexlOptions options) {
assertEquals("zero", jexl.createExpression("array.0").evaluate(jc));
assertEquals("one", jexl.createExpression("array.1").evaluate(jc));
}
+
+ // ----- security fixes f012 / f013 / f014 -----
+
+ /**
+ * f014: BigInteger literal with more digits than MAX_BIGINTEGER_DIGITS
must be rejected at parse time.
+ */
+ @Test
+ void testBigIntegerLiteralTooLong() {
+ // normal H-literal still works
+ assertNotNull(JEXL.createScript("42H"));
+ // a literal just over the cap (256 + 1 digits + 'H') must fail to
parse
+ final char[] digits = new char[256 + 1];
Review Comment:
The test hard-codes the digit cap as 256, which will become brittle if the
cap is changed (or if parsing behavior becomes engine-precision-dependent).
Consider deriving this from a single source of truth (e.g., a constant that
tests can access, or constructing an engine/thread-engine context with known
precision and computing the expected max digit count from that) so the test
remains stable across future adjustments.
##########
src/test/java/org/apache/commons/jexl3/ArithmeticTest.java:
##########
@@ -2409,4 +2409,101 @@ void setOptions(final JexlOptions options) {
assertEquals("zero", jexl.createExpression("array.0").evaluate(jc));
assertEquals("one", jexl.createExpression("array.1").evaluate(jc));
}
+
+ // ----- security fixes f012 / f013 / f014 -----
+
+ /**
+ * f014: BigInteger literal with more digits than MAX_BIGINTEGER_DIGITS
must be rejected at parse time.
+ */
+ @Test
+ void testBigIntegerLiteralTooLong() {
+ // normal H-literal still works
+ assertNotNull(JEXL.createScript("42H"));
+ // a literal just over the cap (256 + 1 digits + 'H') must fail to
parse
+ final char[] digits = new char[256 + 1];
+ java.util.Arrays.fill(digits, '1');
+ final String huge = new String(digits) + "H";
+ assertThrows(JexlException.Parsing.class, () ->
JEXL.createScript(huge));
+ }
+
+ /**
+ * f013: BigInteger arithmetic results that exceed the MathContext
precision must throw.
+ */
+ @Test
+ void testBigIntegerArithmeticPrecisionCap() {
+ // precision=3 caps at ~11 bits (formula: 3 * 10 / 3 + 1 = 11), so
results > 2047 are rejected
+ final JexlArithmetic bounded = new JexlArithmetic(true, new
MathContext(3), JexlArithmetic.BIGD_SCALE);
+ final JexlEngine jexl = new JexlBuilder().arithmetic(bounded).create();
+ // small values are fine
+ assertEquals(new BigInteger("3"), jexl.createScript("a + b", "a", "b")
+ .execute(null, BigInteger.ONE, BigInteger.valueOf(2L)));
+ // result > 2047 is rejected: 1500 + 1000 = 2500, bitLength=12 > 11
+ assertThrows(JexlException.class, () ->
+ jexl.createScript("a + b", "a", "b")
+ .execute(null, BigInteger.valueOf(1500L),
BigInteger.valueOf(1000L)));
+ // multiply pre-check: 64 (7 bits) * 64 (7 bits), sum of operand bits
= 14 > 11
+ assertThrows(JexlException.class, () ->
+ jexl.createScript("a * b", "a", "b")
+ .execute(null, BigInteger.valueOf(64L),
BigInteger.valueOf(64L)));
+ }
+
+ /**
+ * f012: a regex pattern string longer than REGEX_PATTERN_MAX_LENGTH must
throw.
+ */
+ @Test
+ void testRegexPatternTooLong() {
+ final JexlEngine jexl = new JexlBuilder().strict(true).create();
+ final JexlScript script = jexl.createScript("x =~ y", "x", "y");
+ final char[] chars = new char[JexlArithmetic.REGEX_PATTERN_MAX_LENGTH
+ 1];
+ java.util.Arrays.fill(chars, 'a');
+ final String longPattern = new String(chars);
+ assertThrows(JexlException.class, () -> script.execute(null, "abc",
longPattern));
+ }
+
+ /**
+ * f012: regex matching must respond to thread interruption so a
catastrophic-backtracking
+ * pattern does not hang a cancellable engine indefinitely.
+ */
+ @Test
+ void testRegexMatchingInterruptible() throws InterruptedException {
+ final JexlEngine jexl = new JexlBuilder().cancellable(true).create();
+ final JexlScript script = jexl.createScript("x =~ y", "x", "y");
+ // Catastrophic backtracking pattern on non-matching input to force
long character scanning
+ final String evilPattern = "(a+)+b";
+ // Use a larger set of 'a's to extend matching time
+ final char[] chars = new char[50];
+ java.util.Arrays.fill(chars, 'a');
+ final String evilValue = new String(chars) + "c";
+
+ final java.util.concurrent.atomic.AtomicReference<Exception> caught =
+ new java.util.concurrent.atomic.AtomicReference<>();
+ final java.util.concurrent.CountDownLatch started = new
java.util.concurrent.CountDownLatch(1);
+
+ final Thread t = new Thread(() -> {
+ try {
+ started.countDown();
+ script.execute(null, evilValue, evilPattern);
+ } catch (final Exception e) {
+ caught.set(e);
+ }
+ });
+
+ t.start();
+ // Wait for thread to actually start executing
+ started.await();
+ // Give regex matching time to engage (50 'a's with (a+)+b pattern
causes backtracking)
+ Thread.sleep(300);
+ // Interrupt the matching thread
+ t.interrupt();
+ // Wait for thread to complete (should exit promptly if
InterruptibleCharSequence is working)
+ t.join(5000);
+
+ assertFalse(t.isAlive(), "Thread should have completed after
interruption (regex should be interruptible)");
+ // The thread may complete without exception if the regex finishes
faster than interruption catches it,
+ // or it may throw Cancel if interrupted during charset access. Both
are acceptable here.
+ if (caught.get() != null) {
+ assertTrue(caught.get() instanceof JexlException.Cancel,
+ "If interrupted during matching, expected
JexlException.Cancel, got " + caught.get().getClass().getSimpleName());
+ }
Review Comment:
This test is timing-dependent and can pass even if interruptibility is
broken (e.g., if the regex finishes before the interrupt, `caught` remains null
and the test still succeeds). To make this deterministic, prefer asserting that
an interrupt during matching reliably triggers `JexlException.Cancel` (or at
least that an interrupt results in a failure outcome), and avoid fixed sleeps
by using a stronger synchronization signal (e.g., larger input + polling until
the worker thread is inside execution, or repeat until you observe the
cancellation path within a bounded overall timeout).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]