Merge "Mirroring servlet-spec 4.0.1 at 5574e9b"
diff --git a/third_party/jetty-http/pom.xml b/third_party/jetty-http/pom.xml
new file mode 100644
index 0000000..945e0b2
--- /dev/null
+++ b/third_party/jetty-http/pom.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>jetty-project</artifactId>
+ <groupId>org.eclipse.jetty</groupId>
+ <version>9.4.44.v20210927</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>jetty-http</artifactId>
+ <name>Jetty :: Http Utility</name>
+ <properties>
+ <bundle-symbolic-name>${project.groupId}.http</bundle-symbolic-name>
+ <spotbugs.onlyAnalyze>org.eclipse.jetty.http.*</spotbugs.onlyAnalyze>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-util</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-io</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.toolchain</groupId>
+ <artifactId>jetty-test-helper</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Require-Capability>osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.http.HttpFieldPreEncoder)";resolution:=optional;cardinality:=multiple, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional</Require-Capability>
+ <Provide-Capability>osgi.serviceloader; osgi.serviceloader=org.eclipse.jetty.http.HttpFieldPreEncoder</Provide-Capability>
+ </instructions>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>test-jar</id>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <finalName>${jmhjar.name}</finalName>
+ <shadeTestJar>true</shadeTestJar>
+ <artifactSet>
+ <includes>
+ <include>org.openjdk.jmh:jmh-core</include>
+ </includes>
+ </artifactSet>
+ <transformers>
+ <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+ <mainClass>org.openjdk.jmh.Main</mainClass>
+ </transformer>
+ </transformers>
+ <filters>
+ <filter>
+ <artifact>org.openjdk.jmh:jmh-core</artifact>
+ <includes>
+ <include>**</include>
+ </includes>
+ </filter>
+ <filter>
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>META-INF/*.SF</exclude>
+ <exclude>META-INF/*.DSA</exclude>
+ <exclude>META-INF/*.RSA</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/BadMessageException.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/BadMessageException.java
new file mode 100644
index 0000000..b24f80b
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/BadMessageException.java
@@ -0,0 +1,73 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+/**
+ * <p>Exception thrown to indicate a Bad HTTP Message has either been received
+ * or attempted to be generated. Typically these are handled with either 400
+ * or 500 responses.</p>
+ */
+@SuppressWarnings("serial")
+public class BadMessageException extends RuntimeException
+{
+ final int _code;
+ final String _reason;
+
+ public BadMessageException()
+ {
+ this(400, null);
+ }
+
+ public BadMessageException(int code)
+ {
+ this(code, null);
+ }
+
+ public BadMessageException(String reason)
+ {
+ this(400, reason);
+ }
+
+ public BadMessageException(String reason, Throwable cause)
+ {
+ this(400, reason, cause);
+ }
+
+ public BadMessageException(int code, String reason)
+ {
+ this(code, reason, null);
+ }
+
+ public BadMessageException(int code, String reason, Throwable cause)
+ {
+ super(code + ": " + reason, cause);
+ _code = code;
+ _reason = reason;
+ }
+
+ public int getCode()
+ {
+ return _code;
+ }
+
+ public String getReason()
+ {
+ return _reason;
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/CompressedContentFormat.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/CompressedContentFormat.java
new file mode 100644
index 0000000..ce0620d
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/CompressedContentFormat.java
@@ -0,0 +1,165 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.Objects;
+
+import org.eclipse.jetty.util.QuotedStringTokenizer;
+import org.eclipse.jetty.util.StringUtil;
+
+public class CompressedContentFormat
+{
+ /**
+ * The separator within an etag used to indicate a compressed variant. By default the separator is "--"
+ * So etag for compressed resource that normally has an etag of <code>W/"28c772d6"</code>
+ * is <code>W/"28c772d6--gzip"</code>. The separator may be changed by the
+ * "org.eclipse.jetty.http.CompressedContentFormat.ETAG_SEPARATOR" System property. If changed, it should be changed to a string
+ * that will not be found in a normal etag or at least is very unlikely to be a substring of a normal etag.
+ */
+ public static final String ETAG_SEPARATOR = System.getProperty(CompressedContentFormat.class.getName() + ".ETAG_SEPARATOR", "--");
+
+ public static final CompressedContentFormat GZIP = new CompressedContentFormat("gzip", ".gz");
+ public static final CompressedContentFormat BR = new CompressedContentFormat("br", ".br");
+ public static final CompressedContentFormat[] NONE = new CompressedContentFormat[0];
+
+ private final String _encoding;
+ private final String _extension;
+ private final String _etagSuffix;
+ private final String _etagSuffixQuote;
+ private final PreEncodedHttpField _contentEncoding;
+
+ public CompressedContentFormat(String encoding, String extension)
+ {
+ _encoding = StringUtil.asciiToLowerCase(encoding);
+ _extension = StringUtil.asciiToLowerCase(extension);
+ _etagSuffix = StringUtil.isEmpty(ETAG_SEPARATOR) ? "" : (ETAG_SEPARATOR + _encoding);
+ _etagSuffixQuote = _etagSuffix + "\"";
+ _contentEncoding = new PreEncodedHttpField(HttpHeader.CONTENT_ENCODING, _encoding);
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (!(o instanceof CompressedContentFormat))
+ return false;
+ CompressedContentFormat ccf = (CompressedContentFormat)o;
+ return Objects.equals(_encoding, ccf._encoding) && Objects.equals(_extension, ccf._extension);
+ }
+
+ public String getEncoding()
+ {
+ return _encoding;
+ }
+
+ public String getExtension()
+ {
+ return _extension;
+ }
+
+ public String getEtagSuffix()
+ {
+ return _etagSuffix;
+ }
+
+ public HttpField getContentEncoding()
+ {
+ return _contentEncoding;
+ }
+
+ /** Get an etag with suffix that represents this compressed type.
+ * @param etag An etag
+ * @return An etag with compression suffix, or the etag itself if no suffix is configured.
+ */
+ public String etag(String etag)
+ {
+ if (StringUtil.isEmpty(ETAG_SEPARATOR))
+ return etag;
+ int end = etag.length() - 1;
+ if (etag.charAt(end) == '"')
+ return etag.substring(0, end) + _etagSuffixQuote;
+ return etag + _etagSuffix;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(_encoding, _extension);
+ }
+
+ /** Check etags for equality, accounting for quoting and compression suffixes.
+ * @param etag An etag without a compression suffix
+ * @param etagWithSuffix An etag optionally with a compression suffix.
+ * @return True if the tags are equal.
+ */
+ public static boolean tagEquals(String etag, String etagWithSuffix)
+ {
+ // Handle simple equality
+ if (etag.equals(etagWithSuffix))
+ return true;
+
+ // If no separator defined, then simple equality is only possible positive
+ if (StringUtil.isEmpty(ETAG_SEPARATOR))
+ return false;
+
+ // Are both tags quoted?
+ boolean etagQuoted = etag.endsWith("\"");
+ boolean etagSuffixQuoted = etagWithSuffix.endsWith("\"");
+
+ // Look for a separator
+ int separator = etagWithSuffix.lastIndexOf(ETAG_SEPARATOR);
+
+ // If both tags are quoted the same (the norm) then any difference must be the suffix
+ if (etagQuoted == etagSuffixQuoted)
+ return separator > 0 && etag.regionMatches(0, etagWithSuffix, 0, separator);
+
+ // If either tag is weak then we can't match because weak tags must be quoted
+ if (etagWithSuffix.startsWith("W/") || etag.startsWith("W/"))
+ return false;
+
+ // compare unquoted strong etags
+ etag = etagQuoted ? QuotedStringTokenizer.unquote(etag) : etag;
+ etagWithSuffix = etagSuffixQuoted ? QuotedStringTokenizer.unquote(etagWithSuffix) : etagWithSuffix;
+ separator = etagWithSuffix.lastIndexOf(ETAG_SEPARATOR);
+ if (separator > 0)
+ return etag.regionMatches(0, etagWithSuffix, 0, separator);
+
+ return Objects.equals(etag, etagWithSuffix);
+ }
+
+ public String stripSuffixes(String etagsList)
+ {
+ if (StringUtil.isEmpty(ETAG_SEPARATOR))
+ return etagsList;
+
+ // This is a poor implementation that ignores list and tag structure
+ while (true)
+ {
+ int i = etagsList.lastIndexOf(_etagSuffix);
+ if (i < 0)
+ return etagsList;
+ etagsList = etagsList.substring(0, i) + etagsList.substring(i + _etagSuffix.length());
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return _encoding;
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java
new file mode 100644
index 0000000..b6b640b
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java
@@ -0,0 +1,28 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+/**
+ * The compliance for Cookie handling.
+ */
+public enum CookieCompliance
+{
+ RFC6265,
+ RFC2965
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/DateGenerator.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/DateGenerator.java
new file mode 100644
index 0000000..4a38a1b
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/DateGenerator.java
@@ -0,0 +1,177 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import org.eclipse.jetty.util.StringUtil;
+
+/**
+ * ThreadLocal Date formatters for HTTP style dates.
+ */
+public class DateGenerator
+{
+ private static final TimeZone __GMT = TimeZone.getTimeZone("GMT");
+
+ static
+ {
+ __GMT.setID("GMT");
+ }
+
+ static final String[] DAYS =
+ {"Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
+ static final String[] MONTHS =
+ {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Jan"};
+
+ private static final ThreadLocal<DateGenerator> __dateGenerator = new ThreadLocal<DateGenerator>()
+ {
+ @Override
+ protected DateGenerator initialValue()
+ {
+ return new DateGenerator();
+ }
+ };
+
+ public static final String __01Jan1970 = DateGenerator.formatDate(0);
+
+ /**
+ * Format HTTP date "EEE, dd MMM yyyy HH:mm:ss 'GMT'"
+ *
+ * @param date the date in milliseconds
+ * @return the formatted date
+ */
+ public static String formatDate(long date)
+ {
+ return __dateGenerator.get().doFormatDate(date);
+ }
+
+ /**
+ * Format "EEE, dd-MMM-yyyy HH:mm:ss 'GMT'" for cookies
+ *
+ * @param buf the buffer to put the formatted date into
+ * @param date the date in milliseconds
+ */
+ public static void formatCookieDate(StringBuilder buf, long date)
+ {
+ __dateGenerator.get().doFormatCookieDate(buf, date);
+ }
+
+ /**
+ * Format "EEE, dd-MMM-yyyy HH:mm:ss 'GMT'" for cookies
+ *
+ * @param date the date in milliseconds
+ * @return the formatted date
+ */
+ public static String formatCookieDate(long date)
+ {
+ StringBuilder buf = new StringBuilder(28);
+ formatCookieDate(buf, date);
+ return buf.toString();
+ }
+
+ private final StringBuilder buf = new StringBuilder(32);
+ private final GregorianCalendar gc = new GregorianCalendar(__GMT);
+
+ /**
+ * Format HTTP date "EEE, dd MMM yyyy HH:mm:ss 'GMT'"
+ *
+ * @param date the date in milliseconds
+ * @return the formatted date
+ */
+ public String doFormatDate(long date)
+ {
+ buf.setLength(0);
+ gc.setTimeInMillis(date);
+
+ int dayOfWeek = gc.get(Calendar.DAY_OF_WEEK);
+ int dayOfMonth = gc.get(Calendar.DAY_OF_MONTH);
+ int month = gc.get(Calendar.MONTH);
+ int year = gc.get(Calendar.YEAR);
+ int century = year / 100;
+ year = year % 100;
+
+ int hours = gc.get(Calendar.HOUR_OF_DAY);
+ int minutes = gc.get(Calendar.MINUTE);
+ int seconds = gc.get(Calendar.SECOND);
+
+ buf.append(DAYS[dayOfWeek]);
+ buf.append(',');
+ buf.append(' ');
+ StringUtil.append2digits(buf, dayOfMonth);
+
+ buf.append(' ');
+ buf.append(MONTHS[month]);
+ buf.append(' ');
+ StringUtil.append2digits(buf, century);
+ StringUtil.append2digits(buf, year);
+
+ buf.append(' ');
+ StringUtil.append2digits(buf, hours);
+ buf.append(':');
+ StringUtil.append2digits(buf, minutes);
+ buf.append(':');
+ StringUtil.append2digits(buf, seconds);
+ buf.append(" GMT");
+ return buf.toString();
+ }
+
+ /**
+ * Format "EEE, dd-MMM-yy HH:mm:ss 'GMT'" for cookies
+ *
+ * @param buf the buffer to format the date into
+ * @param date the date in milliseconds
+ */
+ public void doFormatCookieDate(StringBuilder buf, long date)
+ {
+ gc.setTimeInMillis(date);
+
+ int dayOfWeek = gc.get(Calendar.DAY_OF_WEEK);
+ int dayOfMonth = gc.get(Calendar.DAY_OF_MONTH);
+ int month = gc.get(Calendar.MONTH);
+ int year = gc.get(Calendar.YEAR);
+ year = year % 10000;
+
+ int epoch = (int)((date / 1000) % (60 * 60 * 24));
+ int seconds = epoch % 60;
+ epoch = epoch / 60;
+ int minutes = epoch % 60;
+ int hours = epoch / 60;
+
+ buf.append(DAYS[dayOfWeek]);
+ buf.append(',');
+ buf.append(' ');
+ StringUtil.append2digits(buf, dayOfMonth);
+
+ buf.append('-');
+ buf.append(MONTHS[month]);
+ buf.append('-');
+ StringUtil.append2digits(buf, year / 100);
+ StringUtil.append2digits(buf, year % 100);
+
+ buf.append(' ');
+ StringUtil.append2digits(buf, hours);
+ buf.append(':');
+ StringUtil.append2digits(buf, minutes);
+ buf.append(':');
+ StringUtil.append2digits(buf, seconds);
+ buf.append(" GMT");
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/DateParser.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/DateParser.java
new file mode 100644
index 0000000..879674a
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/DateParser.java
@@ -0,0 +1,109 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * ThreadLocal data parsers for HTTP style dates
+ */
+public class DateParser
+{
+ private static final TimeZone __GMT = TimeZone.getTimeZone("GMT");
+
+ static
+ {
+ __GMT.setID("GMT");
+ }
+
+ static final String[] __dateReceiveFmt =
+ {
+ "EEE, dd MMM yyyy HH:mm:ss zzz",
+ "EEE, dd-MMM-yy HH:mm:ss",
+ "EEE MMM dd HH:mm:ss yyyy",
+
+ "EEE, dd MMM yyyy HH:mm:ss", "EEE dd MMM yyyy HH:mm:ss zzz",
+ "EEE dd MMM yyyy HH:mm:ss", "EEE MMM dd yyyy HH:mm:ss zzz", "EEE MMM dd yyyy HH:mm:ss",
+ "EEE MMM-dd-yyyy HH:mm:ss zzz", "EEE MMM-dd-yyyy HH:mm:ss", "dd MMM yyyy HH:mm:ss zzz",
+ "dd MMM yyyy HH:mm:ss", "dd-MMM-yy HH:mm:ss zzz", "dd-MMM-yy HH:mm:ss", "MMM dd HH:mm:ss yyyy zzz",
+ "MMM dd HH:mm:ss yyyy", "EEE MMM dd HH:mm:ss yyyy zzz",
+ "EEE, MMM dd HH:mm:ss yyyy zzz", "EEE, MMM dd HH:mm:ss yyyy", "EEE, dd-MMM-yy HH:mm:ss zzz",
+ "EEE dd-MMM-yy HH:mm:ss zzz", "EEE dd-MMM-yy HH:mm:ss"
+ };
+
+ public static long parseDate(String date)
+ {
+ return __dateParser.get().parse(date);
+ }
+
+ private static final ThreadLocal<DateParser> __dateParser = new ThreadLocal<DateParser>()
+ {
+ @Override
+ protected DateParser initialValue()
+ {
+ return new DateParser();
+ }
+ };
+
+ final SimpleDateFormat[] _dateReceive = new SimpleDateFormat[__dateReceiveFmt.length];
+
+ private long parse(final String dateVal)
+ {
+ for (int i = 0; i < _dateReceive.length; i++)
+ {
+ if (_dateReceive[i] == null)
+ {
+ _dateReceive[i] = new SimpleDateFormat(__dateReceiveFmt[i], Locale.US);
+ _dateReceive[i].setTimeZone(__GMT);
+ }
+
+ try
+ {
+ Date date = (Date)_dateReceive[i].parseObject(dateVal);
+ return date.getTime();
+ }
+ catch (java.lang.Exception e)
+ {
+ // LOG.ignore(e);
+ }
+ }
+
+ if (dateVal.endsWith(" GMT"))
+ {
+ final String val = dateVal.substring(0, dateVal.length() - 4);
+
+ for (SimpleDateFormat element : _dateReceive)
+ {
+ try
+ {
+ Date date = (Date)element.parseObject(val);
+ return date.getTime();
+ }
+ catch (java.lang.Exception e)
+ {
+ // LOG.ignore(e);
+ }
+ }
+ }
+ return -1;
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/GZIPContentDecoder.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/GZIPContentDecoder.java
new file mode 100644
index 0000000..1426e68
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/GZIPContentDecoder.java
@@ -0,0 +1,459 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+import java.util.zip.ZipException;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.component.Destroyable;
+
+/**
+ * <p>Decoder for the "gzip" content encoding.</p>
+ * <p>This decoder inflates gzip compressed data, and has
+ * been optimized for async usage with minimal data copies.</p>
+ */
+public class GZIPContentDecoder implements Destroyable
+{
+ // Unsigned Integer Max == 2^32
+ private static final long UINT_MAX = 0xFFFFFFFFL;
+
+ private final List<ByteBuffer> _inflateds = new ArrayList<>();
+ private final Inflater _inflater = new Inflater(true);
+ private final ByteBufferPool _pool;
+ private final int _bufferSize;
+ private State _state;
+ private int _size;
+ private long _value;
+ private byte _flags;
+ private ByteBuffer _inflated;
+
+ public GZIPContentDecoder()
+ {
+ this(null, 2048);
+ }
+
+ public GZIPContentDecoder(int bufferSize)
+ {
+ this(null, bufferSize);
+ }
+
+ public GZIPContentDecoder(ByteBufferPool pool, int bufferSize)
+ {
+ _bufferSize = bufferSize;
+ _pool = pool;
+ reset();
+ }
+
+ /**
+ * <p>Inflates compressed data from a buffer.</p>
+ * <p>The buffers returned by this method should be released
+ * via {@link #release(ByteBuffer)}.</p>
+ * <p>This method may fully consume the input buffer, but return
+ * only a chunk of the inflated bytes, to allow applications to
+ * consume the inflated chunk before performing further inflation,
+ * applying backpressure. In this case, this method should be
+ * invoked again with the same input buffer (even if
+ * it's already fully consumed) and that will produce another
+ * chunk of inflated bytes. Termination happens when the input
+ * buffer is fully consumed, and the returned buffer is empty.</p>
+ * <p>See {@link #decodedChunk(ByteBuffer)} to perform inflating
+ * in a non-blocking way that allows to apply backpressure.</p>
+ *
+ * @param compressed the buffer containing compressed data.
+ * @return a buffer containing inflated data.
+ */
+ public ByteBuffer decode(ByteBuffer compressed)
+ {
+ decodeChunks(compressed);
+
+ if (_inflateds.isEmpty())
+ {
+ if (BufferUtil.isEmpty(_inflated) || _state == State.CRC || _state == State.ISIZE)
+ return BufferUtil.EMPTY_BUFFER;
+ ByteBuffer result = _inflated;
+ _inflated = null;
+ return result;
+ }
+ else
+ {
+ _inflateds.add(_inflated);
+ _inflated = null;
+ int length = _inflateds.stream().mapToInt(Buffer::remaining).sum();
+ ByteBuffer result = acquire(length);
+ for (ByteBuffer buffer : _inflateds)
+ {
+ BufferUtil.append(result, buffer);
+ release(buffer);
+ }
+ _inflateds.clear();
+ return result;
+ }
+ }
+
+ /**
+ * <p>Called when a chunk of data is inflated.</p>
+ * <p>The default implementation aggregates all the chunks
+ * into a single buffer returned from {@link #decode(ByteBuffer)}.</p>
+ * <p>Derived implementations may choose to consume inflated chunks
+ * individually and return {@code true} from this method to prevent
+ * further inflation until a subsequent call to {@link #decode(ByteBuffer)}
+ * or {@link #decodeChunks(ByteBuffer)} is made.
+ *
+ * @param chunk the inflated chunk of data
+ * @return false if inflating should continue, or true if the call
+ * to {@link #decodeChunks(ByteBuffer)} or {@link #decode(ByteBuffer)}
+ * should return, allowing to consume the inflated chunk and apply
+ * backpressure
+ */
+ protected boolean decodedChunk(ByteBuffer chunk)
+ {
+ if (_inflated == null)
+ {
+ _inflated = chunk;
+ }
+ else
+ {
+ if (BufferUtil.space(_inflated) >= chunk.remaining())
+ {
+ BufferUtil.append(_inflated, chunk);
+ release(chunk);
+ }
+ else
+ {
+ _inflateds.add(_inflated);
+ _inflated = chunk;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * <p>Inflates compressed data.</p>
+ * <p>Inflation continues until the compressed block end is reached, there is no
+ * more compressed data or a call to {@link #decodedChunk(ByteBuffer)} returns true.</p>
+ *
+ * @param compressed the buffer of compressed data to inflate
+ */
+ protected void decodeChunks(ByteBuffer compressed)
+ {
+ ByteBuffer buffer = null;
+ try
+ {
+ while (true)
+ {
+ switch (_state)
+ {
+ case INITIAL:
+ {
+ _state = State.ID;
+ break;
+ }
+
+ case FLAGS:
+ {
+ if ((_flags & 0x04) == 0x04)
+ {
+ _state = State.EXTRA_LENGTH;
+ _size = 0;
+ _value = 0;
+ }
+ else if ((_flags & 0x08) == 0x08)
+ _state = State.NAME;
+ else if ((_flags & 0x10) == 0x10)
+ _state = State.COMMENT;
+ else if ((_flags & 0x2) == 0x2)
+ {
+ _state = State.HCRC;
+ _size = 0;
+ _value = 0;
+ }
+ else
+ {
+ _state = State.DATA;
+ continue;
+ }
+ break;
+ }
+
+ case DATA:
+ {
+ while (true)
+ {
+ if (buffer == null)
+ buffer = acquire(_bufferSize);
+
+ try
+ {
+ int length = _inflater.inflate(buffer.array(), buffer.arrayOffset(), buffer.capacity());
+ buffer.limit(length);
+ }
+ catch (DataFormatException x)
+ {
+ throw new ZipException(x.getMessage());
+ }
+
+ if (buffer.hasRemaining())
+ {
+ ByteBuffer chunk = buffer;
+ buffer = null;
+ if (decodedChunk(chunk))
+ return;
+ }
+ else if (_inflater.needsInput())
+ {
+ if (!compressed.hasRemaining())
+ return;
+ if (compressed.hasArray())
+ {
+ _inflater.setInput(compressed.array(), compressed.arrayOffset() + compressed.position(), compressed.remaining());
+ compressed.position(compressed.limit());
+ }
+ else
+ {
+ // TODO use the pool
+ byte[] input = new byte[compressed.remaining()];
+ compressed.get(input);
+ _inflater.setInput(input);
+ }
+ }
+ else if (_inflater.finished())
+ {
+ int remaining = _inflater.getRemaining();
+ compressed.position(compressed.limit() - remaining);
+ _state = State.CRC;
+ _size = 0;
+ _value = 0;
+ break;
+ }
+ }
+ continue;
+ }
+
+ default:
+ break;
+ }
+
+ if (!compressed.hasRemaining())
+ break;
+
+ byte currByte = compressed.get();
+ switch (_state)
+ {
+ case ID:
+ {
+ _value += (currByte & 0xFF) << 8 * _size;
+ ++_size;
+ if (_size == 2)
+ {
+ if (_value != 0x8B1F)
+ throw new ZipException("Invalid gzip bytes");
+ _state = State.CM;
+ }
+ break;
+ }
+ case CM:
+ {
+ if ((currByte & 0xFF) != 0x08)
+ throw new ZipException("Invalid gzip compression method");
+ _state = State.FLG;
+ break;
+ }
+ case FLG:
+ {
+ _flags = currByte;
+ _state = State.MTIME;
+ _size = 0;
+ _value = 0;
+ break;
+ }
+ case MTIME:
+ {
+ // Skip the 4 MTIME bytes
+ ++_size;
+ if (_size == 4)
+ _state = State.XFL;
+ break;
+ }
+ case XFL:
+ {
+ // Skip XFL
+ _state = State.OS;
+ break;
+ }
+ case OS:
+ {
+ // Skip OS
+ _state = State.FLAGS;
+ break;
+ }
+ case EXTRA_LENGTH:
+ {
+ _value += (currByte & 0xFF) << 8 * _size;
+ ++_size;
+ if (_size == 2)
+ _state = State.EXTRA;
+ break;
+ }
+ case EXTRA:
+ {
+ // Skip EXTRA bytes
+ --_value;
+ if (_value == 0)
+ {
+ // Clear the EXTRA flag and loop on the flags
+ _flags &= ~0x04;
+ _state = State.FLAGS;
+ }
+ break;
+ }
+ case NAME:
+ {
+ // Skip NAME bytes
+ if (currByte == 0)
+ {
+ // Clear the NAME flag and loop on the flags
+ _flags &= ~0x08;
+ _state = State.FLAGS;
+ }
+ break;
+ }
+ case COMMENT:
+ {
+ // Skip COMMENT bytes
+ if (currByte == 0)
+ {
+ // Clear the COMMENT flag and loop on the flags
+ _flags &= ~0x10;
+ _state = State.FLAGS;
+ }
+ break;
+ }
+ case HCRC:
+ {
+ // Skip HCRC
+ ++_size;
+ if (_size == 2)
+ {
+ // Clear the HCRC flag and loop on the flags
+ _flags &= ~0x02;
+ _state = State.FLAGS;
+ }
+ break;
+ }
+ case CRC:
+ {
+ _value += (currByte & 0xFF) << 8 * _size;
+ ++_size;
+ if (_size == 4)
+ {
+ // From RFC 1952, compliant decoders need not to verify the CRC
+ _state = State.ISIZE;
+ _size = 0;
+ _value = 0;
+ }
+ break;
+ }
+ case ISIZE:
+ {
+ _value = _value | ((currByte & 0xFFL) << (8 * _size));
+ ++_size;
+ if (_size == 4)
+ {
+ // RFC 1952: Section 2.3.1; ISIZE is the input size modulo 2^32
+ if (_value != (_inflater.getBytesWritten() & UINT_MAX))
+ throw new ZipException("Invalid input size");
+
+ // TODO ByteBuffer result = output == null ? BufferUtil.EMPTY_BUFFER : ByteBuffer.wrap(output);
+ reset();
+ return;
+ }
+ break;
+ }
+ default:
+ throw new ZipException();
+ }
+ }
+ }
+ catch (ZipException x)
+ {
+ throw new RuntimeException(x);
+ }
+ finally
+ {
+ if (buffer != null)
+ release(buffer);
+ }
+ }
+
+ private void reset()
+ {
+ _inflater.reset();
+ _state = State.INITIAL;
+ _size = 0;
+ _value = 0;
+ _flags = 0;
+ }
+
+ @Override
+ public void destroy()
+ {
+ _inflater.end();
+ }
+
+ public boolean isFinished()
+ {
+ return _state == State.INITIAL;
+ }
+
+ private enum State
+ {
+ INITIAL, ID, CM, FLG, MTIME, XFL, OS, FLAGS, EXTRA_LENGTH, EXTRA, NAME, COMMENT, HCRC, DATA, CRC, ISIZE
+ }
+
+ /**
+ * @param capacity capacity of the ByteBuffer to acquire
+ * @return a heap buffer of the configured capacity either from the pool or freshly allocated.
+ */
+ public ByteBuffer acquire(int capacity)
+ {
+ return _pool == null ? BufferUtil.allocate(capacity) : _pool.acquire(capacity, false);
+ }
+
+ /**
+ * <p>Releases an allocated buffer.</p>
+ * <p>This method calls {@link ByteBufferPool#release(ByteBuffer)} if a buffer pool has
+ * been configured.</p>
+ * <p>This method should be called once for all buffers returned from {@link #decode(ByteBuffer)}
+ * or passed to {@link #decodedChunk(ByteBuffer)}.</p>
+ *
+ * @param buffer the buffer to release.
+ */
+ public void release(ByteBuffer buffer)
+ {
+ if (_pool != null && !BufferUtil.isTheEmptyBuffer(buffer))
+ _pool.release(buffer);
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java
new file mode 100644
index 0000000..2182373
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java
@@ -0,0 +1,96 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import org.eclipse.jetty.util.HostPort;
+
+/**
+ * An HttpField holding a preparsed Host and port number
+ *
+ * @see HostPort
+ */
+public class HostPortHttpField extends HttpField
+{
+ final HostPort _hostPort;
+
+ public HostPortHttpField(String authority)
+ {
+ this(HttpHeader.HOST, HttpHeader.HOST.asString(), authority);
+ }
+
+ protected HostPortHttpField(HttpHeader header, String name, String authority)
+ {
+ super(header, name, authority);
+ try
+ {
+ _hostPort = new HostPort(authority);
+ }
+ catch (Exception e)
+ {
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad HostPort", e);
+ }
+ }
+
+ public HostPortHttpField(String host, int port)
+ {
+ this(new HostPort(host, port));
+ }
+
+ public HostPortHttpField(HostPort hostport)
+ {
+ super(HttpHeader.HOST, HttpHeader.HOST.asString(), hostport.toString());
+ _hostPort = hostport;
+ }
+
+ /**
+ * Get the host.
+ *
+ * @return the host
+ */
+ public String getHost()
+ {
+ return _hostPort.getHost();
+ }
+
+ /**
+ * Get the port.
+ *
+ * @return the port
+ */
+ public int getPort()
+ {
+ return _hostPort.getPort();
+ }
+
+ /**
+ * Get the port.
+ *
+ * @param defaultPort The default port to return if no port set
+ * @return the port
+ */
+ public int getPort(int defaultPort)
+ {
+ return _hostPort.getPort(defaultPort);
+ }
+
+ public HostPort getHostPort()
+ {
+ return _hostPort;
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java
new file mode 100644
index 0000000..5f40032
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java
@@ -0,0 +1,67 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.Arrays;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+/**
+ *
+ */
+public class Http1FieldPreEncoder implements HttpFieldPreEncoder
+{
+
+ /**
+ * @see org.eclipse.jetty.http.HttpFieldPreEncoder#getHttpVersion()
+ */
+ @Override
+ public HttpVersion getHttpVersion()
+ {
+ return HttpVersion.HTTP_1_0;
+ }
+
+ /**
+ * @see org.eclipse.jetty.http.HttpFieldPreEncoder#getEncodedField(org.eclipse.jetty.http.HttpHeader, java.lang.String, java.lang.String)
+ */
+ @Override
+ public byte[] getEncodedField(HttpHeader header, String headerString, String value)
+ {
+ if (header != null)
+ {
+ int cbl = header.getBytesColonSpace().length;
+ byte[] bytes = Arrays.copyOf(header.getBytesColonSpace(), cbl + value.length() + 2);
+ System.arraycopy(value.getBytes(ISO_8859_1), 0, bytes, cbl, value.length());
+ bytes[bytes.length - 2] = (byte)'\r';
+ bytes[bytes.length - 1] = (byte)'\n';
+ return bytes;
+ }
+
+ byte[] n = headerString.getBytes(ISO_8859_1);
+ byte[] v = value.getBytes(ISO_8859_1);
+ byte[] bytes = Arrays.copyOf(n, n.length + 2 + v.length + 2);
+ bytes[n.length] = (byte)':';
+ bytes[n.length + 1] = (byte)' ';
+ System.arraycopy(v, 0, bytes, n.length + 2, v.length);
+ bytes[bytes.length - 2] = (byte)'\r';
+ bytes[bytes.length - 1] = (byte)'\n';
+
+ return bytes;
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java
new file mode 100644
index 0000000..319b4af
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java
@@ -0,0 +1,260 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * HTTP compliance modes for Jetty HTTP parsing and handling.
+ * A Compliance mode consists of a set of {@link HttpComplianceSection}s which are applied
+ * when the mode is enabled.
+ * <p>
+ * Currently the set of modes is an enum and cannot be dynamically extended, but future major releases may convert this
+ * to a class. To modify modes there are four custom modes that can be modified by setting the property
+ * <code>org.eclipse.jetty.http.HttpCompliance.CUSTOMn</code> (where 'n' is '0', '1', '2' or '3'), to a comma separated
+ * list of sections. The list should start with one of the following strings:<dl>
+ * <dt>0</dt><dd>No {@link HttpComplianceSection}s</dd>
+ * <dt>*</dt><dd>All {@link HttpComplianceSection}s</dd>
+ * <dt>RFC2616</dt><dd>The set of {@link HttpComplianceSection}s application to https://tools.ietf.org/html/rfc2616,
+ * but not https://tools.ietf.org/html/rfc7230</dd>
+ * <dt>RFC7230</dt><dd>The set of {@link HttpComplianceSection}s application to https://tools.ietf.org/html/rfc7230</dd>
+ * </dl>
+ * The remainder of the list can contain then names of {@link HttpComplianceSection}s to include them in the mode, or prefixed
+ * with a '-' to exclude thm from the mode. Note that Jetty's modes may have some historic minor differences from the strict
+ * RFC compliance, for example the <code>RFC2616_LEGACY</code> HttpCompliance is defined as
+ * <code>RFC2616,-FIELD_COLON,-METHOD_CASE_SENSITIVE</code>.
+ * <p>
+ * Note also that the {@link EnumSet} return by {@link HttpCompliance#sections()} is mutable, so that modes may
+ * be altered in code and will affect all usages of the mode.
+ */
+public enum HttpCompliance // TODO in Jetty-10 convert this enum to a class so that extra custom modes can be defined dynamically
+{
+ /**
+ * A Legacy compliance mode to match jetty's behavior prior to RFC2616 and RFC7230.
+ */
+ LEGACY(sectionsBySpec("0,METHOD_CASE_SENSITIVE")),
+
+ /**
+ * The legacy RFC2616 support, which excludes
+ * {@link HttpComplianceSection#METHOD_CASE_SENSITIVE},
+ * {@link HttpComplianceSection#FIELD_COLON},
+ * {@link HttpComplianceSection#TRANSFER_ENCODING_WITH_CONTENT_LENGTH},
+ * {@link HttpComplianceSection#MULTIPLE_CONTENT_LENGTHS},
+ * {@link HttpComplianceSection#NO_AMBIGUOUS_PATH_SEGMENTS} and
+ * {@link HttpComplianceSection#NO_AMBIGUOUS_PATH_SEPARATORS}.
+ */
+ RFC2616_LEGACY(sectionsBySpec("RFC2616,-FIELD_COLON,-METHOD_CASE_SENSITIVE,-TRANSFER_ENCODING_WITH_CONTENT_LENGTH,-MULTIPLE_CONTENT_LENGTHS")),
+
+ /**
+ * The strict RFC2616 support mode
+ */
+ RFC2616(sectionsBySpec("RFC2616")),
+
+ /**
+ * Jetty's legacy RFC7230 support, which excludes
+ * {@link HttpComplianceSection#METHOD_CASE_SENSITIVE}.
+ */
+ RFC7230_LEGACY(sectionsBySpec("RFC7230,-METHOD_CASE_SENSITIVE")),
+
+ /**
+ * The RFC7230 support mode
+ */
+ RFC7230(sectionsBySpec("RFC7230")),
+
+ /**
+ * The RFC7230 support mode with no ambiguous URIs
+ */
+ RFC7230_NO_AMBIGUOUS_URIS(sectionsBySpec("RFC7230,NO_AMBIGUOUS_PATH_SEGMENTS,NO_AMBIGUOUS_PATH_SEPARATORS")),
+
+ /**
+ * Custom compliance mode that can be defined with System property <code>org.eclipse.jetty.http.HttpCompliance.CUSTOM0</code>
+ */
+ @Deprecated
+ CUSTOM0(sectionsByProperty("CUSTOM0")),
+ /**
+ * Custom compliance mode that can be defined with System property <code>org.eclipse.jetty.http.HttpCompliance.CUSTOM1</code>
+ */
+ @Deprecated
+ CUSTOM1(sectionsByProperty("CUSTOM1")),
+ /**
+ * Custom compliance mode that can be defined with System property <code>org.eclipse.jetty.http.HttpCompliance.CUSTOM2</code>
+ */
+ @Deprecated
+ CUSTOM2(sectionsByProperty("CUSTOM2")),
+ /**
+ * Custom compliance mode that can be defined with System property <code>org.eclipse.jetty.http.HttpCompliance.CUSTOM3</code>
+ */
+ @Deprecated
+ CUSTOM3(sectionsByProperty("CUSTOM3"));
+
+ public static final String VIOLATIONS_ATTR = "org.eclipse.jetty.http.compliance.violations";
+
+ private static EnumSet<HttpComplianceSection> sectionsByProperty(String property)
+ {
+ String s = System.getProperty(HttpCompliance.class.getName() + property);
+ return sectionsBySpec(s == null ? "*" : s);
+ }
+
+ static EnumSet<HttpComplianceSection> sectionsBySpec(String spec)
+ {
+ EnumSet<HttpComplianceSection> sections;
+ String[] elements = spec.split("\\s*,\\s*");
+ int i = 0;
+
+ switch (elements[i])
+ {
+ case "0":
+ sections = EnumSet.noneOf(HttpComplianceSection.class);
+ i++;
+ break;
+
+ case "RFC2616":
+ i++;
+ sections = EnumSet.complementOf(EnumSet.of(
+ HttpComplianceSection.NO_FIELD_FOLDING,
+ HttpComplianceSection.NO_HTTP_0_9,
+ HttpComplianceSection.NO_AMBIGUOUS_PATH_SEGMENTS,
+ HttpComplianceSection.NO_AMBIGUOUS_PATH_SEPARATORS,
+ HttpComplianceSection.NO_UTF16_ENCODINGS,
+ HttpComplianceSection.NO_AMBIGUOUS_EMPTY_SEGMENT,
+ HttpComplianceSection.NO_AMBIGUOUS_PATH_ENCODING));
+ break;
+
+ case "*":
+ case "RFC7230":
+ i++;
+ sections = EnumSet.complementOf(EnumSet.of(
+ HttpComplianceSection.NO_AMBIGUOUS_PATH_SEGMENTS,
+ HttpComplianceSection.NO_AMBIGUOUS_PATH_SEPARATORS,
+ HttpComplianceSection.NO_UTF16_ENCODINGS,
+ HttpComplianceSection.NO_AMBIGUOUS_EMPTY_SEGMENT,
+ HttpComplianceSection.NO_AMBIGUOUS_PATH_ENCODING));
+ break;
+
+ default:
+ sections = EnumSet.noneOf(HttpComplianceSection.class);
+ break;
+ }
+
+ while (i < elements.length)
+ {
+ String element = elements[i++];
+ boolean exclude = element.startsWith("-");
+ if (exclude)
+ element = element.substring(1);
+ HttpComplianceSection section = HttpComplianceSection.valueOf(element);
+ if (exclude)
+ sections.remove(section);
+ else
+ sections.add(section);
+ }
+
+ return sections;
+ }
+
+ private static final Map<HttpComplianceSection, HttpCompliance> __required = new HashMap<>();
+
+ static
+ {
+ for (HttpComplianceSection section : HttpComplianceSection.values())
+ {
+ for (HttpCompliance compliance : HttpCompliance.values())
+ {
+ if (compliance.sections().contains(section))
+ {
+ __required.put(section, compliance);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * @param section The section to query
+ * @return The minimum compliance required to enable the section.
+ */
+ public static HttpCompliance requiredCompliance(HttpComplianceSection section)
+ {
+ return __required.get(section);
+ }
+
+ private final EnumSet<HttpComplianceSection> _sections;
+
+ HttpCompliance(EnumSet<HttpComplianceSection> sections)
+ {
+ _sections = sections;
+ }
+
+ /**
+ * Get the set of {@link HttpComplianceSection}s supported by this compliance mode. This set
+ * is mutable, so it can be modified. Any modification will affect all usages of the mode
+ * within the same {@link ClassLoader}.
+ *
+ * @return The set of {@link HttpComplianceSection}s supported by this compliance mode.
+ */
+ public EnumSet<HttpComplianceSection> sections()
+ {
+ return _sections;
+ }
+
+ private static final EnumMap<HttpURI.Violation, HttpComplianceSection> __uriViolations = new EnumMap<>(HttpURI.Violation.class);
+ static
+ {
+ // create a map from Violation to compliance in a loop, so that any new violations added are detected with ISE
+ for (HttpURI.Violation violation : HttpURI.Violation.values())
+ {
+ switch (violation)
+ {
+ case SEPARATOR:
+ __uriViolations.put(violation, HttpComplianceSection.NO_AMBIGUOUS_PATH_SEPARATORS);
+ break;
+ case SEGMENT:
+ __uriViolations.put(violation, HttpComplianceSection.NO_AMBIGUOUS_PATH_SEGMENTS);
+ break;
+ case PARAM:
+ __uriViolations.put(violation, HttpComplianceSection.NO_AMBIGUOUS_PATH_PARAMETERS);
+ break;
+ case ENCODING:
+ __uriViolations.put(violation, HttpComplianceSection.NO_AMBIGUOUS_PATH_ENCODING);
+ break;
+ case EMPTY:
+ __uriViolations.put(violation, HttpComplianceSection.NO_AMBIGUOUS_EMPTY_SEGMENT);
+ break;
+ case UTF16:
+ __uriViolations.put(violation, HttpComplianceSection.NO_UTF16_ENCODINGS);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ public static String checkUriCompliance(HttpCompliance compliance, HttpURI uri)
+ {
+ for (HttpURI.Violation violation : HttpURI.Violation.values())
+ {
+ if (uri.hasViolation(violation) && (compliance == null || compliance.sections().contains(__uriViolations.get(violation))))
+ return violation.getMessage();
+ }
+ return null;
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpComplianceSection.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpComplianceSection.java
new file mode 100644
index 0000000..2ebef24
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpComplianceSection.java
@@ -0,0 +1,60 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+/**
+ *
+ */
+public enum HttpComplianceSection
+{
+ CASE_INSENSITIVE_FIELD_VALUE_CACHE("", "Use case insensitive field value cache"),
+ METHOD_CASE_SENSITIVE("https://tools.ietf.org/html/rfc7230#section-3.1.1", "Method is case-sensitive"),
+ FIELD_COLON("https://tools.ietf.org/html/rfc7230#section-3.2", "Fields must have a Colon"),
+ FIELD_NAME_CASE_INSENSITIVE("https://tools.ietf.org/html/rfc7230#section-3.2", "Field name is case-insensitive"),
+ NO_WS_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2.4", "Whitespace not allowed after field name"),
+ NO_FIELD_FOLDING("https://tools.ietf.org/html/rfc7230#section-3.2.4", "No line Folding"),
+ NO_HTTP_0_9("https://tools.ietf.org/html/rfc7230#appendix-A.2", "No HTTP/0.9"),
+ TRANSFER_ENCODING_WITH_CONTENT_LENGTH("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Transfer-Encoding and Content-Length"),
+ MULTIPLE_CONTENT_LENGTHS("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Multiple Content-Lengths"),
+ NO_AMBIGUOUS_PATH_SEGMENTS("https://tools.ietf.org/html/rfc3986#section-3.3", "No ambiguous URI path segments"),
+ NO_AMBIGUOUS_PATH_SEPARATORS("https://tools.ietf.org/html/rfc3986#section-3.3", "No ambiguous URI path separators"),
+ NO_AMBIGUOUS_PATH_PARAMETERS("https://tools.ietf.org/html/rfc3986#section-3.3", "No ambiguous URI path parameters"),
+ NO_UTF16_ENCODINGS("https://www.w3.org/International/iri-edit/draft-duerst-iri.html#anchor29", "UTF16 encoding"),
+ NO_AMBIGUOUS_EMPTY_SEGMENT("https://tools.ietf.org/html/rfc3986#section-3.3", "Ambiguous URI empty segment"),
+ NO_AMBIGUOUS_PATH_ENCODING("https://tools.ietf.org/html/rfc3986#section-3.3", "Ambiguous URI path encoding");
+
+ final String url;
+ final String description;
+
+ HttpComplianceSection(String url, String description)
+ {
+ this.url = url;
+ this.description = description;
+ }
+
+ public String getURL()
+ {
+ return url;
+ }
+
+ public String getDescription()
+ {
+ return description;
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java
new file mode 100644
index 0000000..248e0b3
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java
@@ -0,0 +1,91 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.util.Map;
+
+import org.eclipse.jetty.http.MimeTypes.Type;
+import org.eclipse.jetty.util.resource.Resource;
+
+/**
+ * HttpContent interface.
+ * <p>This information represents all the information about a
+ * static resource that is needed to evaluate conditional headers
+ * and to serve the content if need be. It can be implemented
+ * either transiently (values and fields generated on demand) or
+ * persistently (values and fields pre-generated in anticipation of
+ * reuse in from a cache).
+ * </p>
+ */
+public interface HttpContent
+{
+ HttpField getContentType();
+
+ String getContentTypeValue();
+
+ String getCharacterEncoding();
+
+ Type getMimeType();
+
+ HttpField getContentEncoding();
+
+ String getContentEncodingValue();
+
+ HttpField getContentLength();
+
+ long getContentLengthValue();
+
+ HttpField getLastModified();
+
+ String getLastModifiedValue();
+
+ HttpField getETag();
+
+ String getETagValue();
+
+ ByteBuffer getIndirectBuffer();
+
+ ByteBuffer getDirectBuffer();
+
+ Resource getResource();
+
+ InputStream getInputStream() throws IOException;
+
+ ReadableByteChannel getReadableByteChannel() throws IOException;
+
+ void release();
+
+ Map<CompressedContentFormat, ? extends HttpContent> getPrecompressedContents();
+
+ interface ContentFactory
+ {
+ /**
+ * @param path The path within the context to the resource
+ * @param maxBuffer The maximum buffer to allocated for this request. For cached content, a larger buffer may have
+ * previously been allocated and returned by the {@link HttpContent#getDirectBuffer()} or {@link HttpContent#getIndirectBuffer()} calls.
+ * @return A {@link HttpContent}
+ * @throws IOException if unable to get content
+ */
+ HttpContent getContent(String path, int maxBuffer) throws IOException;
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java
new file mode 100644
index 0000000..d1a06e4
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java
@@ -0,0 +1,568 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.ServletContext;
+
+import org.eclipse.jetty.util.QuotedStringTokenizer;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+// TODO consider replacing this with java.net.HttpCookie (once it supports RFC6265)
+public class HttpCookie
+{
+ private static final Logger LOG = Log.getLogger(HttpCookie.class);
+
+ private static final String __COOKIE_DELIM = "\",;\\ \t";
+ private static final String __01Jan1970_COOKIE = DateGenerator.formatCookieDate(0).trim();
+
+ /**
+ * If this string is found within the comment parsed with {@link #isHttpOnlyInComment(String)} the check will return true
+ **/
+ public static final String HTTP_ONLY_COMMENT = "__HTTP_ONLY__";
+ /**
+ * These strings are used by {@link #getSameSiteFromComment(String)} to check for a SameSite specifier in the comment
+ **/
+ private static final String SAME_SITE_COMMENT = "__SAME_SITE_";
+ public static final String SAME_SITE_NONE_COMMENT = SAME_SITE_COMMENT + "NONE__";
+ public static final String SAME_SITE_LAX_COMMENT = SAME_SITE_COMMENT + "LAX__";
+ public static final String SAME_SITE_STRICT_COMMENT = SAME_SITE_COMMENT + "STRICT__";
+
+ /**
+ * Name of context attribute with default SameSite cookie value
+ */
+ public static final String SAME_SITE_DEFAULT_ATTRIBUTE = "org.eclipse.jetty.cookie.sameSiteDefault";
+
+ public enum SameSite
+ {
+ NONE("None"), STRICT("Strict"), LAX("Lax");
+
+ private String attributeValue;
+
+ SameSite(String attributeValue)
+ {
+ this.attributeValue = attributeValue;
+ }
+
+ public String getAttributeValue()
+ {
+ return this.attributeValue;
+ }
+ }
+
+ private final String _name;
+ private final String _value;
+ private final String _comment;
+ private final String _domain;
+ private final long _maxAge;
+ private final String _path;
+ private final boolean _secure;
+ private final int _version;
+ private final boolean _httpOnly;
+ private final long _expiration;
+ private final SameSite _sameSite;
+
+ public HttpCookie(String name, String value)
+ {
+ this(name, value, -1);
+ }
+
+ public HttpCookie(String name, String value, String domain, String path)
+ {
+ this(name, value, domain, path, -1, false, false);
+ }
+
+ public HttpCookie(String name, String value, long maxAge)
+ {
+ this(name, value, null, null, maxAge, false, false);
+ }
+
+ public HttpCookie(String name, String value, String domain, String path, long maxAge, boolean httpOnly, boolean secure)
+ {
+ this(name, value, domain, path, maxAge, httpOnly, secure, null, 0);
+ }
+
+ public HttpCookie(String name, String value, String domain, String path, long maxAge, boolean httpOnly, boolean secure, String comment, int version)
+ {
+ this(name, value, domain, path, maxAge, httpOnly, secure, comment, version, null);
+ }
+
+ public HttpCookie(String name, String value, String domain, String path, long maxAge, boolean httpOnly, boolean secure, String comment, int version, SameSite sameSite)
+ {
+ _name = name;
+ _value = value;
+ _domain = domain;
+ _path = path;
+ _maxAge = maxAge;
+ _httpOnly = httpOnly;
+ _secure = secure;
+ _comment = comment;
+ _version = version;
+ _expiration = maxAge < 0 ? -1 : System.nanoTime() + TimeUnit.SECONDS.toNanos(maxAge);
+ _sameSite = sameSite;
+ }
+
+ public HttpCookie(String setCookie)
+ {
+ List<java.net.HttpCookie> cookies = java.net.HttpCookie.parse(setCookie);
+ if (cookies.size() != 1)
+ throw new IllegalStateException();
+
+ java.net.HttpCookie cookie = cookies.get(0);
+
+ _name = cookie.getName();
+ _value = cookie.getValue();
+ _domain = cookie.getDomain();
+ _path = cookie.getPath();
+ _maxAge = cookie.getMaxAge();
+ _httpOnly = cookie.isHttpOnly();
+ _secure = cookie.getSecure();
+ _comment = cookie.getComment();
+ _version = cookie.getVersion();
+ _expiration = _maxAge < 0 ? -1 : System.nanoTime() + TimeUnit.SECONDS.toNanos(_maxAge);
+ // support for SameSite values has not yet been added to java.net.HttpCookie
+ _sameSite = getSameSiteFromComment(cookie.getComment());
+ }
+
+ /**
+ * @return the cookie name
+ */
+ public String getName()
+ {
+ return _name;
+ }
+
+ /**
+ * @return the cookie value
+ */
+ public String getValue()
+ {
+ return _value;
+ }
+
+ /**
+ * @return the cookie comment
+ */
+ public String getComment()
+ {
+ return _comment;
+ }
+
+ /**
+ * @return the cookie domain
+ */
+ public String getDomain()
+ {
+ return _domain;
+ }
+
+ /**
+ * @return the cookie max age in seconds
+ */
+ public long getMaxAge()
+ {
+ return _maxAge;
+ }
+
+ /**
+ * @return the cookie path
+ */
+ public String getPath()
+ {
+ return _path;
+ }
+
+ /**
+ * @return whether the cookie is valid for secure domains
+ */
+ public boolean isSecure()
+ {
+ return _secure;
+ }
+
+ /**
+ * @return the cookie version
+ */
+ public int getVersion()
+ {
+ return _version;
+ }
+
+ /**
+ * @return the cookie SameSite enum attribute
+ */
+ public SameSite getSameSite()
+ {
+ return _sameSite;
+ }
+
+ /**
+ * @return whether the cookie is valid for the http protocol only
+ */
+ public boolean isHttpOnly()
+ {
+ return _httpOnly;
+ }
+
+ /**
+ * @param timeNanos the time to check for cookie expiration, in nanoseconds
+ * @return whether the cookie is expired by the given time
+ */
+ public boolean isExpired(long timeNanos)
+ {
+ return _expiration >= 0 && timeNanos >= _expiration;
+ }
+
+ /**
+ * @return a string representation of this cookie
+ */
+ public String asString()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append(getName()).append("=").append(getValue());
+ if (getDomain() != null)
+ builder.append(";$Domain=").append(getDomain());
+ if (getPath() != null)
+ builder.append(";$Path=").append(getPath());
+ return builder.toString();
+ }
+
+ private static void quoteOnlyOrAppend(StringBuilder buf, String s, boolean quote)
+ {
+ if (quote)
+ QuotedStringTokenizer.quoteOnly(buf, s);
+ else
+ buf.append(s);
+ }
+
+ /**
+ * Does a cookie value need to be quoted?
+ *
+ * @param s value string
+ * @return true if quoted;
+ * @throws IllegalArgumentException If there a control characters in the string
+ */
+ private static boolean isQuoteNeededForCookie(String s)
+ {
+ if (s == null || s.length() == 0)
+ return true;
+
+ if (QuotedStringTokenizer.isQuoted(s))
+ return false;
+
+ for (int i = 0; i < s.length(); i++)
+ {
+ char c = s.charAt(i);
+ if (__COOKIE_DELIM.indexOf(c) >= 0)
+ return true;
+
+ if (c < 0x20 || c >= 0x7f)
+ throw new IllegalArgumentException("Illegal character in cookie value");
+ }
+
+ return false;
+ }
+
+ public String getSetCookie(CookieCompliance compliance)
+ {
+ switch (compliance)
+ {
+ case RFC2965:
+ return getRFC2965SetCookie();
+ case RFC6265:
+ return getRFC6265SetCookie();
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ public String getRFC2965SetCookie()
+ {
+ // Check arguments
+ if (_name == null || _name.length() == 0)
+ throw new IllegalArgumentException("Bad cookie name");
+
+ // Format value and params
+ StringBuilder buf = new StringBuilder();
+
+ // Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting
+ boolean quoteName = isQuoteNeededForCookie(_name);
+ quoteOnlyOrAppend(buf, _name, quoteName);
+
+ buf.append('=');
+
+ // Append the value
+ boolean quoteValue = isQuoteNeededForCookie(_value);
+ quoteOnlyOrAppend(buf, _value, quoteValue);
+
+ // Look for domain and path fields and check if they need to be quoted
+ boolean hasDomain = _domain != null && _domain.length() > 0;
+ boolean quoteDomain = hasDomain && isQuoteNeededForCookie(_domain);
+ boolean hasPath = _path != null && _path.length() > 0;
+ boolean quotePath = hasPath && isQuoteNeededForCookie(_path);
+
+ // Upgrade the version if we have a comment or we need to quote value/path/domain or if they were already quoted
+ int version = _version;
+ if (version == 0 && (_comment != null || quoteName || quoteValue || quoteDomain || quotePath ||
+ QuotedStringTokenizer.isQuoted(_name) || QuotedStringTokenizer.isQuoted(_value) ||
+ QuotedStringTokenizer.isQuoted(_path) || QuotedStringTokenizer.isQuoted(_domain)))
+ version = 1;
+
+ // Append version
+ if (version == 1)
+ buf.append(";Version=1");
+ else if (version > 1)
+ buf.append(";Version=").append(version);
+
+ // Append path
+ if (hasPath)
+ {
+ buf.append(";Path=");
+ quoteOnlyOrAppend(buf, _path, quotePath);
+ }
+
+ // Append domain
+ if (hasDomain)
+ {
+ buf.append(";Domain=");
+ quoteOnlyOrAppend(buf, _domain, quoteDomain);
+ }
+
+ // Handle max-age and/or expires
+ if (_maxAge >= 0)
+ {
+ // Always use expires
+ // This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies
+ buf.append(";Expires=");
+ if (_maxAge == 0)
+ buf.append(__01Jan1970_COOKIE);
+ else
+ DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * _maxAge);
+
+ // for v1 cookies, also send max-age
+ if (version >= 1)
+ {
+ buf.append(";Max-Age=");
+ buf.append(_maxAge);
+ }
+ }
+
+ // add the other fields
+ if (_secure)
+ buf.append(";Secure");
+ if (_httpOnly)
+ buf.append(";HttpOnly");
+ if (_comment != null)
+ {
+ buf.append(";Comment=");
+ quoteOnlyOrAppend(buf, _comment, isQuoteNeededForCookie(_comment));
+ }
+ return buf.toString();
+ }
+
+ public String getRFC6265SetCookie()
+ {
+ // Check arguments
+ if (_name == null || _name.length() == 0)
+ throw new IllegalArgumentException("Bad cookie name");
+
+ // Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting
+ // Per RFC6265, Cookie.name follows RFC2616 Section 2.2 token rules
+ Syntax.requireValidRFC2616Token(_name, "RFC6265 Cookie name");
+ // Ensure that Per RFC6265, Cookie.value follows syntax rules
+ Syntax.requireValidRFC6265CookieValue(_value);
+
+ // Format value and params
+ StringBuilder buf = new StringBuilder();
+ buf.append(_name).append('=').append(_value == null ? "" : _value);
+
+ // Append path
+ if (_path != null && _path.length() > 0)
+ buf.append("; Path=").append(_path);
+
+ // Append domain
+ if (_domain != null && _domain.length() > 0)
+ buf.append("; Domain=").append(_domain);
+
+ // Handle max-age and/or expires
+ if (_maxAge >= 0)
+ {
+ // Always use expires
+ // This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies
+ buf.append("; Expires=");
+ if (_maxAge == 0)
+ buf.append(__01Jan1970_COOKIE);
+ else
+ DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * _maxAge);
+
+ buf.append("; Max-Age=");
+ buf.append(_maxAge);
+ }
+
+ // add the other fields
+ if (_secure)
+ buf.append("; Secure");
+ if (_httpOnly)
+ buf.append("; HttpOnly");
+ if (_sameSite != null)
+ {
+ buf.append("; SameSite=");
+ buf.append(_sameSite.getAttributeValue());
+ }
+
+ return buf.toString();
+ }
+
+ public static boolean isHttpOnlyInComment(String comment)
+ {
+ return comment != null && comment.contains(HTTP_ONLY_COMMENT);
+ }
+
+ public static SameSite getSameSiteFromComment(String comment)
+ {
+ if (comment != null)
+ {
+ if (comment.contains(SAME_SITE_STRICT_COMMENT))
+ {
+ return SameSite.STRICT;
+ }
+ if (comment.contains(SAME_SITE_LAX_COMMENT))
+ {
+ return SameSite.LAX;
+ }
+ if (comment.contains(SAME_SITE_NONE_COMMENT))
+ {
+ return SameSite.NONE;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the default value for SameSite cookie attribute, if one
+ * has been set for the given context.
+ *
+ * @param context the context to check for default SameSite value
+ * @return the default SameSite value or null if one does not exist
+ * @throws IllegalStateException if the default value is not a permitted value
+ */
+ public static SameSite getSameSiteDefault(ServletContext context)
+ {
+ if (context == null)
+ return null;
+ Object o = context.getAttribute(SAME_SITE_DEFAULT_ATTRIBUTE);
+ if (o == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("No default value for SameSite");
+ return null;
+ }
+
+ if (o instanceof SameSite)
+ return (SameSite)o;
+
+ try
+ {
+ SameSite samesite = Enum.valueOf(SameSite.class, o.toString().trim().toUpperCase(Locale.ENGLISH));
+ context.setAttribute(SAME_SITE_DEFAULT_ATTRIBUTE, samesite);
+ return samesite;
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Bad default value {} for SameSite", o);
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public static String getCommentWithoutAttributes(String comment)
+ {
+ if (comment == null)
+ {
+ return null;
+ }
+
+ String strippedComment = comment.trim();
+
+ strippedComment = StringUtil.strip(strippedComment, HTTP_ONLY_COMMENT);
+ strippedComment = StringUtil.strip(strippedComment, SAME_SITE_NONE_COMMENT);
+ strippedComment = StringUtil.strip(strippedComment, SAME_SITE_LAX_COMMENT);
+ strippedComment = StringUtil.strip(strippedComment, SAME_SITE_STRICT_COMMENT);
+
+ return strippedComment.length() == 0 ? null : strippedComment;
+ }
+
+ public static String getCommentWithAttributes(String comment, boolean httpOnly, SameSite sameSite)
+ {
+ if (comment == null && sameSite == null)
+ return null;
+
+ StringBuilder builder = new StringBuilder();
+ if (StringUtil.isNotBlank(comment))
+ {
+ comment = getCommentWithoutAttributes(comment);
+ if (StringUtil.isNotBlank(comment))
+ builder.append(comment);
+ }
+ if (httpOnly)
+ builder.append(HTTP_ONLY_COMMENT);
+
+ if (sameSite != null)
+ {
+ switch (sameSite)
+ {
+ case NONE:
+ builder.append(SAME_SITE_NONE_COMMENT);
+ break;
+ case STRICT:
+ builder.append(SAME_SITE_STRICT_COMMENT);
+ break;
+ case LAX:
+ builder.append(SAME_SITE_LAX_COMMENT);
+ break;
+ default:
+ throw new IllegalArgumentException(sameSite.toString());
+ }
+ }
+
+ if (builder.length() == 0)
+ return null;
+ return builder.toString();
+ }
+
+ public static class SetCookieHttpField extends HttpField
+ {
+ final HttpCookie _cookie;
+
+ public SetCookieHttpField(HttpCookie cookie, CookieCompliance compliance)
+ {
+ super(HttpHeader.SET_COOKIE, cookie.getSetCookie(compliance));
+ this._cookie = cookie;
+ }
+
+ public HttpCookie getHttpCookie()
+ {
+ return _cookie;
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java
new file mode 100644
index 0000000..c406aca
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java
@@ -0,0 +1,419 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.Objects;
+
+import org.eclipse.jetty.util.StringUtil;
+
+/**
+ * An HTTP Field
+ */
+public class HttpField
+{
+ private static final String ZEROQUALITY = "q=0";
+ private final HttpHeader _header;
+ private final String _name;
+ private final String _value;
+ // cached hashcode for case insensitive name
+ private int hash = 0;
+
+ public HttpField(HttpHeader header, String name, String value)
+ {
+ _header = header;
+ if (_header != null && name == null)
+ _name = _header.asString();
+ else
+ _name = Objects.requireNonNull(name, "name");
+ _value = value;
+ }
+
+ public HttpField(HttpHeader header, String value)
+ {
+ this(header, header.asString(), value);
+ }
+
+ public HttpField(HttpHeader header, HttpHeaderValue value)
+ {
+ this(header, header.asString(), value.asString());
+ }
+
+ public HttpField(String name, String value)
+ {
+ this(HttpHeader.CACHE.get(name), name, value);
+ }
+
+ public HttpHeader getHeader()
+ {
+ return _header;
+ }
+
+ public String getName()
+ {
+ return _name;
+ }
+
+ public String getLowerCaseName()
+ {
+ return _header != null ? _header.lowerCaseName() : StringUtil.asciiToLowerCase(_name);
+ }
+
+ public String getValue()
+ {
+ return _value;
+ }
+
+ public int getIntValue()
+ {
+ return Integer.parseInt(_value);
+ }
+
+ public long getLongValue()
+ {
+ return Long.parseLong(_value);
+ }
+
+ public String[] getValues()
+ {
+ if (_value == null)
+ return null;
+
+ QuotedCSV list = new QuotedCSV(false, _value);
+ return list.getValues().toArray(new String[list.size()]);
+ }
+
+ /**
+ * Look for a value in a possible multi valued field
+ *
+ * @param search Values to search for (case insensitive)
+ * @return True iff the value is contained in the field value entirely or
+ * as an element of a quoted comma separated list. List element parameters (eg qualities) are ignored,
+ * except if they are q=0, in which case the item itself is ignored.
+ */
+ public boolean contains(String search)
+ {
+ if (search == null)
+ return _value == null;
+ if (search.isEmpty())
+ return false;
+ if (_value == null)
+ return false;
+ if (search.equals(_value))
+ return true;
+
+ search = StringUtil.asciiToLowerCase(search);
+
+ int state = 0;
+ int match = 0;
+ int param = 0;
+
+ for (int i = 0; i < _value.length(); i++)
+ {
+ char c = _value.charAt(i);
+ switch (state)
+ {
+ case 0: // initial white space
+ switch (c)
+ {
+ case '"': // open quote
+ match = 0;
+ state = 2;
+ break;
+
+ case ',': // ignore leading empty field
+ break;
+
+ case ';': // ignore leading empty field parameter
+ param = -1;
+ match = -1;
+ state = 5;
+ break;
+
+ case ' ': // more white space
+ case '\t':
+ break;
+
+ default: // character
+ match = Character.toLowerCase(c) == search.charAt(0) ? 1 : -1;
+ state = 1;
+ break;
+ }
+ break;
+
+ case 1: // In token
+ switch (c)
+ {
+ case ',': // next field
+ // Have we matched the token?
+ if (match == search.length())
+ return true;
+ state = 0;
+ break;
+
+ case ';':
+ param = match >= 0 ? 0 : -1;
+ state = 5; // parameter
+ break;
+
+ default:
+ if (match > 0)
+ {
+ if (match < search.length())
+ match = Character.toLowerCase(c) == search.charAt(match) ? (match + 1) : -1;
+ else if (c != ' ' && c != '\t')
+ match = -1;
+ }
+ break;
+ }
+ break;
+
+ case 2: // In Quoted token
+ switch (c)
+ {
+ case '\\': // quoted character
+ state = 3;
+ break;
+
+ case '"': // end quote
+ state = 4;
+ break;
+
+ default:
+ if (match >= 0)
+ {
+ if (match < search.length())
+ match = Character.toLowerCase(c) == search.charAt(match) ? (match + 1) : -1;
+ else
+ match = -1;
+ }
+ }
+ break;
+
+ case 3: // In Quoted character in quoted token
+ if (match >= 0)
+ {
+ if (match < search.length())
+ match = Character.toLowerCase(c) == search.charAt(match) ? (match + 1) : -1;
+ else
+ match = -1;
+ }
+ state = 2;
+ break;
+
+ case 4: // WS after end quote
+ switch (c)
+ {
+ case ' ': // white space
+ case '\t': // white space
+ break;
+
+ case ';':
+ state = 5; // parameter
+ break;
+
+ case ',': // end token
+ // Have we matched the token?
+ if (match == search.length())
+ return true;
+ state = 0;
+ break;
+
+ default:
+ // This is an illegal token, just ignore
+ match = -1;
+ }
+ break;
+
+ case 5: // parameter
+ switch (c)
+ {
+ case ',': // end token
+ // Have we matched the token and not q=0?
+ if (param != ZEROQUALITY.length() && match == search.length())
+ return true;
+ param = 0;
+ state = 0;
+ break;
+
+ case ' ': // white space
+ case '\t': // white space
+ break;
+
+ default:
+ if (param >= 0)
+ {
+ if (param < ZEROQUALITY.length())
+ param = Character.toLowerCase(c) == ZEROQUALITY.charAt(param) ? (param + 1) : -1;
+ else if (c != '0' && c != '.')
+ param = -1;
+ }
+ }
+ break;
+
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ return param != ZEROQUALITY.length() && match == search.length();
+ }
+
+ @Override
+ public String toString()
+ {
+ String v = getValue();
+ return getName() + ": " + (v == null ? "" : v);
+ }
+
+ public boolean isSameName(HttpField field)
+ {
+ if (field == null)
+ return false;
+ if (field == this)
+ return true;
+ if (_header != null && _header == field.getHeader())
+ return true;
+ return _name.equalsIgnoreCase(field.getName());
+ }
+
+ public boolean is(String name)
+ {
+ return _name.equalsIgnoreCase(name);
+ }
+
+ private int nameHashCode()
+ {
+ int h = this.hash;
+ int len = _name.length();
+ if (h == 0 && len > 0)
+ {
+ for (int i = 0; i < len; i++)
+ {
+ // simple case insensitive hash
+ char c = _name.charAt(i);
+ // assuming us-ascii (per last paragraph on http://tools.ietf.org/html/rfc7230#section-3.2.4)
+ if ((c >= 'a' && c <= 'z'))
+ c -= 0x20;
+ h = 31 * h + c;
+ }
+ this.hash = h;
+ }
+ return h;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ int vhc = Objects.hashCode(_value);
+ if (_header == null)
+ return vhc ^ nameHashCode();
+ return vhc ^ _header.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (o == this)
+ return true;
+ if (!(o instanceof HttpField))
+ return false;
+ HttpField field = (HttpField)o;
+ if (_header != field.getHeader())
+ return false;
+ if (!_name.equalsIgnoreCase(field.getName()))
+ return false;
+ return Objects.equals(_value, field.getValue());
+ }
+
+ public static class IntValueHttpField extends HttpField
+ {
+ private final int _int;
+
+ public IntValueHttpField(HttpHeader header, String name, String value, int intValue)
+ {
+ super(header, name, value);
+ _int = intValue;
+ }
+
+ public IntValueHttpField(HttpHeader header, String name, String value)
+ {
+ this(header, name, value, Integer.parseInt(value));
+ }
+
+ public IntValueHttpField(HttpHeader header, String name, int intValue)
+ {
+ this(header, name, Integer.toString(intValue), intValue);
+ }
+
+ public IntValueHttpField(HttpHeader header, int value)
+ {
+ this(header, header.asString(), value);
+ }
+
+ @Override
+ public int getIntValue()
+ {
+ return _int;
+ }
+
+ @Override
+ public long getLongValue()
+ {
+ return _int;
+ }
+ }
+
+ public static class LongValueHttpField extends HttpField
+ {
+ private final long _long;
+
+ public LongValueHttpField(HttpHeader header, String name, String value, long longValue)
+ {
+ super(header, name, value);
+ _long = longValue;
+ }
+
+ public LongValueHttpField(HttpHeader header, String name, String value)
+ {
+ this(header, name, value, Long.parseLong(value));
+ }
+
+ public LongValueHttpField(HttpHeader header, String name, long value)
+ {
+ this(header, name, Long.toString(value), value);
+ }
+
+ public LongValueHttpField(HttpHeader header, long value)
+ {
+ this(header, header.asString(), value);
+ }
+
+ @Override
+ public int getIntValue()
+ {
+ return (int)_long;
+ }
+
+ @Override
+ public long getLongValue()
+ {
+ return _long;
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFieldPreEncoder.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFieldPreEncoder.java
new file mode 100644
index 0000000..2ad95f5
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFieldPreEncoder.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+/**
+ * Interface to pre-encode HttpFields. Used by {@link PreEncodedHttpField}
+ */
+public interface HttpFieldPreEncoder
+{
+
+ /**
+ * The major version this encoder is for. Both HTTP/1.0 and HTTP/1.1
+ * use the same field encoding, so the {@link HttpVersion#HTTP_1_0} should
+ * be return for all HTTP/1.x encodings.
+ *
+ * @return The major version this encoder is for.
+ */
+ HttpVersion getHttpVersion();
+
+ byte[] getEncodedField(HttpHeader header, String headerString, String value);
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java
new file mode 100644
index 0000000..dac7b46
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java
@@ -0,0 +1,1409 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.function.BiFunction;
+import java.util.function.ToIntFunction;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.util.ArrayTernaryTrie;
+import org.eclipse.jetty.util.QuotedStringTokenizer;
+import org.eclipse.jetty.util.Trie;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * HTTP Fields. A collection of HTTP header and or Trailer fields.
+ *
+ * <p>This class is not synchronized as it is expected that modifications will only be performed by a
+ * single thread.
+ *
+ * <p>The cookie handling provided by this class is guided by the Servlet specification and RFC6265.
+ */
+public class HttpFields implements Iterable<HttpField>
+{
+ @Deprecated
+ public static final String __separators = ", \t";
+
+ private static final Logger LOG = Log.getLogger(HttpFields.class);
+
+ private HttpField[] _fields;
+ private int _size;
+
+ /**
+ * Initialize an empty HttpFields.
+ */
+ public HttpFields()
+ {
+ this(16); // Based on small sample of Chrome requests.
+ }
+
+ /**
+ * Initialize an empty HttpFields.
+ *
+ * @param capacity the capacity of the http fields
+ */
+ public HttpFields(int capacity)
+ {
+ _fields = new HttpField[capacity];
+ }
+
+ /**
+ * Initialize HttpFields from copy.
+ *
+ * @param fields the fields to copy data from
+ */
+ public HttpFields(HttpFields fields)
+ {
+ _fields = Arrays.copyOf(fields._fields, fields._fields.length);
+ _size = fields._size;
+ }
+
+ /**
+ * <p>Computes a single field for the given HttpHeader and for existing fields with the same header.</p>
+ *
+ * <p>The compute function receives the field name and a list of fields with the same name
+ * so that their values can be used to compute the value of the field that is returned
+ * by the compute function.
+ * If the compute function returns {@code null}, the fields with the given name are removed.</p>
+ * <p>This method comes handy when you want to add an HTTP header if it does not exist,
+ * or add a value if the HTTP header already exists, similarly to
+ * {@link Map#compute(Object, BiFunction)}.</p>
+ *
+ * <p>This method can be used to {@link #put(HttpField) put} a new field (or blindly replace its value):</p>
+ * <pre>
+ * httpFields.computeField("X-New-Header",
+ * (name, fields) -> new HttpField(name, "NewValue"));
+ * </pre>
+ *
+ * <p>This method can be used to coalesce many fields into one:</p>
+ * <pre>
+ * // Input:
+ * GET / HTTP/1.1
+ * Host: localhost
+ * Cookie: foo=1
+ * Cookie: bar=2,baz=3
+ * User-Agent: Jetty
+ *
+ * // Computation:
+ * httpFields.computeField("Cookie", (name, fields) ->
+ * {
+ * // No cookies, nothing to do.
+ * if (fields == null)
+ * return null;
+ *
+ * // Coalesces all cookies.
+ * String coalesced = fields.stream()
+ * .flatMap(field -> Stream.of(field.getValues()))
+ * .collect(Collectors.joining(", "));
+ *
+ * // Returns a single Cookie header with all cookies.
+ * return new HttpField(name, coalesced);
+ * }
+ *
+ * // Output:
+ * GET / HTTP/1.1
+ * Host: localhost
+ * Cookie: foo=1, bar=2, baz=3
+ * User-Agent: Jetty
+ * </pre>
+ *
+ * <p>This method can be used to replace a field:</p>
+ * <pre>
+ * httpFields.computeField("X-Length", (name, fields) ->
+ * {
+ * if (fields == null)
+ * return null;
+ *
+ * // Get any value among the X-Length headers.
+ * String length = fields.stream()
+ * .map(HttpField::getValue)
+ * .findAny()
+ * .orElse("0");
+ *
+ * // Replace X-Length headers with X-Capacity header.
+ * return new HttpField("X-Capacity", length);
+ * });
+ * </pre>
+ *
+ * <p>This method can be used to remove a field:</p>
+ * <pre>
+ * httpFields.computeField("Connection", (name, fields) -> null);
+ * </pre>
+ *
+ * @param header the HTTP header
+ * @param computeFn the compute function
+ */
+ public void computeField(HttpHeader header, BiFunction<HttpHeader, List<HttpField>, HttpField> computeFn)
+ {
+ computeField(header, computeFn, (f, h) -> f.getHeader() == h);
+ }
+
+ /**
+ * <p>Computes a single field for the given HTTP header name and for existing fields with the same name.</p>
+ *
+ * @param name the HTTP header name
+ * @param computeFn the compute function
+ * @see #computeField(HttpHeader, BiFunction)
+ */
+ public void computeField(String name, BiFunction<String, List<HttpField>, HttpField> computeFn)
+ {
+ computeField(name, computeFn, HttpField::is);
+ }
+
+ private <T> void computeField(T header, BiFunction<T, List<HttpField>, HttpField> computeFn, BiFunction<HttpField, T, Boolean> matcher)
+ {
+ // Look for first occurrence
+ int first = -1;
+ for (int i = 0; i < _size; i++)
+ {
+ HttpField f = _fields[i];
+ if (matcher.apply(f, header))
+ {
+ first = i;
+ break;
+ }
+ }
+
+ // If the header is not found, add a new one;
+ if (first < 0)
+ {
+ HttpField newField = computeFn.apply(header, null);
+ if (newField != null)
+ add(newField);
+ return;
+ }
+
+ // Are there any more occurrences?
+ List<HttpField> found = null;
+ for (int i = first + 1; i < _size; i++)
+ {
+ HttpField f = _fields[i];
+ if (matcher.apply(f, header))
+ {
+ if (found == null)
+ {
+ found = new ArrayList<>();
+ found.add(_fields[first]);
+ }
+ // Remember and remove additional fields
+ found.add(f);
+ remove(i--);
+ }
+ }
+
+ // If no additional fields were found, handle singleton case
+ if (found == null)
+ found = Collections.singletonList(_fields[first]);
+ else
+ found = Collections.unmodifiableList(found);
+
+ HttpField newField = computeFn.apply(header, found);
+ if (newField == null)
+ remove(first);
+ else
+ _fields[first] = newField;
+ }
+
+ public int size()
+ {
+ return _size;
+ }
+
+ @Override
+ public Iterator<HttpField> iterator()
+ {
+ return new ListItr();
+ }
+
+ public ListIterator<HttpField> listIterator()
+ {
+ return new ListItr();
+ }
+
+ public Stream<HttpField> stream()
+ {
+ return Arrays.stream(_fields).limit(_size);
+ }
+
+ /**
+ * Get Collection of header names.
+ *
+ * @return the unique set of field names.
+ */
+ public Set<String> getFieldNamesCollection()
+ {
+ Set<String> set = null;
+ for (int i = 0; i < _size; i++)
+ {
+ HttpField f = _fields[i];
+ if (set == null)
+ set = new HashSet<>();
+ set.add(f.getName());
+ }
+ return set == null ? Collections.emptySet() : set;
+ }
+
+ /**
+ * Get enumeration of header _names. Returns an enumeration of strings representing the header
+ * _names for this request.
+ *
+ * @return an enumeration of field names
+ */
+ public Enumeration<String> getFieldNames()
+ {
+ return Collections.enumeration(getFieldNamesCollection());
+ }
+
+ /**
+ * Get a Field by index.
+ *
+ * @param index the field index
+ * @return A Field value or null if the Field value has not been set
+ */
+ public HttpField getField(int index)
+ {
+ if (index >= _size)
+ throw new NoSuchElementException();
+ return _fields[index];
+ }
+
+ public HttpField getField(HttpHeader header)
+ {
+ for (int i = 0; i < _size; i++)
+ {
+ HttpField f = _fields[i];
+ if (f.getHeader() == header)
+ return f;
+ }
+ return null;
+ }
+
+ public HttpField getField(String name)
+ {
+ for (int i = 0; i < _size; i++)
+ {
+ HttpField f = _fields[i];
+ if (f.is(name))
+ return f;
+ }
+ return null;
+ }
+
+ public List<HttpField> getFields(HttpHeader header)
+ {
+ List<HttpField> fields = null;
+ for (int i = 0; i < _size; i++)
+ {
+ HttpField f = _fields[i];
+ if (f.getHeader() == header)
+ {
+ if (fields == null)
+ fields = new ArrayList<>();
+ fields.add(f);
+ }
+ }
+ return fields == null ? Collections.emptyList() : fields;
+ }
+
+ public List<HttpField> getFields(String name)
+ {
+ List<HttpField> fields = null;
+ for (int i = 0; i < _size; i++)
+ {
+ HttpField f = _fields[i];
+ if (f.is(name))
+ {
+ if (fields == null)
+ fields = new ArrayList<>();
+ fields.add(f);
+ }
+ }
+ return fields == null ? Collections.emptyList() : fields;
+ }
+
+ public boolean contains(HttpField field)
+ {
+ for (int i = _size; i-- > 0; )
+ {
+ HttpField f = _fields[i];
+ if (f.isSameName(field) && (f.equals(field) || f.contains(field.getValue())))
+ return true;
+ }
+ return false;
+ }
+
+ public boolean contains(HttpHeader header, String value)
+ {
+ for (int i = _size; i-- > 0; )
+ {
+ HttpField f = _fields[i];
+ if (f.getHeader() == header && f.contains(value))
+ return true;
+ }
+ return false;
+ }
+
+ public boolean contains(String name, String value)
+ {
+ for (int i = _size; i-- > 0; )
+ {
+ HttpField f = _fields[i];
+ if (f.is(name) && f.contains(value))
+ return true;
+ }
+ return false;
+ }
+
+ public boolean contains(HttpHeader header)
+ {
+ for (int i = _size; i-- > 0; )
+ {
+ HttpField f = _fields[i];
+ if (f.getHeader() == header)
+ return true;
+ }
+ return false;
+ }
+
+ public boolean containsKey(String name)
+ {
+ for (int i = _size; i-- > 0; )
+ {
+ HttpField f = _fields[i];
+ if (f.is(name))
+ return true;
+ }
+ return false;
+ }
+
+ @Deprecated
+ public String getStringField(HttpHeader header)
+ {
+ return get(header);
+ }
+
+ public String get(HttpHeader header)
+ {
+ for (int i = 0; i < _size; i++)
+ {
+ HttpField f = _fields[i];
+ if (f.getHeader() == header)
+ return f.getValue();
+ }
+ return null;
+ }
+
+ @Deprecated
+ public String getStringField(String name)
+ {
+ return get(name);
+ }
+
+ public String get(String header)
+ {
+ for (int i = 0; i < _size; i++)
+ {
+ HttpField f = _fields[i];
+ if (f.is(header))
+ return f.getValue();
+ }
+ return null;
+ }
+
+ /**
+ * Get multiple header of the same name
+ *
+ * @param header the header
+ * @return List the values
+ */
+ public List<String> getValuesList(HttpHeader header)
+ {
+ final List<String> list = new ArrayList<>();
+ for (int i = 0; i < _size; i++)
+ {
+ HttpField f = _fields[i];
+ if (f.getHeader() == header)
+ list.add(f.getValue());
+ }
+ return list;
+ }
+
+ /**
+ * Get multiple header of the same name
+ *
+ * @param name the case-insensitive field name
+ * @return List the header values
+ */
+ public List<String> getValuesList(String name)
+ {
+ List<String> list = null;
+ for (int i = 0; i < _size; i++)
+ {
+ HttpField f = _fields[i];
+ if (f.is(name))
+ {
+ if (list == null)
+ list = new ArrayList<>(size() - i);
+ list.add(f.getValue());
+ }
+ }
+ return list == null ? Collections.emptyList() : list;
+ }
+
+ /**
+ * Add comma separated values, but only if not already
+ * present.
+ *
+ * @param header The header to add the value(s) to
+ * @param values The value(s) to add
+ * @return True if headers were modified
+ */
+ public boolean addCSV(HttpHeader header, String... values)
+ {
+ QuotedCSV existing = null;
+ for (int i = 0; i < _size; i++)
+ {
+ HttpField f = _fields[i];
+ if (f.getHeader() == header)
+ {
+ if (existing == null)
+ existing = new QuotedCSV(false);
+ existing.addValue(f.getValue());
+ }
+ }
+
+ String value = addCSV(existing, values);
+ if (value != null)
+ {
+ add(header, value);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Add comma separated values, but only if not already
+ * present.
+ *
+ * @param name The header to add the value(s) to
+ * @param values The value(s) to add
+ * @return True if headers were modified
+ */
+ public boolean addCSV(String name, String... values)
+ {
+ QuotedCSV existing = null;
+ for (int i = 0; i < _size; i++)
+ {
+ HttpField f = _fields[i];
+ if (f.is(name))
+ {
+ if (existing == null)
+ existing = new QuotedCSV(false);
+ existing.addValue(f.getValue());
+ }
+ }
+ String value = addCSV(existing, values);
+ if (value != null)
+ {
+ add(name, value);
+ return true;
+ }
+ return false;
+ }
+
+ protected String addCSV(QuotedCSV existing, String... values)
+ {
+ // remove any existing values from the new values
+ boolean add = true;
+ if (existing != null && !existing.isEmpty())
+ {
+ add = false;
+
+ for (int i = values.length; i-- > 0; )
+ {
+ String unquoted = QuotedCSV.unquote(values[i]);
+ if (existing.getValues().contains(unquoted))
+ values[i] = null;
+ else
+ add = true;
+ }
+ }
+
+ if (add)
+ {
+ StringBuilder value = new StringBuilder();
+ for (String v : values)
+ {
+ if (v == null)
+ continue;
+ if (value.length() > 0)
+ value.append(", ");
+ value.append(v);
+ }
+ if (value.length() > 0)
+ return value.toString();
+ }
+
+ return null;
+ }
+
+ /**
+ * Get multiple field values of the same name, split
+ * as a {@link QuotedCSV}
+ *
+ * @param header The header
+ * @param keepQuotes True if the fields are kept quoted
+ * @return List the values with OWS stripped
+ */
+ public List<String> getCSV(HttpHeader header, boolean keepQuotes)
+ {
+ QuotedCSV values = null;
+ for (HttpField f : this)
+ {
+ if (f.getHeader() == header)
+ {
+ if (values == null)
+ values = new QuotedCSV(keepQuotes);
+ values.addValue(f.getValue());
+ }
+ }
+ return values == null ? Collections.emptyList() : values.getValues();
+ }
+
+ /**
+ * Get multiple field values of the same name
+ * as a {@link QuotedCSV}
+ *
+ * @param name the case-insensitive field name
+ * @param keepQuotes True if the fields are kept quoted
+ * @return List the values with OWS stripped
+ */
+ public List<String> getCSV(String name, boolean keepQuotes)
+ {
+ QuotedCSV values = null;
+ for (HttpField f : this)
+ {
+ if (f.is(name))
+ {
+ if (values == null)
+ values = new QuotedCSV(keepQuotes);
+ values.addValue(f.getValue());
+ }
+ }
+ return values == null ? Collections.emptyList() : values.getValues();
+ }
+
+ /**
+ * Get multiple field values of the same name, split and
+ * sorted as a {@link QuotedQualityCSV}
+ *
+ * @param header The header
+ * @return List the values in quality order with the q param and OWS stripped
+ */
+ public List<String> getQualityCSV(HttpHeader header)
+ {
+ return getQualityCSV(header, null);
+ }
+
+ /**
+ * Get multiple field values of the same name, split and
+ * sorted as a {@link QuotedQualityCSV}
+ *
+ * @param header The header
+ * @param secondaryOrdering Function to apply an ordering other than specified by quality
+ * @return List the values in quality order with the q param and OWS stripped
+ */
+ public List<String> getQualityCSV(HttpHeader header, ToIntFunction<String> secondaryOrdering)
+ {
+ QuotedQualityCSV values = null;
+ for (HttpField f : this)
+ {
+ if (f.getHeader() == header)
+ {
+ if (values == null)
+ values = new QuotedQualityCSV(secondaryOrdering);
+ values.addValue(f.getValue());
+ }
+ }
+
+ return values == null ? Collections.emptyList() : values.getValues();
+ }
+
+ /**
+ * Get multiple field values of the same name, split and
+ * sorted as a {@link QuotedQualityCSV}
+ *
+ * @param name the case-insensitive field name
+ * @return List the values in quality order with the q param and OWS stripped
+ */
+ public List<String> getQualityCSV(String name)
+ {
+ QuotedQualityCSV values = null;
+ for (HttpField f : this)
+ {
+ if (f.is(name))
+ {
+ if (values == null)
+ values = new QuotedQualityCSV();
+ values.addValue(f.getValue());
+ }
+ }
+ return values == null ? Collections.emptyList() : values.getValues();
+ }
+
+ /**
+ * Get multi headers
+ *
+ * @param name the case-insensitive field name
+ * @return Enumeration of the values
+ */
+ public Enumeration<String> getValues(final String name)
+ {
+ for (int i = 0; i < _size; i++)
+ {
+ final HttpField f = _fields[i];
+
+ if (f.is(name) && f.getValue() != null)
+ {
+ final int first = i;
+ return new Enumeration<String>()
+ {
+ HttpField field = f;
+ int i = first + 1;
+
+ @Override
+ public boolean hasMoreElements()
+ {
+ if (field == null)
+ {
+ while (i < _size)
+ {
+ field = _fields[i++];
+ if (field.is(name) && field.getValue() != null)
+ return true;
+ }
+ field = null;
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String nextElement() throws NoSuchElementException
+ {
+ if (hasMoreElements())
+ {
+ String value = field.getValue();
+ field = null;
+ return value;
+ }
+ throw new NoSuchElementException();
+ }
+ };
+ }
+ }
+
+ List<String> empty = Collections.emptyList();
+ return Collections.enumeration(empty);
+ }
+
+ /**
+ * Get multi field values with separator. The multiple values can be represented as separate
+ * headers of the same name, or by a single header using the separator(s), or a combination of
+ * both. Separators may be quoted.
+ *
+ * @param name the case-insensitive field name
+ * @param separators String of separators.
+ * @return Enumeration of the values, or null if no such header.
+ */
+ @Deprecated
+ public Enumeration<String> getValues(String name, final String separators)
+ {
+ final Enumeration<String> e = getValues(name);
+ if (e == null)
+ return null;
+ return new Enumeration<String>()
+ {
+ QuotedStringTokenizer tok = null;
+
+ @Override
+ public boolean hasMoreElements()
+ {
+ if (tok != null && tok.hasMoreElements())
+ return true;
+ while (e.hasMoreElements())
+ {
+ String value = e.nextElement();
+ if (value != null)
+ {
+ tok = new QuotedStringTokenizer(value, separators, false, false);
+ if (tok.hasMoreElements())
+ return true;
+ }
+ }
+ tok = null;
+ return false;
+ }
+
+ @Override
+ public String nextElement() throws NoSuchElementException
+ {
+ if (!hasMoreElements())
+ throw new NoSuchElementException();
+ String next = (String)tok.nextElement();
+ if (next != null)
+ next = next.trim();
+ return next;
+ }
+ };
+ }
+
+ public void put(HttpField field)
+ {
+ boolean put = false;
+ for (int i = _size; i-- > 0; )
+ {
+ HttpField f = _fields[i];
+ if (f.isSameName(field))
+ {
+ if (put)
+ {
+ _size--;
+ System.arraycopy(_fields, i + 1, _fields, i, _size - i);
+ }
+ else
+ {
+ _fields[i] = field;
+ put = true;
+ }
+ }
+ }
+ if (!put)
+ add(field);
+ }
+
+ /**
+ * Set a field.
+ *
+ * @param name the name of the field
+ * @param value the value of the field. If null the field is cleared.
+ */
+ public void put(String name, String value)
+ {
+ if (value == null)
+ remove(name);
+ else
+ put(new HttpField(name, value));
+ }
+
+ public void put(HttpHeader header, HttpHeaderValue value)
+ {
+ put(header, value.toString());
+ }
+
+ /**
+ * Set a field.
+ *
+ * @param header the header name of the field
+ * @param value the value of the field. If null the field is cleared.
+ */
+ public void put(HttpHeader header, String value)
+ {
+ Objects.requireNonNull(header, "header must not be null");
+
+ if (value == null)
+ remove(header);
+ else
+ put(new HttpField(header, value));
+ }
+
+ /**
+ * Set a field.
+ *
+ * @param name the name of the field
+ * @param list the List value of the field. If null the field is cleared.
+ */
+ public void put(String name, List<String> list)
+ {
+ Objects.requireNonNull(name, "name must not be null");
+
+ remove(name);
+ if (list == null)
+ return;
+
+ for (String v : list)
+ {
+ if (v != null)
+ add(name, v);
+ }
+ }
+
+ /**
+ * Add to or set a field. If the field is allowed to have multiple values, add will add multiple
+ * headers of the same name.
+ *
+ * @param name the name of the field
+ * @param value the value of the field.
+ */
+ public void add(String name, String value)
+ {
+ if (value == null)
+ return;
+
+ HttpField field = new HttpField(name, value);
+ add(field);
+ }
+
+ public void add(HttpHeader header, HttpHeaderValue value)
+ {
+ if (value != null)
+ add(header, value.toString());
+ }
+
+ /**
+ * Add to or set a field. If the field is allowed to have multiple values, add will add multiple
+ * headers of the same name.
+ *
+ * @param header the header
+ * @param value the value of the field.
+ */
+ public void add(HttpHeader header, String value)
+ {
+ Objects.requireNonNull(header, "header must not be null");
+
+ if (value == null)
+ throw new IllegalArgumentException("null value");
+
+ HttpField field = new HttpField(header, value);
+ add(field);
+ }
+
+ /**
+ * Remove a field.
+ *
+ * @param name the field to remove
+ * @return the header that was removed
+ */
+ public HttpField remove(HttpHeader name)
+ {
+ HttpField removed = null;
+ for (int i = _size; i-- > 0; )
+ {
+ HttpField f = _fields[i];
+ if (f.getHeader() == name)
+ {
+ removed = f;
+ remove(i);
+ }
+ }
+ return removed;
+ }
+
+ /**
+ * Remove a field.
+ *
+ * @param name the field to remove
+ * @return the header that was removed
+ */
+ public HttpField remove(String name)
+ {
+ HttpField removed = null;
+ for (int i = _size; i-- > 0; )
+ {
+ HttpField f = _fields[i];
+ if (f.is(name))
+ {
+ removed = f;
+ remove(i);
+ }
+ }
+ return removed;
+ }
+
+ private void remove(int i)
+ {
+ _size--;
+ System.arraycopy(_fields, i + 1, _fields, i, _size - i);
+ _fields[_size] = null;
+ }
+
+ /**
+ * Get a header as an long value. Returns the value of an integer field or -1 if not found. The
+ * case of the field name is ignored.
+ *
+ * @param name the case-insensitive field name
+ * @return the value of the field as a long
+ * @throws NumberFormatException If bad long found
+ */
+ public long getLongField(String name) throws NumberFormatException
+ {
+ HttpField field = getField(name);
+ return field == null ? -1L : field.getLongValue();
+ }
+
+ /**
+ * Get a header as a date value. Returns the value of a date field, or -1 if not found. The case
+ * of the field name is ignored.
+ *
+ * @param name the case-insensitive field name
+ * @return the value of the field as a number of milliseconds since unix epoch
+ */
+ public long getDateField(String name)
+ {
+ HttpField field = getField(name);
+ if (field == null)
+ return -1;
+
+ String val = valueParameters(field.getValue(), null);
+ if (val == null)
+ return -1;
+
+ final long date = DateParser.parseDate(val);
+ if (date == -1)
+ throw new IllegalArgumentException("Cannot convert date: " + val);
+ return date;
+ }
+
+ /**
+ * Sets the value of an long field.
+ *
+ * @param name the field name
+ * @param value the field long value
+ */
+ public void putLongField(HttpHeader name, long value)
+ {
+ String v = Long.toString(value);
+ put(name, v);
+ }
+
+ /**
+ * Sets the value of an long field.
+ *
+ * @param name the field name
+ * @param value the field long value
+ */
+ public void putLongField(String name, long value)
+ {
+ String v = Long.toString(value);
+ put(name, v);
+ }
+
+ /**
+ * Sets the value of a date field.
+ *
+ * @param name the field name
+ * @param date the field date value
+ */
+ public void putDateField(HttpHeader name, long date)
+ {
+ String d = DateGenerator.formatDate(date);
+ put(name, d);
+ }
+
+ /**
+ * Sets the value of a date field.
+ *
+ * @param name the field name
+ * @param date the field date value
+ */
+ public void putDateField(String name, long date)
+ {
+ String d = DateGenerator.formatDate(date);
+ put(name, d);
+ }
+
+ /**
+ * Sets the value of a date field.
+ *
+ * @param name the field name
+ * @param date the field date value
+ */
+ public void addDateField(String name, long date)
+ {
+ String d = DateGenerator.formatDate(date);
+ add(name, d);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ int hash = 0;
+ for (HttpField field : _fields)
+ {
+ hash += field.hashCode();
+ }
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o)
+ return true;
+ if (!(o instanceof HttpFields))
+ return false;
+
+ HttpFields that = (HttpFields)o;
+
+ // Order is not important, so we cannot rely on List.equals().
+ if (size() != that.size())
+ return false;
+
+ loop:
+ for (HttpField fi : this)
+ {
+ for (HttpField fa : that)
+ {
+ if (fi.equals(fa))
+ continue loop;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString()
+ {
+ try
+ {
+ StringBuilder buffer = new StringBuilder();
+ for (HttpField field : this)
+ {
+ if (field != null)
+ {
+ String tmp = field.getName();
+ if (tmp != null)
+ buffer.append(tmp);
+ buffer.append(": ");
+ tmp = field.getValue();
+ if (tmp != null)
+ buffer.append(tmp);
+ buffer.append("\r\n");
+ }
+ }
+ buffer.append("\r\n");
+ return buffer.toString();
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ return e.toString();
+ }
+ }
+
+ public void clear()
+ {
+ _size = 0;
+ }
+
+ public void add(HttpField field)
+ {
+ if (field != null)
+ {
+ if (_size == _fields.length)
+ _fields = Arrays.copyOf(_fields, _size * 2);
+ _fields[_size++] = field;
+ }
+ }
+
+ public void addAll(HttpFields fields)
+ {
+ for (int i = 0; i < fields._size; i++)
+ {
+ add(fields._fields[i]);
+ }
+ }
+
+ /**
+ * Add fields from another HttpFields instance. Single valued fields are replaced, while all
+ * others are added.
+ *
+ * @param fields the fields to add
+ */
+ @Deprecated
+ public void add(HttpFields fields)
+ {
+ // TODO this implementation doesn't do what the javadoc says and is really the same
+ // as addAll, which is renamed to add anyway in 10.
+ if (fields == null)
+ return;
+
+ Enumeration<String> e = fields.getFieldNames();
+ while (e.hasMoreElements())
+ {
+ String name = e.nextElement();
+ Enumeration<String> values = fields.getValues(name);
+ while (values.hasMoreElements())
+ {
+ add(name, values.nextElement());
+ }
+ }
+ }
+
+ /**
+ * Get field value without parameters. Some field values can have parameters. This method separates the
+ * value from the parameters and optionally populates a map with the parameters. For example:
+ *
+ * <PRE>
+ *
+ * FieldName : Value ; param1=val1 ; param2=val2
+ *
+ * </PRE>
+ *
+ * @param value The Field value, possibly with parameters.
+ * @return The value.
+ */
+ public static String stripParameters(String value)
+ {
+ if (value == null)
+ return null;
+
+ int i = value.indexOf(';');
+ if (i < 0)
+ return value;
+ return value.substring(0, i).trim();
+ }
+
+ /**
+ * Get field value parameters. Some field values can have parameters. This method separates the
+ * value from the parameters and optionally populates a map with the parameters. For example:
+ *
+ * <PRE>
+ *
+ * FieldName : Value ; param1=val1 ; param2=val2
+ *
+ * </PRE>
+ *
+ * @param value The Field value, possibly with parameters.
+ * @param parameters A map to populate with the parameters, or null
+ * @return The value.
+ */
+ public static String valueParameters(String value, Map<String, String> parameters)
+ {
+ if (value == null)
+ return null;
+
+ int i = value.indexOf(';');
+ if (i < 0)
+ return value;
+ if (parameters == null)
+ return value.substring(0, i).trim();
+
+ StringTokenizer tok1 = new QuotedStringTokenizer(value.substring(i), ";", false, true);
+ while (tok1.hasMoreTokens())
+ {
+ String token = tok1.nextToken();
+ StringTokenizer tok2 = new QuotedStringTokenizer(token, "= ");
+ if (tok2.hasMoreTokens())
+ {
+ String paramName = tok2.nextToken();
+ String paramVal = null;
+ if (tok2.hasMoreTokens())
+ paramVal = tok2.nextToken();
+ parameters.put(paramName, paramVal);
+ }
+ }
+
+ return value.substring(0, i).trim();
+ }
+
+ @Deprecated
+ private static final Float __one = new Float("1.0");
+ @Deprecated
+ private static final Float __zero = new Float("0.0");
+ @Deprecated
+ private static final Trie<Float> __qualities = new ArrayTernaryTrie<>();
+
+ static
+ {
+ __qualities.put("*", __one);
+ __qualities.put("1.0", __one);
+ __qualities.put("1", __one);
+ __qualities.put("0.9", new Float("0.9"));
+ __qualities.put("0.8", new Float("0.8"));
+ __qualities.put("0.7", new Float("0.7"));
+ __qualities.put("0.66", new Float("0.66"));
+ __qualities.put("0.6", new Float("0.6"));
+ __qualities.put("0.5", new Float("0.5"));
+ __qualities.put("0.4", new Float("0.4"));
+ __qualities.put("0.33", new Float("0.33"));
+ __qualities.put("0.3", new Float("0.3"));
+ __qualities.put("0.2", new Float("0.2"));
+ __qualities.put("0.1", new Float("0.1"));
+ __qualities.put("0", __zero);
+ __qualities.put("0.0", __zero);
+ }
+
+ @Deprecated
+ public static Float getQuality(String value)
+ {
+ if (value == null)
+ return __zero;
+
+ int qe = value.indexOf(";");
+ if (qe++ < 0 || qe == value.length())
+ return __one;
+
+ if (value.charAt(qe++) == 'q')
+ {
+ qe++;
+ Float q = __qualities.get(value, qe, value.length() - qe);
+ if (q != null)
+ return q;
+ }
+
+ Map<String, String> params = new HashMap<>(4);
+ valueParameters(value, params);
+ String qs = params.get("q");
+ if (qs == null)
+ qs = "*";
+ Float q = __qualities.get(qs);
+ if (q == null)
+ {
+ try
+ {
+ q = new Float(qs);
+ }
+ catch (Exception e)
+ {
+ q = __one;
+ }
+ }
+ return q;
+ }
+
+ /**
+ * List values in quality order.
+ *
+ * @param e Enumeration of values with quality parameters
+ * @return values in quality order.
+ */
+ @Deprecated
+ public static List<String> qualityList(Enumeration<String> e)
+ {
+ if (e == null || !e.hasMoreElements())
+ return Collections.emptyList();
+
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ while (e.hasMoreElements())
+ {
+ values.addValue(e.nextElement());
+ }
+ return values.getValues();
+ }
+
+ private class ListItr implements ListIterator<HttpField>
+ {
+ int _cursor; // index of next element to return
+ int _current = -1;
+
+ @Override
+ public boolean hasNext()
+ {
+ return _cursor != _size;
+ }
+
+ @Override
+ public HttpField next()
+ {
+ if (_cursor == _size)
+ throw new NoSuchElementException();
+ _current = _cursor++;
+ return _fields[_current];
+ }
+
+ @Override
+ public void remove()
+ {
+ if (_current < 0)
+ throw new IllegalStateException();
+ HttpFields.this.remove(_current);
+ _fields[_size] = null;
+ _cursor = _current;
+ _current = -1;
+ }
+
+ @Override
+ public boolean hasPrevious()
+ {
+ return _cursor > 0;
+ }
+
+ @Override
+ public HttpField previous()
+ {
+ if (_cursor == 0)
+ throw new NoSuchElementException();
+ _current = --_cursor;
+ return _fields[_current];
+ }
+
+ @Override
+ public int nextIndex()
+ {
+ return _cursor;
+ }
+
+ @Override
+ public int previousIndex()
+ {
+ return _cursor - 1;
+ }
+
+ @Override
+ public void set(HttpField field)
+ {
+ if (_current < 0)
+ throw new IllegalStateException();
+ if (field == null)
+ remove();
+ else
+ _fields[_current] = field;
+ }
+
+ @Override
+ public void add(HttpField field)
+ {
+ if (field != null)
+ {
+ _fields = Arrays.copyOf(_fields, _fields.length + 1);
+ System.arraycopy(_fields, _cursor, _fields, _cursor + 1, _size - _cursor);
+ _fields[_cursor++] = field;
+ _size++;
+ _current = -1;
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java
new file mode 100644
index 0000000..bfd958d
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java
@@ -0,0 +1,961 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.http.HttpTokens.EndOfContent;
+import org.eclipse.jetty.util.ArrayTrie;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.Trie;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+import static org.eclipse.jetty.http.HttpStatus.INTERNAL_SERVER_ERROR_500;
+
+/**
+ * HttpGenerator. Builds HTTP Messages.
+ * <p>
+ * If the system property "org.eclipse.jetty.http.HttpGenerator.STRICT" is set to true,
+ * then the generator will strictly pass on the exact strings received from methods and header
+ * fields. Otherwise a fast case insensitive string lookup is used that may alter the
+ * case and white space of some methods/headers
+ */
+public class HttpGenerator
+{
+ private static final Logger LOG = Log.getLogger(HttpGenerator.class);
+
+ public static final boolean __STRICT = Boolean.getBoolean("org.eclipse.jetty.http.HttpGenerator.STRICT");
+
+ private static final byte[] __colon_space = new byte[]{':', ' '};
+ public static final MetaData.Response CONTINUE_100_INFO = new MetaData.Response(HttpVersion.HTTP_1_1, 100, null, null, -1);
+ public static final MetaData.Response PROGRESS_102_INFO = new MetaData.Response(HttpVersion.HTTP_1_1, 102, null, null, -1);
+ public static final MetaData.Response RESPONSE_500_INFO =
+ new MetaData.Response(HttpVersion.HTTP_1_1, INTERNAL_SERVER_ERROR_500, null, new HttpFields()
+ {
+ {
+ put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE);
+ }
+ }, 0);
+
+ // states
+ public enum State
+ {
+ START,
+ COMMITTED,
+ COMPLETING,
+ COMPLETING_1XX,
+ END
+ }
+
+ public enum Result
+ {
+ NEED_CHUNK, // Need a small chunk buffer of CHUNK_SIZE
+ NEED_INFO, // Need the request/response metadata info
+ NEED_HEADER, // Need a buffer to build HTTP headers into
+ HEADER_OVERFLOW, // The header buffer overflowed
+ NEED_CHUNK_TRAILER, // Need a large chunk buffer for last chunk and trailers
+ FLUSH, // The buffers previously generated should be flushed
+ CONTINUE, // Continue generating the message
+ SHUTDOWN_OUT, // Need EOF to be signaled
+ DONE // The current phase of generation is complete
+ }
+
+ // other statics
+ public static final int CHUNK_SIZE = 12;
+
+ private State _state = State.START;
+ private EndOfContent _endOfContent = EndOfContent.UNKNOWN_CONTENT;
+
+ private long _contentPrepared = 0;
+ private boolean _noContentResponse = false;
+ private Boolean _persistent = null;
+ private Supplier<HttpFields> _trailers = null;
+
+ private final int _send;
+ private static final int SEND_SERVER = 0x01;
+ private static final int SEND_XPOWEREDBY = 0x02;
+ private static final Trie<Boolean> ASSUMED_CONTENT_METHODS = new ArrayTrie<>(8);
+
+ static
+ {
+ ASSUMED_CONTENT_METHODS.put(HttpMethod.POST.asString(), Boolean.TRUE);
+ ASSUMED_CONTENT_METHODS.put(HttpMethod.PUT.asString(), Boolean.TRUE);
+ }
+
+ public static void setJettyVersion(String serverVersion)
+ {
+ SEND[SEND_SERVER] = StringUtil.getBytes("Server: " + serverVersion + "\r\n");
+ SEND[SEND_XPOWEREDBY] = StringUtil.getBytes("X-Powered-By: " + serverVersion + "\r\n");
+ SEND[SEND_SERVER | SEND_XPOWEREDBY] = StringUtil.getBytes("Server: " + serverVersion + "\r\nX-Powered-By: " + serverVersion + "\r\n");
+ }
+
+ // data
+ private boolean _needCRLF = false;
+
+ public HttpGenerator()
+ {
+ this(false, false);
+ }
+
+ public HttpGenerator(boolean sendServerVersion, boolean sendXPoweredBy)
+ {
+ _send = (sendServerVersion ? SEND_SERVER : 0) | (sendXPoweredBy ? SEND_XPOWEREDBY : 0);
+ }
+
+ public void reset()
+ {
+ _state = State.START;
+ _endOfContent = EndOfContent.UNKNOWN_CONTENT;
+ _noContentResponse = false;
+ _persistent = null;
+ _contentPrepared = 0;
+ _needCRLF = false;
+ _trailers = null;
+ }
+
+ @Deprecated
+ public boolean getSendServerVersion()
+ {
+ return (_send & SEND_SERVER) != 0;
+ }
+
+ @Deprecated
+ public void setSendServerVersion(boolean sendServerVersion)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ public State getState()
+ {
+ return _state;
+ }
+
+ public boolean isState(State state)
+ {
+ return _state == state;
+ }
+
+ public boolean isIdle()
+ {
+ return _state == State.START;
+ }
+
+ public boolean isEnd()
+ {
+ return _state == State.END;
+ }
+
+ public boolean isCommitted()
+ {
+ return _state.ordinal() >= State.COMMITTED.ordinal();
+ }
+
+ public boolean isChunking()
+ {
+ return _endOfContent == EndOfContent.CHUNKED_CONTENT;
+ }
+
+ public boolean isNoContent()
+ {
+ return _noContentResponse;
+ }
+
+ public void setPersistent(boolean persistent)
+ {
+ _persistent = persistent;
+ }
+
+ /**
+ * @return true if known to be persistent
+ */
+ public boolean isPersistent()
+ {
+ return Boolean.TRUE.equals(_persistent);
+ }
+
+ public boolean isWritten()
+ {
+ return _contentPrepared > 0;
+ }
+
+ public long getContentPrepared()
+ {
+ return _contentPrepared;
+ }
+
+ public void abort()
+ {
+ _persistent = false;
+ _state = State.END;
+ _endOfContent = null;
+ }
+
+ public Result generateRequest(MetaData.Request info, ByteBuffer header, ByteBuffer chunk, ByteBuffer content, boolean last) throws IOException
+ {
+ switch (_state)
+ {
+ case START:
+ {
+ if (info == null)
+ return Result.NEED_INFO;
+
+ if (header == null)
+ return Result.NEED_HEADER;
+
+ // prepare the header
+ int pos = BufferUtil.flipToFill(header);
+ try
+ {
+ // generate ResponseLine
+ generateRequestLine(info, header);
+
+ if (info.getHttpVersion() == HttpVersion.HTTP_0_9)
+ throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "HTTP/0.9 not supported");
+
+ generateHeaders(info, header, content, last);
+
+ boolean expect100 = info.getFields().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString());
+
+ if (expect100)
+ {
+ _state = State.COMMITTED;
+ }
+ else
+ {
+ // handle the content.
+ int len = BufferUtil.length(content);
+ if (len > 0)
+ {
+ _contentPrepared += len;
+ if (isChunking())
+ prepareChunk(header, len);
+ }
+ _state = last ? State.COMPLETING : State.COMMITTED;
+ }
+
+ return Result.FLUSH;
+ }
+ catch (BadMessageException e)
+ {
+ throw e;
+ }
+ catch (BufferOverflowException e)
+ {
+ LOG.ignore(e);
+ return Result.HEADER_OVERFLOW;
+ }
+ catch (Exception e)
+ {
+ throw new BadMessageException(INTERNAL_SERVER_ERROR_500, e.getMessage(), e);
+ }
+ finally
+ {
+ BufferUtil.flipToFlush(header, pos);
+ }
+ }
+
+ case COMMITTED:
+ {
+ return committed(chunk, content, last);
+ }
+
+ case COMPLETING:
+ {
+ return completing(chunk, content);
+ }
+
+ case END:
+ if (BufferUtil.hasContent(content))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("discarding content in COMPLETING");
+ BufferUtil.clear(content);
+ }
+ return Result.DONE;
+
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ private Result committed(ByteBuffer chunk, ByteBuffer content, boolean last)
+ {
+ int len = BufferUtil.length(content);
+
+ // handle the content.
+ if (len > 0)
+ {
+ if (isChunking())
+ {
+ if (chunk == null)
+ return Result.NEED_CHUNK;
+ BufferUtil.clearToFill(chunk);
+ prepareChunk(chunk, len);
+ BufferUtil.flipToFlush(chunk, 0);
+ }
+ _contentPrepared += len;
+ }
+
+ if (last)
+ {
+ _state = State.COMPLETING;
+ return len > 0 ? Result.FLUSH : Result.CONTINUE;
+ }
+ return len > 0 ? Result.FLUSH : Result.DONE;
+ }
+
+ private Result completing(ByteBuffer chunk, ByteBuffer content)
+ {
+ if (BufferUtil.hasContent(content))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("discarding content in COMPLETING");
+ BufferUtil.clear(content);
+ }
+
+ if (isChunking())
+ {
+ if (_trailers != null)
+ {
+ // Do we need a chunk buffer?
+ if (chunk == null || chunk.capacity() <= CHUNK_SIZE)
+ return Result.NEED_CHUNK_TRAILER;
+
+ HttpFields trailers = _trailers.get();
+
+ if (trailers != null)
+ {
+ // Write the last chunk
+ BufferUtil.clearToFill(chunk);
+ generateTrailers(chunk, trailers);
+ BufferUtil.flipToFlush(chunk, 0);
+ _endOfContent = EndOfContent.UNKNOWN_CONTENT;
+ return Result.FLUSH;
+ }
+ }
+
+ // Do we need a chunk buffer?
+ if (chunk == null)
+ return Result.NEED_CHUNK;
+
+ // Write the last chunk
+ BufferUtil.clearToFill(chunk);
+ prepareChunk(chunk, 0);
+ BufferUtil.flipToFlush(chunk, 0);
+ _endOfContent = EndOfContent.UNKNOWN_CONTENT;
+ return Result.FLUSH;
+ }
+
+ _state = State.END;
+ return Boolean.TRUE.equals(_persistent) ? Result.DONE : Result.SHUTDOWN_OUT;
+ }
+
+ @Deprecated
+ public Result generateResponse(MetaData.Response info, ByteBuffer header, ByteBuffer chunk, ByteBuffer content, boolean last) throws IOException
+ {
+ return generateResponse(info, false, header, chunk, content, last);
+ }
+
+ public Result generateResponse(MetaData.Response info, boolean head, ByteBuffer header, ByteBuffer chunk, ByteBuffer content, boolean last) throws IOException
+ {
+ switch (_state)
+ {
+ case START:
+ {
+ if (info == null)
+ return Result.NEED_INFO;
+ HttpVersion version = info.getHttpVersion();
+ if (version == null)
+ throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "No version");
+
+ if (version == HttpVersion.HTTP_0_9)
+ {
+ _persistent = false;
+ _endOfContent = EndOfContent.EOF_CONTENT;
+ if (BufferUtil.hasContent(content))
+ _contentPrepared += content.remaining();
+ _state = last ? State.COMPLETING : State.COMMITTED;
+ return Result.FLUSH;
+ }
+
+ // Do we need a response header
+ if (header == null)
+ return Result.NEED_HEADER;
+
+ // prepare the header
+ int pos = BufferUtil.flipToFill(header);
+ try
+ {
+ // generate ResponseLine
+ generateResponseLine(info, header);
+
+ // Handle 1xx and no content responses
+ int status = info.getStatus();
+ if (status >= 100 && status < 200)
+ {
+ _noContentResponse = true;
+
+ if (status != HttpStatus.SWITCHING_PROTOCOLS_101)
+ {
+ header.put(HttpTokens.CRLF);
+ _state = State.COMPLETING_1XX;
+ return Result.FLUSH;
+ }
+ }
+ else if (status == HttpStatus.NO_CONTENT_204 || status == HttpStatus.NOT_MODIFIED_304)
+ {
+ _noContentResponse = true;
+ }
+
+ generateHeaders(info, header, content, last);
+
+ // handle the content.
+ int len = BufferUtil.length(content);
+ if (len > 0)
+ {
+ _contentPrepared += len;
+ if (isChunking() && !head)
+ prepareChunk(header, len);
+ }
+ _state = last ? State.COMPLETING : State.COMMITTED;
+ }
+ catch (BadMessageException e)
+ {
+ throw e;
+ }
+ catch (BufferOverflowException e)
+ {
+ LOG.ignore(e);
+ return Result.HEADER_OVERFLOW;
+ }
+ catch (Exception e)
+ {
+ throw new BadMessageException(INTERNAL_SERVER_ERROR_500, e.getMessage(), e);
+ }
+ finally
+ {
+ BufferUtil.flipToFlush(header, pos);
+ }
+
+ return Result.FLUSH;
+ }
+
+ case COMMITTED:
+ {
+ return committed(chunk, content, last);
+ }
+
+ case COMPLETING_1XX:
+ {
+ reset();
+ return Result.DONE;
+ }
+
+ case COMPLETING:
+ {
+ return completing(chunk, content);
+ }
+
+ case END:
+ if (BufferUtil.hasContent(content))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("discarding content in COMPLETING");
+ BufferUtil.clear(content);
+ }
+ return Result.DONE;
+
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ private void prepareChunk(ByteBuffer chunk, int remaining)
+ {
+ // if we need CRLF add this to header
+ if (_needCRLF)
+ BufferUtil.putCRLF(chunk);
+
+ // Add the chunk size to the header
+ if (remaining > 0)
+ {
+ BufferUtil.putHexInt(chunk, remaining);
+ BufferUtil.putCRLF(chunk);
+ _needCRLF = true;
+ }
+ else
+ {
+ chunk.put(LAST_CHUNK);
+ _needCRLF = false;
+ }
+ }
+
+ private void generateTrailers(ByteBuffer buffer, HttpFields trailer)
+ {
+ // if we need CRLF add this to header
+ if (_needCRLF)
+ BufferUtil.putCRLF(buffer);
+
+ // Add the chunk size to the header
+ buffer.put(ZERO_CHUNK);
+
+ int n = trailer.size();
+ for (int f = 0; f < n; f++)
+ {
+ HttpField field = trailer.getField(f);
+ putTo(field, buffer);
+ }
+
+ BufferUtil.putCRLF(buffer);
+ }
+
+ private void generateRequestLine(MetaData.Request request, ByteBuffer header)
+ {
+ header.put(StringUtil.getBytes(request.getMethod()));
+ header.put((byte)' ');
+ header.put(StringUtil.getBytes(request.getURIString()));
+ header.put((byte)' ');
+ header.put(request.getHttpVersion().toBytes());
+ header.put(HttpTokens.CRLF);
+ }
+
+ private void generateResponseLine(MetaData.Response response, ByteBuffer header)
+ {
+ // Look for prepared response line
+ int status = response.getStatus();
+ PreparedResponse preprepared = status < __preprepared.length ? __preprepared[status] : null;
+ String reason = response.getReason();
+ if (preprepared != null)
+ {
+ if (reason == null)
+ header.put(preprepared._responseLine);
+ else
+ {
+ header.put(preprepared._schemeCode);
+ header.put(getReasonBytes(reason));
+ header.put(HttpTokens.CRLF);
+ }
+ }
+ else // generate response line
+ {
+ header.put(HTTP_1_1_SPACE);
+ header.put((byte)('0' + status / 100));
+ header.put((byte)('0' + (status % 100) / 10));
+ header.put((byte)('0' + (status % 10)));
+ header.put((byte)' ');
+ if (reason == null)
+ {
+ header.put((byte)('0' + status / 100));
+ header.put((byte)('0' + (status % 100) / 10));
+ header.put((byte)('0' + (status % 10)));
+ }
+ else
+ header.put(getReasonBytes(reason));
+ header.put(HttpTokens.CRLF);
+ }
+ }
+
+ private byte[] getReasonBytes(String reason)
+ {
+ if (reason.length() > 1024)
+ reason = reason.substring(0, 1024);
+ byte[] bytes = StringUtil.getBytes(reason);
+
+ for (int i = bytes.length; i-- > 0; )
+ {
+ if (bytes[i] == '\r' || bytes[i] == '\n')
+ bytes[i] = '?';
+ }
+ return bytes;
+ }
+
+ private void generateHeaders(MetaData info, ByteBuffer header, ByteBuffer content, boolean last)
+ {
+ final MetaData.Request request = (info instanceof MetaData.Request) ? (MetaData.Request)info : null;
+ final MetaData.Response response = (info instanceof MetaData.Response) ? (MetaData.Response)info : null;
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("generateHeaders {} last={} content={}", info, last, BufferUtil.toDetailString(content));
+ LOG.debug(info.getFields().toString());
+ }
+
+ // default field values
+ int send = _send;
+ HttpField transferEncoding = null;
+ boolean http11 = info.getHttpVersion() == HttpVersion.HTTP_1_1;
+ boolean close = false;
+ _trailers = http11 ? info.getTrailerSupplier() : null;
+ boolean chunkedHint = _trailers != null;
+ boolean contentType = false;
+ long contentLength = info.getContentLength();
+ boolean contentLengthField = false;
+
+ // Generate fields
+ HttpFields fields = info.getFields();
+ if (fields != null)
+ {
+ int n = fields.size();
+ for (int f = 0; f < n; f++)
+ {
+ HttpField field = fields.getField(f);
+ HttpHeader h = field.getHeader();
+ if (h == null)
+ putTo(field, header);
+ else
+ {
+ switch (h)
+ {
+ case CONTENT_LENGTH:
+ if (contentLength < 0)
+ contentLength = field.getLongValue();
+ else if (contentLength != field.getLongValue())
+ throw new BadMessageException(INTERNAL_SERVER_ERROR_500, String.format("Incorrect Content-Length %d!=%d", contentLength, field.getLongValue()));
+ contentLengthField = true;
+ break;
+
+ case CONTENT_TYPE:
+ {
+ // write the field to the header
+ contentType = true;
+ putTo(field, header);
+ break;
+ }
+
+ case TRANSFER_ENCODING:
+ {
+ if (http11)
+ {
+ // Don't add yet, treat this only as a hint that there is content
+ // with a preference to chunk if we can
+ transferEncoding = field;
+ chunkedHint = field.contains(HttpHeaderValue.CHUNKED.asString());
+ }
+ break;
+ }
+
+ case CONNECTION:
+ {
+ boolean keepAlive = field.contains(HttpHeaderValue.KEEP_ALIVE.asString());
+ if (keepAlive && info.getHttpVersion() == HttpVersion.HTTP_1_0 && _persistent == null)
+ {
+ _persistent = true;
+ }
+ if (field.contains(HttpHeaderValue.CLOSE.asString()))
+ {
+ close = true;
+ _persistent = false;
+ }
+ if (keepAlive && _persistent == Boolean.FALSE)
+ {
+ field = new HttpField(HttpHeader.CONNECTION,
+ Stream.of(field.getValues()).filter(s -> !HttpHeaderValue.KEEP_ALIVE.is(s))
+ .collect(Collectors.joining(", ")));
+ }
+ putTo(field, header);
+ break;
+ }
+
+ case SERVER:
+ {
+ send = send & ~SEND_SERVER;
+ putTo(field, header);
+ break;
+ }
+
+ default:
+ putTo(field, header);
+ }
+ }
+ }
+ }
+
+ // Can we work out the content length?
+ if (last && contentLength < 0 && _trailers == null)
+ contentLength = _contentPrepared + BufferUtil.length(content);
+
+ // Calculate how to end _content and connection, _content length and transfer encoding
+ // settings from http://tools.ietf.org/html/rfc7230#section-3.3.3
+
+ boolean assumedContentRequest = request != null && Boolean.TRUE.equals(ASSUMED_CONTENT_METHODS.get(request.getMethod()));
+ boolean assumedContent = assumedContentRequest || contentType || chunkedHint;
+ boolean nocontentRequest = request != null && contentLength <= 0 && !assumedContent;
+
+ if (_persistent == null)
+ _persistent = http11 || (request != null && HttpMethod.CONNECT.is(request.getMethod()));
+
+ // If the message is known not to have content
+ if (_noContentResponse || nocontentRequest)
+ {
+ // We don't need to indicate a body length
+ _endOfContent = EndOfContent.NO_CONTENT;
+
+ // But it is an error if there actually is content
+ if (_contentPrepared > 0)
+ throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Content for no content response");
+
+ if (contentLengthField)
+ {
+ if (response != null && response.getStatus() == HttpStatus.NOT_MODIFIED_304)
+ putContentLength(header, contentLength);
+ else if (contentLength > 0)
+ {
+ if (_contentPrepared == 0 && last)
+ {
+ // TODO discard content for backward compatibility with 9.3 releases
+ // TODO review if it is still needed in 9.4 or can we just throw.
+ content.clear();
+ }
+ else
+ throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Content for no content response");
+ }
+ }
+ }
+ // Else if we are HTTP/1.1 and the content length is unknown and we are either persistent
+ // or it is a request with content (which cannot EOF) or the app has requested chunking
+ else if (http11 && (chunkedHint || contentLength < 0 && (_persistent || assumedContentRequest)))
+ {
+ // we use chunking
+ _endOfContent = EndOfContent.CHUNKED_CONTENT;
+
+ // try to use user supplied encoding as it may have other values.
+ if (transferEncoding == null)
+ header.put(TRANSFER_ENCODING_CHUNKED);
+ else if (transferEncoding.toString().endsWith(HttpHeaderValue.CHUNKED.toString()))
+ {
+ putTo(transferEncoding, header);
+ transferEncoding = null;
+ }
+ else if (!chunkedHint)
+ {
+ putTo(new HttpField(HttpHeader.TRANSFER_ENCODING, transferEncoding.getValue() + ",chunked"), header);
+ transferEncoding = null;
+ }
+ else
+ throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Bad Transfer-Encoding");
+ }
+ // Else if we known the content length and are a request or a persistent response,
+ else if (contentLength >= 0 && (request != null || _persistent))
+ {
+ // Use the content length
+ _endOfContent = EndOfContent.CONTENT_LENGTH;
+ putContentLength(header, contentLength);
+ }
+ // Else if we are a response
+ else if (response != null)
+ {
+ // We must use EOF - even if we were trying to be persistent
+ _endOfContent = EndOfContent.EOF_CONTENT;
+ _persistent = false;
+ if (contentLength >= 0 && (contentLength > 0 || assumedContent || contentLengthField))
+ putContentLength(header, contentLength);
+
+ if (http11 && !close)
+ header.put(CONNECTION_CLOSE);
+ }
+ // Else we must be a request
+ else
+ {
+ // with no way to indicate body length
+ throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Unknown content length for request");
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug(_endOfContent.toString());
+
+ // Add transfer encoding if it is not chunking
+ if (transferEncoding != null)
+ {
+ if (chunkedHint)
+ {
+ String v = transferEncoding.getValue();
+ int c = v.lastIndexOf(',');
+ if (c > 0 && v.lastIndexOf(HttpHeaderValue.CHUNKED.toString(), c) > c)
+ putTo(new HttpField(HttpHeader.TRANSFER_ENCODING, v.substring(0, c).trim()), header);
+ }
+ else
+ {
+ putTo(transferEncoding, header);
+ }
+ }
+
+ // Send server?
+ int status = response != null ? response.getStatus() : -1;
+ if (status > 199)
+ header.put(SEND[send]);
+
+ // end the header.
+ header.put(HttpTokens.CRLF);
+ }
+
+ private static void putContentLength(ByteBuffer header, long contentLength)
+ {
+ if (contentLength == 0)
+ header.put(CONTENT_LENGTH_0);
+ else
+ {
+ header.put(HttpHeader.CONTENT_LENGTH.getBytesColonSpace());
+ BufferUtil.putDecLong(header, contentLength);
+ header.put(HttpTokens.CRLF);
+ }
+ }
+
+ public static byte[] getReasonBuffer(int code)
+ {
+ PreparedResponse status = code < __preprepared.length ? __preprepared[code] : null;
+ if (status != null)
+ return status._reason;
+ return null;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{s=%s}",
+ getClass().getSimpleName(),
+ hashCode(),
+ _state);
+ }
+
+ // common _content
+ private static final byte[] ZERO_CHUNK = {(byte)'0', (byte)'\r', (byte)'\n'};
+ private static final byte[] LAST_CHUNK = {(byte)'0', (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n'};
+ private static final byte[] CONTENT_LENGTH_0 = StringUtil.getBytes("Content-Length: 0\r\n");
+ private static final byte[] CONNECTION_CLOSE = StringUtil.getBytes("Connection: close\r\n");
+ private static final byte[] HTTP_1_1_SPACE = StringUtil.getBytes(HttpVersion.HTTP_1_1 + " ");
+ private static final byte[] TRANSFER_ENCODING_CHUNKED = StringUtil.getBytes("Transfer-Encoding: chunked\r\n");
+ private static final byte[][] SEND = new byte[][]{
+ new byte[0],
+ StringUtil.getBytes("Server: Jetty(9.x.x)\r\n"),
+ StringUtil.getBytes("X-Powered-By: Jetty(9.x.x)\r\n"),
+ StringUtil.getBytes("Server: Jetty(9.x.x)\r\nX-Powered-By: Jetty(9.x.x)\r\n")
+ };
+
+ // Build cache of response lines for status
+ private static class PreparedResponse
+ {
+ byte[] _reason;
+ byte[] _schemeCode;
+ byte[] _responseLine;
+ }
+
+ private static final PreparedResponse[] __preprepared = new PreparedResponse[HttpStatus.MAX_CODE + 1];
+
+ static
+ {
+ int versionLength = HttpVersion.HTTP_1_1.toString().length();
+
+ for (int i = 0; i < __preprepared.length; i++)
+ {
+ HttpStatus.Code code = HttpStatus.getCode(i);
+ if (code == null)
+ continue;
+ String reason = code.getMessage();
+ byte[] line = new byte[versionLength + 5 + reason.length() + 2];
+ HttpVersion.HTTP_1_1.toBuffer().get(line, 0, versionLength);
+ line[versionLength + 0] = ' ';
+ line[versionLength + 1] = (byte)('0' + i / 100);
+ line[versionLength + 2] = (byte)('0' + (i % 100) / 10);
+ line[versionLength + 3] = (byte)('0' + (i % 10));
+ line[versionLength + 4] = ' ';
+ for (int j = 0; j < reason.length(); j++)
+ {
+ line[versionLength + 5 + j] = (byte)reason.charAt(j);
+ }
+ line[versionLength + 5 + reason.length()] = HttpTokens.CARRIAGE_RETURN;
+ line[versionLength + 6 + reason.length()] = HttpTokens.LINE_FEED;
+
+ __preprepared[i] = new PreparedResponse();
+ __preprepared[i]._schemeCode = Arrays.copyOfRange(line, 0, versionLength + 5);
+ __preprepared[i]._reason = Arrays.copyOfRange(line, versionLength + 5, line.length - 2);
+ __preprepared[i]._responseLine = line;
+ }
+ }
+
+ private static void putSanitisedName(String s, ByteBuffer buffer)
+ {
+ int l = s.length();
+ for (int i = 0; i < l; i++)
+ {
+ char c = s.charAt(i);
+
+ if (c < 0 || c > 0xff || c == '\r' || c == '\n' || c == ':')
+ buffer.put((byte)'?');
+ else
+ buffer.put((byte)(0xff & c));
+ }
+ }
+
+ private static void putSanitisedValue(String s, ByteBuffer buffer)
+ {
+ int l = s.length();
+ for (int i = 0; i < l; i++)
+ {
+ char c = s.charAt(i);
+
+ if (c < 0 || c > 0xff || c == '\r' || c == '\n')
+ buffer.put((byte)' ');
+ else
+ buffer.put((byte)(0xff & c));
+ }
+ }
+
+ public static void putTo(HttpField field, ByteBuffer bufferInFillMode)
+ {
+ if (field instanceof PreEncodedHttpField)
+ {
+ ((PreEncodedHttpField)field).putTo(bufferInFillMode, HttpVersion.HTTP_1_0);
+ }
+ else
+ {
+ HttpHeader header = field.getHeader();
+ if (header != null)
+ {
+ bufferInFillMode.put(header.getBytesColonSpace());
+ putSanitisedValue(field.getValue(), bufferInFillMode);
+ }
+ else
+ {
+ putSanitisedName(field.getName(), bufferInFillMode);
+ bufferInFillMode.put(__colon_space);
+ putSanitisedValue(field.getValue(), bufferInFillMode);
+ }
+
+ BufferUtil.putCRLF(bufferInFillMode);
+ }
+ }
+
+ public static void putTo(HttpFields fields, ByteBuffer bufferInFillMode)
+ {
+ for (HttpField field : fields)
+ {
+ if (field != null)
+ putTo(field, bufferInFillMode);
+ }
+ BufferUtil.putCRLF(bufferInFillMode);
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java
new file mode 100644
index 0000000..4dcb2b3
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java
@@ -0,0 +1,217 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.ArrayTrie;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.Trie;
+
+public enum HttpHeader
+{
+
+ /**
+ * General Fields.
+ */
+ CONNECTION("Connection"),
+ CACHE_CONTROL("Cache-Control"),
+ DATE("Date"),
+ PRAGMA("Pragma"),
+ PROXY_CONNECTION("Proxy-Connection"),
+ TRAILER("Trailer"),
+ TRANSFER_ENCODING("Transfer-Encoding"),
+ UPGRADE("Upgrade"),
+ VIA("Via"),
+ WARNING("Warning"),
+ NEGOTIATE("Negotiate"),
+
+ /**
+ * Entity Fields.
+ */
+ ALLOW("Allow"),
+ CONTENT_ENCODING("Content-Encoding"),
+ CONTENT_LANGUAGE("Content-Language"),
+ CONTENT_LENGTH("Content-Length"),
+ CONTENT_LOCATION("Content-Location"),
+ CONTENT_MD5("Content-MD5"),
+ CONTENT_RANGE("Content-Range"),
+ CONTENT_TYPE("Content-Type"),
+ EXPIRES("Expires"),
+ LAST_MODIFIED("Last-Modified"),
+
+ /**
+ * Request Fields.
+ */
+ ACCEPT("Accept"),
+ ACCEPT_CHARSET("Accept-Charset"),
+ ACCEPT_ENCODING("Accept-Encoding"),
+ ACCEPT_LANGUAGE("Accept-Language"),
+ AUTHORIZATION("Authorization"),
+ EXPECT("Expect"),
+ FORWARDED("Forwarded"),
+ FROM("From"),
+ HOST("Host"),
+ IF_MATCH("If-Match"),
+ IF_MODIFIED_SINCE("If-Modified-Since"),
+ IF_NONE_MATCH("If-None-Match"),
+ IF_RANGE("If-Range"),
+ IF_UNMODIFIED_SINCE("If-Unmodified-Since"),
+ KEEP_ALIVE("Keep-Alive"),
+ MAX_FORWARDS("Max-Forwards"),
+ PROXY_AUTHORIZATION("Proxy-Authorization"),
+ RANGE("Range"),
+ REQUEST_RANGE("Request-Range"),
+ REFERER("Referer"),
+ TE("TE"),
+ USER_AGENT("User-Agent"),
+ X_FORWARDED_FOR("X-Forwarded-For"),
+ X_FORWARDED_PORT("X-Forwarded-Port"),
+ X_FORWARDED_PROTO("X-Forwarded-Proto"),
+ X_FORWARDED_SERVER("X-Forwarded-Server"),
+ X_FORWARDED_HOST("X-Forwarded-Host"),
+
+ /**
+ * Response Fields.
+ */
+ ACCEPT_RANGES("Accept-Ranges"),
+ AGE("Age"),
+ ETAG("ETag"),
+ LOCATION("Location"),
+ PROXY_AUTHENTICATE("Proxy-Authenticate"),
+ RETRY_AFTER("Retry-After"),
+ SERVER("Server"),
+ SERVLET_ENGINE("Servlet-Engine"),
+ VARY("Vary"),
+ WWW_AUTHENTICATE("WWW-Authenticate"),
+
+ /**
+ * WebSocket Fields.
+ */
+ ORIGIN("Origin"),
+ SEC_WEBSOCKET_KEY("Sec-WebSocket-Key"),
+ SEC_WEBSOCKET_VERSION("Sec-WebSocket-Version"),
+ SEC_WEBSOCKET_EXTENSIONS("Sec-WebSocket-Extensions"),
+ SEC_WEBSOCKET_SUBPROTOCOL("Sec-WebSocket-Protocol"),
+ SEC_WEBSOCKET_ACCEPT("Sec-WebSocket-Accept"),
+
+ /**
+ * Other Fields.
+ */
+ COOKIE("Cookie"),
+ SET_COOKIE("Set-Cookie"),
+ SET_COOKIE2("Set-Cookie2"),
+ MIME_VERSION("MIME-Version"),
+ IDENTITY("identity"),
+
+ X_POWERED_BY("X-Powered-By"),
+ HTTP2_SETTINGS("HTTP2-Settings"),
+
+ STRICT_TRANSPORT_SECURITY("Strict-Transport-Security"),
+
+ /**
+ * HTTP2 Fields.
+ */
+ C_METHOD(":method", true),
+ C_SCHEME(":scheme", true),
+ C_AUTHORITY(":authority", true),
+ C_PATH(":path", true),
+ C_STATUS(":status", true),
+
+ UNKNOWN("::UNKNOWN::", true);
+
+ public static final Trie<HttpHeader> CACHE = new ArrayTrie<>(630);
+
+ static
+ {
+ for (HttpHeader header : HttpHeader.values())
+ {
+ if (header != UNKNOWN)
+ if (!CACHE.put(header.toString(), header))
+ throw new IllegalStateException();
+ }
+ }
+
+ private final String _string;
+ private final String _lowerCase;
+ private final byte[] _bytes;
+ private final byte[] _bytesColonSpace;
+ private final ByteBuffer _buffer;
+ private final boolean _pseudo;
+
+ HttpHeader(String s)
+ {
+ this(s, false);
+ }
+
+ HttpHeader(String s, boolean pseudo)
+ {
+ _string = s;
+ _lowerCase = StringUtil.asciiToLowerCase(s);
+ _bytes = StringUtil.getBytes(s);
+ _bytesColonSpace = StringUtil.getBytes(s + ": ");
+ _buffer = ByteBuffer.wrap(_bytes);
+ _pseudo = pseudo;
+ }
+
+ public String lowerCaseName()
+ {
+ return _lowerCase;
+ }
+
+ public ByteBuffer toBuffer()
+ {
+ return _buffer.asReadOnlyBuffer();
+ }
+
+ public byte[] getBytes()
+ {
+ return _bytes;
+ }
+
+ public byte[] getBytesColonSpace()
+ {
+ return _bytesColonSpace;
+ }
+
+ public boolean is(String s)
+ {
+ return _string.equalsIgnoreCase(s);
+ }
+
+ /**
+ * @return True if the header is a HTTP2 Pseudo header (eg ':path')
+ */
+ public boolean isPseudo()
+ {
+ return _pseudo;
+ }
+
+ public String asString()
+ {
+ return _string;
+ }
+
+ @Override
+ public String toString()
+ {
+ return _string;
+ }
+}
+
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeaderValue.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeaderValue.java
new file mode 100644
index 0000000..b06bc43
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeaderValue.java
@@ -0,0 +1,98 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+import java.util.EnumSet;
+
+import org.eclipse.jetty.util.ArrayTrie;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Trie;
+
+/**
+ *
+ */
+public enum HttpHeaderValue
+{
+ CLOSE("close"),
+ CHUNKED("chunked"),
+ GZIP("gzip"),
+ IDENTITY("identity"),
+ KEEP_ALIVE("keep-alive"),
+ CONTINUE("100-continue"),
+ PROCESSING("102-processing"),
+ TE("TE"),
+ BYTES("bytes"),
+ NO_CACHE("no-cache"),
+ UPGRADE("Upgrade"),
+ UNKNOWN("::UNKNOWN::");
+
+ public static final Trie<HttpHeaderValue> CACHE = new ArrayTrie<HttpHeaderValue>();
+
+ static
+ {
+ for (HttpHeaderValue value : HttpHeaderValue.values())
+ {
+ if (value != UNKNOWN)
+ CACHE.put(value.toString(), value);
+ }
+ }
+
+ private final String _string;
+ private final ByteBuffer _buffer;
+
+ HttpHeaderValue(String s)
+ {
+ _string = s;
+ _buffer = BufferUtil.toBuffer(s);
+ }
+
+ public ByteBuffer toBuffer()
+ {
+ return _buffer.asReadOnlyBuffer();
+ }
+
+ public boolean is(String s)
+ {
+ return _string.equalsIgnoreCase(s);
+ }
+
+ public String asString()
+ {
+ return _string;
+ }
+
+ @Override
+ public String toString()
+ {
+ return _string;
+ }
+
+ private static EnumSet<HttpHeader> __known =
+ EnumSet.of(HttpHeader.CONNECTION,
+ HttpHeader.TRANSFER_ENCODING,
+ HttpHeader.CONTENT_ENCODING);
+
+ public static boolean hasKnownValues(HttpHeader header)
+ {
+ if (header == null)
+ return false;
+ return __known.contains(header);
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java
new file mode 100644
index 0000000..e528e05
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java
@@ -0,0 +1,233 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.ArrayTernaryTrie;
+import org.eclipse.jetty.util.ArrayTrie;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.Trie;
+
+/**
+ * Known HTTP Methods
+ */
+public enum HttpMethod
+{
+ // From https://www.iana.org/assignments/http-methods/http-methods.xhtml
+ ACL(Type.IDEMPOTENT),
+ BASELINE_CONTROL(Type.IDEMPOTENT),
+ BIND(Type.IDEMPOTENT),
+ CHECKIN(Type.IDEMPOTENT),
+ CHECKOUT(Type.IDEMPOTENT),
+ CONNECT(Type.NORMAL),
+ COPY(Type.IDEMPOTENT),
+ DELETE(Type.IDEMPOTENT),
+ GET(Type.SAFE),
+ HEAD(Type.SAFE),
+ LABEL(Type.IDEMPOTENT),
+ LINK(Type.IDEMPOTENT),
+ LOCK(Type.NORMAL),
+ MERGE(Type.IDEMPOTENT),
+ MKACTIVITY(Type.IDEMPOTENT),
+ MKCALENDAR(Type.IDEMPOTENT),
+ MKCOL(Type.IDEMPOTENT),
+ MKREDIRECTREF(Type.IDEMPOTENT),
+ MKWORKSPACE(Type.IDEMPOTENT),
+ MOVE(Type.IDEMPOTENT),
+ OPTIONS(Type.SAFE),
+ ORDERPATCH(Type.IDEMPOTENT),
+ PATCH(Type.NORMAL),
+ POST(Type.NORMAL),
+ PRI(Type.SAFE),
+ PROPFIND(Type.SAFE),
+ PROPPATCH(Type.IDEMPOTENT),
+ PUT(Type.IDEMPOTENT),
+ REBIND(Type.IDEMPOTENT),
+ REPORT(Type.SAFE),
+ SEARCH(Type.SAFE),
+ TRACE(Type.SAFE),
+ UNBIND(Type.IDEMPOTENT),
+ UNCHECKOUT(Type.IDEMPOTENT),
+ UNLINK(Type.IDEMPOTENT),
+ UNLOCK(Type.IDEMPOTENT),
+ UPDATE(Type.IDEMPOTENT),
+ UPDATEREDIRECTREF(Type.IDEMPOTENT),
+ VERSION_CONTROL(Type.IDEMPOTENT),
+
+ // Other methods
+ PROXY(Type.NORMAL);
+
+ // The type of the method
+ private enum Type
+ {
+ NORMAL,
+ IDEMPOTENT,
+ SAFE
+ }
+
+ private final String _method;
+ private final byte[] _bytes;
+ private final ByteBuffer _buffer;
+ private final Type _type;
+
+ HttpMethod(Type type)
+ {
+ _method = name().replace('_', '-');
+ _type = type;
+ _bytes = StringUtil.getBytes(_method);
+ _buffer = ByteBuffer.wrap(_bytes);
+ }
+
+ public byte[] getBytes()
+ {
+ return _bytes;
+ }
+
+ public boolean is(String s)
+ {
+ return toString().equalsIgnoreCase(s);
+ }
+
+ /**
+ * An HTTP method is safe if it doesn't alter the state of the server.
+ * In other words, a method is safe if it leads to a read-only operation.
+ * Several common HTTP methods are safe: GET , HEAD , or OPTIONS .
+ * All safe methods are also idempotent, but not all idempotent methods are safe
+ * @return if the method is safe.
+ */
+ public boolean isSafe()
+ {
+ return _type == Type.SAFE;
+ }
+
+ /**
+ * An idempotent HTTP method is an HTTP method that can be called many times without different outcomes.
+ * It would not matter if the method is called only once, or ten times over. The result should be the same.
+ * @return true if the method is idempotent.
+ */
+ public boolean isIdempotent()
+ {
+ return _type.ordinal() >= Type.IDEMPOTENT.ordinal();
+ }
+
+ public ByteBuffer asBuffer()
+ {
+ return _buffer.asReadOnlyBuffer();
+ }
+
+ public String asString()
+ {
+ return _method;
+ }
+
+ public String toString()
+ {
+ return _method;
+ }
+
+ public static final Trie<HttpMethod> INSENSITIVE_CACHE = new ArrayTrie<>(252);
+ public static final Trie<HttpMethod> CACHE = new ArrayTernaryTrie<>(false, 300);
+ public static final Trie<HttpMethod> LOOK_AHEAD = new ArrayTernaryTrie<>(false, 330);
+ public static final int ACL_AS_INT = ('A' & 0xff) << 24 | ('C' & 0xFF) << 16 | ('L' & 0xFF) << 8 | (' ' & 0xFF);
+ public static final int GET_AS_INT = ('G' & 0xff) << 24 | ('E' & 0xFF) << 16 | ('T' & 0xFF) << 8 | (' ' & 0xFF);
+ public static final int PRI_AS_INT = ('P' & 0xff) << 24 | ('R' & 0xFF) << 16 | ('I' & 0xFF) << 8 | (' ' & 0xFF);
+ public static final int PUT_AS_INT = ('P' & 0xff) << 24 | ('U' & 0xFF) << 16 | ('T' & 0xFF) << 8 | (' ' & 0xFF);
+ public static final int POST_AS_INT = ('P' & 0xff) << 24 | ('O' & 0xFF) << 16 | ('S' & 0xFF) << 8 | ('T' & 0xFF);
+ public static final int HEAD_AS_INT = ('H' & 0xff) << 24 | ('E' & 0xFF) << 16 | ('A' & 0xFF) << 8 | ('D' & 0xFF);
+ static
+ {
+ for (HttpMethod method : HttpMethod.values())
+ {
+ if (!INSENSITIVE_CACHE.put(method.asString(), method))
+ throw new IllegalStateException("INSENSITIVE_CACHE too small: " + method);
+
+ if (!CACHE.put(method.asString(), method))
+ throw new IllegalStateException("CACHE too small: " + method);
+
+ if (!LOOK_AHEAD.put(method.asString() + ' ', method))
+ throw new IllegalStateException("LOOK_AHEAD too small: " + method);
+ }
+ }
+
+ /**
+ * Optimized lookup to find a method name and trailing space in a byte array.
+ *
+ * @param bytes Array containing ISO-8859-1 characters
+ * @param position The first valid index
+ * @param limit The first non valid index
+ * @return An HttpMethod if a match or null if no easy match.
+ * @deprecated Not used
+ */
+ @Deprecated
+ public static HttpMethod lookAheadGet(byte[] bytes, final int position, int limit)
+ {
+ return LOOK_AHEAD.getBest(bytes, position, limit - position);
+ }
+
+ /**
+ * Optimized lookup to find a method name and trailing space in a byte array.
+ *
+ * @param buffer buffer containing ISO-8859-1 characters, it is not modified.
+ * @return An HttpMethod if a match or null if no easy match.
+ */
+ public static HttpMethod lookAheadGet(ByteBuffer buffer)
+ {
+ int len = buffer.remaining();
+ // Short cut for 3 char methods, mostly for GET optimisation
+ if (len > 3)
+ {
+ switch (buffer.getInt(buffer.position()))
+ {
+ case ACL_AS_INT:
+ return ACL;
+ case GET_AS_INT:
+ return GET;
+ case PRI_AS_INT:
+ return PRI;
+ case PUT_AS_INT:
+ return PUT;
+ case POST_AS_INT:
+ if (len > 4 && buffer.get(buffer.position() + 4) == ' ')
+ return POST;
+ break;
+ case HEAD_AS_INT:
+ if (len > 4 && buffer.get(buffer.position() + 4) == ' ')
+ return HEAD;
+ break;
+ default:
+ break;
+ }
+ }
+ return LOOK_AHEAD.getBest(buffer, 0, len);
+ }
+
+ /**
+ * Converts the given String parameter to an HttpMethod.
+ * The string may differ from the Enum name as a '-' in the method
+ * name is represented as a '_' in the Enum name.
+ *
+ * @param method the String to get the equivalent HttpMethod from
+ * @return the HttpMethod or null if the parameter method is unknown
+ */
+ public static HttpMethod fromString(String method)
+ {
+ return CACHE.get(method);
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
new file mode 100644
index 0000000..d67a895
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
@@ -0,0 +1,2049 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Locale;
+
+import org.eclipse.jetty.http.HttpTokens.EndOfContent;
+import org.eclipse.jetty.util.ArrayTernaryTrie;
+import org.eclipse.jetty.util.ArrayTrie;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.Trie;
+import org.eclipse.jetty.util.Utf8StringBuilder;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+import static org.eclipse.jetty.http.HttpComplianceSection.MULTIPLE_CONTENT_LENGTHS;
+import static org.eclipse.jetty.http.HttpComplianceSection.TRANSFER_ENCODING_WITH_CONTENT_LENGTH;
+
+/**
+ * A Parser for 1.0 and 1.1 as defined by RFC7230
+ * <p>
+ * This parser parses HTTP client and server messages from buffers
+ * passed in the {@link #parseNext(ByteBuffer)} method. The parsed
+ * elements of the HTTP message are passed as event calls to the
+ * {@link HttpHandler} instance the parser is constructed with.
+ * If the passed handler is a {@link RequestHandler} then server side
+ * parsing is performed and if it is a {@link ResponseHandler}, then
+ * client side parsing is done.
+ * </p>
+ * <p>
+ * The contract of the {@link HttpHandler} API is that if a call returns
+ * true then the call to {@link #parseNext(ByteBuffer)} will return as
+ * soon as possible also with a true response. Typically this indicates
+ * that the parsing has reached a stage where the caller should process
+ * the events accumulated by the handler. It is the preferred calling
+ * style that handling such as calling a servlet to process a request,
+ * should be done after a true return from {@link #parseNext(ByteBuffer)}
+ * rather than from within the scope of a call like
+ * {@link RequestHandler#messageComplete()}
+ * </p>
+ * <p>
+ * For performance, the parse is heavily dependent on the
+ * {@link Trie#getBest(ByteBuffer, int, int)} method to look ahead in a
+ * single pass for both the structure ( : and CRLF ) and semantic (which
+ * header and value) of a header. Specifically the static {@link HttpHeader#CACHE}
+ * is used to lookup common combinations of headers and values
+ * (eg. "Connection: close"), or just header names (eg. "Connection:" ).
+ * For headers who's value is not known statically (eg. Host, COOKIE) then a
+ * per parser dynamic Trie of {@link HttpFields} from previous parsed messages
+ * is used to help the parsing of subsequent messages.
+ * </p>
+ * <p>
+ * The parser can work in varying compliance modes:
+ * <dl>
+ * <dt>RFC7230</dt><dd>(default) Compliance with RFC7230</dd>
+ * <dt>RFC2616</dt><dd>Wrapped headers and HTTP/0.9 supported</dd>
+ * <dt>LEGACY</dt><dd>(aka STRICT) Adherence to Servlet Specification requirement for
+ * exact case of header names, bypassing the header caches, which are case insensitive,
+ * otherwise equivalent to RFC2616</dd>
+ * </dl>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc7230">RFC 7230</a>
+ */
+public class HttpParser
+{
+ public static final Logger LOG = Log.getLogger(HttpParser.class);
+ @Deprecated
+ public static final String __STRICT = "org.eclipse.jetty.http.HttpParser.STRICT";
+ public static final int INITIAL_URI_LENGTH = 256;
+ private static final int MAX_CHUNK_LENGTH = Integer.MAX_VALUE / 16 - 16;
+
+ /**
+ * Cache of common {@link HttpField}s including: <UL>
+ * <LI>Common static combinations such as:<UL>
+ * <li>Connection: close
+ * <li>Accept-Encoding: gzip
+ * <li>Content-Length: 0
+ * </ul>
+ * <li>Combinations of Content-Type header for common mime types by common charsets
+ * <li>Most common headers with null values so that a lookup will at least
+ * determine the header name even if the name:value combination is not cached
+ * </ul>
+ */
+ public static final Trie<HttpField> CACHE = new ArrayTrie<>(2048);
+ private static final Trie<HttpField> NO_CACHE = Trie.empty(true);
+
+ // States
+ public enum FieldState
+ {
+ FIELD,
+ IN_NAME,
+ VALUE,
+ IN_VALUE,
+ WS_AFTER_NAME,
+ }
+
+ // States
+ public enum State
+ {
+ START,
+ METHOD,
+ RESPONSE_VERSION,
+ SPACE1,
+ STATUS,
+ URI,
+ SPACE2,
+ REQUEST_VERSION,
+ REASON,
+ PROXY,
+ HEADER,
+ CONTENT,
+ EOF_CONTENT,
+ CHUNKED_CONTENT,
+ CHUNK_SIZE,
+ CHUNK_PARAMS,
+ CHUNK,
+ CONTENT_END,
+ TRAILER,
+ END,
+ CLOSE, // The associated stream/endpoint should be closed
+ CLOSED // The associated stream/endpoint is at EOF
+ }
+
+ private static final EnumSet<State> __idleStates = EnumSet.of(State.START, State.END, State.CLOSE, State.CLOSED);
+ private static final EnumSet<State> __completeStates = EnumSet.of(State.END, State.CLOSE, State.CLOSED);
+ private static final EnumSet<State> __terminatedStates = EnumSet.of(State.CLOSE, State.CLOSED);
+
+ private final boolean debug = LOG.isDebugEnabled(); // Cache debug to help branch prediction
+ private final HttpHandler _handler;
+ private final RequestHandler _requestHandler;
+ private final ResponseHandler _responseHandler;
+ private final ComplianceHandler _complianceHandler;
+ private final int _maxHeaderBytes;
+ private final HttpCompliance _compliance;
+ private final EnumSet<HttpComplianceSection> _compliances;
+ private final Utf8StringBuilder _uri = new Utf8StringBuilder(INITIAL_URI_LENGTH);
+ private HttpField _field;
+ private HttpHeader _header;
+ private String _headerString;
+ private String _valueString;
+ private int _responseStatus;
+ private int _headerBytes;
+ private boolean _host;
+ private boolean _headerComplete;
+
+ private volatile State _state = State.START;
+ private volatile FieldState _fieldState = FieldState.FIELD;
+ private volatile boolean _eof;
+ private HttpMethod _method;
+ private String _methodString;
+ private HttpVersion _version;
+ private EndOfContent _endOfContent;
+ private boolean _hasContentLength;
+ private boolean _hasTransferEncoding;
+ private long _contentLength = -1;
+ private long _contentPosition;
+ private int _chunkLength;
+ private int _chunkPosition;
+ private boolean _headResponse;
+ private boolean _cr;
+ private ByteBuffer _contentChunk;
+ private Trie<HttpField> _fieldCache;
+
+ private int _length;
+ private final StringBuilder _string = new StringBuilder();
+
+ static
+ {
+ CACHE.put(new HttpField(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE));
+ CACHE.put(new HttpField(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE));
+ CACHE.put(new HttpField(HttpHeader.CONNECTION, HttpHeaderValue.UPGRADE));
+ CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip"));
+ CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip, deflate"));
+ CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip, deflate, br"));
+ CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip,deflate,sdch"));
+ CACHE.put(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-US,en;q=0.5"));
+ CACHE.put(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-GB,en-US;q=0.8,en;q=0.6"));
+ CACHE.put(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-AU,en;q=0.9,it-IT;q=0.8,it;q=0.7,en-GB;q=0.6,en-US;q=0.5"));
+ CACHE.put(new HttpField(HttpHeader.ACCEPT_CHARSET, "ISO-8859-1,utf-8;q=0.7,*;q=0.3"));
+ CACHE.put(new HttpField(HttpHeader.ACCEPT, "*/*"));
+ CACHE.put(new HttpField(HttpHeader.ACCEPT, "image/png,image/*;q=0.8,*/*;q=0.5"));
+ CACHE.put(new HttpField(HttpHeader.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"));
+ CACHE.put(new HttpField(HttpHeader.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"));
+ CACHE.put(new HttpField(HttpHeader.ACCEPT_RANGES, HttpHeaderValue.BYTES));
+ CACHE.put(new HttpField(HttpHeader.PRAGMA, "no-cache"));
+ CACHE.put(new HttpField(HttpHeader.CACHE_CONTROL, "private, no-cache, no-cache=Set-Cookie, proxy-revalidate"));
+ CACHE.put(new HttpField(HttpHeader.CACHE_CONTROL, "no-cache"));
+ CACHE.put(new HttpField(HttpHeader.CACHE_CONTROL, "max-age=0"));
+ CACHE.put(new HttpField(HttpHeader.CONTENT_LENGTH, "0"));
+ CACHE.put(new HttpField(HttpHeader.CONTENT_ENCODING, "gzip"));
+ CACHE.put(new HttpField(HttpHeader.CONTENT_ENCODING, "deflate"));
+ CACHE.put(new HttpField(HttpHeader.TRANSFER_ENCODING, "chunked"));
+ CACHE.put(new HttpField(HttpHeader.EXPIRES, "Fri, 01 Jan 1990 00:00:00 GMT"));
+
+ // Add common Content types as fields
+ for (String type : new String[]{
+ "text/plain", "text/html", "text/xml", "text/json", "application/json", "application/x-www-form-urlencoded"
+ })
+ {
+ HttpField field = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type);
+ CACHE.put(field);
+
+ for (String charset : new String[]{"utf-8", "iso-8859-1"})
+ {
+ CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + ";charset=" + charset));
+ CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + "; charset=" + charset));
+ CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + ";charset=" + charset.toUpperCase(Locale.ENGLISH)));
+ CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + "; charset=" + charset.toUpperCase(Locale.ENGLISH)));
+ }
+ }
+
+ // Add headers with null values so HttpParser can avoid looking up name again for unknown values
+ for (HttpHeader h : HttpHeader.values())
+ {
+ if (!h.isPseudo() && !CACHE.put(new HttpField(h, (String)null)))
+ throw new IllegalStateException("CACHE FULL");
+ }
+ }
+
+ private static HttpCompliance compliance()
+ {
+ boolean strict = Boolean.getBoolean(__STRICT);
+ if (strict)
+ {
+ LOG.warn("Deprecated property used: " + __STRICT);
+ return HttpCompliance.LEGACY;
+ }
+ return HttpCompliance.RFC7230;
+ }
+
+ public HttpParser(RequestHandler handler)
+ {
+ this(handler, -1, compliance());
+ }
+
+ public HttpParser(ResponseHandler handler)
+ {
+ this(handler, -1, compliance());
+ }
+
+ public HttpParser(RequestHandler handler, int maxHeaderBytes)
+ {
+ this(handler, maxHeaderBytes, compliance());
+ }
+
+ public HttpParser(ResponseHandler handler, int maxHeaderBytes)
+ {
+ this(handler, maxHeaderBytes, compliance());
+ }
+
+ @Deprecated
+ public HttpParser(RequestHandler handler, int maxHeaderBytes, boolean strict)
+ {
+ this(handler, maxHeaderBytes, strict ? HttpCompliance.LEGACY : compliance());
+ }
+
+ @Deprecated
+ public HttpParser(ResponseHandler handler, int maxHeaderBytes, boolean strict)
+ {
+ this(handler, maxHeaderBytes, strict ? HttpCompliance.LEGACY : compliance());
+ }
+
+ public HttpParser(RequestHandler handler, HttpCompliance compliance)
+ {
+ this(handler, -1, compliance);
+ }
+
+ public HttpParser(RequestHandler handler, int maxHeaderBytes, HttpCompliance compliance)
+ {
+ this(handler, null, maxHeaderBytes, compliance == null ? compliance() : compliance);
+ }
+
+ public HttpParser(ResponseHandler handler, int maxHeaderBytes, HttpCompliance compliance)
+ {
+ this(null, handler, maxHeaderBytes, compliance == null ? compliance() : compliance);
+ }
+
+ private HttpParser(RequestHandler requestHandler, ResponseHandler responseHandler, int maxHeaderBytes, HttpCompliance compliance)
+ {
+ _handler = requestHandler != null ? requestHandler : responseHandler;
+ _requestHandler = requestHandler;
+ _responseHandler = responseHandler;
+ _maxHeaderBytes = maxHeaderBytes;
+ _compliance = compliance;
+ _compliances = compliance.sections();
+ _complianceHandler = (ComplianceHandler)(_handler instanceof ComplianceHandler ? _handler : null);
+ }
+
+ public HttpHandler getHandler()
+ {
+ return _handler;
+ }
+
+ public HttpCompliance getHttpCompliance()
+ {
+ return _compliance;
+ }
+
+ /**
+ * Check RFC compliance violation
+ *
+ * @param violation The compliance section violation
+ * @return True if the current compliance level is set so as to Not allow this violation
+ */
+ protected boolean complianceViolation(HttpComplianceSection violation)
+ {
+ return complianceViolation(violation, null);
+ }
+
+ /**
+ * Check RFC compliance violation
+ *
+ * @param violation The compliance section violation
+ * @param reason The reason for the violation
+ * @return True if the current compliance level is set so as to Not allow this violation
+ */
+ protected boolean complianceViolation(HttpComplianceSection violation, String reason)
+ {
+ if (_compliances.contains(violation))
+ return true;
+ if (reason == null)
+ reason = violation.description;
+ if (_complianceHandler != null)
+ _complianceHandler.onComplianceViolation(_compliance, violation, reason);
+
+ return false;
+ }
+
+ protected void handleViolation(HttpComplianceSection section, String reason)
+ {
+ if (_complianceHandler != null)
+ _complianceHandler.onComplianceViolation(_compliance, section, reason);
+ }
+
+ protected String caseInsensitiveHeader(String orig, String normative)
+ {
+ if (_compliances.contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE))
+ return normative;
+ if (!orig.equals(normative))
+ handleViolation(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE, orig);
+ return orig;
+ }
+
+ public long getContentLength()
+ {
+ return _contentLength;
+ }
+
+ public long getContentRead()
+ {
+ return _contentPosition;
+ }
+
+ public int getHeaderLength()
+ {
+ return _headerBytes;
+ }
+
+ /**
+ * Set if a HEAD response is expected
+ *
+ * @param head true if head response is expected
+ */
+ public void setHeadResponse(boolean head)
+ {
+ _headResponse = head;
+ }
+
+ protected void setResponseStatus(int status)
+ {
+ _responseStatus = status;
+ }
+
+ public State getState()
+ {
+ return _state;
+ }
+
+ public boolean inContentState()
+ {
+ return _state.ordinal() >= State.CONTENT.ordinal() && _state.ordinal() < State.END.ordinal();
+ }
+
+ public boolean inHeaderState()
+ {
+ return _state.ordinal() < State.CONTENT.ordinal();
+ }
+
+ public boolean isChunking()
+ {
+ return _endOfContent == EndOfContent.CHUNKED_CONTENT;
+ }
+
+ public boolean isStart()
+ {
+ return isState(State.START);
+ }
+
+ public boolean isClose()
+ {
+ return isState(State.CLOSE);
+ }
+
+ public boolean isClosed()
+ {
+ return isState(State.CLOSED);
+ }
+
+ public boolean isIdle()
+ {
+ return __idleStates.contains(_state);
+ }
+
+ public boolean isComplete()
+ {
+ return __completeStates.contains(_state);
+ }
+
+ public boolean isTerminated()
+ {
+ return __terminatedStates.contains(_state);
+ }
+
+ public boolean isState(State state)
+ {
+ return _state == state;
+ }
+
+ private HttpTokens.Token next(ByteBuffer buffer)
+ {
+ byte ch = buffer.get();
+
+ HttpTokens.Token t = HttpTokens.TOKENS[0xff & ch];
+
+ switch (t.getType())
+ {
+ case CNTL:
+ throw new IllegalCharacterException(_state, t, buffer);
+
+ case LF:
+ _cr = false;
+ break;
+
+ case CR:
+ if (_cr)
+ throw new BadMessageException("Bad EOL");
+
+ _cr = true;
+ if (buffer.hasRemaining())
+ {
+ // Don't count the CRs and LFs of the chunked encoding.
+ if (_maxHeaderBytes > 0 && (_state == State.HEADER || _state == State.TRAILER))
+ _headerBytes++;
+ return next(buffer);
+ }
+
+ return null;
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ case HTAB:
+ case SPACE:
+ case OTEXT:
+ case COLON:
+ if (_cr)
+ throw new BadMessageException("Bad EOL");
+ break;
+
+ default:
+ break;
+ }
+
+ return t;
+ }
+
+ /* Quick lookahead for the start state looking for a request method or an HTTP version,
+ * otherwise skip white space until something else to parse.
+ */
+ private boolean quickStart(ByteBuffer buffer)
+ {
+ if (_requestHandler != null)
+ {
+ _method = HttpMethod.lookAheadGet(buffer);
+ if (_method != null)
+ {
+ _methodString = _method.asString();
+ buffer.position(buffer.position() + _methodString.length() + 1);
+
+ setState(State.SPACE1);
+ return false;
+ }
+ }
+ else if (_responseHandler != null)
+ {
+ _version = HttpVersion.lookAheadGet(buffer);
+ if (_version != null)
+ {
+ buffer.position(buffer.position() + _version.asString().length() + 1);
+ setState(State.SPACE1);
+ return false;
+ }
+ }
+
+ // Quick start look
+ while (_state == State.START && buffer.hasRemaining())
+ {
+ HttpTokens.Token t = next(buffer);
+ if (t == null)
+ break;
+
+ switch (t.getType())
+ {
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ {
+ _string.setLength(0);
+ _string.append(t.getChar());
+ setState(_requestHandler != null ? State.METHOD : State.RESPONSE_VERSION);
+ return false;
+ }
+ case OTEXT:
+ case SPACE:
+ case HTAB:
+ throw new IllegalCharacterException(_state, t, buffer);
+
+ default:
+ break;
+ }
+
+ // count this white space as a header byte to avoid DOS
+ if (_maxHeaderBytes > 0 && ++_headerBytes > _maxHeaderBytes)
+ {
+ LOG.warn("padding is too large >" + _maxHeaderBytes);
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400);
+ }
+ }
+ return false;
+ }
+
+ private void setString(String s)
+ {
+ _string.setLength(0);
+ _string.append(s);
+ _length = s.length();
+ }
+
+ private String takeString()
+ {
+ _string.setLength(_length);
+ String s = _string.toString();
+ _string.setLength(0);
+ _length = -1;
+ return s;
+ }
+
+ private boolean handleHeaderContentMessage()
+ {
+ boolean handleHeader = _handler.headerComplete();
+ _headerComplete = true;
+ if (handleHeader)
+ return true;
+ setState(State.CONTENT_END);
+ return handleContentMessage();
+ }
+
+ private boolean handleContentMessage()
+ {
+ boolean handleContent = _handler.contentComplete();
+ if (handleContent)
+ return true;
+ setState(State.END);
+ return _handler.messageComplete();
+ }
+
+ /* Parse a request or response line
+ */
+ private boolean parseLine(ByteBuffer buffer)
+ {
+ boolean handle = false;
+
+ // Process headers
+ while (_state.ordinal() < State.HEADER.ordinal() && buffer.hasRemaining() && !handle)
+ {
+ // process each character
+ HttpTokens.Token t = next(buffer);
+ if (t == null)
+ break;
+
+ if (_maxHeaderBytes > 0 && ++_headerBytes > _maxHeaderBytes)
+ {
+ if (_state == State.URI)
+ {
+ LOG.warn("URI is too large >" + _maxHeaderBytes);
+ throw new BadMessageException(HttpStatus.URI_TOO_LONG_414);
+ }
+ else
+ {
+ if (_requestHandler != null)
+ LOG.warn("request is too large >" + _maxHeaderBytes);
+ else
+ LOG.warn("response is too large >" + _maxHeaderBytes);
+ throw new BadMessageException(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431);
+ }
+ }
+
+ switch (_state)
+ {
+ case METHOD:
+ switch (t.getType())
+ {
+ case SPACE:
+ _length = _string.length();
+ _methodString = takeString();
+
+ if (_compliances.contains(HttpComplianceSection.METHOD_CASE_SENSITIVE))
+ {
+ HttpMethod method = HttpMethod.CACHE.get(_methodString);
+ if (method != null)
+ _methodString = method.asString();
+ }
+ else
+ {
+ HttpMethod method = HttpMethod.INSENSITIVE_CACHE.get(_methodString);
+
+ if (method != null)
+ {
+ if (!method.asString().equals(_methodString))
+ handleViolation(HttpComplianceSection.METHOD_CASE_SENSITIVE, _methodString);
+ _methodString = method.asString();
+ }
+ }
+
+ setState(State.SPACE1);
+ break;
+
+ case LF:
+ throw new BadMessageException("No URI");
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ _string.append(t.getChar());
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case RESPONSE_VERSION:
+ switch (t.getType())
+ {
+ case SPACE:
+ _length = _string.length();
+ String version = takeString();
+ _version = HttpVersion.CACHE.get(version);
+ checkVersion();
+ setState(State.SPACE1);
+ break;
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ case COLON:
+ _string.append(t.getChar());
+ break;
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case SPACE1:
+ switch (t.getType())
+ {
+ case SPACE:
+ break;
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ case COLON:
+ if (_responseHandler != null)
+ {
+ if (t.getType() != HttpTokens.Type.DIGIT)
+ throw new IllegalCharacterException(_state, t, buffer);
+ setState(State.STATUS);
+ setResponseStatus(t.getByte() - '0');
+ }
+ else
+ {
+ _uri.reset();
+ setState(State.URI);
+ // quick scan for space or EoBuffer
+ if (buffer.hasArray())
+ {
+ byte[] array = buffer.array();
+ int p = buffer.arrayOffset() + buffer.position();
+ int l = buffer.arrayOffset() + buffer.limit();
+ int i = p;
+ while (i < l && array[i] > HttpTokens.SPACE)
+ {
+ i++;
+ }
+
+ int len = i - p;
+ _headerBytes += len;
+
+ if (_maxHeaderBytes > 0 && ++_headerBytes > _maxHeaderBytes)
+ {
+ LOG.warn("URI is too large >" + _maxHeaderBytes);
+ throw new BadMessageException(HttpStatus.URI_TOO_LONG_414);
+ }
+ _uri.append(array, p - 1, len + 1);
+ buffer.position(i - buffer.arrayOffset());
+ }
+ else
+ _uri.append(t.getByte());
+ }
+ break;
+
+ default:
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, _requestHandler != null ? "No URI" : "No Status");
+ }
+ break;
+
+ case STATUS:
+ switch (t.getType())
+ {
+ case SPACE:
+ setState(State.SPACE2);
+ break;
+
+ case DIGIT:
+ _responseStatus = _responseStatus * 10 + (t.getByte() - '0');
+ if (_responseStatus >= 1000)
+ throw new BadMessageException("Bad status");
+ break;
+
+ case LF:
+ setState(State.HEADER);
+ _responseHandler.startResponse(_version, _responseStatus, null);
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case URI:
+ switch (t.getType())
+ {
+ case SPACE:
+ setState(State.SPACE2);
+ break;
+
+ case LF:
+ // HTTP/0.9
+ if (complianceViolation(HttpComplianceSection.NO_HTTP_0_9, "No request version"))
+ throw new BadMessageException(HttpStatus.HTTP_VERSION_NOT_SUPPORTED_505, "HTTP/0.9 not supported");
+ _requestHandler.startRequest(_methodString, _uri.toString(), HttpVersion.HTTP_0_9);
+ setState(State.CONTENT);
+ _endOfContent = EndOfContent.NO_CONTENT;
+ BufferUtil.clear(buffer);
+ handle = handleHeaderContentMessage();
+ break;
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ case COLON:
+ case OTEXT:
+ _uri.append(t.getByte());
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case SPACE2:
+ switch (t.getType())
+ {
+ case SPACE:
+ break;
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ case COLON:
+ _string.setLength(0);
+ _string.append(t.getChar());
+ if (_responseHandler != null)
+ {
+ _length = 1;
+ setState(State.REASON);
+ }
+ else
+ {
+ setState(State.REQUEST_VERSION);
+
+ // try quick look ahead for HTTP Version
+ HttpVersion version;
+ if (buffer.position() > 0 && buffer.hasArray())
+ version = HttpVersion.lookAheadGet(buffer.array(), buffer.arrayOffset() + buffer.position() - 1, buffer.arrayOffset() + buffer.limit());
+ else
+ version = HttpVersion.CACHE.getBest(buffer, 0, buffer.remaining());
+
+ if (version != null)
+ {
+ int pos = buffer.position() + version.asString().length() - 1;
+ if (pos < buffer.limit())
+ {
+ byte n = buffer.get(pos);
+ if (n == HttpTokens.CARRIAGE_RETURN)
+ {
+ _cr = true;
+ _version = version;
+ checkVersion();
+ _string.setLength(0);
+ buffer.position(pos + 1);
+ }
+ else if (n == HttpTokens.LINE_FEED)
+ {
+ _version = version;
+ checkVersion();
+ _string.setLength(0);
+ buffer.position(pos);
+ }
+ }
+ }
+ }
+ break;
+
+ case LF:
+ if (_responseHandler != null)
+ {
+ setState(State.HEADER);
+ _responseHandler.startResponse(_version, _responseStatus, null);
+ }
+ else
+ {
+ // HTTP/0.9
+ if (complianceViolation(HttpComplianceSection.NO_HTTP_0_9, "No request version"))
+ throw new BadMessageException("HTTP/0.9 not supported");
+
+ _requestHandler.startRequest(_methodString, _uri.toString(), HttpVersion.HTTP_0_9);
+ setState(State.CONTENT);
+ _endOfContent = EndOfContent.NO_CONTENT;
+ BufferUtil.clear(buffer);
+ handle = handleHeaderContentMessage();
+ }
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case REQUEST_VERSION:
+ switch (t.getType())
+ {
+ case LF:
+ if (_version == null)
+ {
+ _length = _string.length();
+ _version = HttpVersion.CACHE.get(takeString());
+ }
+ checkVersion();
+
+ setState(State.HEADER);
+
+ _requestHandler.startRequest(_methodString, _uri.toString(), _version);
+ continue;
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ case COLON:
+ _string.append(t.getChar());
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case REASON:
+ switch (t.getType())
+ {
+ case LF:
+ String reason = takeString();
+ setState(State.HEADER);
+ _responseHandler.startResponse(_version, _responseStatus, reason);
+ continue;
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ case COLON:
+ case OTEXT: // TODO should this be UTF8
+ _string.append(t.getChar());
+ _length = _string.length();
+ break;
+
+ case SPACE:
+ case HTAB:
+ _string.append(t.getChar());
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ default:
+ throw new IllegalStateException(_state.toString());
+ }
+ }
+
+ return handle;
+ }
+
+ private void checkVersion()
+ {
+ if (_version == null)
+ throw new BadMessageException(HttpStatus.HTTP_VERSION_NOT_SUPPORTED_505, "Unknown Version");
+
+ if (_version.getVersion() < 10 || _version.getVersion() > 20)
+ throw new BadMessageException(HttpStatus.HTTP_VERSION_NOT_SUPPORTED_505, "Unsupported Version");
+ }
+
+ private void parsedHeader()
+ {
+ // handler last header if any. Delayed to here just in case there was a continuation line (above)
+ if (_headerString != null || _valueString != null)
+ {
+ // Handle known headers
+ if (_header != null)
+ {
+ boolean addToFieldCache = false;
+ switch (_header)
+ {
+ case CONTENT_LENGTH:
+ if (_hasTransferEncoding && complianceViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH))
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Transfer-Encoding and Content-Length");
+
+ if (_hasContentLength)
+ {
+ if (complianceViolation(MULTIPLE_CONTENT_LENGTHS))
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, MULTIPLE_CONTENT_LENGTHS.description);
+ if (convertContentLength(_valueString) != _contentLength)
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, MULTIPLE_CONTENT_LENGTHS.description);
+ }
+ _hasContentLength = true;
+
+ if (_endOfContent != EndOfContent.CHUNKED_CONTENT)
+ {
+ _contentLength = convertContentLength(_valueString);
+ if (_contentLength <= 0)
+ _endOfContent = EndOfContent.NO_CONTENT;
+ else
+ _endOfContent = EndOfContent.CONTENT_LENGTH;
+ }
+ break;
+
+ case TRANSFER_ENCODING:
+ _hasTransferEncoding = true;
+
+ if (_hasContentLength && complianceViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH))
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Transfer-Encoding and Content-Length");
+
+ // we encountered another Transfer-Encoding header, but chunked was already set
+ if (_endOfContent == EndOfContent.CHUNKED_CONTENT)
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, chunked not last");
+
+ if (HttpHeaderValue.CHUNKED.is(_valueString))
+ {
+ _endOfContent = EndOfContent.CHUNKED_CONTENT;
+ _contentLength = -1;
+ }
+ else
+ {
+ List<String> values = new QuotedCSV(_valueString).getValues();
+ int chunked = -1;
+ int len = values.size();
+ for (int i = 0; i < len; i++)
+ {
+ if (HttpHeaderValue.CHUNKED.is(values.get(i)))
+ {
+ if (chunked != -1)
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, multiple chunked tokens");
+ chunked = i;
+ // declared chunked
+ _endOfContent = EndOfContent.CHUNKED_CONTENT;
+ _contentLength = -1;
+ }
+ // we have a non-chunked token after a declared chunked token
+ else if (_endOfContent == EndOfContent.CHUNKED_CONTENT)
+ {
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, chunked not last");
+ }
+ }
+ }
+ break;
+
+ case HOST:
+ _host = true;
+ if (!(_field instanceof HostPortHttpField) && _valueString != null && !_valueString.isEmpty())
+ {
+ _field = new HostPortHttpField(_header,
+ _compliances.contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE) ? _header.asString() : _headerString,
+ _valueString);
+ addToFieldCache = true;
+ }
+ break;
+
+ case CONNECTION:
+ // Don't cache headers if not persistent
+ if (_field == null)
+ _field = new HttpField(_header, caseInsensitiveHeader(_headerString, _header.asString()), _valueString);
+ if (_handler.getHeaderCacheSize() > 0 && _field.contains(HttpHeaderValue.CLOSE.asString()))
+ _fieldCache = NO_CACHE;
+ break;
+
+ case AUTHORIZATION:
+ case ACCEPT:
+ case ACCEPT_CHARSET:
+ case ACCEPT_ENCODING:
+ case ACCEPT_LANGUAGE:
+ case COOKIE:
+ case CACHE_CONTROL:
+ case USER_AGENT:
+ addToFieldCache = _field == null;
+ break;
+
+ default:
+ break;
+ }
+
+ // Cache field?
+ if (addToFieldCache && _header != null && _valueString != null)
+ {
+ if (_fieldCache == null)
+ {
+ _fieldCache = (_handler.getHeaderCacheSize() > 0 && (_version != null && _version == HttpVersion.HTTP_1_1))
+ ? new ArrayTernaryTrie<>(_handler.getHeaderCacheSize())
+ : NO_CACHE;
+ }
+
+ if (!_fieldCache.isFull())
+ {
+ if (_field == null)
+ _field = new HttpField(_header, caseInsensitiveHeader(_headerString, _header.asString()), _valueString);
+ _fieldCache.put(_field);
+ }
+ }
+ }
+ _handler.parsedHeader(_field != null ? _field : new HttpField(_header, _headerString, _valueString));
+ }
+
+ _headerString = _valueString = null;
+ _header = null;
+ _field = null;
+ }
+
+ private void parsedTrailer()
+ {
+ // handler last header if any. Delayed to here just in case there was a continuation line (above)
+ if (_headerString != null || _valueString != null)
+ _handler.parsedTrailer(_field != null ? _field : new HttpField(_header, _headerString, _valueString));
+
+ _headerString = _valueString = null;
+ _header = null;
+ _field = null;
+ }
+
+ private long convertContentLength(String valueString)
+ {
+ try
+ {
+ return Long.parseLong(valueString);
+ }
+ catch (NumberFormatException e)
+ {
+ LOG.ignore(e);
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Invalid Content-Length Value", e);
+ }
+ }
+
+ /*
+ * Parse the message headers and return true if the handler has signalled for a return
+ */
+ protected boolean parseFields(ByteBuffer buffer)
+ {
+ // Process headers
+ while ((_state == State.HEADER || _state == State.TRAILER) && buffer.hasRemaining())
+ {
+ // process each character
+ HttpTokens.Token t = next(buffer);
+ if (t == null)
+ break;
+
+ if (_maxHeaderBytes > 0 && ++_headerBytes > _maxHeaderBytes)
+ {
+ boolean header = _state == State.HEADER;
+ LOG.warn("{} is too large {}>{}", header ? "Header" : "Trailer", _headerBytes, _maxHeaderBytes);
+ throw new BadMessageException(header
+ ? HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431
+ : HttpStatus.PAYLOAD_TOO_LARGE_413);
+ }
+
+ switch (_fieldState)
+ {
+ case FIELD:
+ switch (t.getType())
+ {
+ case COLON:
+ case SPACE:
+ case HTAB:
+ {
+ if (complianceViolation(HttpComplianceSection.NO_FIELD_FOLDING, _headerString))
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Header Folding");
+
+ // header value without name - continuation?
+ if (StringUtil.isEmpty(_valueString))
+ {
+ _string.setLength(0);
+ _length = 0;
+ }
+ else
+ {
+ setString(_valueString);
+ _string.append(' ');
+ _length++;
+ _valueString = null;
+ }
+ setState(FieldState.VALUE);
+ break;
+ }
+
+ case LF:
+ {
+ // process previous header
+ if (_state == State.HEADER)
+ parsedHeader();
+ else
+ parsedTrailer();
+
+ _contentPosition = 0;
+
+ // End of headers or trailers?
+ if (_state == State.TRAILER)
+ {
+ setState(State.END);
+ return _handler.messageComplete();
+ }
+
+ // We found Transfer-Encoding headers, but none declared the 'chunked' token
+ if (_hasTransferEncoding && _endOfContent != EndOfContent.CHUNKED_CONTENT)
+ {
+ if (_responseHandler == null || _endOfContent != EndOfContent.EOF_CONTENT)
+ {
+ // Transfer-Encoding chunked not specified
+ // https://tools.ietf.org/html/rfc7230#section-3.3.1
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, chunked not last");
+ }
+ }
+
+ // Was there a required host header?
+ if (!_host && _version == HttpVersion.HTTP_1_1 && _requestHandler != null)
+ {
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "No Host");
+ }
+
+ // is it a response that cannot have a body?
+ if (_responseHandler != null && // response
+ (_responseStatus == 304 || // not-modified response
+ _responseStatus == 204 || // no-content response
+ _responseStatus < 200)) // 1xx response
+ _endOfContent = EndOfContent.NO_CONTENT; // ignore any other headers set
+
+ // else if we don't know framing
+ else if (_endOfContent == EndOfContent.UNKNOWN_CONTENT)
+ {
+ if (_responseStatus == 0 || // request
+ _responseStatus == 304 || // not-modified response
+ _responseStatus == 204 || // no-content response
+ _responseStatus < 200) // 1xx response
+ _endOfContent = EndOfContent.NO_CONTENT;
+ else
+ _endOfContent = EndOfContent.EOF_CONTENT;
+ }
+
+ // How is the message ended?
+ switch (_endOfContent)
+ {
+ case EOF_CONTENT:
+ {
+ setState(State.EOF_CONTENT);
+ boolean handle = _handler.headerComplete();
+ _headerComplete = true;
+ return handle;
+ }
+ case CHUNKED_CONTENT:
+ {
+ setState(State.CHUNKED_CONTENT);
+ boolean handle = _handler.headerComplete();
+ _headerComplete = true;
+ return handle;
+ }
+ default:
+ {
+ setState(State.CONTENT);
+ boolean handle = _handler.headerComplete();
+ _headerComplete = true;
+ return handle;
+ }
+ }
+ }
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ {
+ // process previous header
+ if (_state == State.HEADER)
+ parsedHeader();
+ else
+ parsedTrailer();
+
+ // handle new header
+ if (buffer.hasRemaining())
+ {
+ // Try a look ahead for the known header name and value.
+ HttpField cachedField = _fieldCache == null ? null : _fieldCache.getBest(buffer, -1, buffer.remaining());
+ if (cachedField == null)
+ cachedField = CACHE.getBest(buffer, -1, buffer.remaining());
+
+ if (cachedField != null)
+ {
+ String n = cachedField.getName();
+ String v = cachedField.getValue();
+
+ if (!_compliances.contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE))
+ {
+ // Have to get the fields exactly from the buffer to match case
+ String en = BufferUtil.toString(buffer, buffer.position() - 1, n.length(), StandardCharsets.US_ASCII);
+ if (!n.equals(en))
+ {
+ handleViolation(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE, en);
+ n = en;
+ cachedField = new HttpField(cachedField.getHeader(), n, v);
+ }
+ }
+
+ if (v != null && !_compliances.contains(HttpComplianceSection.CASE_INSENSITIVE_FIELD_VALUE_CACHE))
+ {
+ String ev = BufferUtil.toString(buffer, buffer.position() + n.length() + 1, v.length(), StandardCharsets.ISO_8859_1);
+ if (!v.equals(ev))
+ {
+ handleViolation(HttpComplianceSection.CASE_INSENSITIVE_FIELD_VALUE_CACHE, ev + "!=" + v);
+ v = ev;
+ cachedField = new HttpField(cachedField.getHeader(), n, v);
+ }
+ }
+
+ _header = cachedField.getHeader();
+ _headerString = n;
+
+ if (v == null)
+ {
+ // Header only
+ setState(FieldState.VALUE);
+ _string.setLength(0);
+ _length = 0;
+ buffer.position(buffer.position() + n.length() + 1);
+ break;
+ }
+
+ // Header and value
+ int pos = buffer.position() + n.length() + v.length() + 1;
+ byte peek = buffer.get(pos);
+ if (peek == HttpTokens.CARRIAGE_RETURN || peek == HttpTokens.LINE_FEED)
+ {
+ _field = cachedField;
+ _valueString = v;
+ setState(FieldState.IN_VALUE);
+
+ if (peek == HttpTokens.CARRIAGE_RETURN)
+ {
+ _cr = true;
+ buffer.position(pos + 1);
+ }
+ else
+ buffer.position(pos);
+ break;
+ }
+ setState(FieldState.IN_VALUE);
+ setString(v);
+ buffer.position(pos);
+ break;
+ }
+ }
+
+ // New header
+ setState(FieldState.IN_NAME);
+ _string.setLength(0);
+ _string.append(t.getChar());
+ _length = 1;
+ break;
+ }
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case IN_NAME:
+ switch (t.getType())
+ {
+ case SPACE:
+ case HTAB:
+ //Ignore trailing whitespaces ?
+ if (!complianceViolation(HttpComplianceSection.NO_WS_AFTER_FIELD_NAME, null))
+ {
+ _headerString = takeString();
+ _header = HttpHeader.CACHE.get(_headerString);
+ _length = -1;
+ setState(FieldState.WS_AFTER_NAME);
+ break;
+ }
+ throw new IllegalCharacterException(_state, t, buffer);
+
+ case COLON:
+ _headerString = takeString();
+ _header = HttpHeader.CACHE.get(_headerString);
+ _length = -1;
+ setState(FieldState.VALUE);
+ break;
+
+ case LF:
+ _headerString = takeString();
+ _header = HttpHeader.CACHE.get(_headerString);
+ _string.setLength(0);
+ _valueString = "";
+ _length = -1;
+
+ if (!complianceViolation(HttpComplianceSection.FIELD_COLON, _headerString))
+ {
+ setState(FieldState.FIELD);
+ break;
+ }
+ throw new IllegalCharacterException(_state, t, buffer);
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ _string.append(t.getChar());
+ _length = _string.length();
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case WS_AFTER_NAME:
+
+ switch (t.getType())
+ {
+ case SPACE:
+ case HTAB:
+ break;
+
+ case COLON:
+ setState(FieldState.VALUE);
+ break;
+
+ case LF:
+ if (!complianceViolation(HttpComplianceSection.FIELD_COLON, _headerString))
+ {
+ setState(FieldState.FIELD);
+ break;
+ }
+ throw new IllegalCharacterException(_state, t, buffer);
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case VALUE:
+ switch (t.getType())
+ {
+ case LF:
+ _string.setLength(0);
+ _valueString = "";
+ _length = -1;
+
+ setState(FieldState.FIELD);
+ break;
+
+ case SPACE:
+ case HTAB:
+ break;
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ case COLON:
+ case OTEXT: // TODO review? should this be a utf8 string?
+ _string.append(t.getChar());
+ _length = _string.length();
+ setState(FieldState.IN_VALUE);
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case IN_VALUE:
+ switch (t.getType())
+ {
+ case LF:
+ if (_length > 0)
+ {
+ _valueString = takeString();
+ _length = -1;
+ }
+ setState(FieldState.FIELD);
+ break;
+
+ case SPACE:
+ case HTAB:
+ _string.append(t.getChar());
+ break;
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ case COLON:
+ case OTEXT: // TODO review? should this be a utf8 string?
+ _string.append(t.getChar());
+ _length = _string.length();
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ default:
+ throw new IllegalStateException(_state.toString());
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse until next Event.
+ *
+ * @param buffer the buffer to parse
+ * @return True if an {@link RequestHandler} method was called and it returned true;
+ */
+ public boolean parseNext(ByteBuffer buffer)
+ {
+ if (debug)
+ LOG.debug("parseNext s={} {}", _state, BufferUtil.toDetailString(buffer));
+ try
+ {
+ // Start a request/response
+ if (_state == State.START)
+ {
+ _version = null;
+ _method = null;
+ _methodString = null;
+ _endOfContent = EndOfContent.UNKNOWN_CONTENT;
+ _header = null;
+ if (quickStart(buffer))
+ return true;
+ }
+
+ // Request/response line
+ if (_state.ordinal() >= State.START.ordinal() && _state.ordinal() < State.HEADER.ordinal())
+ {
+ if (parseLine(buffer))
+ return true;
+ }
+
+ // parse headers
+ if (_state == State.HEADER)
+ {
+ if (parseFields(buffer))
+ return true;
+ }
+
+ // parse content
+ if (_state.ordinal() >= State.CONTENT.ordinal() && _state.ordinal() < State.TRAILER.ordinal())
+ {
+ // Handle HEAD response
+ if (_responseStatus > 0 && _headResponse)
+ {
+ if (_state != State.CONTENT_END)
+ {
+ setState(State.CONTENT_END);
+ return handleContentMessage();
+ }
+ else
+ {
+ setState(State.END);
+ return _handler.messageComplete();
+ }
+ }
+ else
+ {
+ if (parseContent(buffer))
+ return true;
+ }
+ }
+
+ // parse headers
+ if (_state == State.TRAILER)
+ {
+ if (parseFields(buffer))
+ return true;
+ }
+
+ // handle end states
+ if (_state == State.END)
+ {
+ // Eat CR or LF white space, but not SP.
+ int whiteSpace = 0;
+ while (buffer.remaining() > 0)
+ {
+ byte b = buffer.get(buffer.position());
+ if (b != HttpTokens.CARRIAGE_RETURN && b != HttpTokens.LINE_FEED)
+ break;
+ buffer.get();
+ ++whiteSpace;
+ }
+ if (debug && whiteSpace > 0)
+ LOG.debug("Discarded {} CR or LF characters", whiteSpace);
+ }
+ else if (isTerminated())
+ {
+ BufferUtil.clear(buffer);
+ }
+
+ // Handle EOF
+ if (isAtEOF() && !buffer.hasRemaining())
+ {
+ switch (_state)
+ {
+ case CLOSED:
+ break;
+
+ case END:
+ case CLOSE:
+ setState(State.CLOSED);
+ break;
+
+ case EOF_CONTENT:
+ case TRAILER:
+ if (_fieldState == FieldState.FIELD)
+ {
+ // Be forgiving of missing last CRLF
+ setState(State.CONTENT_END);
+ boolean handle = handleContentMessage();
+ if (handle && _state == State.CONTENT_END)
+ return true;
+ setState(State.CLOSED);
+ return handle;
+ }
+ setState(State.CLOSED);
+ _handler.earlyEOF();
+ break;
+
+ case START:
+ case CONTENT:
+ case CHUNKED_CONTENT:
+ case CHUNK_SIZE:
+ case CHUNK_PARAMS:
+ case CHUNK:
+ setState(State.CLOSED);
+ _handler.earlyEOF();
+ break;
+
+ default:
+ if (debug)
+ LOG.debug("{} EOF in {}", this, _state);
+ setState(State.CLOSED);
+ _handler.badMessage(new BadMessageException(HttpStatus.BAD_REQUEST_400));
+ break;
+ }
+ }
+ }
+ catch (BadMessageException x)
+ {
+ BufferUtil.clear(buffer);
+ badMessage(x);
+ }
+ catch (Throwable x)
+ {
+ BufferUtil.clear(buffer);
+ badMessage(new BadMessageException(HttpStatus.BAD_REQUEST_400, _requestHandler != null ? "Bad Request" : "Bad Response", x));
+ }
+ return false;
+ }
+
+ protected void badMessage(BadMessageException x)
+ {
+ if (debug)
+ LOG.debug("Parse exception: " + this + " for " + _handler, x);
+ setState(State.CLOSE);
+ if (_headerComplete)
+ _handler.earlyEOF();
+ else
+ _handler.badMessage(x);
+ }
+
+ protected boolean parseContent(ByteBuffer buffer)
+ {
+ int remaining = buffer.remaining();
+ if (remaining == 0)
+ {
+ switch (_state)
+ {
+ case CONTENT:
+ long content = _contentLength - _contentPosition;
+ if (_endOfContent == EndOfContent.NO_CONTENT || content == 0)
+ {
+ setState(State.CONTENT_END);
+ return handleContentMessage();
+ }
+ break;
+ case CONTENT_END:
+ setState(_endOfContent == EndOfContent.EOF_CONTENT ? State.CLOSED : State.END);
+ return _handler.messageComplete();
+ default:
+ // No bytes to parse, return immediately.
+ return false;
+ }
+ }
+
+ // Handle content.
+ while (_state.ordinal() < State.TRAILER.ordinal() && remaining > 0)
+ {
+ switch (_state)
+ {
+ case EOF_CONTENT:
+ _contentChunk = buffer.asReadOnlyBuffer();
+ _contentPosition += remaining;
+ buffer.position(buffer.position() + remaining);
+ if (_handler.content(_contentChunk))
+ return true;
+ break;
+
+ case CONTENT:
+ {
+ long content = _contentLength - _contentPosition;
+ if (_endOfContent == EndOfContent.NO_CONTENT || content == 0)
+ {
+ setState(State.CONTENT_END);
+ return handleContentMessage();
+ }
+ else
+ {
+ _contentChunk = buffer.asReadOnlyBuffer();
+
+ // limit content by expected size
+ if (remaining > content)
+ {
+ // We can cast remaining to an int as we know that it is smaller than
+ // or equal to length which is already an int.
+ _contentChunk.limit(_contentChunk.position() + (int)content);
+ }
+
+ _contentPosition += _contentChunk.remaining();
+ buffer.position(buffer.position() + _contentChunk.remaining());
+
+ if (_handler.content(_contentChunk))
+ return true;
+
+ if (_contentPosition == _contentLength)
+ {
+ setState(State.CONTENT_END);
+ return handleContentMessage();
+ }
+ }
+ break;
+ }
+
+ case CHUNKED_CONTENT:
+ {
+ HttpTokens.Token t = next(buffer);
+ if (t == null)
+ break;
+ switch (t.getType())
+ {
+ case LF:
+ break;
+
+ case DIGIT:
+ _chunkLength = t.getHexDigit();
+ _chunkPosition = 0;
+ setState(State.CHUNK_SIZE);
+ break;
+
+ case ALPHA:
+ if (t.isHexDigit())
+ {
+ _chunkLength = t.getHexDigit();
+ _chunkPosition = 0;
+ setState(State.CHUNK_SIZE);
+ break;
+ }
+ throw new IllegalCharacterException(_state, t, buffer);
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+ }
+
+ case CHUNK_SIZE:
+ {
+ HttpTokens.Token t = next(buffer);
+ if (t == null)
+ break;
+
+ switch (t.getType())
+ {
+ case LF:
+ if (_chunkLength == 0)
+ {
+ setState(State.TRAILER);
+ if (_handler.contentComplete())
+ return true;
+ }
+ else
+ setState(State.CHUNK);
+ break;
+
+ case SPACE:
+ setState(State.CHUNK_PARAMS);
+ break;
+
+ default:
+ if (t.isHexDigit())
+ {
+ if (_chunkLength > MAX_CHUNK_LENGTH)
+ throw new BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413);
+ _chunkLength = _chunkLength * 16 + t.getHexDigit();
+ }
+ else
+ {
+ setState(State.CHUNK_PARAMS);
+ }
+ }
+ break;
+ }
+
+ case CHUNK_PARAMS:
+ {
+ HttpTokens.Token t = next(buffer);
+ if (t == null)
+ break;
+
+ switch (t.getType())
+ {
+ case LF:
+ if (_chunkLength == 0)
+ {
+ setState(State.TRAILER);
+ if (_handler.contentComplete())
+ return true;
+ }
+ else
+ setState(State.CHUNK);
+ break;
+ default:
+ break; // TODO review
+ }
+ break;
+ }
+
+ case CHUNK:
+ {
+ int chunk = _chunkLength - _chunkPosition;
+ if (chunk == 0)
+ {
+ setState(State.CHUNKED_CONTENT);
+ }
+ else
+ {
+ _contentChunk = buffer.asReadOnlyBuffer();
+
+ if (remaining > chunk)
+ _contentChunk.limit(_contentChunk.position() + chunk);
+ chunk = _contentChunk.remaining();
+
+ _contentPosition += chunk;
+ _chunkPosition += chunk;
+ buffer.position(buffer.position() + chunk);
+ if (_handler.content(_contentChunk))
+ return true;
+ }
+ break;
+ }
+
+ case CONTENT_END:
+ {
+ setState(_endOfContent == EndOfContent.EOF_CONTENT ? State.CLOSED : State.END);
+ return _handler.messageComplete();
+ }
+
+ default:
+ break;
+ }
+
+ remaining = buffer.remaining();
+ }
+ return false;
+ }
+
+ public boolean isAtEOF()
+ {
+ return _eof;
+ }
+
+ /**
+ * Signal that the associated data source is at EOF
+ */
+ public void atEOF()
+ {
+ if (debug)
+ LOG.debug("atEOF {}", this);
+ _eof = true;
+ }
+
+ /**
+ * Request that the associated data source be closed
+ */
+ public void close()
+ {
+ if (debug)
+ LOG.debug("close {}", this);
+ setState(State.CLOSE);
+ }
+
+ public void reset()
+ {
+ if (debug)
+ LOG.debug("reset {}", this);
+
+ // reset state
+ if (_state == State.CLOSE || _state == State.CLOSED)
+ return;
+
+ setState(State.START);
+ _endOfContent = EndOfContent.UNKNOWN_CONTENT;
+ _contentLength = -1;
+ _hasContentLength = false;
+ _hasTransferEncoding = false;
+ _contentPosition = 0;
+ _responseStatus = 0;
+ _contentChunk = null;
+ _headerBytes = 0;
+ _host = false;
+ _headerComplete = false;
+ }
+
+ protected void setState(State state)
+ {
+ if (debug)
+ LOG.debug("{} --> {}", _state, state);
+ _state = state;
+ }
+
+ protected void setState(FieldState state)
+ {
+ if (debug)
+ LOG.debug("{}:{} --> {}", _state, _field != null ? _field : _headerString != null ? _headerString : _string, state);
+ _fieldState = state;
+ }
+
+ public Trie<HttpField> getFieldCache()
+ {
+ return _fieldCache;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s{s=%s,%d of %d}",
+ getClass().getSimpleName(),
+ _state,
+ getContentRead(),
+ getContentLength());
+ }
+
+ /* Event Handler interface
+ * These methods return true if the caller should process the events
+ * so far received (eg return from parseNext and call HttpChannel.handle).
+ * If multiple callbacks are called in sequence (eg
+ * headerComplete then messageComplete) from the same point in the parsing
+ * then it is sufficient for the caller to process the events only once.
+ */
+ public interface HttpHandler
+ {
+ boolean content(ByteBuffer item);
+
+ boolean headerComplete();
+
+ boolean contentComplete();
+
+ boolean messageComplete();
+
+ /**
+ * This is the method called by parser when an HTTP Header name and value is found
+ *
+ * @param field The field parsed
+ */
+ void parsedHeader(HttpField field);
+
+ /**
+ * This is the method called by parser when an HTTP Trailer name and value is found
+ *
+ * @param field The field parsed
+ */
+ default void parsedTrailer(HttpField field)
+ {
+ }
+
+ /**
+ * Called to signal that an EOF was received unexpectedly
+ * during the parsing of an HTTP message
+ */
+ void earlyEOF();
+
+ /**
+ * Called to signal that a bad HTTP message has been received.
+ *
+ * @param failure the failure with the bad message information
+ */
+ default void badMessage(BadMessageException failure)
+ {
+ badMessage(failure.getCode(), failure.getReason());
+ }
+
+ /**
+ * @deprecated use {@link #badMessage(BadMessageException)} instead
+ */
+ @Deprecated
+ default void badMessage(int status, String reason)
+ {
+ }
+
+ /**
+ * @return the size in bytes of the per parser header cache
+ */
+ int getHeaderCacheSize();
+ }
+
+ public interface RequestHandler extends HttpHandler
+ {
+ /**
+ * This is the method called by parser when the HTTP request line is parsed
+ *
+ * @param method The method
+ * @param uri The raw bytes of the URI. These are copied into a ByteBuffer that will not be changed until this parser is reset and reused.
+ * @param version the http version in use
+ * @return true if handling parsing should return.
+ */
+ boolean startRequest(String method, String uri, HttpVersion version);
+ }
+
+ public interface ResponseHandler extends HttpHandler
+ {
+ /**
+ * This is the method called by parser when the HTTP request line is parsed
+ *
+ * @param version the http version in use
+ * @param status the response status
+ * @param reason the response reason phrase
+ * @return true if handling parsing should return
+ */
+ boolean startResponse(HttpVersion version, int status, String reason);
+ }
+
+ public interface ComplianceHandler extends HttpHandler
+ {
+ @Deprecated
+ default void onComplianceViolation(HttpCompliance compliance, HttpCompliance required, String reason)
+ {
+ }
+
+ default void onComplianceViolation(HttpCompliance compliance, HttpComplianceSection violation, String details)
+ {
+ onComplianceViolation(compliance, HttpCompliance.requiredCompliance(violation), details);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ private static class IllegalCharacterException extends BadMessageException
+ {
+ private IllegalCharacterException(State state, HttpTokens.Token token, ByteBuffer buffer)
+ {
+ super(400, String.format("Illegal character %s", token));
+ if (LOG.isDebugEnabled())
+ LOG.debug(String.format("Illegal character %s in state=%s for buffer %s", token, state, BufferUtil.toDetailString(buffer)));
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpScheme.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpScheme.java
new file mode 100644
index 0000000..ccc4c64
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpScheme.java
@@ -0,0 +1,76 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.ArrayTrie;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Trie;
+
+/**
+ *
+ */
+public enum HttpScheme
+{
+ HTTP("http"),
+ HTTPS("https"),
+ WS("ws"),
+ WSS("wss");
+
+ public static final Trie<HttpScheme> CACHE = new ArrayTrie<HttpScheme>();
+
+ static
+ {
+ for (HttpScheme version : HttpScheme.values())
+ {
+ CACHE.put(version.asString(), version);
+ }
+ }
+
+ private final String _string;
+ private final ByteBuffer _buffer;
+
+ HttpScheme(String s)
+ {
+ _string = s;
+ _buffer = BufferUtil.toBuffer(s);
+ }
+
+ public ByteBuffer asByteBuffer()
+ {
+ return _buffer.asReadOnlyBuffer();
+ }
+
+ public boolean is(String s)
+ {
+ return s != null && _string.equalsIgnoreCase(s);
+ }
+
+ public String asString()
+ {
+ return _string;
+ }
+
+ @Override
+ public String toString()
+ {
+ return _string;
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java
new file mode 100644
index 0000000..fe0242d
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java
@@ -0,0 +1,412 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+/**
+ * <p>
+ * Http Status Codes
+ * </p>
+ *
+ * @see <a href="http://www.iana.org/assignments/http-status-codes/">IANA HTTP Status Code Registry</a>
+ */
+public class HttpStatus
+{
+ public static final int CONTINUE_100 = 100;
+ public static final int SWITCHING_PROTOCOLS_101 = 101;
+ public static final int PROCESSING_102 = 102;
+
+ public static final int OK_200 = 200;
+ public static final int CREATED_201 = 201;
+ public static final int ACCEPTED_202 = 202;
+ public static final int NON_AUTHORITATIVE_INFORMATION_203 = 203;
+ public static final int NO_CONTENT_204 = 204;
+ public static final int RESET_CONTENT_205 = 205;
+ public static final int PARTIAL_CONTENT_206 = 206;
+ public static final int MULTI_STATUS_207 = 207;
+
+ public static final int MULTIPLE_CHOICES_300 = 300;
+ public static final int MOVED_PERMANENTLY_301 = 301;
+ public static final int MOVED_TEMPORARILY_302 = 302;
+ public static final int FOUND_302 = 302;
+ public static final int SEE_OTHER_303 = 303;
+ public static final int NOT_MODIFIED_304 = 304;
+ public static final int USE_PROXY_305 = 305;
+ public static final int TEMPORARY_REDIRECT_307 = 307;
+ public static final int PERMANENT_REDIRECT_308 = 308;
+
+ public static final int BAD_REQUEST_400 = 400;
+ public static final int UNAUTHORIZED_401 = 401;
+ public static final int PAYMENT_REQUIRED_402 = 402;
+ public static final int FORBIDDEN_403 = 403;
+ public static final int NOT_FOUND_404 = 404;
+ public static final int METHOD_NOT_ALLOWED_405 = 405;
+ public static final int NOT_ACCEPTABLE_406 = 406;
+ public static final int PROXY_AUTHENTICATION_REQUIRED_407 = 407;
+ public static final int REQUEST_TIMEOUT_408 = 408;
+ public static final int CONFLICT_409 = 409;
+ public static final int GONE_410 = 410;
+ public static final int LENGTH_REQUIRED_411 = 411;
+ public static final int PRECONDITION_FAILED_412 = 412;
+ @Deprecated
+ public static final int REQUEST_ENTITY_TOO_LARGE_413 = 413;
+ public static final int PAYLOAD_TOO_LARGE_413 = 413;
+ @Deprecated
+ public static final int REQUEST_URI_TOO_LONG_414 = 414;
+ public static final int URI_TOO_LONG_414 = 414;
+ public static final int UNSUPPORTED_MEDIA_TYPE_415 = 415;
+ @Deprecated
+ public static final int REQUESTED_RANGE_NOT_SATISFIABLE_416 = 416;
+ public static final int RANGE_NOT_SATISFIABLE_416 = 416;
+ public static final int EXPECTATION_FAILED_417 = 417;
+ public static final int IM_A_TEAPOT_418 = 418;
+ public static final int ENHANCE_YOUR_CALM_420 = 420;
+ public static final int MISDIRECTED_REQUEST_421 = 421;
+ public static final int UNPROCESSABLE_ENTITY_422 = 422;
+ public static final int LOCKED_423 = 423;
+ public static final int FAILED_DEPENDENCY_424 = 424;
+ public static final int UPGRADE_REQUIRED_426 = 426;
+ public static final int PRECONDITION_REQUIRED_428 = 428;
+ public static final int TOO_MANY_REQUESTS_429 = 429;
+ public static final int REQUEST_HEADER_FIELDS_TOO_LARGE_431 = 431;
+ public static final int UNAVAILABLE_FOR_LEGAL_REASONS_451 = 451;
+
+ public static final int INTERNAL_SERVER_ERROR_500 = 500;
+ public static final int NOT_IMPLEMENTED_501 = 501;
+ public static final int BAD_GATEWAY_502 = 502;
+ public static final int SERVICE_UNAVAILABLE_503 = 503;
+ public static final int GATEWAY_TIMEOUT_504 = 504;
+ public static final int HTTP_VERSION_NOT_SUPPORTED_505 = 505;
+ public static final int INSUFFICIENT_STORAGE_507 = 507;
+ public static final int LOOP_DETECTED_508 = 508;
+ public static final int NOT_EXTENDED_510 = 510;
+ public static final int NETWORK_AUTHENTICATION_REQUIRED_511 = 511;
+
+ public static final int MAX_CODE = 511;
+
+ private static final Code[] codeMap = new Code[MAX_CODE + 1];
+
+ static
+ {
+ for (Code code : Code.values())
+ {
+ codeMap[code._code] = code;
+ }
+ }
+
+ public enum Code
+ {
+ CONTINUE(CONTINUE_100, "Continue"),
+ SWITCHING_PROTOCOLS(SWITCHING_PROTOCOLS_101, "Switching Protocols"),
+ PROCESSING(PROCESSING_102, "Processing"),
+
+ OK(OK_200, "OK"),
+ CREATED(CREATED_201, "Created"),
+ ACCEPTED(ACCEPTED_202, "Accepted"),
+ NON_AUTHORITATIVE_INFORMATION(NON_AUTHORITATIVE_INFORMATION_203, "Non Authoritative Information"),
+ NO_CONTENT(NO_CONTENT_204, "No Content"),
+ RESET_CONTENT(RESET_CONTENT_205, "Reset Content"),
+ PARTIAL_CONTENT(PARTIAL_CONTENT_206, "Partial Content"),
+ MULTI_STATUS(MULTI_STATUS_207, "Multi-Status"),
+
+ MULTIPLE_CHOICES(MULTIPLE_CHOICES_300, "Multiple Choices"),
+ MOVED_PERMANENTLY(MOVED_PERMANENTLY_301, "Moved Permanently"),
+ MOVED_TEMPORARILY(MOVED_TEMPORARILY_302, "Moved Temporarily"),
+ FOUND(FOUND_302, "Found"),
+ SEE_OTHER(SEE_OTHER_303, "See Other"),
+ NOT_MODIFIED(NOT_MODIFIED_304, "Not Modified"),
+ USE_PROXY(USE_PROXY_305, "Use Proxy"),
+ TEMPORARY_REDIRECT(TEMPORARY_REDIRECT_307, "Temporary Redirect"),
+ // Keeping the typo for backward compatibility for a while
+ PERMANET_REDIRECT(PERMANENT_REDIRECT_308, "Permanent Redirect"),
+ PERMANENT_REDIRECT(PERMANENT_REDIRECT_308, "Permanent Redirect"),
+
+ BAD_REQUEST(BAD_REQUEST_400, "Bad Request"),
+ UNAUTHORIZED(UNAUTHORIZED_401, "Unauthorized"),
+ PAYMENT_REQUIRED(PAYMENT_REQUIRED_402, "Payment Required"),
+ FORBIDDEN(FORBIDDEN_403, "Forbidden"),
+ NOT_FOUND(NOT_FOUND_404, "Not Found"),
+ METHOD_NOT_ALLOWED(METHOD_NOT_ALLOWED_405, "Method Not Allowed"),
+ NOT_ACCEPTABLE(NOT_ACCEPTABLE_406, "Not Acceptable"),
+ PROXY_AUTHENTICATION_REQUIRED(PROXY_AUTHENTICATION_REQUIRED_407, "Proxy Authentication Required"),
+ REQUEST_TIMEOUT(REQUEST_TIMEOUT_408, "Request Timeout"),
+ CONFLICT(CONFLICT_409, "Conflict"),
+ GONE(GONE_410, "Gone"),
+ LENGTH_REQUIRED(LENGTH_REQUIRED_411, "Length Required"),
+ PRECONDITION_FAILED(PRECONDITION_FAILED_412, "Precondition Failed"),
+ PAYLOAD_TOO_LARGE(PAYLOAD_TOO_LARGE_413, "Payload Too Large"),
+ URI_TOO_LONG(URI_TOO_LONG_414, "URI Too Long"),
+ UNSUPPORTED_MEDIA_TYPE(UNSUPPORTED_MEDIA_TYPE_415, "Unsupported Media Type"),
+ RANGE_NOT_SATISFIABLE(RANGE_NOT_SATISFIABLE_416, "Range Not Satisfiable"),
+ EXPECTATION_FAILED(EXPECTATION_FAILED_417, "Expectation Failed"),
+ IM_A_TEAPOT(IM_A_TEAPOT_418, "I'm a Teapot"),
+ ENHANCE_YOUR_CALM(ENHANCE_YOUR_CALM_420, "Enhance your Calm"),
+ MISDIRECTED_REQUEST(MISDIRECTED_REQUEST_421, "Misdirected Request"),
+ UNPROCESSABLE_ENTITY(UNPROCESSABLE_ENTITY_422, "Unprocessable Entity"),
+ LOCKED(LOCKED_423, "Locked"),
+ FAILED_DEPENDENCY(FAILED_DEPENDENCY_424, "Failed Dependency"),
+ UPGRADE_REQUIRED(UPGRADE_REQUIRED_426, "Upgrade Required"),
+ PRECONDITION_REQUIRED(PRECONDITION_REQUIRED_428, "Precondition Required"),
+ TOO_MANY_REQUESTS(TOO_MANY_REQUESTS_429, "Too Many Requests"),
+ REQUEST_HEADER_FIELDS_TOO_LARGE(REQUEST_HEADER_FIELDS_TOO_LARGE_431, "Request Header Fields Too Large"),
+ UNAVAILABLE_FOR_LEGAL_REASONS(UNAVAILABLE_FOR_LEGAL_REASONS_451, "Unavailable for Legal Reason"),
+
+ INTERNAL_SERVER_ERROR(INTERNAL_SERVER_ERROR_500, "Server Error"),
+ NOT_IMPLEMENTED(NOT_IMPLEMENTED_501, "Not Implemented"),
+ BAD_GATEWAY(BAD_GATEWAY_502, "Bad Gateway"),
+ SERVICE_UNAVAILABLE(SERVICE_UNAVAILABLE_503, "Service Unavailable"),
+ GATEWAY_TIMEOUT(GATEWAY_TIMEOUT_504, "Gateway Timeout"),
+ HTTP_VERSION_NOT_SUPPORTED(HTTP_VERSION_NOT_SUPPORTED_505, "HTTP Version Not Supported"),
+ INSUFFICIENT_STORAGE(INSUFFICIENT_STORAGE_507, "Insufficient Storage"),
+ LOOP_DETECTED(LOOP_DETECTED_508, "Loop Detected"),
+ NOT_EXTENDED(NOT_EXTENDED_510, "Not Extended"),
+ NETWORK_AUTHENTICATION_REQUIRED(NETWORK_AUTHENTICATION_REQUIRED_511, "Network Authentication Required"),
+
+ ;
+
+ private final int _code;
+ private final String _message;
+
+ Code(int code, String message)
+ {
+ this._code = code;
+ _message = message;
+ }
+
+ public int getCode()
+ {
+ return _code;
+ }
+
+ public String getMessage()
+ {
+ return _message;
+ }
+
+ public boolean equals(int code)
+ {
+ return (this._code == code);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("[%03d %s]", this._code, this.getMessage());
+ }
+
+ /**
+ * Simple test against an code to determine if it falls into the
+ * <code>Informational</code> message category as defined in the <a
+ * href="http://tools.ietf.org/html/rfc1945">RFC 1945 - HTTP/1.0</a>,
+ * and <a href="http://tools.ietf.org/html/rfc7231">RFC 7231 -
+ * HTTP/1.1</a>.
+ *
+ * @return true if within range of codes that belongs to
+ * <code>Informational</code> messages.
+ */
+ public boolean isInformational()
+ {
+ return HttpStatus.isInformational(this._code);
+ }
+
+ /**
+ * Simple test against an code to determine if it falls into the
+ * <code>Success</code> message category as defined in the <a
+ * href="http://tools.ietf.org/html/rfc1945">RFC 1945 - HTTP/1.0</a>,
+ * and <a href="http://tools.ietf.org/html/rfc7231">RFC 7231 -
+ * HTTP/1.1</a>.
+ *
+ * @return true if within range of codes that belongs to
+ * <code>Success</code> messages.
+ */
+ public boolean isSuccess()
+ {
+ return HttpStatus.isSuccess(this._code);
+ }
+
+ /**
+ * Simple test against an code to determine if it falls into the
+ * <code>Redirection</code> message category as defined in the <a
+ * href="http://tools.ietf.org/html/rfc1945">RFC 1945 - HTTP/1.0</a>,
+ * and <a href="http://tools.ietf.org/html/rfc7231">RFC 7231 -
+ * HTTP/1.1</a>.
+ *
+ * @return true if within range of codes that belongs to
+ * <code>Redirection</code> messages.
+ */
+ public boolean isRedirection()
+ {
+ return HttpStatus.isRedirection(this._code);
+ }
+
+ /**
+ * Simple test against an code to determine if it falls into the
+ * <code>Client Error</code> message category as defined in the <a
+ * href="http://tools.ietf.org/html/rfc1945">RFC 1945 - HTTP/1.0</a>,
+ * and <a href="http://tools.ietf.org/html/rfc7231">RFC 7231 -
+ * HTTP/1.1</a>.
+ *
+ * @return true if within range of codes that belongs to
+ * <code>Client Error</code> messages.
+ */
+ public boolean isClientError()
+ {
+ return HttpStatus.isClientError(this._code);
+ }
+
+ /**
+ * Simple test against an code to determine if it falls into the
+ * <code>Server Error</code> message category as defined in the <a
+ * href="http://tools.ietf.org/html/rfc1945">RFC 1945 - HTTP/1.0</a>,
+ * and <a href="http://tools.ietf.org/html/rfc7231">RFC 7231 -
+ * HTTP/1.1</a>.
+ *
+ * @return true if within range of codes that belongs to
+ * <code>Server Error</code> messages.
+ */
+ public boolean isServerError()
+ {
+ return HttpStatus.isServerError(this._code);
+ }
+ }
+
+ /**
+ * Get the HttpStatusCode for a specific code
+ *
+ * @param code the code to lookup.
+ * @return the {@link HttpStatus} if found, or null if not found.
+ */
+ public static Code getCode(int code)
+ {
+ if (code <= MAX_CODE)
+ {
+ return codeMap[code];
+ }
+ return null;
+ }
+
+ /**
+ * Get the status message for a specific code.
+ *
+ * @param code the code to look up
+ * @return the specific message, or the code number itself if code
+ * does not match known list.
+ */
+ public static String getMessage(int code)
+ {
+ Code codeEnum = getCode(code);
+ if (codeEnum != null)
+ {
+ return codeEnum.getMessage();
+ }
+ else
+ {
+ return Integer.toString(code);
+ }
+ }
+
+ public static boolean hasNoBody(int status)
+ {
+ switch (status)
+ {
+ case NO_CONTENT_204:
+ case RESET_CONTENT_205:
+ case PARTIAL_CONTENT_206:
+ case NOT_MODIFIED_304:
+ return true;
+
+ default:
+ return status < OK_200;
+ }
+ }
+
+ /**
+ * Simple test against an code to determine if it falls into the
+ * <code>Informational</code> message category as defined in the <a
+ * href="http://tools.ietf.org/html/rfc1945">RFC 1945 - HTTP/1.0</a>, and <a
+ * href="http://tools.ietf.org/html/rfc7231">RFC 7231 - HTTP/1.1</a>.
+ *
+ * @param code the code to test.
+ * @return true if within range of codes that belongs to
+ * <code>Informational</code> messages.
+ */
+ public static boolean isInformational(int code)
+ {
+ return ((100 <= code) && (code <= 199));
+ }
+
+ /**
+ * Simple test against an code to determine if it falls into the
+ * <code>Success</code> message category as defined in the <a
+ * href="http://tools.ietf.org/html/rfc1945">RFC 1945 - HTTP/1.0</a>, and <a
+ * href="http://tools.ietf.org/html/rfc7231">RFC 7231 - HTTP/1.1</a>.
+ *
+ * @param code the code to test.
+ * @return true if within range of codes that belongs to
+ * <code>Success</code> messages.
+ */
+ public static boolean isSuccess(int code)
+ {
+ return ((200 <= code) && (code <= 299));
+ }
+
+ /**
+ * Simple test against an code to determine if it falls into the
+ * <code>Redirection</code> message category as defined in the <a
+ * href="http://tools.ietf.org/html/rfc1945">RFC 1945 - HTTP/1.0</a>, and <a
+ * href="http://tools.ietf.org/html/rfc7231">RFC 7231 - HTTP/1.1</a>.
+ *
+ * @param code the code to test.
+ * @return true if within range of codes that belongs to
+ * <code>Redirection</code> messages.
+ */
+ public static boolean isRedirection(int code)
+ {
+ return ((300 <= code) && (code <= 399));
+ }
+
+ /**
+ * Simple test against an code to determine if it falls into the
+ * <code>Client Error</code> message category as defined in the <a
+ * href="http://tools.ietf.org/html/rfc1945">RFC 1945 - HTTP/1.0</a>, and <a
+ * href="http://tools.ietf.org/html/rfc7231">RFC 7231 - HTTP/1.1</a>.
+ *
+ * @param code the code to test.
+ * @return true if within range of codes that belongs to
+ * <code>Client Error</code> messages.
+ */
+ public static boolean isClientError(int code)
+ {
+ return ((400 <= code) && (code <= 499));
+ }
+
+ /**
+ * Simple test against an code to determine if it falls into the
+ * <code>Server Error</code> message category as defined in the <a
+ * href="http://tools.ietf.org/html/rfc1945">RFC 1945 - HTTP/1.0</a>, and <a
+ * href="http://tools.ietf.org/html/rfc7231">RFC 7231 - HTTP/1.1</a>.
+ *
+ * @param code the code to test.
+ * @return true if within range of codes that belongs to
+ * <code>Server Error</code> messages.
+ */
+ public static boolean isServerError(int code)
+ {
+ return ((500 <= code) && (code <= 599));
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java
new file mode 100644
index 0000000..add5c97
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java
@@ -0,0 +1,192 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import org.eclipse.jetty.util.TypeUtil;
+
+/**
+ * HTTP constants
+ */
+public class HttpTokens
+{
+ static final byte COLON = (byte)':';
+ static final byte TAB = 0x09;
+ static final byte LINE_FEED = 0x0A;
+ static final byte CARRIAGE_RETURN = 0x0D;
+ static final byte SPACE = 0x20;
+ static final byte[] CRLF = {CARRIAGE_RETURN, LINE_FEED};
+
+ public enum EndOfContent
+ {
+ UNKNOWN_CONTENT, NO_CONTENT, EOF_CONTENT, CONTENT_LENGTH, CHUNKED_CONTENT
+ }
+
+ public enum Type
+ {
+ CNTL, // Control characters excluding LF, CR
+ HTAB, // Horizontal tab
+ LF, // Line feed
+ CR, // Carriage return
+ SPACE, // Space
+ COLON, // Colon character
+ DIGIT, // Digit
+ ALPHA, // Alpha
+ TCHAR, // token characters excluding COLON,DIGIT,ALPHA, which is equivalent to VCHAR excluding delimiters
+ VCHAR, // Visible characters excluding COLON,DIGIT,ALPHA
+ OTEXT // Obsolete text
+ }
+
+ public static class Token
+ {
+ private final Type _type;
+ private final byte _b;
+ private final char _c;
+ private final int _x;
+
+ private Token(byte b, Type type)
+ {
+ _type = type;
+ _b = b;
+ _c = (char)(0xff & b);
+ char lc = (_c >= 'A' & _c <= 'Z') ? ((char)(_c - 'A' + 'a')) : _c;
+ _x = (_type == Type.DIGIT || _type == Type.ALPHA && lc >= 'a' && lc <= 'f') ? TypeUtil.convertHexDigit(b) : -1;
+ }
+
+ public Type getType()
+ {
+ return _type;
+ }
+
+ public byte getByte()
+ {
+ return _b;
+ }
+
+ public char getChar()
+ {
+ return _c;
+ }
+
+ public boolean isHexDigit()
+ {
+ return _x >= 0;
+ }
+
+ public int getHexDigit()
+ {
+ return _x;
+ }
+
+ @Override
+ public String toString()
+ {
+ switch (_type)
+ {
+ case SPACE:
+ case COLON:
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ return _type + "='" + _c + "'";
+
+ case CR:
+ return "CR=\\r";
+
+ case LF:
+ return "LF=\\n";
+
+ default:
+ return String.format("%s=0x%x", _type, _b);
+ }
+ }
+ }
+
+ public static final Token[] TOKENS = new Token[256];
+
+ static
+ {
+ for (int b = 0; b < 256; b++)
+ {
+ // token = 1*tchar
+ // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
+ // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
+ // / DIGIT / ALPHA
+ // ; any VCHAR, except delimiters
+ // quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
+ // qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
+ // obs-text = %x80-FF
+ // comment = "(" *( ctext / quoted-pair / comment ) ")"
+ // ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
+ // quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
+
+ switch (b)
+ {
+ case LINE_FEED:
+ TOKENS[b] = new Token((byte)b, Type.LF);
+ break;
+ case CARRIAGE_RETURN:
+ TOKENS[b] = new Token((byte)b, Type.CR);
+ break;
+ case SPACE:
+ TOKENS[b] = new Token((byte)b, Type.SPACE);
+ break;
+ case TAB:
+ TOKENS[b] = new Token((byte)b, Type.HTAB);
+ break;
+ case COLON:
+ TOKENS[b] = new Token((byte)b, Type.COLON);
+ break;
+
+ case '!':
+ case '#':
+ case '$':
+ case '%':
+ case '&':
+ case '\'':
+ case '*':
+ case '+':
+ case '-':
+ case '.':
+ case '^':
+ case '_':
+ case '`':
+ case '|':
+ case '~':
+ TOKENS[b] = new Token((byte)b, Type.TCHAR);
+ break;
+
+ default:
+ if (b >= 0x30 && b <= 0x39) // DIGIT
+ TOKENS[b] = new Token((byte)b, Type.DIGIT);
+ else if (b >= 0x41 && b <= 0x5A) // ALPHA (uppercase)
+ TOKENS[b] = new Token((byte)b, Type.ALPHA);
+ else if (b >= 0x61 && b <= 0x7A) // ALPHA (lowercase)
+ TOKENS[b] = new Token((byte)b, Type.ALPHA);
+ else if (b >= 0x21 && b <= 0x7E) // Visible
+ TOKENS[b] = new Token((byte)b, Type.VCHAR);
+ else if (b >= 0x80) // OBS
+ TOKENS[b] = new Token((byte)b, Type.OTEXT);
+ else
+ TOKENS[b] = new Token((byte)b, Type.CNTL);
+ }
+ }
+ }
+}
+
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java
new file mode 100644
index 0000000..0a2e758
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java
@@ -0,0 +1,1096 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.EnumSet;
+import java.util.Objects;
+
+import org.eclipse.jetty.util.ArrayTrie;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.Trie;
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.UrlEncoded;
+
+/**
+ * Http URI.
+ * Parse an HTTP URI from a string or byte array. Given a URI
+ * {@code http://user@host:port/path;param1/%2e/info;param2?query#fragment}
+ * this class will split it into the following optional elements:<ul>
+ * <li>{@link #getScheme()} - http:</li>
+ * <li>{@link #getAuthority()} - //name@host:port</li>
+ * <li>{@link #getHost()} - host</li>
+ * <li>{@link #getPort()} - port</li>
+ * <li>{@link #getPath()} - /path;param1/%2e/info;param2</li>
+ * <li>{@link #getDecodedPath()} - /path/info</li>
+ * <li>{@link #getParam()} - param2</li>
+ * <li>{@link #getQuery()} - query</li>
+ * <li>{@link #getFragment()} - fragment</li>
+ * </ul>
+ *
+ * <p>The path part of the URI is provided in both raw form ({@link #getPath()}) and
+ * decoded form ({@link #getDecodedPath}), which has: path parameters removed,
+ * percent encoded characters expanded and relative segments resolved. This approach
+ * is somewhat contrary to <a href="https://tools.ietf.org/html/rfc3986#section-3.3">RFC3986</a>
+ * which no longer defines path parameters (removed after
+ * <a href="https://tools.ietf.org/html/rfc2396#section-3.3">RFC2396</a>) and specifies
+ * that relative segment normalization should take place before percent encoded character
+ * expansion. A literal interpretation of the RFC can result in URI paths with ambiguities
+ * when viewed as strings. For example, a URI of {@code /foo%2f..%2fbar} is technically a single
+ * segment of "/foo/../bar", but could easily be misinterpreted as 3 segments resolving to "/bar"
+ * by a file system.
+ * </p>
+ * <p>
+ * Thus this class avoid and/or detects such ambiguities. Furthermore, by decoding characters and
+ * removing parameters before relative path normalization, ambiguous paths will be resolved in such
+ * a way to be non-standard-but-non-ambiguous to down stream interpretation of the decoded path string.
+ * The violations are recorded and available by API such as {@link #hasAmbiguousSegment()} so that requests
+ * containing them may be rejected in case the non-standard-but-non-ambiguous interpretations
+ * are not satisfactory for a given compliance configuration.
+ * </p>
+ * <p>
+ * Implementations that wish to process ambiguous URI paths must configure the compliance modes
+ * to accept them and then perform their own decoding of {@link #getPath()}.
+ * </p>
+ * <p>
+ * If there are multiple path parameters, only the last one is returned by {@link #getParam()}.
+ * </p>
+ **/
+public class HttpURI
+{
+ private enum State
+ {
+ START,
+ HOST_OR_PATH,
+ SCHEME_OR_PATH,
+ HOST,
+ IPV6,
+ PORT,
+ PATH,
+ PARAM,
+ QUERY,
+ FRAGMENT,
+ ASTERISK
+ }
+
+ /**
+ * Violations of safe URI interpretations
+ */
+ enum Violation
+ {
+ /**
+ * Ambiguous path segments e.g. {@code /foo/%2E%2E/bar}
+ */
+ SEGMENT("Ambiguous path segments"),
+ /**
+ * Ambiguous path separator within a URI segment e.g. {@code /foo%2Fbar}
+ */
+ SEPARATOR("Ambiguous path separator"),
+ /**
+ * Ambiguous path parameters within a URI segment e.g. {@code /foo/..;/bar}
+ */
+ PARAM("Ambiguous path parameters"),
+ /**
+ * Ambiguous double encoding within a URI segment e.g. {@code /%2557EB-INF}
+ */
+ ENCODING("Ambiguous double encoding"),
+ /**
+ * Ambiguous empty segments e.g. {@code /foo//bar}
+ */
+ EMPTY("Ambiguous empty segments"),
+ /**
+ * Non standard UTF-16 encoding eg {@code /foo%u2192bar}.
+ */
+ UTF16("Non standard UTF-16 encoding");
+
+ private final String _message;
+
+ Violation(String message)
+ {
+ _message = message;
+ }
+
+ String getMessage()
+ {
+ return _message;
+ }
+ }
+
+ private static final Trie<Boolean> __ambiguousSegments = new ArrayTrie<>();
+
+ static
+ {
+ __ambiguousSegments.put(".", Boolean.FALSE);
+ __ambiguousSegments.put("%2e", Boolean.TRUE);
+ __ambiguousSegments.put("%u002e", Boolean.TRUE);
+ __ambiguousSegments.put("..", Boolean.FALSE);
+ __ambiguousSegments.put(".%2e", Boolean.TRUE);
+ __ambiguousSegments.put(".%u002e", Boolean.TRUE);
+ __ambiguousSegments.put("%2e.", Boolean.TRUE);
+ __ambiguousSegments.put("%2e%2e", Boolean.TRUE);
+ __ambiguousSegments.put("%2e%u002e", Boolean.TRUE);
+ __ambiguousSegments.put("%u002e.", Boolean.TRUE);
+ __ambiguousSegments.put("%u002e%2e", Boolean.TRUE);
+ __ambiguousSegments.put("%u002e%u002e", Boolean.TRUE);
+ }
+
+ private String _scheme;
+ private String _user;
+ private String _host;
+ private int _port;
+ private String _path;
+ private String _param;
+ private String _query;
+ private String _fragment;
+ private String _uri;
+ private String _decodedPath;
+ private final EnumSet<Violation> _violations = EnumSet.noneOf(Violation.class);
+ private boolean _emptySegment;
+
+ /**
+ * Construct a normalized URI.
+ * Port is not set if it is the default port.
+ *
+ * @param scheme the URI scheme
+ * @param host the URI hose
+ * @param port the URI port
+ * @param path the URI path
+ * @param param the URI param
+ * @param query the URI query
+ * @param fragment the URI fragment
+ * @return the normalized URI
+ */
+ public static HttpURI createHttpURI(String scheme, String host, int port, String path, String param, String query, String fragment)
+ {
+ if (port == 80 && HttpScheme.HTTP.is(scheme))
+ port = 0;
+ if (port == 443 && HttpScheme.HTTPS.is(scheme))
+ port = 0;
+ return new HttpURI(scheme, host, port, path, param, query, fragment);
+ }
+
+ public HttpURI()
+ {
+ }
+
+ public HttpURI(String scheme, String host, int port, String path, String param, String query, String fragment)
+ {
+ _scheme = scheme;
+ _host = host;
+ _port = port;
+ if (path != null)
+ parse(State.PATH, path, 0, path.length());
+ if (param != null)
+ _param = param;
+ if (query != null)
+ _query = query;
+ if (fragment != null)
+ _fragment = fragment;
+ }
+
+ public HttpURI(HttpURI uri)
+ {
+ _scheme = uri._scheme;
+ _user = uri._user;
+ _host = uri._host;
+ _port = uri._port;
+ _path = uri._path;
+ _param = uri._param;
+ _query = uri._query;
+ _fragment = uri._fragment;
+ _uri = uri._uri;
+ _decodedPath = uri._decodedPath;
+ _violations.addAll(uri._violations);
+ _emptySegment = false;
+ }
+
+ public HttpURI(HttpURI schemeHostPort, HttpURI uri)
+ {
+ _scheme = schemeHostPort._scheme;
+ _user = schemeHostPort._user;
+ _host = schemeHostPort._host;
+ _port = schemeHostPort._port;
+ _path = uri._path;
+ _param = uri._param;
+ _query = uri._query;
+ _fragment = uri._fragment;
+ _uri = uri._uri;
+ _decodedPath = uri._decodedPath;
+ _violations.addAll(uri._violations);
+ _emptySegment = false;
+ }
+
+ public HttpURI(String uri)
+ {
+ _port = -1;
+ parse(State.START, uri, 0, uri.length());
+ }
+
+ public HttpURI(URI uri)
+ {
+ _uri = null;
+ _scheme = uri.getScheme();
+ _host = uri.getHost();
+ if (_host == null && uri.getRawSchemeSpecificPart().startsWith("//"))
+ _host = "";
+ _port = uri.getPort();
+ _user = uri.getUserInfo();
+ String path = uri.getRawPath();
+ if (path != null)
+ parse(State.PATH, path, 0, path.length());
+ _query = uri.getRawQuery();
+ _fragment = uri.getFragment();
+ }
+
+ public HttpURI(String scheme, String host, int port, String pathQuery)
+ {
+ _uri = null;
+ _scheme = scheme;
+ _host = host;
+ _port = port;
+ if (pathQuery != null)
+ parse(State.PATH, pathQuery, 0, pathQuery.length());
+ }
+
+ public void clear()
+ {
+ _uri = null;
+ _scheme = null;
+ _user = null;
+ _host = null;
+ _port = -1;
+ _path = null;
+ _param = null;
+ _query = null;
+ _fragment = null;
+ _decodedPath = null;
+ _emptySegment = false;
+ _violations.clear();
+ }
+
+ public void parse(String uri)
+ {
+ clear();
+ _uri = uri;
+ parse(State.START, uri, 0, uri.length());
+ }
+
+ /**
+ * Parse according to https://tools.ietf.org/html/rfc7230#section-5.3
+ *
+ * @param method the request method
+ * @param uri the request uri
+ */
+ public void parseRequestTarget(String method, String uri)
+ {
+ clear();
+ _uri = uri;
+
+ if (HttpMethod.CONNECT.is(method))
+ _path = uri;
+ else
+ parse(uri.startsWith("/") ? State.PATH : State.START, uri, 0, uri.length());
+ }
+
+ @Deprecated
+ public void parseConnect(String uri)
+ {
+ clear();
+ _uri = uri;
+ _path = uri;
+ }
+
+ public void parse(String uri, int offset, int length)
+ {
+ clear();
+ int end = offset + length;
+ _uri = uri.substring(offset, end);
+ parse(State.START, uri, offset, end);
+ }
+
+ private void parse(State state, final String uri, final int offset, final int end)
+ {
+ int mark = offset; // the start of the current section being parsed
+ int pathMark = 0; // the start of the path section
+ int segment = 0; // the start of the current segment within the path
+ boolean encodedPath = false; // set to true if the path contains % encoded characters
+ boolean encodedUtf16 = false; // Is the current encoding for UTF16?
+ int encodedCharacters = 0; // partial state of parsing a % encoded character<x>
+ int encodedValue = 0; // the partial encoded value
+ boolean dot = false; // set to true if the path contains . or .. segments
+
+ for (int i = offset; i < end; i++)
+ {
+ char c = uri.charAt(i);
+
+ switch (state)
+ {
+ case START:
+ {
+ switch (c)
+ {
+ case '/':
+ mark = i;
+ state = State.HOST_OR_PATH;
+ break;
+ case ';':
+ checkSegment(uri, segment, i, true);
+ mark = i + 1;
+ state = State.PARAM;
+ break;
+ case '?':
+ // assume empty path (if seen at start)
+ checkSegment(uri, segment, i, false);
+ _path = "";
+ mark = i + 1;
+ state = State.QUERY;
+ break;
+ case '#':
+ // assume empty path (if seen at start)
+ checkSegment(uri, segment, i, false);
+ _path = "";
+ mark = i + 1;
+ state = State.FRAGMENT;
+ break;
+ case '*':
+ _path = "*";
+ state = State.ASTERISK;
+ break;
+ case '%':
+ encodedPath = true;
+ encodedCharacters = 2;
+ encodedValue = 0;
+ mark = pathMark = segment = i;
+ state = State.PATH;
+ break;
+ case '.':
+ dot = true;
+ pathMark = segment = i;
+ state = State.PATH;
+ break;
+ default:
+ mark = i;
+ if (_scheme == null)
+ state = State.SCHEME_OR_PATH;
+ else
+ {
+ pathMark = segment = i;
+ state = State.PATH;
+ }
+ break;
+ }
+ continue;
+ }
+
+ case SCHEME_OR_PATH:
+ {
+ switch (c)
+ {
+ case ':':
+ // must have been a scheme
+ _scheme = uri.substring(mark, i);
+ // Start again with scheme set
+ state = State.START;
+ break;
+ case '/':
+ // must have been in a path and still are
+ segment = i + 1;
+ state = State.PATH;
+ break;
+ case ';':
+ // must have been in a path
+ mark = i + 1;
+ state = State.PARAM;
+ break;
+ case '?':
+ // must have been in a path
+ _path = uri.substring(mark, i);
+ mark = i + 1;
+ state = State.QUERY;
+ break;
+ case '%':
+ // must have been in an encoded path
+ encodedPath = true;
+ encodedCharacters = 2;
+ encodedValue = 0;
+ state = State.PATH;
+ break;
+ case '#':
+ // must have been in a path
+ _path = uri.substring(mark, i);
+ state = State.FRAGMENT;
+ break;
+ default:
+ break;
+ }
+ continue;
+ }
+ case HOST_OR_PATH:
+ {
+ switch (c)
+ {
+ case '/':
+ _host = "";
+ mark = i + 1;
+ state = State.HOST;
+ break;
+ case '%':
+ case '@':
+ case ';':
+ case '?':
+ case '#':
+ case '.':
+ // was a path, look again
+ i--;
+ pathMark = mark;
+ segment = mark + 1;
+ state = State.PATH;
+ break;
+ default:
+ // it is a path
+ pathMark = mark;
+ segment = mark + 1;
+ state = State.PATH;
+ }
+ continue;
+ }
+
+ case HOST:
+ {
+ switch (c)
+ {
+ case '/':
+ _host = uri.substring(mark, i);
+ pathMark = mark = i;
+ segment = mark + 1;
+ state = State.PATH;
+ break;
+ case ':':
+ if (i > mark)
+ _host = uri.substring(mark, i);
+ mark = i + 1;
+ state = State.PORT;
+ break;
+ case '@':
+ if (_user != null)
+ throw new IllegalArgumentException("Bad authority");
+ _user = uri.substring(mark, i);
+ mark = i + 1;
+ break;
+ case '[':
+ state = State.IPV6;
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case IPV6:
+ {
+ switch (c)
+ {
+ case '/':
+ throw new IllegalArgumentException("No closing ']' for ipv6 in " + uri);
+ case ']':
+ c = uri.charAt(++i);
+ _host = uri.substring(mark, i);
+ if (c == ':')
+ {
+ mark = i + 1;
+ state = State.PORT;
+ }
+ else
+ {
+ pathMark = mark = i;
+ state = State.PATH;
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case PORT:
+ {
+ if (c == '@')
+ {
+ if (_user != null)
+ throw new IllegalArgumentException("Bad authority");
+ // It wasn't a port, but a password!
+ _user = _host + ":" + uri.substring(mark, i);
+ mark = i + 1;
+ state = State.HOST;
+ }
+ else if (c == '/')
+ {
+ _port = TypeUtil.parseInt(uri, mark, i - mark, 10);
+ pathMark = mark = i;
+ segment = i + 1;
+ state = State.PATH;
+ }
+ break;
+ }
+ case PATH:
+ {
+ if (encodedCharacters > 0)
+ {
+ if (encodedCharacters == 2 && c == 'u' && !encodedUtf16)
+ {
+ _violations.add(Violation.UTF16);
+ encodedUtf16 = true;
+ encodedCharacters = 4;
+ continue;
+ }
+ encodedValue = (encodedValue << 4) + TypeUtil.convertHexDigit(c);
+
+ if (--encodedCharacters == 0)
+ {
+ switch (encodedValue)
+ {
+ case 0:
+ // Byte 0 cannot be present in a UTF-8 sequence in any position
+ // other than as the NUL ASCII byte which we do not wish to allow.
+ throw new IllegalArgumentException("Illegal character in path");
+ case '/':
+ _violations.add(Violation.SEPARATOR);
+ break;
+ case '%':
+ _violations.add(Violation.ENCODING);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ else
+ {
+ switch (c)
+ {
+ case ';':
+ checkSegment(uri, segment, i, true);
+ mark = i + 1;
+ state = State.PARAM;
+ break;
+ case '?':
+ checkSegment(uri, segment, i, false);
+ _path = uri.substring(pathMark, i);
+ mark = i + 1;
+ state = State.QUERY;
+ break;
+ case '#':
+ checkSegment(uri, segment, i, false);
+ _path = uri.substring(pathMark, i);
+ mark = i + 1;
+ state = State.FRAGMENT;
+ break;
+ case '/':
+ // There is no leading segment when parsing only a path that starts with slash.
+ if (i != 0)
+ checkSegment(uri, segment, i, false);
+ segment = i + 1;
+ break;
+ case '.':
+ dot |= segment == i;
+ break;
+ case '%':
+ encodedPath = true;
+ encodedUtf16 = false;
+ encodedCharacters = 2;
+ encodedValue = 0;
+ break;
+ default:
+ break;
+ }
+ }
+ break;
+ }
+ case PARAM:
+ {
+ switch (c)
+ {
+ case '?':
+ _path = uri.substring(pathMark, i);
+ _param = uri.substring(mark, i);
+ mark = i + 1;
+ state = State.QUERY;
+ break;
+ case '#':
+ _path = uri.substring(pathMark, i);
+ _param = uri.substring(mark, i);
+ mark = i + 1;
+ state = State.FRAGMENT;
+ break;
+ case '/':
+ encodedPath = true;
+ segment = i + 1;
+ state = State.PATH;
+ break;
+ case ';':
+ // multiple parameters
+ mark = i + 1;
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case QUERY:
+ {
+ if (c == '#')
+ {
+ _query = uri.substring(mark, i);
+ mark = i + 1;
+ state = State.FRAGMENT;
+ }
+ break;
+ }
+ case ASTERISK:
+ {
+ throw new IllegalArgumentException("Bad character '*'");
+ }
+ case FRAGMENT:
+ {
+ _fragment = uri.substring(mark, end);
+ i = end;
+ break;
+ }
+ default:
+ {
+ throw new IllegalStateException(state.toString());
+ }
+ }
+ }
+
+ switch (state)
+ {
+ case START:
+ _path = "";
+ checkSegment(uri, segment, end, false);
+ break;
+ case ASTERISK:
+ break;
+ case SCHEME_OR_PATH:
+ case HOST_OR_PATH:
+ _path = uri.substring(mark, end);
+ break;
+ case HOST:
+ if (end > mark)
+ _host = uri.substring(mark, end);
+ break;
+ case IPV6:
+ throw new IllegalArgumentException("No closing ']' for ipv6 in " + uri);
+ case PORT:
+ _port = TypeUtil.parseInt(uri, mark, end - mark, 10);
+ break;
+ case PARAM:
+ _path = uri.substring(pathMark, end);
+ _param = uri.substring(mark, end);
+ break;
+ case PATH:
+ checkSegment(uri, segment, end, false);
+ _path = uri.substring(pathMark, end);
+ break;
+ case QUERY:
+ _query = uri.substring(mark, end);
+ break;
+ case FRAGMENT:
+ _fragment = uri.substring(mark, end);
+ break;
+ default:
+ throw new IllegalStateException(state.toString());
+ }
+
+ if (!encodedPath && !dot)
+ {
+ if (_param == null)
+ _decodedPath = _path;
+ else
+ _decodedPath = _path.substring(0, _path.length() - _param.length() - 1);
+ }
+ else if (_path != null)
+ {
+ // The RFC requires this to be canonical before decoding, but this can leave dot segments and dot dot segments
+ // which are not canonicalized and could be used in an attempt to bypass security checks.
+ String decodedNonCanonical = URIUtil.decodePath(_path);
+ _decodedPath = URIUtil.canonicalPath(decodedNonCanonical);
+ if (_decodedPath == null)
+ throw new IllegalArgumentException("Bad URI");
+ }
+ }
+
+ /**
+ * Check for ambiguous path segments.
+ *
+ * An ambiguous path segment is one that is perhaps technically legal, but is considered undesirable to handle
+ * due to possible ambiguity. Examples include segments like '..;', '%2e', '%2e%2e' etc.
+ *
+ * @param uri The URI string
+ * @param segment The inclusive starting index of the segment (excluding any '/')
+ * @param end The exclusive end index of the segment
+ */
+ private void checkSegment(String uri, int segment, int end, boolean param)
+ {
+ // This method is called once for every segment parsed.
+ // A URI like "/foo/" has two segments: "foo" and an empty segment.
+ // Empty segments are only ambiguous if they are not the last segment
+ // So if this method is called for any segment and we have previously
+ // seen an empty segment, then it was ambiguous.
+ if (_emptySegment)
+ _violations.add(Violation.EMPTY);
+
+ if (end == segment)
+ {
+ // Empty segments are not ambiguous if followed by a '#', '?' or end of string.
+ if (end >= uri.length() || ("#?".indexOf(uri.charAt(end)) >= 0))
+ return;
+
+ // If this empty segment is the first segment then it is ambiguous.
+ if (segment == 0)
+ {
+ _violations.add(Violation.EMPTY);
+ return;
+ }
+
+ // Otherwise remember we have seen an empty segment, which is check if we see a subsequent segment.
+ if (!_emptySegment)
+ {
+ _emptySegment = true;
+ return;
+ }
+ }
+
+ // Look for segment in the ambiguous segment index.
+ Boolean ambiguous = __ambiguousSegments.get(uri, segment, end - segment);
+ if (ambiguous == Boolean.TRUE)
+ {
+ // The segment is always ambiguous.
+ _violations.add(Violation.SEGMENT);
+ }
+ else if (param && ambiguous == Boolean.FALSE)
+ {
+ // The segment is ambiguous only when followed by a parameter.
+ _violations.add(Violation.PARAM);
+ }
+ }
+
+ /**
+ * @return True if the URI has a possibly ambiguous segment like '..;' or '%2e%2e'
+ */
+ public boolean hasAmbiguousSegment()
+ {
+ return _violations.contains(Violation.SEGMENT);
+ }
+
+ /**
+ * @return True if the URI empty segment that is ambiguous like '//' or '/;param/'.
+ */
+ public boolean hasAmbiguousEmptySegment()
+ {
+ return _violations.contains(Violation.EMPTY);
+ }
+
+ /**
+ * @return True if the URI has a possibly ambiguous separator of %2f
+ */
+ public boolean hasAmbiguousSeparator()
+ {
+ return _violations.contains(Violation.SEPARATOR);
+ }
+
+ /**
+ * @return True if the URI has a possibly ambiguous path parameter like '..;'
+ */
+ public boolean hasAmbiguousParameter()
+ {
+ return _violations.contains(Violation.PARAM);
+ }
+
+ /**
+ * @return True if the URI has an encoded '%' character.
+ */
+ public boolean hasAmbiguousEncoding()
+ {
+ return _violations.contains(Violation.ENCODING);
+ }
+
+ /**
+ * @return True if the URI has either an {@link #hasAmbiguousSegment()} or {@link #hasAmbiguousEmptySegment()}
+ * or {@link #hasAmbiguousSeparator()} or {@link #hasAmbiguousParameter()}
+ */
+ public boolean isAmbiguous()
+ {
+ return !_violations.isEmpty() && !(_violations.size() == 1 && _violations.contains(Violation.UTF16));
+ }
+
+ /**
+ * @return True if the URI has any Violations.
+ */
+ public boolean hasViolations()
+ {
+ return !_violations.isEmpty();
+ }
+
+ boolean hasViolation(Violation violation)
+ {
+ return _violations.contains(violation);
+ }
+
+ /**
+ * @return True if the URI encodes UTF-16 characters with '%u'.
+ */
+ public boolean hasUtf16Encoding()
+ {
+ return _violations.contains(Violation.UTF16);
+ }
+
+ public String getScheme()
+ {
+ return _scheme;
+ }
+
+ public String getHost()
+ {
+ // Return null for empty host to retain compatibility with java.net.URI
+ if (_host != null && _host.isEmpty())
+ return null;
+ return _host;
+ }
+
+ public int getPort()
+ {
+ return _port;
+ }
+
+ /**
+ * The parsed Path.
+ *
+ * @return the path as parsed on valid URI. null for invalid URI.
+ */
+ public String getPath()
+ {
+ return _path;
+ }
+
+ /**
+ * @return The decoded canonical path.
+ * @see URIUtil#canonicalPath(String)
+ */
+ public String getDecodedPath()
+ {
+ return _decodedPath;
+ }
+
+ /**
+ * Get a URI path parameter. Multiple and in segment parameters are ignored and only
+ * the last trailing parameter is returned.
+ * @return The last path parameter or null
+ */
+ public String getParam()
+ {
+ return _param;
+ }
+
+ public void setParam(String param)
+ {
+ if (!Objects.equals(_param, param))
+ {
+ if (_param != null && _path.endsWith(";" + _param))
+ _path = _path.substring(0, _path.length() - 1 - _param.length());
+ _param = param;
+ if (_param != null)
+ _path = (_path == null ? "" : _path) + ";" + _param;
+ _uri = null;
+ }
+ }
+
+ public String getQuery()
+ {
+ return _query;
+ }
+
+ public boolean hasQuery()
+ {
+ return _query != null && !_query.isEmpty();
+ }
+
+ public String getFragment()
+ {
+ return _fragment;
+ }
+
+ public void decodeQueryTo(MultiMap<String> parameters)
+ {
+ if (_query == null)
+ return;
+ UrlEncoded.decodeUtf8To(_query, parameters);
+ }
+
+ public void decodeQueryTo(MultiMap<String> parameters, String encoding) throws UnsupportedEncodingException
+ {
+ decodeQueryTo(parameters, Charset.forName(encoding));
+ }
+
+ public void decodeQueryTo(MultiMap<String> parameters, Charset encoding) throws UnsupportedEncodingException
+ {
+ if (_query == null)
+ return;
+
+ if (encoding == null || StandardCharsets.UTF_8.equals(encoding))
+ UrlEncoded.decodeUtf8To(_query, parameters);
+ else
+ UrlEncoded.decodeTo(_query, parameters, encoding);
+ }
+
+ public boolean isAbsolute()
+ {
+ return _scheme != null && !_scheme.isEmpty();
+ }
+
+ @Override
+ public String toString()
+ {
+ if (_uri == null)
+ {
+ StringBuilder out = new StringBuilder();
+
+ if (_scheme != null)
+ out.append(_scheme).append(':');
+
+ if (_host != null)
+ {
+ out.append("//");
+ if (_user != null)
+ out.append(_user).append('@');
+ out.append(_host);
+ }
+
+ if (_port > 0)
+ out.append(':').append(_port);
+
+ if (_path != null)
+ out.append(_path);
+
+ if (_query != null)
+ out.append('?').append(_query);
+
+ if (_fragment != null)
+ out.append('#').append(_fragment);
+
+ if (out.length() > 0)
+ _uri = out.toString();
+ else
+ _uri = "";
+ }
+ return _uri;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (o == this)
+ return true;
+ if (!(o instanceof HttpURI))
+ return false;
+ return toString().equals(o.toString());
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return toString().hashCode();
+ }
+
+ public void setScheme(String scheme)
+ {
+ _scheme = scheme;
+ _uri = null;
+ }
+
+ /**
+ * @param host the host
+ * @param port the port
+ */
+ public void setAuthority(String host, int port)
+ {
+ _host = host;
+ _port = port;
+ _uri = null;
+ }
+
+ /**
+ * @param path the path
+ */
+ public void setPath(String path)
+ {
+ _uri = null;
+ _path = null;
+ if (path != null)
+ parse(State.PATH, path, 0, path.length());
+ }
+
+ public void setPathQuery(String pathQuery)
+ {
+ _uri = null;
+ _path = null;
+ _decodedPath = null;
+ _param = null;
+ _fragment = null;
+ /*
+ * The query is not cleared here and old values may be retained if there is no query in
+ * the pathQuery. This has been fixed in 10, but left as is here to preserve behaviour in 9.
+ */
+ if (pathQuery != null)
+ parse(State.PATH, pathQuery, 0, pathQuery.length());
+ }
+
+ public void setQuery(String query)
+ {
+ _query = query;
+ _uri = null;
+ }
+
+ public URI toURI() throws URISyntaxException
+ {
+ return new URI(_scheme, null, _host, _port, _path, _query == null ? null : UrlEncoded.decodeString(_query), _fragment);
+ }
+
+ public String getPathQuery()
+ {
+ if (_query == null)
+ return _path;
+ return _path + "?" + _query;
+ }
+
+ public String getAuthority()
+ {
+ if (_port > 0)
+ return _host + ":" + _port;
+ return _host;
+ }
+
+ public String getUser()
+ {
+ return _user;
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpVersion.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpVersion.java
new file mode 100644
index 0000000..27e7557
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/HttpVersion.java
@@ -0,0 +1,170 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.ArrayTrie;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.Trie;
+
+public enum HttpVersion
+{
+ HTTP_0_9("HTTP/0.9", 9),
+ HTTP_1_0("HTTP/1.0", 10),
+ HTTP_1_1("HTTP/1.1", 11),
+ HTTP_2("HTTP/2.0", 20);
+
+ public static final Trie<HttpVersion> CACHE = new ArrayTrie<HttpVersion>();
+
+ static
+ {
+ for (HttpVersion version : HttpVersion.values())
+ {
+ CACHE.put(version.toString(), version);
+ }
+ }
+
+ /**
+ * Optimised lookup to find an Http Version and whitespace in a byte array.
+ *
+ * @param bytes Array containing ISO-8859-1 characters
+ * @param position The first valid index
+ * @param limit The first non valid index
+ * @return An HttpMethod if a match or null if no easy match.
+ */
+ public static HttpVersion lookAheadGet(byte[] bytes, int position, int limit)
+ {
+ int length = limit - position;
+ if (length < 9)
+ return null;
+
+ if (bytes[position + 4] == '/' && bytes[position + 6] == '.' && Character.isWhitespace((char)bytes[position + 8]) &&
+ ((bytes[position] == 'H' && bytes[position + 1] == 'T' && bytes[position + 2] == 'T' && bytes[position + 3] == 'P') ||
+ (bytes[position] == 'h' && bytes[position + 1] == 't' && bytes[position + 2] == 't' && bytes[position + 3] == 'p')))
+ {
+ switch (bytes[position + 5])
+ {
+ case '1':
+ switch (bytes[position + 7])
+ {
+ case '0':
+ return HTTP_1_0;
+ case '1':
+ return HTTP_1_1;
+ }
+ break;
+ case '2':
+ switch (bytes[position + 7])
+ {
+ case '0':
+ return HTTP_2;
+ }
+ break;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Optimised lookup to find an HTTP Version and trailing white space in a byte array.
+ *
+ * @param buffer buffer containing ISO-8859-1 characters
+ * @return An HttpVersion if a match or null if no easy match.
+ */
+ public static HttpVersion lookAheadGet(ByteBuffer buffer)
+ {
+ if (buffer.hasArray())
+ return lookAheadGet(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.arrayOffset() + buffer.limit());
+ return null;
+ }
+
+ private final String _string;
+ private final byte[] _bytes;
+ private final ByteBuffer _buffer;
+ private final int _version;
+
+ HttpVersion(String s, int version)
+ {
+ _string = s;
+ _bytes = StringUtil.getBytes(s);
+ _buffer = ByteBuffer.wrap(_bytes);
+ _version = version;
+ }
+
+ public byte[] toBytes()
+ {
+ return _bytes;
+ }
+
+ public ByteBuffer toBuffer()
+ {
+ return _buffer.asReadOnlyBuffer();
+ }
+
+ public int getVersion()
+ {
+ return _version;
+ }
+
+ public boolean is(String s)
+ {
+ return _string.equalsIgnoreCase(s);
+ }
+
+ public String asString()
+ {
+ return _string;
+ }
+
+ @Override
+ public String toString()
+ {
+ return _string;
+ }
+
+ /**
+ * Case insensitive fromString() conversion
+ *
+ * @param version the String to convert to enum constant
+ * @return the enum constant or null if version unknown
+ */
+ public static HttpVersion fromString(String version)
+ {
+ return CACHE.get(version);
+ }
+
+ public static HttpVersion fromVersion(int version)
+ {
+ switch (version)
+ {
+ case 9:
+ return HttpVersion.HTTP_0_9;
+ case 10:
+ return HttpVersion.HTTP_1_0;
+ case 11:
+ return HttpVersion.HTTP_1_1;
+ case 20:
+ return HttpVersion.HTTP_2;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java
new file mode 100644
index 0000000..dc7b795
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java
@@ -0,0 +1,337 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.function.Supplier;
+
+public class MetaData implements Iterable<HttpField>
+{
+ private HttpVersion _httpVersion;
+ private final HttpFields _fields;
+ private long _contentLength;
+ private Supplier<HttpFields> _trailers;
+
+ public MetaData(HttpVersion version, HttpFields fields)
+ {
+ this(version, fields, Long.MIN_VALUE);
+ }
+
+ public MetaData(HttpVersion version, HttpFields fields, long contentLength)
+ {
+ _httpVersion = version;
+ _fields = fields;
+ _contentLength = contentLength;
+ }
+
+ protected void recycle()
+ {
+ _httpVersion = null;
+ if (_fields != null)
+ _fields.clear();
+ _contentLength = Long.MIN_VALUE;
+ }
+
+ public boolean isRequest()
+ {
+ return false;
+ }
+
+ public boolean isResponse()
+ {
+ return false;
+ }
+
+ /**
+ * @return the HTTP version of this MetaData object
+ * @deprecated use {@link #getHttpVersion()} instead
+ */
+ @Deprecated
+ public HttpVersion getVersion()
+ {
+ return getHttpVersion();
+ }
+
+ /**
+ * @return the HTTP version of this MetaData object
+ */
+ public HttpVersion getHttpVersion()
+ {
+ return _httpVersion;
+ }
+
+ /**
+ * @param httpVersion the HTTP version to set
+ */
+ public void setHttpVersion(HttpVersion httpVersion)
+ {
+ _httpVersion = httpVersion;
+ }
+
+ /**
+ * @return the HTTP fields of this MetaData object
+ */
+ public HttpFields getFields()
+ {
+ return _fields;
+ }
+
+ public Supplier<HttpFields> getTrailerSupplier()
+ {
+ return _trailers;
+ }
+
+ public void setTrailerSupplier(Supplier<HttpFields> trailers)
+ {
+ _trailers = trailers;
+ }
+
+ /**
+ * @return the content length if available, otherwise {@link Long#MIN_VALUE}
+ */
+ public long getContentLength()
+ {
+ if (_contentLength == Long.MIN_VALUE)
+ {
+ if (_fields != null)
+ {
+ HttpField field = _fields.getField(HttpHeader.CONTENT_LENGTH);
+ _contentLength = field == null ? -1 : field.getLongValue();
+ }
+ }
+ return _contentLength;
+ }
+
+ public void setContentLength(long contentLength)
+ {
+ _contentLength = contentLength;
+ }
+
+ /**
+ * @return an iterator over the HTTP fields
+ * @see #getFields()
+ */
+ @Override
+ public Iterator<HttpField> iterator()
+ {
+ HttpFields fields = getFields();
+ return fields == null ? Collections.emptyIterator() : fields.iterator();
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder out = new StringBuilder();
+ for (HttpField field : this)
+ {
+ out.append(field).append(System.lineSeparator());
+ }
+ return out.toString();
+ }
+
+ public static class Request extends MetaData
+ {
+ private String _method;
+ private HttpURI _uri;
+
+ public Request(HttpFields fields)
+ {
+ this(null, null, null, fields);
+ }
+
+ public Request(String method, HttpURI uri, HttpVersion version, HttpFields fields)
+ {
+ this(method, uri, version, fields, Long.MIN_VALUE);
+ }
+
+ public Request(String method, HttpURI uri, HttpVersion version, HttpFields fields, long contentLength)
+ {
+ super(version, fields, contentLength);
+ _method = method;
+ _uri = uri;
+ }
+
+ public Request(String method, HttpScheme scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields)
+ {
+ this(method, new HttpURI(scheme == null ? null : scheme.asString(),
+ hostPort == null ? null : hostPort.getHost(),
+ hostPort == null ? -1 : hostPort.getPort(),
+ uri), version, fields);
+ }
+
+ public Request(String method, HttpScheme scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields, long contentLength)
+ {
+ this(method, new HttpURI(scheme == null ? null : scheme.asString(),
+ hostPort == null ? null : hostPort.getHost(),
+ hostPort == null ? -1 : hostPort.getPort(),
+ uri), version, fields, contentLength);
+ }
+
+ public Request(String method, String scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields, long contentLength)
+ {
+ this(method, new HttpURI(scheme,
+ hostPort == null ? null : hostPort.getHost(),
+ hostPort == null ? -1 : hostPort.getPort(),
+ uri), version, fields, contentLength);
+ }
+
+ public Request(Request request)
+ {
+ this(request.getMethod(), new HttpURI(request.getURI()), request.getHttpVersion(), new HttpFields(request.getFields()), request.getContentLength());
+ }
+
+ @Override
+ public void recycle()
+ {
+ super.recycle();
+ _method = null;
+ if (_uri != null)
+ _uri.clear();
+ }
+
+ @Override
+ public boolean isRequest()
+ {
+ return true;
+ }
+
+ /**
+ * @return the HTTP method
+ */
+ public String getMethod()
+ {
+ return _method;
+ }
+
+ /**
+ * @param method the HTTP method to set
+ */
+ public void setMethod(String method)
+ {
+ _method = method;
+ }
+
+ /**
+ * @return the HTTP URI
+ */
+ public HttpURI getURI()
+ {
+ return _uri;
+ }
+
+ /**
+ * @return the HTTP URI in string form
+ */
+ public String getURIString()
+ {
+ return _uri == null ? null : _uri.toString();
+ }
+
+ /**
+ * @param uri the HTTP URI to set
+ */
+ public void setURI(HttpURI uri)
+ {
+ _uri = uri;
+ }
+
+ @Override
+ public String toString()
+ {
+ HttpFields fields = getFields();
+ return String.format("%s{u=%s,%s,h=%d,cl=%d}",
+ getMethod(), getURI(), getHttpVersion(), fields == null ? -1 : fields.size(), getContentLength());
+ }
+ }
+
+ public static class Response extends MetaData
+ {
+ private int _status;
+ private String _reason;
+
+ public Response()
+ {
+ this(null, 0, null);
+ }
+
+ public Response(HttpVersion version, int status, HttpFields fields)
+ {
+ this(version, status, fields, Long.MIN_VALUE);
+ }
+
+ public Response(HttpVersion version, int status, HttpFields fields, long contentLength)
+ {
+ super(version, fields, contentLength);
+ _status = status;
+ }
+
+ public Response(HttpVersion version, int status, String reason, HttpFields fields, long contentLength)
+ {
+ super(version, fields, contentLength);
+ _reason = reason;
+ _status = status;
+ }
+
+ @Override
+ public boolean isResponse()
+ {
+ return true;
+ }
+
+ /**
+ * @return the HTTP status
+ */
+ public int getStatus()
+ {
+ return _status;
+ }
+
+ /**
+ * @return the HTTP reason
+ */
+ public String getReason()
+ {
+ return _reason;
+ }
+
+ /**
+ * @param status the HTTP status to set
+ */
+ public void setStatus(int status)
+ {
+ _status = status;
+ }
+
+ /**
+ * @param reason the HTTP reason to set
+ */
+ public void setReason(String reason)
+ {
+ _reason = reason;
+ }
+
+ @Override
+ public String toString()
+ {
+ HttpFields fields = getFields();
+ return String.format("%s{s=%d,h=%d,cl=%d}", getHttpVersion(), getStatus(), fields == null ? -1 : fields.size(), getContentLength());
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java
new file mode 100644
index 0000000..81f01a3
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java
@@ -0,0 +1,685 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+
+import org.eclipse.jetty.util.ArrayTrie;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.Trie;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * MIME Type enum and utilities
+ */
+public class MimeTypes
+{
+
+ private static final Logger LOG = Log.getLogger(MimeTypes.class);
+ private static final Trie<ByteBuffer> TYPES = new ArrayTrie<ByteBuffer>(512);
+ private static final Map<String, String> __dftMimeMap = new HashMap<String, String>();
+ private static final Map<String, String> __inferredEncodings = new HashMap<String, String>();
+ private static final Map<String, String> __assumedEncodings = new HashMap<String, String>();
+
+ public enum Type
+ {
+ FORM_ENCODED("application/x-www-form-urlencoded"),
+ MESSAGE_HTTP("message/http"),
+ MULTIPART_BYTERANGES("multipart/byteranges"),
+ MULTIPART_FORM_DATA("multipart/form-data"),
+
+ TEXT_HTML("text/html"),
+ TEXT_PLAIN("text/plain"),
+ TEXT_XML("text/xml"),
+ TEXT_JSON("text/json", StandardCharsets.UTF_8),
+ APPLICATION_JSON("application/json", StandardCharsets.UTF_8),
+
+ TEXT_HTML_8859_1("text/html;charset=iso-8859-1", TEXT_HTML),
+ TEXT_HTML_UTF_8("text/html;charset=utf-8", TEXT_HTML),
+
+ TEXT_PLAIN_8859_1("text/plain;charset=iso-8859-1", TEXT_PLAIN),
+ TEXT_PLAIN_UTF_8("text/plain;charset=utf-8", TEXT_PLAIN),
+
+ TEXT_XML_8859_1("text/xml;charset=iso-8859-1", TEXT_XML),
+ TEXT_XML_UTF_8("text/xml;charset=utf-8", TEXT_XML),
+
+ TEXT_JSON_8859_1("text/json;charset=iso-8859-1", TEXT_JSON),
+ TEXT_JSON_UTF_8("text/json;charset=utf-8", TEXT_JSON),
+
+ APPLICATION_JSON_8859_1("application/json;charset=iso-8859-1", APPLICATION_JSON),
+ APPLICATION_JSON_UTF_8("application/json;charset=utf-8", APPLICATION_JSON);
+
+ private final String _string;
+ private final Type _base;
+ private final ByteBuffer _buffer;
+ private final Charset _charset;
+ private final String _charsetString;
+ private final boolean _assumedCharset;
+ private final HttpField _field;
+
+ Type(String s)
+ {
+ _string = s;
+ _buffer = BufferUtil.toBuffer(s);
+ _base = this;
+ _charset = null;
+ _charsetString = null;
+ _assumedCharset = false;
+ _field = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, _string);
+ }
+
+ Type(String s, Type base)
+ {
+ _string = s;
+ _buffer = BufferUtil.toBuffer(s);
+ _base = base;
+ int i = s.indexOf(";charset=");
+ _charset = Charset.forName(s.substring(i + 9));
+ _charsetString = _charset.toString().toLowerCase(Locale.ENGLISH);
+ _assumedCharset = false;
+ _field = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, _string);
+ }
+
+ Type(String s, Charset cs)
+ {
+ _string = s;
+ _base = this;
+ _buffer = BufferUtil.toBuffer(s);
+ _charset = cs;
+ _charsetString = _charset == null ? null : _charset.toString().toLowerCase(Locale.ENGLISH);
+ _assumedCharset = true;
+ _field = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, _string);
+ }
+
+ public ByteBuffer asBuffer()
+ {
+ return _buffer.asReadOnlyBuffer();
+ }
+
+ public Charset getCharset()
+ {
+ return _charset;
+ }
+
+ public String getCharsetString()
+ {
+ return _charsetString;
+ }
+
+ public boolean is(String s)
+ {
+ return _string.equalsIgnoreCase(s);
+ }
+
+ public String asString()
+ {
+ return _string;
+ }
+
+ @Override
+ public String toString()
+ {
+ return _string;
+ }
+
+ public boolean isCharsetAssumed()
+ {
+ return _assumedCharset;
+ }
+
+ public HttpField getContentTypeField()
+ {
+ return _field;
+ }
+
+ public Type getBaseType()
+ {
+ return _base;
+ }
+ }
+
+ public static final Trie<MimeTypes.Type> CACHE = new ArrayTrie<>(512);
+
+ static
+ {
+ for (MimeTypes.Type type : MimeTypes.Type.values())
+ {
+ CACHE.put(type.toString(), type);
+ TYPES.put(type.toString(), type.asBuffer());
+
+ int charset = type.toString().indexOf(";charset=");
+ if (charset > 0)
+ {
+ String alt = StringUtil.replace(type.toString(), ";charset=", "; charset=");
+ CACHE.put(alt, type);
+ TYPES.put(alt, type.asBuffer());
+ }
+
+ if (type.isCharsetAssumed())
+ __assumedEncodings.put(type.asString(), type.getCharsetString());
+ }
+
+ String resourceName = "org/eclipse/jetty/http/mime.properties";
+ try (InputStream stream = MimeTypes.class.getClassLoader().getResourceAsStream(resourceName))
+ {
+ if (stream == null)
+ {
+ LOG.warn("Missing mime-type resource: {}", resourceName);
+ }
+ else
+ {
+ try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8))
+ {
+ Properties props = new Properties();
+ props.load(reader);
+ props.stringPropertyNames().stream()
+ .filter(x -> x != null)
+ .forEach(x ->
+ __dftMimeMap.put(StringUtil.asciiToLowerCase(x), normalizeMimeType(props.getProperty(x))));
+
+ if (__dftMimeMap.isEmpty())
+ {
+ LOG.warn("Empty mime types at {}", resourceName);
+ }
+ else if (__dftMimeMap.size() < props.keySet().size())
+ {
+ LOG.warn("Duplicate or null mime-type extension in resource: {}", resourceName);
+ }
+ }
+ catch (IOException e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ }
+ }
+ }
+ catch (IOException e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ }
+
+ resourceName = "org/eclipse/jetty/http/encoding.properties";
+ try (InputStream stream = MimeTypes.class.getClassLoader().getResourceAsStream(resourceName))
+ {
+ if (stream == null)
+ LOG.warn("Missing encoding resource: {}", resourceName);
+ else
+ {
+ try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8))
+ {
+ Properties props = new Properties();
+ props.load(reader);
+ props.stringPropertyNames().stream()
+ .filter(t -> t != null)
+ .forEach(t ->
+ {
+ String charset = props.getProperty(t);
+ if (charset.startsWith("-"))
+ __assumedEncodings.put(t, charset.substring(1));
+ else
+ __inferredEncodings.put(t, props.getProperty(t));
+ });
+
+ if (__inferredEncodings.isEmpty())
+ {
+ LOG.warn("Empty encodings at {}", resourceName);
+ }
+ else if ((__inferredEncodings.size() + __assumedEncodings.size()) < props.keySet().size())
+ {
+ LOG.warn("Null or duplicate encodings in resource: {}", resourceName);
+ }
+ }
+ catch (IOException e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ }
+ }
+ }
+ catch (IOException e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ }
+ }
+
+ private final Map<String, String> _mimeMap = new HashMap<String, String>();
+
+ /**
+ * Constructor.
+ */
+ public MimeTypes()
+ {
+ }
+
+ public synchronized Map<String, String> getMimeMap()
+ {
+ return _mimeMap;
+ }
+
+ /**
+ * @param mimeMap A Map of file extension to mime-type.
+ */
+ public void setMimeMap(Map<String, String> mimeMap)
+ {
+ _mimeMap.clear();
+ if (mimeMap != null)
+ {
+ for (Entry<String, String> ext : mimeMap.entrySet())
+ {
+ _mimeMap.put(StringUtil.asciiToLowerCase(ext.getKey()), normalizeMimeType(ext.getValue()));
+ }
+ }
+ }
+
+ /**
+ * Get the MIME type by filename extension.
+ * Lookup only the static default mime map.
+ *
+ * @param filename A file name
+ * @return MIME type matching the longest dot extension of the
+ * file name.
+ */
+ public static String getDefaultMimeByExtension(String filename)
+ {
+ String type = null;
+
+ if (filename != null)
+ {
+ int i = -1;
+ while (type == null)
+ {
+ i = filename.indexOf(".", i + 1);
+
+ if (i < 0 || i >= filename.length())
+ break;
+
+ String ext = StringUtil.asciiToLowerCase(filename.substring(i + 1));
+ if (type == null)
+ type = __dftMimeMap.get(ext);
+ }
+ }
+
+ if (type == null)
+ {
+ type = __dftMimeMap.get("*");
+ }
+
+ return type;
+ }
+
+ /**
+ * Get the MIME type by filename extension.
+ * Lookup the content and static default mime maps.
+ *
+ * @param filename A file name
+ * @return MIME type matching the longest dot extension of the
+ * file name.
+ */
+ public String getMimeByExtension(String filename)
+ {
+ String type = null;
+
+ if (filename != null)
+ {
+ int i = -1;
+ while (type == null)
+ {
+ i = filename.indexOf(".", i + 1);
+
+ if (i < 0 || i >= filename.length())
+ break;
+
+ String ext = StringUtil.asciiToLowerCase(filename.substring(i + 1));
+ if (_mimeMap != null)
+ type = _mimeMap.get(ext);
+ if (type == null)
+ type = __dftMimeMap.get(ext);
+ }
+ }
+
+ if (type == null)
+ {
+ if (_mimeMap != null)
+ type = _mimeMap.get("*");
+ if (type == null)
+ type = __dftMimeMap.get("*");
+ }
+
+ return type;
+ }
+
+ /**
+ * Set a mime mapping
+ *
+ * @param extension the extension
+ * @param type the mime type
+ */
+ public void addMimeMapping(String extension, String type)
+ {
+ _mimeMap.put(StringUtil.asciiToLowerCase(extension), normalizeMimeType(type));
+ }
+
+ public static Set<String> getKnownMimeTypes()
+ {
+ return new HashSet<>(__dftMimeMap.values());
+ }
+
+ private static String normalizeMimeType(String type)
+ {
+ MimeTypes.Type t = CACHE.get(type);
+ if (t != null)
+ return t.asString();
+
+ return StringUtil.asciiToLowerCase(type);
+ }
+
+ public static String getCharsetFromContentType(String value)
+ {
+ if (value == null)
+ return null;
+ int end = value.length();
+ int state = 0;
+ int start = 0;
+ boolean quote = false;
+ int i = 0;
+ for (; i < end; i++)
+ {
+ char b = value.charAt(i);
+
+ if (quote && state != 10)
+ {
+ if ('"' == b)
+ quote = false;
+ continue;
+ }
+
+ if (';' == b && state <= 8)
+ {
+ state = 1;
+ continue;
+ }
+
+ switch (state)
+ {
+ case 0:
+ if ('"' == b)
+ {
+ quote = true;
+ break;
+ }
+ break;
+
+ case 1:
+ if ('c' == b)
+ state = 2;
+ else if (' ' != b)
+ state = 0;
+ break;
+ case 2:
+ if ('h' == b)
+ state = 3;
+ else
+ state = 0;
+ break;
+ case 3:
+ if ('a' == b)
+ state = 4;
+ else
+ state = 0;
+ break;
+ case 4:
+ if ('r' == b)
+ state = 5;
+ else
+ state = 0;
+ break;
+ case 5:
+ if ('s' == b)
+ state = 6;
+ else
+ state = 0;
+ break;
+ case 6:
+ if ('e' == b)
+ state = 7;
+ else
+ state = 0;
+ break;
+ case 7:
+ if ('t' == b)
+ state = 8;
+ else
+ state = 0;
+ break;
+
+ case 8:
+ if ('=' == b)
+ state = 9;
+ else if (' ' != b)
+ state = 0;
+ break;
+
+ case 9:
+ if (' ' == b)
+ break;
+ if ('"' == b)
+ {
+ quote = true;
+ start = i + 1;
+ state = 10;
+ break;
+ }
+ start = i;
+ state = 10;
+ break;
+
+ case 10:
+ if (!quote && (';' == b || ' ' == b) ||
+ (quote && '"' == b))
+ return StringUtil.normalizeCharset(value, start, i - start);
+ }
+ }
+
+ if (state == 10)
+ return StringUtil.normalizeCharset(value, start, i - start);
+
+ return null;
+ }
+
+ /**
+ * Access a mutable map of mime type to the charset inferred from that content type.
+ * An inferred encoding is used by when encoding/decoding a stream and is
+ * explicitly set in any metadata (eg Content-Type).
+ *
+ * @return Map of mime type to charset
+ */
+ public static Map<String, String> getInferredEncodings()
+ {
+ return __inferredEncodings;
+ }
+
+ /**
+ * Access a mutable map of mime type to the charset assumed for that content type.
+ * An assumed encoding is used by when encoding/decoding a stream, but is not
+ * explicitly set in any metadata (eg Content-Type).
+ *
+ * @return Map of mime type to charset
+ */
+ public static Map<String, String> getAssumedEncodings()
+ {
+ return __assumedEncodings;
+ }
+
+ @Deprecated
+ public static String inferCharsetFromContentType(String contentType)
+ {
+ return getCharsetAssumedFromContentType(contentType);
+ }
+
+ public static String getCharsetInferredFromContentType(String contentType)
+ {
+ return __inferredEncodings.get(contentType);
+ }
+
+ public static String getCharsetAssumedFromContentType(String contentType)
+ {
+ return __assumedEncodings.get(contentType);
+ }
+
+ public static String getContentTypeWithoutCharset(String value)
+ {
+ int end = value.length();
+ int state = 0;
+ int start = 0;
+ boolean quote = false;
+ int i = 0;
+ StringBuilder builder = null;
+ for (; i < end; i++)
+ {
+ char b = value.charAt(i);
+
+ if ('"' == b)
+ {
+ quote = !quote;
+
+ switch (state)
+ {
+ case 11:
+ builder.append(b);
+ break;
+ case 10:
+ break;
+ case 9:
+ builder = new StringBuilder();
+ builder.append(value, 0, start + 1);
+ state = 10;
+ break;
+ default:
+ start = i;
+ state = 0;
+ }
+ continue;
+ }
+
+ if (quote)
+ {
+ if (builder != null && state != 10)
+ builder.append(b);
+ continue;
+ }
+
+ switch (state)
+ {
+ case 0:
+ if (';' == b)
+ state = 1;
+ else if (' ' != b)
+ start = i;
+ break;
+
+ case 1:
+ if ('c' == b)
+ state = 2;
+ else if (' ' != b)
+ state = 0;
+ break;
+ case 2:
+ if ('h' == b)
+ state = 3;
+ else
+ state = 0;
+ break;
+ case 3:
+ if ('a' == b)
+ state = 4;
+ else
+ state = 0;
+ break;
+ case 4:
+ if ('r' == b)
+ state = 5;
+ else
+ state = 0;
+ break;
+ case 5:
+ if ('s' == b)
+ state = 6;
+ else
+ state = 0;
+ break;
+ case 6:
+ if ('e' == b)
+ state = 7;
+ else
+ state = 0;
+ break;
+ case 7:
+ if ('t' == b)
+ state = 8;
+ else
+ state = 0;
+ break;
+ case 8:
+ if ('=' == b)
+ state = 9;
+ else if (' ' != b)
+ state = 0;
+ break;
+
+ case 9:
+ if (' ' == b)
+ break;
+ builder = new StringBuilder();
+ builder.append(value, 0, start + 1);
+ state = 10;
+ break;
+
+ case 10:
+ if (';' == b)
+ {
+ builder.append(b);
+ state = 11;
+ }
+ break;
+ case 11:
+ if (' ' != b)
+ builder.append(b);
+ }
+ }
+ if (builder == null)
+ return value;
+ return builder.toString();
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java
new file mode 100644
index 0000000..a92c5a1
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java
@@ -0,0 +1,828 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import javax.servlet.MultipartConfigElement;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.Part;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.ByteArrayOutputStream2;
+import org.eclipse.jetty.util.LazyList;
+import org.eclipse.jetty.util.MultiException;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.QuotedStringTokenizer;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * MultiPartInputStream
+ * <p>
+ * Handle a MultiPart Mime input stream, breaking it up on the boundary into files and strings.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7578">https://tools.ietf.org/html/rfc7578</a>
+ */
+public class MultiPartFormInputStream
+{
+ private static final Logger LOG = Log.getLogger(MultiPartFormInputStream.class);
+ private static final MultiMap<Part> EMPTY_MAP = new MultiMap<>(Collections.emptyMap());
+ private final MultiMap<Part> _parts;
+ private InputStream _in;
+ private MultipartConfigElement _config;
+ private String _contentType;
+ private Throwable _err;
+ private File _tmpDir;
+ private File _contextTmpDir;
+ private boolean _writeFilesWithFilenames;
+ private boolean _parsed;
+ private int _bufferSize = 16 * 1024;
+
+ public class MultiPart implements Part
+ {
+ protected String _name;
+ protected String _filename;
+ protected File _file;
+ protected OutputStream _out;
+ protected ByteArrayOutputStream2 _bout;
+ protected String _contentType;
+ protected MultiMap<String> _headers;
+ protected long _size = 0;
+ protected boolean _temporary = true;
+
+ public MultiPart(String name, String filename)
+ {
+ _name = name;
+ _filename = filename;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("Part{n=%s,fn=%s,ct=%s,s=%d,tmp=%b,file=%s}", _name, _filename, _contentType, _size, _temporary, _file);
+ }
+
+ protected void setContentType(String contentType)
+ {
+ _contentType = contentType;
+ }
+
+ protected void open() throws IOException
+ {
+ // We will either be writing to a file, if it has a filename on the content-disposition
+ // and otherwise a byte-array-input-stream, OR if we exceed the getFileSizeThreshold, we
+ // will need to change to write to a file.
+ if (isWriteFilesWithFilenames() && _filename != null && !_filename.trim().isEmpty())
+ {
+ createFile();
+ }
+ else
+ {
+ // Write to a buffer in memory until we discover we've exceed the
+ // MultipartConfig fileSizeThreshold
+ _out = _bout = new ByteArrayOutputStream2();
+ }
+ }
+
+ protected void close() throws IOException
+ {
+ _out.close();
+ }
+
+ protected void write(int b) throws IOException
+ {
+ if (MultiPartFormInputStream.this._config.getMaxFileSize() > 0 && _size + 1 > MultiPartFormInputStream.this._config.getMaxFileSize())
+ throw new IllegalStateException("Multipart Mime part " + _name + " exceeds max filesize");
+
+ if (MultiPartFormInputStream.this._config.getFileSizeThreshold() > 0 &&
+ _size + 1 > MultiPartFormInputStream.this._config.getFileSizeThreshold() && _file == null)
+ createFile();
+
+ _out.write(b);
+ _size++;
+ }
+
+ protected void write(byte[] bytes, int offset, int length) throws IOException
+ {
+ if (MultiPartFormInputStream.this._config.getMaxFileSize() > 0 && _size + length > MultiPartFormInputStream.this._config.getMaxFileSize())
+ throw new IllegalStateException("Multipart Mime part " + _name + " exceeds max filesize");
+
+ if (MultiPartFormInputStream.this._config.getFileSizeThreshold() > 0 &&
+ _size + length > MultiPartFormInputStream.this._config.getFileSizeThreshold() && _file == null)
+ createFile();
+
+ _out.write(bytes, offset, length);
+ _size += length;
+ }
+
+ protected void createFile() throws IOException
+ {
+ Path parent = MultiPartFormInputStream.this._tmpDir.toPath();
+ Path tempFile = Files.createTempFile(parent, "MultiPart", "");
+ _file = tempFile.toFile();
+
+ OutputStream fos = Files.newOutputStream(tempFile, StandardOpenOption.WRITE);
+ BufferedOutputStream bos = new BufferedOutputStream(fos);
+
+ if (_size > 0 && _out != null)
+ {
+ // already written some bytes, so need to copy them into the file
+ _out.flush();
+ _bout.writeTo(bos);
+ _out.close();
+ }
+ _bout = null;
+ _out = bos;
+ }
+
+ protected void setHeaders(MultiMap<String> headers)
+ {
+ _headers = headers;
+ }
+
+ @Override
+ public String getContentType()
+ {
+ return _contentType;
+ }
+
+ @Override
+ public String getHeader(String name)
+ {
+ if (name == null)
+ return null;
+ return _headers.getValue(StringUtil.asciiToLowerCase(name), 0);
+ }
+
+ @Override
+ public Collection<String> getHeaderNames()
+ {
+ return _headers.keySet();
+ }
+
+ @Override
+ public Collection<String> getHeaders(String name)
+ {
+ Collection<String> headers = _headers.getValues(name);
+ return headers == null ? Collections.emptyList() : headers;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException
+ {
+ if (_file != null)
+ {
+ // written to a file, whether temporary or not
+ return new BufferedInputStream(new FileInputStream(_file));
+ }
+ else
+ {
+ // part content is in memory
+ return new ByteArrayInputStream(_bout.getBuf(), 0, _bout.size());
+ }
+ }
+
+ @Override
+ public String getSubmittedFileName()
+ {
+ return getContentDispositionFilename();
+ }
+
+ public byte[] getBytes()
+ {
+ if (_bout != null)
+ return _bout.toByteArray();
+ return null;
+ }
+
+ @Override
+ public String getName()
+ {
+ return _name;
+ }
+
+ @Override
+ public long getSize()
+ {
+ return _size;
+ }
+
+ @Override
+ public void write(String fileName) throws IOException
+ {
+ if (_file == null)
+ {
+ _temporary = false;
+
+ // part data is only in the ByteArrayOutputStream and never been written to disk
+ _file = new File(_tmpDir, fileName);
+
+ try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(_file)))
+ {
+ _bout.writeTo(bos);
+ bos.flush();
+ }
+ finally
+ {
+ _bout = null;
+ }
+ }
+ else
+ {
+ // the part data is already written to a temporary file, just rename it
+ _temporary = false;
+
+ Path src = _file.toPath();
+ Path target = src.resolveSibling(fileName);
+ Files.move(src, target, StandardCopyOption.REPLACE_EXISTING);
+ _file = target.toFile();
+ }
+ }
+
+ /**
+ * Remove the file, whether or not Part.write() was called on it (ie no longer temporary)
+ */
+ @Override
+ public void delete() throws IOException
+ {
+ if (_file != null && _file.exists())
+ if (!_file.delete())
+ throw new IOException("Could Not Delete File");
+ }
+
+ /**
+ * Only remove tmp files.
+ *
+ * @throws IOException if unable to delete the file
+ */
+ public void cleanUp() throws IOException
+ {
+ if (_temporary)
+ delete();
+ }
+
+ /**
+ * Get the file
+ *
+ * @return the file, if any, the data has been written to.
+ */
+ public File getFile()
+ {
+ return _file;
+ }
+
+ /**
+ * Get the filename from the content-disposition.
+ *
+ * @return null or the filename
+ */
+ public String getContentDispositionFilename()
+ {
+ return _filename;
+ }
+ }
+
+ /**
+ * @param in Request input stream
+ * @param contentType Content-Type header
+ * @param config MultipartConfigElement
+ * @param contextTmpDir javax.servlet.context.tempdir
+ */
+ public MultiPartFormInputStream(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
+ {
+ _contentType = contentType;
+ _config = config;
+ _contextTmpDir = contextTmpDir;
+ if (_contextTmpDir == null)
+ _contextTmpDir = new File(System.getProperty("java.io.tmpdir"));
+
+ if (_config == null)
+ _config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
+
+ MultiMap parts = new MultiMap();
+
+ if (in instanceof ServletInputStream)
+ {
+ if (((ServletInputStream)in).isFinished())
+ {
+ parts = EMPTY_MAP;
+ _parsed = true;
+ }
+ }
+ if (!_parsed)
+ _in = new BufferedInputStream(in);
+ _parts = parts;
+ }
+
+ /**
+ * @return whether the list of parsed parts is empty
+ */
+ public boolean isEmpty()
+ {
+ if (_parts == null)
+ return true;
+
+ Collection<List<Part>> values = _parts.values();
+ for (List<Part> partList : values)
+ {
+ if (!partList.isEmpty())
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the already parsed parts.
+ *
+ * @return the parts that were parsed
+ */
+ @Deprecated
+ public Collection<Part> getParsedParts()
+ {
+ if (_parts == null)
+ return Collections.emptyList();
+
+ Collection<List<Part>> values = _parts.values();
+ List<Part> parts = new ArrayList<>();
+ for (List<Part> o : values)
+ {
+ List<Part> asList = LazyList.getList(o, false);
+ parts.addAll(asList);
+ }
+ return parts;
+ }
+
+ /**
+ * Delete any tmp storage for parts, and clear out the parts list.
+ */
+ public void deleteParts()
+ {
+ MultiException err = null;
+ for (List<Part> parts : _parts.values())
+ {
+ for (Part p : parts)
+ {
+ try
+ {
+ ((MultiPart)p).cleanUp();
+ }
+ catch (Exception e)
+ {
+ if (err == null)
+ err = new MultiException();
+ err.add(e);
+ }
+ }
+ }
+ _parts.clear();
+
+ if (err != null)
+ err.ifExceptionThrowRuntime();
+ }
+
+ /**
+ * Parse, if necessary, the multipart data and return the list of Parts.
+ *
+ * @return the parts
+ * @throws IOException if unable to get the parts
+ */
+ public Collection<Part> getParts() throws IOException
+ {
+ if (!_parsed)
+ parse();
+ throwIfError();
+
+ if (_parts.isEmpty())
+ return Collections.emptyList();
+
+ Collection<List<Part>> values = _parts.values();
+ List<Part> parts = new ArrayList<>();
+ for (List<Part> o : values)
+ {
+ List<Part> asList = LazyList.getList(o, false);
+ parts.addAll(asList);
+ }
+ return parts;
+ }
+
+ /**
+ * Get the named Part.
+ *
+ * @param name the part name
+ * @return the parts
+ * @throws IOException if unable to get the part
+ */
+ public Part getPart(String name) throws IOException
+ {
+ if (!_parsed)
+ parse();
+ throwIfError();
+ return _parts.getValue(name, 0);
+ }
+
+ /**
+ * Throws an exception if one has been latched.
+ *
+ * @throws IOException the exception (if present)
+ */
+ protected void throwIfError() throws IOException
+ {
+ if (_err != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("MultiPart parsing failure ", _err);
+
+ _err.addSuppressed(new Throwable());
+ if (_err instanceof IOException)
+ throw (IOException)_err;
+ if (_err instanceof IllegalStateException)
+ throw (IllegalStateException)_err;
+ throw new IllegalStateException(_err);
+ }
+ }
+
+ /**
+ * Parse, if necessary, the multipart stream.
+ */
+ protected void parse()
+ {
+ // have we already parsed the input?
+ if (_parsed)
+ return;
+ _parsed = true;
+
+ MultiPartParser parser = null;
+ Handler handler = new Handler();
+ try
+ {
+ // if its not a multipart request, don't parse it
+ if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
+ return;
+
+ // sort out the location to which to write the files
+ if (_config.getLocation() == null)
+ _tmpDir = _contextTmpDir;
+ else if ("".equals(_config.getLocation()))
+ _tmpDir = _contextTmpDir;
+ else
+ {
+ File f = new File(_config.getLocation());
+ if (f.isAbsolute())
+ _tmpDir = f;
+ else
+ _tmpDir = new File(_contextTmpDir, _config.getLocation());
+ }
+
+ if (!_tmpDir.exists())
+ _tmpDir.mkdirs();
+
+ String contentTypeBoundary = "";
+ int bstart = _contentType.indexOf("boundary=");
+ if (bstart >= 0)
+ {
+ int bend = _contentType.indexOf(";", bstart);
+ bend = (bend < 0 ? _contentType.length() : bend);
+ contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart, bend)).trim());
+ }
+
+ parser = new MultiPartParser(handler, contentTypeBoundary);
+ byte[] data = new byte[_bufferSize];
+ int len;
+ long total = 0;
+
+ while (true)
+ {
+
+ len = _in.read(data);
+
+ if (len > 0)
+ {
+ // keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize
+ total += len;
+ if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
+ {
+ _err = new IllegalStateException("Request exceeds maxRequestSize (" + _config.getMaxRequestSize() + ")");
+ return;
+ }
+
+ ByteBuffer buffer = BufferUtil.toBuffer(data);
+ buffer.limit(len);
+ if (parser.parse(buffer, false))
+ break;
+
+ if (buffer.hasRemaining())
+ throw new IllegalStateException("Buffer did not fully consume");
+ }
+ else if (len == -1)
+ {
+ parser.parse(BufferUtil.EMPTY_BUFFER, true);
+ break;
+ }
+ }
+
+ // check for exceptions
+ if (_err != null)
+ {
+ return;
+ }
+
+ // check we read to the end of the message
+ if (parser.getState() != MultiPartParser.State.END)
+ {
+ if (parser.getState() == MultiPartParser.State.PREAMBLE)
+ _err = new IOException("Missing initial multi part boundary");
+ else
+ _err = new IOException("Incomplete Multipart");
+ }
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Parsing Complete {} err={}", parser, _err);
+ }
+ }
+ catch (Throwable e)
+ {
+ _err = e;
+
+ // Notify parser if failure occurs
+ if (parser != null)
+ parser.parse(BufferUtil.EMPTY_BUFFER, true);
+ }
+ }
+
+ class Handler implements MultiPartParser.Handler
+ {
+ private MultiPart _part = null;
+ private String contentDisposition = null;
+ private String contentType = null;
+ private MultiMap<String> headers = new MultiMap<>();
+
+ @Override
+ public boolean messageComplete()
+ {
+ return true;
+ }
+
+ @Override
+ public void parsedField(String key, String value)
+ {
+ // Add to headers and mark if one of these fields. //
+ headers.put(StringUtil.asciiToLowerCase(key), value);
+ if (key.equalsIgnoreCase("content-disposition"))
+ contentDisposition = value;
+ else if (key.equalsIgnoreCase("content-type"))
+ contentType = value;
+
+ // Transfer encoding is not longer considers as it is deprecated as per
+ // https://tools.ietf.org/html/rfc7578#section-4.7
+
+ }
+
+ @Override
+ public boolean headerComplete()
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("headerComplete {}", this);
+ }
+
+ try
+ {
+ // Extract content-disposition
+ boolean formData = false;
+ if (contentDisposition == null)
+ {
+ throw new IOException("Missing content-disposition");
+ }
+
+ QuotedStringTokenizer tok = new QuotedStringTokenizer(contentDisposition, ";", false, true);
+ String name = null;
+ String filename = null;
+ while (tok.hasMoreTokens())
+ {
+ String t = tok.nextToken().trim();
+ String tl = StringUtil.asciiToLowerCase(t);
+ if (tl.startsWith("form-data"))
+ formData = true;
+ else if (tl.startsWith("name="))
+ name = value(t);
+ else if (tl.startsWith("filename="))
+ filename = filenameValue(t);
+ }
+
+ // Check disposition
+ if (!formData)
+ throw new IOException("Part not form-data");
+
+ // It is valid for reset and submit buttons to have an empty name.
+ // If no name is supplied, the browser skips sending the info for that field.
+ // However, if you supply the empty string as the name, the browser sends the
+ // field, with name as the empty string. So, only continue this loop if we
+ // have not yet seen a name field.
+ if (name == null)
+ throw new IOException("No name in part");
+
+ // create the new part
+ _part = new MultiPart(name, filename);
+ _part.setHeaders(headers);
+ _part.setContentType(contentType);
+ _parts.add(name, _part);
+
+ try
+ {
+ _part.open();
+ }
+ catch (IOException e)
+ {
+ _err = e;
+ return true;
+ }
+ }
+ catch (Exception e)
+ {
+ _err = e;
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean content(ByteBuffer buffer, boolean last)
+ {
+ if (_part == null)
+ return false;
+
+ if (BufferUtil.hasContent(buffer))
+ {
+ try
+ {
+ _part.write(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
+ }
+ catch (IOException e)
+ {
+ _err = e;
+ return true;
+ }
+ }
+
+ if (last)
+ {
+ try
+ {
+ _part.close();
+ }
+ catch (IOException e)
+ {
+ _err = e;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void startPart()
+ {
+ reset();
+ }
+
+ @Override
+ public void earlyEOF()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Early EOF {}", MultiPartFormInputStream.this);
+
+ try
+ {
+ if (_part != null)
+ _part.close();
+ }
+ catch (IOException e)
+ {
+ LOG.warn("part could not be closed", e);
+ }
+ }
+
+ public void reset()
+ {
+ _part = null;
+ contentDisposition = null;
+ contentType = null;
+ headers = new MultiMap<>();
+ }
+ }
+
+ /**
+ * @deprecated no replacement provided.
+ */
+ @Deprecated
+ public void setDeleteOnExit(boolean deleteOnExit)
+ {
+ // does nothing.
+ }
+
+ public void setWriteFilesWithFilenames(boolean writeFilesWithFilenames)
+ {
+ _writeFilesWithFilenames = writeFilesWithFilenames;
+ }
+
+ public boolean isWriteFilesWithFilenames()
+ {
+ return _writeFilesWithFilenames;
+ }
+
+ /**
+ * @deprecated no replacement provided
+ */
+ @Deprecated
+ public boolean isDeleteOnExit()
+ {
+ return false;
+ }
+
+ private static String value(String nameEqualsValue)
+ {
+ int idx = nameEqualsValue.indexOf('=');
+ String value = nameEqualsValue.substring(idx + 1).trim();
+ return QuotedStringTokenizer.unquoteOnly(value);
+ }
+
+ private static String filenameValue(String nameEqualsValue)
+ {
+ int idx = nameEqualsValue.indexOf('=');
+ String value = nameEqualsValue.substring(idx + 1).trim();
+
+ if (value.matches(".??[a-z,A-Z]\\:\\\\[^\\\\].*"))
+ {
+ // incorrectly escaped IE filenames that have the whole path
+ // we just strip any leading & trailing quotes and leave it as is
+ char first = value.charAt(0);
+ if (first == '"' || first == '\'')
+ value = value.substring(1);
+ char last = value.charAt(value.length() - 1);
+ if (last == '"' || last == '\'')
+ value = value.substring(0, value.length() - 1);
+
+ return value;
+ }
+ else
+ // unquote the string, but allow any backslashes that don't
+ // form a valid escape sequence to remain as many browsers
+ // even on *nix systems will not escape a filename containing
+ // backslashes
+ return QuotedStringTokenizer.unquoteOnly(value, true);
+ }
+
+ /**
+ * @return the size of buffer used to read data from the input stream
+ */
+ public int getBufferSize()
+ {
+ return _bufferSize;
+ }
+
+ /**
+ * @param bufferSize the size of buffer used to read data from the input stream
+ */
+ public void setBufferSize(int bufferSize)
+ {
+ _bufferSize = bufferSize;
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartParser.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartParser.java
new file mode 100644
index 0000000..20c2c40
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartParser.java
@@ -0,0 +1,715 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.EnumSet;
+
+import org.eclipse.jetty.http.HttpParser.RequestHandler;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.SearchPattern;
+import org.eclipse.jetty.util.Utf8StringBuilder;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * A parser for MultiPart content type.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc2046#section-5.1">https://tools.ietf.org/html/rfc2046#section-5.1</a>
+ * @see <a href="https://tools.ietf.org/html/rfc2045">https://tools.ietf.org/html/rfc2045</a>
+ */
+public class MultiPartParser
+{
+ public static final Logger LOG = Log.getLogger(MultiPartParser.class);
+
+ // States
+ public enum FieldState
+ {
+ FIELD,
+ IN_NAME,
+ AFTER_NAME,
+ VALUE,
+ IN_VALUE
+ }
+
+ // States
+ public enum State
+ {
+ PREAMBLE,
+ DELIMITER,
+ DELIMITER_PADDING,
+ DELIMITER_CLOSE,
+ BODY_PART,
+ FIRST_OCTETS,
+ OCTETS,
+ EPILOGUE,
+ END
+ }
+
+ private static final EnumSet<State> __delimiterStates = EnumSet.of(State.DELIMITER, State.DELIMITER_CLOSE, State.DELIMITER_PADDING);
+ private static final int MAX_HEADER_LINE_LENGTH = 998;
+
+ private final boolean debug = LOG.isDebugEnabled();
+ private final Handler _handler;
+ private final SearchPattern _delimiterSearch;
+
+ private String _fieldName;
+ private String _fieldValue;
+
+ private State _state = State.PREAMBLE;
+ private FieldState _fieldState = FieldState.FIELD;
+ private int _partialBoundary = 2; // No CRLF if no preamble
+ private boolean _cr;
+ private ByteBuffer _patternBuffer;
+
+ private final Utf8StringBuilder _string = new Utf8StringBuilder();
+ private int _length;
+
+ private int _totalHeaderLineLength = -1;
+
+ public MultiPartParser(Handler handler, String boundary)
+ {
+ _handler = handler;
+
+ String delimiter = "\r\n--" + boundary;
+ _patternBuffer = ByteBuffer.wrap(delimiter.getBytes(StandardCharsets.US_ASCII));
+ _delimiterSearch = SearchPattern.compile(_patternBuffer.array());
+ }
+
+ public void reset()
+ {
+ _state = State.PREAMBLE;
+ _fieldState = FieldState.FIELD;
+ _partialBoundary = 2; // No CRLF if no preamble
+ }
+
+ public Handler getHandler()
+ {
+ return _handler;
+ }
+
+ public State getState()
+ {
+ return _state;
+ }
+
+ public boolean isState(State state)
+ {
+ return _state == state;
+ }
+
+ private static boolean hasNextByte(ByteBuffer buffer)
+ {
+ return BufferUtil.hasContent(buffer);
+ }
+
+ private HttpTokens.Token next(ByteBuffer buffer)
+ {
+ byte ch = buffer.get();
+ HttpTokens.Token t = HttpTokens.TOKENS[0xff & ch];
+
+ switch (t.getType())
+ {
+ case CNTL:
+ throw new IllegalCharacterException(_state, t, buffer);
+
+ case LF:
+ _cr = false;
+ break;
+
+ case CR:
+ if (_cr)
+ throw new BadMessageException("Bad EOL");
+
+ _cr = true;
+ return null;
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ case HTAB:
+ case SPACE:
+ case OTEXT:
+ case COLON:
+ if (_cr)
+ throw new BadMessageException("Bad EOL");
+ break;
+
+ default:
+ break;
+ }
+
+ return t;
+ }
+
+ private void setString(String s)
+ {
+ _string.reset();
+ _string.append(s);
+ _length = s.length();
+ }
+
+ /*
+ * Mime Field strings are treated as UTF-8 as per https://tools.ietf.org/html/rfc7578#section-5.1
+ */
+ private String takeString()
+ {
+ String s = _string.toString();
+ // trim trailing whitespace.
+ if (s.length() > _length)
+ s = s.substring(0, _length);
+ _string.reset();
+ _length = -1;
+ return s;
+ }
+
+ /**
+ * Parse until next Event.
+ *
+ * @param buffer the buffer to parse
+ * @param last whether this buffer contains last bit of content
+ * @return True if an {@link RequestHandler} method was called and it returned true;
+ */
+ public boolean parse(ByteBuffer buffer, boolean last)
+ {
+ boolean handle = false;
+ while (!handle && BufferUtil.hasContent(buffer))
+ {
+ switch (_state)
+ {
+ case PREAMBLE:
+ parsePreamble(buffer);
+ continue;
+
+ case DELIMITER:
+ case DELIMITER_PADDING:
+ case DELIMITER_CLOSE:
+ parseDelimiter(buffer);
+ continue;
+
+ case BODY_PART:
+ handle = parseMimePartHeaders(buffer);
+ break;
+
+ case FIRST_OCTETS:
+ case OCTETS:
+ handle = parseOctetContent(buffer);
+ break;
+
+ case EPILOGUE:
+ BufferUtil.clear(buffer);
+ break;
+
+ case END:
+ handle = true;
+ break;
+
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ if (last && BufferUtil.isEmpty(buffer))
+ {
+ if (_state == State.EPILOGUE)
+ {
+ _state = State.END;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("messageComplete {}", this);
+
+ return _handler.messageComplete();
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("earlyEOF {}", this);
+
+ _handler.earlyEOF();
+ return true;
+ }
+ }
+
+ return handle;
+ }
+
+ private void parsePreamble(ByteBuffer buffer)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("parsePreamble({})", BufferUtil.toDetailString(buffer));
+
+ if (_partialBoundary > 0)
+ {
+ int partial = _delimiterSearch.startsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining(), _partialBoundary);
+ if (partial > 0)
+ {
+ if (partial == _delimiterSearch.getLength())
+ {
+ buffer.position(buffer.position() + partial - _partialBoundary);
+ _partialBoundary = 0;
+ setState(State.DELIMITER);
+ return;
+ }
+
+ _partialBoundary = partial;
+ BufferUtil.clear(buffer);
+ return;
+ }
+
+ _partialBoundary = 0;
+ }
+
+ int delimiter = _delimiterSearch.match(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
+ if (delimiter >= 0)
+ {
+ buffer.position(delimiter - buffer.arrayOffset() + _delimiterSearch.getLength());
+ setState(State.DELIMITER);
+ return;
+ }
+
+ _partialBoundary = _delimiterSearch.endsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
+ BufferUtil.clear(buffer);
+ }
+
+ private void parseDelimiter(ByteBuffer buffer)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("parseDelimiter({})", BufferUtil.toDetailString(buffer));
+
+ while (__delimiterStates.contains(_state) && hasNextByte(buffer))
+ {
+ HttpTokens.Token t = next(buffer);
+ if (t == null)
+ return;
+
+ if (t.getType() == HttpTokens.Type.LF)
+ {
+ setState(State.BODY_PART);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("startPart {}", this);
+
+ _handler.startPart();
+ return;
+ }
+
+ switch (_state)
+ {
+ case DELIMITER:
+ if (t.getChar() == '-')
+ setState(State.DELIMITER_CLOSE);
+ else
+ setState(State.DELIMITER_PADDING);
+ continue;
+
+ case DELIMITER_CLOSE:
+ if (t.getChar() == '-')
+ {
+ setState(State.EPILOGUE);
+ return;
+ }
+ setState(State.DELIMITER_PADDING);
+ continue;
+
+ case DELIMITER_PADDING:
+ default:
+ }
+ }
+ }
+
+ /*
+ * Parse the message headers and return true if the handler has signaled for a return
+ */
+ protected boolean parseMimePartHeaders(ByteBuffer buffer)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("parseMimePartHeaders({})", BufferUtil.toDetailString(buffer));
+
+ // Process headers
+ while (_state == State.BODY_PART && hasNextByte(buffer))
+ {
+ // process each character
+ HttpTokens.Token t = next(buffer);
+ if (t == null)
+ break;
+
+ if (t.getType() != HttpTokens.Type.LF)
+ _totalHeaderLineLength++;
+
+ if (_totalHeaderLineLength > MAX_HEADER_LINE_LENGTH)
+ throw new IllegalStateException("Header Line Exceeded Max Length");
+
+ switch (_fieldState)
+ {
+ case FIELD:
+ switch (t.getType())
+ {
+ case SPACE:
+ case HTAB:
+ {
+ // Folded field value!
+
+ if (_fieldName == null)
+ throw new IllegalStateException("First field folded");
+
+ if (_fieldValue == null)
+ {
+ _string.reset();
+ _length = 0;
+ }
+ else
+ {
+ setString(_fieldValue);
+ _string.append(' ');
+ _length++;
+ _fieldValue = null;
+ }
+ setState(FieldState.VALUE);
+ break;
+ }
+
+ case LF:
+ handleField();
+ setState(State.FIRST_OCTETS);
+ _partialBoundary = 2; // CRLF is option for empty parts
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("headerComplete {}", this);
+
+ if (_handler.headerComplete())
+ return true;
+ break;
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ // process previous header
+ handleField();
+
+ // New header
+ setState(FieldState.IN_NAME);
+ _string.reset();
+ _string.append(t.getChar());
+ _length = 1;
+
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case IN_NAME:
+ switch (t.getType())
+ {
+ case COLON:
+ _fieldName = takeString();
+ _length = -1;
+ setState(FieldState.VALUE);
+ break;
+
+ case SPACE:
+ // Ignore trailing whitespaces
+ setState(FieldState.AFTER_NAME);
+ break;
+
+ case LF:
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Line Feed in Name {}", this);
+
+ handleField();
+ setState(FieldState.FIELD);
+ break;
+ }
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ _string.append(t.getChar());
+ _length = _string.length();
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case AFTER_NAME:
+ switch (t.getType())
+ {
+ case COLON:
+ _fieldName = takeString();
+ _length = -1;
+ setState(FieldState.VALUE);
+ break;
+
+ case LF:
+ _fieldName = takeString();
+ _string.reset();
+ _fieldValue = "";
+ _length = -1;
+ break;
+
+ case SPACE:
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case VALUE:
+ switch (t.getType())
+ {
+ case LF:
+ _string.reset();
+ _fieldValue = "";
+ _length = -1;
+
+ setState(FieldState.FIELD);
+ break;
+
+ case SPACE:
+ case HTAB:
+ break;
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ case COLON:
+ case OTEXT:
+ _string.append(t.getByte());
+ _length = _string.length();
+ setState(FieldState.IN_VALUE);
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ case IN_VALUE:
+ switch (t.getType())
+ {
+ case SPACE:
+ case HTAB:
+ _string.append(' ');
+ break;
+
+ case LF:
+ if (_length > 0)
+ {
+ _fieldValue = takeString();
+ _length = -1;
+ _totalHeaderLineLength = -1;
+ }
+ setState(FieldState.FIELD);
+ break;
+
+ case ALPHA:
+ case DIGIT:
+ case TCHAR:
+ case VCHAR:
+ case COLON:
+ case OTEXT:
+ _string.append(t.getByte());
+ _length = _string.length();
+ break;
+
+ default:
+ throw new IllegalCharacterException(_state, t, buffer);
+ }
+ break;
+
+ default:
+ throw new IllegalStateException(_state.toString());
+ }
+ }
+ return false;
+ }
+
+ private void handleField()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("parsedField: _fieldName={} _fieldValue={} {}", _fieldName, _fieldValue, this);
+
+ if (_fieldName != null && _fieldValue != null)
+ _handler.parsedField(_fieldName, _fieldValue);
+ _fieldName = _fieldValue = null;
+ }
+
+ protected boolean parseOctetContent(ByteBuffer buffer)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("parseOctetContent({})", BufferUtil.toDetailString(buffer));
+
+ // Starts With
+ if (_partialBoundary > 0)
+ {
+ int partial = _delimiterSearch.startsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining(), _partialBoundary);
+ if (partial > 0)
+ {
+ if (partial == _delimiterSearch.getLength())
+ {
+ buffer.position(buffer.position() + _delimiterSearch.getLength() - _partialBoundary);
+ setState(State.DELIMITER);
+ _partialBoundary = 0;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(BufferUtil.EMPTY_BUFFER), true, this);
+
+ return _handler.content(BufferUtil.EMPTY_BUFFER, true);
+ }
+
+ _partialBoundary = partial;
+ BufferUtil.clear(buffer);
+ return false;
+ }
+ else
+ {
+ // output up to _partialBoundary of the search pattern
+ ByteBuffer content = _patternBuffer.slice();
+ if (_state == State.FIRST_OCTETS)
+ {
+ setState(State.OCTETS);
+ content.position(2);
+ }
+ content.limit(_partialBoundary);
+ _partialBoundary = 0;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), false, this);
+
+ if (_handler.content(content, false))
+ return true;
+ }
+ }
+
+ // Contains
+ int delimiter = _delimiterSearch.match(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
+ if (delimiter >= 0)
+ {
+ ByteBuffer content = buffer.slice();
+ content.limit(delimiter - buffer.arrayOffset() - buffer.position());
+
+ buffer.position(delimiter - buffer.arrayOffset() + _delimiterSearch.getLength());
+ setState(State.DELIMITER);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), true, this);
+
+ return _handler.content(content, true);
+ }
+
+ // Ends With
+ _partialBoundary = _delimiterSearch.endsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
+ if (_partialBoundary > 0)
+ {
+ ByteBuffer content = buffer.slice();
+ content.limit(content.limit() - _partialBoundary);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), false, this);
+
+ BufferUtil.clear(buffer);
+ return _handler.content(content, false);
+ }
+
+ // There is normal content with no delimiter
+ ByteBuffer content = buffer.slice();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), false, this);
+
+ BufferUtil.clear(buffer);
+ return _handler.content(content, false);
+ }
+
+ private void setState(State state)
+ {
+ if (debug)
+ LOG.debug("{} --> {}", _state, state);
+ _state = state;
+ }
+
+ private void setState(FieldState state)
+ {
+ if (debug)
+ LOG.debug("{}:{} --> {}", _state, _fieldState, state);
+ _fieldState = state;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s{s=%s}", getClass().getSimpleName(), _state);
+ }
+
+ /*
+ * Event Handler interface These methods return true if the caller should process the events so far received (eg return from parseNext and call
+ * HttpChannel.handle). If multiple callbacks are called in sequence (eg headerComplete then messageComplete) from the same point in the parsing then it is
+ * sufficient for the caller to process the events only once.
+ */
+ public interface Handler
+ {
+ default void startPart()
+ {
+ }
+
+ @SuppressWarnings("unused")
+ default void parsedField(String name, String value)
+ {
+ }
+
+ default boolean headerComplete()
+ {
+ return false;
+ }
+
+ @SuppressWarnings("unused")
+ default boolean content(ByteBuffer item, boolean last)
+ {
+ return false;
+ }
+
+ default boolean messageComplete()
+ {
+ return false;
+ }
+
+ default void earlyEOF()
+ {
+ }
+ }
+
+ @SuppressWarnings("serial")
+ private static class IllegalCharacterException extends BadMessageException
+ {
+ private IllegalCharacterException(State state, HttpTokens.Token token, ByteBuffer buffer)
+ {
+ super(400, String.format("Illegal character %s", token));
+ if (LOG.isDebugEnabled())
+ LOG.debug(String.format("Illegal character %s in state=%s for buffer %s", token, state, BufferUtil.toDetailString(buffer)));
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/PathMap.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/PathMap.java
new file mode 100644
index 0000000..440e948
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/PathMap.java
@@ -0,0 +1,634 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.AbstractSet;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.function.Predicate;
+
+import org.eclipse.jetty.util.ArrayTernaryTrie;
+import org.eclipse.jetty.util.Trie;
+import org.eclipse.jetty.util.URIUtil;
+
+/**
+ * URI path map to Object.
+ * <p>
+ * This mapping implements the path specification recommended
+ * in the 2.2 Servlet API.
+ * </p>
+ *
+ * <p>
+ * Path specifications can be of the following forms:
+ * </p>
+ * <pre>
+ * /foo/bar - an exact path specification.
+ * /foo/* - a prefix path specification (must end '/*').
+ * *.ext - a suffix path specification.
+ * / - the default path specification.
+ * "" - the / path specification
+ * </pre>
+ *
+ * Matching is performed in the following order
+ * <ol>
+ * <li>Exact match.</li>
+ * <li>Longest prefix match.</li>
+ * <li>Longest suffix match.</li>
+ * <li>default.</li>
+ * </ol>
+ *
+ * <p>
+ * Multiple path specifications can be mapped by providing a list of
+ * specifications. By default this class uses characters ":," as path
+ * separators, unless configured differently by calling the static
+ * method @see PathMap#setPathSpecSeparators(String)
+ * <p>
+ * Special characters within paths such as '?� and ';' are not treated specially
+ * as it is assumed they would have been either encoded in the original URL or
+ * stripped from the path.
+ * <p>
+ * This class is not synchronized. If concurrent modifications are
+ * possible then it should be synchronized at a higher level.
+ *
+ * @param <O> the Map.Entry value type
+ * @deprecated replaced with {@link org.eclipse.jetty.http.pathmap.PathMappings} (this class will be removed in Jetty 10)
+ */
+@Deprecated
+public class PathMap<O> extends HashMap<String, O>
+{
+
+ private static String __pathSpecSeparators = ":,";
+
+ /**
+ * Set the path spec separator.
+ * Multiple path specification may be included in a single string
+ * if they are separated by the characters set in this string.
+ * By default this class uses ":," characters as path separators.
+ *
+ * @param s separators
+ */
+ public static void setPathSpecSeparators(String s)
+ {
+ __pathSpecSeparators = s;
+ }
+
+ Trie<MappedEntry<O>> _prefixMap = new ArrayTernaryTrie<>(false);
+ Trie<MappedEntry<O>> _suffixMap = new ArrayTernaryTrie<>(false);
+ final Map<String, MappedEntry<O>> _exactMap = new HashMap<>();
+
+ List<MappedEntry<O>> _defaultSingletonList = null;
+ MappedEntry<O> _prefixDefault = null;
+ MappedEntry<O> _default = null;
+ boolean _nodefault = false;
+
+ public PathMap()
+ {
+ this(11);
+ }
+
+ public PathMap(boolean noDefault)
+ {
+ this(11, noDefault);
+ }
+
+ public PathMap(int capacity)
+ {
+ this(capacity, false);
+ }
+
+ private PathMap(int capacity, boolean noDefault)
+ {
+ super(capacity);
+ _nodefault = noDefault;
+ }
+
+ /**
+ * Construct from dictionary PathMap.
+ *
+ * @param dictMap the map representing the dictionary to build this PathMap from
+ */
+ public PathMap(Map<String, ? extends O> dictMap)
+ {
+ putAll(dictMap);
+ }
+
+ /**
+ * Add a single path match to the PathMap.
+ *
+ * @param pathSpec The path specification, or comma separated list of
+ * path specifications.
+ * @param object The object the path maps to
+ */
+ @Override
+ public O put(String pathSpec, O object)
+ {
+ if ("".equals(pathSpec.trim()))
+ {
+ MappedEntry<O> entry = new MappedEntry<>("", object);
+ entry.setMapped("");
+ _exactMap.put("", entry);
+ return super.put("", object);
+ }
+
+ StringTokenizer tok = new StringTokenizer(pathSpec, __pathSpecSeparators);
+ O old = null;
+
+ while (tok.hasMoreTokens())
+ {
+ String spec = tok.nextToken();
+
+ if (!spec.startsWith("/") && !spec.startsWith("*."))
+ throw new IllegalArgumentException("PathSpec " + spec + ". must start with '/' or '*.'");
+
+ old = super.put(spec, object);
+
+ // Make entry that was just created.
+ MappedEntry<O> entry = new MappedEntry<>(spec, object);
+
+ if (entry.getKey().equals(spec))
+ {
+ if (spec.equals("/*"))
+ _prefixDefault = entry;
+ else if (spec.endsWith("/*"))
+ {
+ String mapped = spec.substring(0, spec.length() - 2);
+ entry.setMapped(mapped);
+ while (!_prefixMap.put(mapped, entry))
+ {
+ _prefixMap = new ArrayTernaryTrie<>((ArrayTernaryTrie<MappedEntry<O>>)_prefixMap, 1.5);
+ }
+ }
+ else if (spec.startsWith("*."))
+ {
+ String suffix = spec.substring(2);
+ while (!_suffixMap.put(suffix, entry))
+ {
+ _suffixMap = new ArrayTernaryTrie<>((ArrayTernaryTrie<MappedEntry<O>>)_suffixMap, 1.5);
+ }
+ }
+ else if (spec.equals(URIUtil.SLASH))
+ {
+ if (_nodefault)
+ _exactMap.put(spec, entry);
+ else
+ {
+ _default = entry;
+ _defaultSingletonList = Collections.singletonList(_default);
+ }
+ }
+ else
+ {
+ entry.setMapped(spec);
+ _exactMap.put(spec, entry);
+ }
+ }
+ }
+
+ return old;
+ }
+
+ /**
+ * Get object matched by the path.
+ *
+ * @param path the path.
+ * @return Best matched object or null.
+ */
+ public O match(String path)
+ {
+ MappedEntry<O> entry = getMatch(path);
+ if (entry != null)
+ return entry.getValue();
+ return null;
+ }
+
+ /**
+ * Get the entry mapped by the best specification.
+ *
+ * @param path the path.
+ * @return Map.Entry of the best matched or null.
+ */
+ public MappedEntry<O> getMatch(String path)
+ {
+ if (path == null)
+ return null;
+
+ int l = path.length();
+
+ MappedEntry<O> entry = null;
+
+ //special case
+ if (l == 1 && path.charAt(0) == '/')
+ {
+ entry = _exactMap.get("");
+ if (entry != null)
+ return entry;
+ }
+
+ // try exact match
+ entry = _exactMap.get(path);
+ if (entry != null)
+ return entry;
+
+ // prefix search
+ int i = l;
+ final Trie<PathMap.MappedEntry<O>> prefix_map = _prefixMap;
+ while (i >= 0)
+ {
+ entry = prefix_map.getBest(path, 0, i);
+ if (entry == null)
+ break;
+ String key = entry.getKey();
+ if (key.length() - 2 >= path.length() || path.charAt(key.length() - 2) == '/')
+ return entry;
+ i = key.length() - 3;
+ }
+
+ // Prefix Default
+ if (_prefixDefault != null)
+ return _prefixDefault;
+
+ // Extension search
+ i = 0;
+ final Trie<PathMap.MappedEntry<O>> suffix_map = _suffixMap;
+ while ((i = path.indexOf('.', i + 1)) > 0)
+ {
+ entry = suffix_map.get(path, i + 1, l - i - 1);
+ if (entry != null)
+ return entry;
+ }
+
+ // Default
+ return _default;
+ }
+
+ /**
+ * Get all entries matched by the path.
+ * Best match first.
+ *
+ * @param path Path to match
+ * @return List of Map.Entry instances key=pathSpec
+ */
+ public List<? extends Map.Entry<String, O>> getMatches(String path)
+ {
+ MappedEntry<O> entry;
+ List<MappedEntry<O>> entries = new ArrayList<>();
+
+ if (path == null)
+ return entries;
+ if (path.isEmpty())
+ return _defaultSingletonList;
+
+ // try exact match
+ entry = _exactMap.get(path);
+ if (entry != null)
+ entries.add(entry);
+
+ // prefix search
+ int l = path.length();
+ int i = l;
+ final Trie<PathMap.MappedEntry<O>> prefix_map = _prefixMap;
+ while (i >= 0)
+ {
+ entry = prefix_map.getBest(path, 0, i);
+ if (entry == null)
+ break;
+ String key = entry.getKey();
+ if (key.length() - 2 >= path.length() || path.charAt(key.length() - 2) == '/')
+ entries.add(entry);
+
+ i = key.length() - 3;
+ }
+
+ // Prefix Default
+ if (_prefixDefault != null)
+ entries.add(_prefixDefault);
+
+ // Extension search
+ i = 0;
+ final Trie<PathMap.MappedEntry<O>> suffix_map = _suffixMap;
+ while ((i = path.indexOf('.', i + 1)) > 0)
+ {
+ entry = suffix_map.get(path, i + 1, l - i - 1);
+ if (entry != null)
+ entries.add(entry);
+ }
+
+ // root match
+ if ("/".equals(path))
+ {
+ entry = _exactMap.get("");
+ if (entry != null)
+ entries.add(entry);
+ }
+
+ // Default
+ if (_default != null)
+ entries.add(_default);
+
+ return entries;
+ }
+
+ /**
+ * Return whether the path matches any entries in the PathMap,
+ * excluding the default entry
+ *
+ * @param path Path to match
+ * @return Whether the PathMap contains any entries that match this
+ */
+ public boolean containsMatch(String path)
+ {
+ MappedEntry<?> match = getMatch(path);
+ return match != null && !match.equals(_default);
+ }
+
+ @Override
+ public O remove(Object pathSpec)
+ {
+ if (pathSpec != null)
+ {
+ String spec = (String)pathSpec;
+ if (spec.equals("/*"))
+ _prefixDefault = null;
+ else if (spec.endsWith("/*"))
+ _prefixMap.remove(spec.substring(0, spec.length() - 2));
+ else if (spec.startsWith("*."))
+ _suffixMap.remove(spec.substring(2));
+ else if (spec.equals(URIUtil.SLASH))
+ {
+ _default = null;
+ _defaultSingletonList = null;
+ }
+ else
+ _exactMap.remove(spec);
+ }
+ return super.remove(pathSpec);
+ }
+
+ @Override
+ public void clear()
+ {
+ _exactMap.clear();
+ _prefixMap = new ArrayTernaryTrie<>(false);
+ _suffixMap = new ArrayTernaryTrie<>(false);
+ _default = null;
+ _defaultSingletonList = null;
+ _prefixDefault = null;
+ super.clear();
+ }
+
+ /**
+ * @param pathSpec the path spec
+ * @param path the path
+ * @return true if match.
+ */
+ public static boolean match(String pathSpec, String path)
+ {
+ return match(pathSpec, path, false);
+ }
+
+ /**
+ * @param pathSpec the path spec
+ * @param path the path
+ * @param noDefault true to not handle the default path "/" special, false to allow matcher rules to run
+ * @return true if match.
+ */
+ public static boolean match(String pathSpec, String path, boolean noDefault)
+ {
+ if (pathSpec.isEmpty())
+ return "/".equals(path);
+
+ char c = pathSpec.charAt(0);
+ if (c == '/')
+ {
+ if (!noDefault && pathSpec.length() == 1 || pathSpec.equals(path))
+ return true;
+
+ return isPathWildcardMatch(pathSpec, path);
+ }
+ else if (c == '*')
+ return path.regionMatches(path.length() - pathSpec.length() + 1,
+ pathSpec, 1, pathSpec.length() - 1);
+ return false;
+ }
+
+ private static boolean isPathWildcardMatch(String pathSpec, String path)
+ {
+ // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
+ int cpl = pathSpec.length() - 2;
+ if (pathSpec.endsWith("/*") && path.regionMatches(0, pathSpec, 0, cpl))
+ {
+ return path.length() == cpl || '/' == path.charAt(cpl);
+ }
+ return false;
+ }
+
+ /**
+ * Return the portion of a path that matches a path spec.
+ *
+ * @param pathSpec the path spec
+ * @param path the path
+ * @return null if no match at all.
+ */
+ public static String pathMatch(String pathSpec, String path)
+ {
+ char c = pathSpec.charAt(0);
+
+ if (c == '/')
+ {
+ if (pathSpec.length() == 1)
+ return path;
+
+ if (pathSpec.equals(path))
+ return path;
+
+ if (isPathWildcardMatch(pathSpec, path))
+ return path.substring(0, pathSpec.length() - 2);
+ }
+ else if (c == '*')
+ {
+ if (path.regionMatches(path.length() - (pathSpec.length() - 1),
+ pathSpec, 1, pathSpec.length() - 1))
+ return path;
+ }
+ return null;
+ }
+
+ /**
+ * Return the portion of a path that is after a path spec.
+ *
+ * @param pathSpec the path spec
+ * @param path the path
+ * @return The path info string
+ */
+ public static String pathInfo(String pathSpec, String path)
+ {
+ if ("".equals(pathSpec))
+ return path; //servlet 3 spec sec 12.2 will be '/'
+
+ char c = pathSpec.charAt(0);
+
+ if (c == '/')
+ {
+ if (pathSpec.length() == 1)
+ return null;
+
+ boolean wildcard = isPathWildcardMatch(pathSpec, path);
+
+ // handle the case where pathSpec uses a wildcard and path info is "/*"
+ if (pathSpec.equals(path) && !wildcard)
+ return null;
+
+ if (wildcard)
+ {
+ if (path.length() == pathSpec.length() - 2)
+ return null;
+ return path.substring(pathSpec.length() - 2);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Relative path.
+ *
+ * @param base The base the path is relative to.
+ * @param pathSpec The spec of the path segment to ignore.
+ * @param path the additional path
+ * @return base plus path with pathspec removed
+ */
+ public static String relativePath(String base,
+ String pathSpec,
+ String path)
+ {
+ String info = pathInfo(pathSpec, path);
+ if (info == null)
+ info = path;
+
+ if (info.startsWith("./"))
+ info = info.substring(2);
+ if (base.endsWith(URIUtil.SLASH))
+ if (info.startsWith(URIUtil.SLASH))
+ path = base + info.substring(1);
+ else
+ path = base + info;
+ else if (info.startsWith(URIUtil.SLASH))
+ path = base + info;
+ else
+ path = base + URIUtil.SLASH + info;
+ return path;
+ }
+
+ public static class MappedEntry<O> implements Map.Entry<String, O>
+ {
+ private final String key;
+ private final O value;
+ private String mapped;
+
+ MappedEntry(String key, O value)
+ {
+ this.key = key;
+ this.value = value;
+ }
+
+ @Override
+ public String getKey()
+ {
+ return key;
+ }
+
+ @Override
+ public O getValue()
+ {
+ return value;
+ }
+
+ @Override
+ public O setValue(O o)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString()
+ {
+ return key + "=" + value;
+ }
+
+ public String getMapped()
+ {
+ return mapped;
+ }
+
+ void setMapped(String mapped)
+ {
+ this.mapped = mapped;
+ }
+ }
+
+ public static class PathSet extends AbstractSet<String> implements Predicate<String>
+ {
+ private final PathMap<Boolean> _map = new PathMap<>();
+
+ @Override
+ public Iterator<String> iterator()
+ {
+ return _map.keySet().iterator();
+ }
+
+ @Override
+ public int size()
+ {
+ return _map.size();
+ }
+
+ @Override
+ public boolean add(String item)
+ {
+ return _map.put(item, Boolean.TRUE) == null;
+ }
+
+ @Override
+ public boolean remove(Object item)
+ {
+ return _map.remove(item) != null;
+ }
+
+ @Override
+ public boolean contains(Object o)
+ {
+ return _map.containsKey(o);
+ }
+
+ @Override
+ public boolean test(String s)
+ {
+ return _map.containsMatch(s);
+ }
+
+ public boolean containsMatch(String s)
+ {
+ return _map.containsMatch(s);
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java
new file mode 100644
index 0000000..c676d6f
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java
@@ -0,0 +1,120 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ServiceLoader;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Pre encoded HttpField.
+ * <p>An HttpField that will be cached and used many times can be created as
+ * a {@link PreEncodedHttpField}, which will use the {@link HttpFieldPreEncoder}
+ * instances discovered by the {@link ServiceLoader} to pre-encode the header
+ * for each version of HTTP in use. This will save garbage
+ * and CPU each time the field is encoded into a response.
+ * </p>
+ */
+public class PreEncodedHttpField extends HttpField
+{
+ private static final Logger LOG = Log.getLogger(PreEncodedHttpField.class);
+ private static final HttpFieldPreEncoder[] __encoders;
+
+ static
+ {
+ List<HttpFieldPreEncoder> encoders = new ArrayList<>();
+ Iterator<HttpFieldPreEncoder> iter = ServiceLoader.load(HttpFieldPreEncoder.class).iterator();
+ while (iter.hasNext())
+ {
+ try
+ {
+ HttpFieldPreEncoder encoder = iter.next();
+ if (index(encoder.getHttpVersion()) >= 0)
+ encoders.add(encoder);
+ }
+ catch (Error | RuntimeException e)
+ {
+ LOG.debug(e);
+ }
+ }
+ LOG.debug("HttpField encoders loaded: {}", encoders);
+ int size = encoders.size();
+
+ __encoders = new HttpFieldPreEncoder[size == 0 ? 1 : size];
+ for (HttpFieldPreEncoder e : encoders)
+ {
+ int i = index(e.getHttpVersion());
+ if (__encoders[i] == null)
+ __encoders[i] = e;
+ else
+ LOG.warn("multiple PreEncoders for " + e.getHttpVersion());
+ }
+
+ // Always support HTTP1
+ if (__encoders[0] == null)
+ __encoders[0] = new Http1FieldPreEncoder();
+ }
+
+ private static int index(HttpVersion version)
+ {
+ switch (version)
+ {
+ case HTTP_1_0:
+ case HTTP_1_1:
+ return 0;
+
+ case HTTP_2:
+ return 1;
+
+ default:
+ return -1;
+ }
+ }
+
+ private final byte[][] _encodedField = new byte[__encoders.length][];
+
+ public PreEncodedHttpField(HttpHeader header, String name, String value)
+ {
+ super(header, name, value);
+ for (int i = 0; i < __encoders.length; i++)
+ {
+ _encodedField[i] = __encoders[i].getEncodedField(header, name, value);
+ }
+ }
+
+ public PreEncodedHttpField(HttpHeader header, String value)
+ {
+ this(header, header.asString(), value);
+ }
+
+ public PreEncodedHttpField(String name, String value)
+ {
+ this(null, name, value);
+ }
+
+ public void putTo(ByteBuffer bufferInFillMode, HttpVersion version)
+ {
+ bufferInFillMode.put(_encodedField[index(version)]);
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/PrecompressedHttpContent.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/PrecompressedHttpContent.java
new file mode 100644
index 0000000..9820493
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/PrecompressedHttpContent.java
@@ -0,0 +1,183 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.util.Map;
+
+import org.eclipse.jetty.http.MimeTypes.Type;
+import org.eclipse.jetty.util.resource.Resource;
+
+public class PrecompressedHttpContent implements HttpContent
+{
+ private final HttpContent _content;
+ private final HttpContent _precompressedContent;
+ private final CompressedContentFormat _format;
+
+ public PrecompressedHttpContent(HttpContent content, HttpContent precompressedContent, CompressedContentFormat format)
+ {
+ _content = content;
+ _precompressedContent = precompressedContent;
+ _format = format;
+ if (_precompressedContent == null || _format == null)
+ {
+ throw new NullPointerException("Missing compressed content and/or format");
+ }
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return _content.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ return _content.equals(obj);
+ }
+
+ @Override
+ public Resource getResource()
+ {
+ return _content.getResource();
+ }
+
+ @Override
+ public HttpField getETag()
+ {
+ return new HttpField(HttpHeader.ETAG, getETagValue());
+ }
+
+ @Override
+ public String getETagValue()
+ {
+ return _content.getResource().getWeakETag(_format.getEtagSuffix());
+ }
+
+ @Override
+ public HttpField getLastModified()
+ {
+ return _content.getLastModified();
+ }
+
+ @Override
+ public String getLastModifiedValue()
+ {
+ return _content.getLastModifiedValue();
+ }
+
+ @Override
+ public HttpField getContentType()
+ {
+ return _content.getContentType();
+ }
+
+ @Override
+ public String getContentTypeValue()
+ {
+ return _content.getContentTypeValue();
+ }
+
+ @Override
+ public HttpField getContentEncoding()
+ {
+ return _format.getContentEncoding();
+ }
+
+ @Override
+ public String getContentEncodingValue()
+ {
+ return _format.getContentEncoding().getValue();
+ }
+
+ @Override
+ public String getCharacterEncoding()
+ {
+ return _content.getCharacterEncoding();
+ }
+
+ @Override
+ public Type getMimeType()
+ {
+ return _content.getMimeType();
+ }
+
+ @Override
+ public void release()
+ {
+ _content.release();
+ }
+
+ @Override
+ public ByteBuffer getIndirectBuffer()
+ {
+ return _precompressedContent.getIndirectBuffer();
+ }
+
+ @Override
+ public ByteBuffer getDirectBuffer()
+ {
+ return _precompressedContent.getDirectBuffer();
+ }
+
+ @Override
+ public HttpField getContentLength()
+ {
+ return _precompressedContent.getContentLength();
+ }
+
+ @Override
+ public long getContentLengthValue()
+ {
+ return _precompressedContent.getContentLengthValue();
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException
+ {
+ return _precompressedContent.getInputStream();
+ }
+
+ @Override
+ public ReadableByteChannel getReadableByteChannel() throws IOException
+ {
+ return _precompressedContent.getReadableByteChannel();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{e=%s,r=%s|%s,lm=%s|%s,ct=%s}",
+ this.getClass().getSimpleName(), hashCode(),
+ _format,
+ _content.getResource(), _precompressedContent.getResource(),
+ _content.getResource().lastModified(), _precompressedContent.getResource().lastModified(),
+ getContentType());
+ }
+
+ @Override
+ public Map<CompressedContentFormat, HttpContent> getPrecompressedContents()
+ {
+ return null;
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java
new file mode 100644
index 0000000..a356213
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java
@@ -0,0 +1,88 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Implements a quoted comma separated list of values
+ * in accordance with RFC7230.
+ * OWS is removed and quoted characters ignored for parsing.
+ *
+ * @see "https://tools.ietf.org/html/rfc7230#section-3.2.6"
+ * @see "https://tools.ietf.org/html/rfc7230#section-7"
+ */
+public class QuotedCSV extends QuotedCSVParser implements Iterable<String>
+{
+ protected final List<String> _values = new ArrayList<>();
+
+ public QuotedCSV(String... values)
+ {
+ this(true, values);
+ }
+
+ public QuotedCSV(boolean keepQuotes, String... values)
+ {
+ super(keepQuotes);
+ for (String v : values)
+ {
+ addValue(v);
+ }
+ }
+
+ @Override
+ protected void parsedValueAndParams(StringBuffer buffer)
+ {
+ _values.add(buffer.toString());
+ }
+
+ public int size()
+ {
+ return _values.size();
+ }
+
+ public boolean isEmpty()
+ {
+ return _values.isEmpty();
+ }
+
+ public List<String> getValues()
+ {
+ return _values;
+ }
+
+ @Override
+ public Iterator<String> iterator()
+ {
+ return _values.iterator();
+ }
+
+ @Override
+ public String toString()
+ {
+ List<String> list = new ArrayList<>();
+ for (String s : this)
+ {
+ list.add(s);
+ }
+ return list.toString();
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSVParser.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSVParser.java
new file mode 100644
index 0000000..7aefcf7
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSVParser.java
@@ -0,0 +1,303 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+/**
+ * Implements a quoted comma separated list parser
+ * in accordance with RFC7230.
+ * OWS is removed and quoted characters ignored for parsing.
+ *
+ * @see "https://tools.ietf.org/html/rfc7230#section-3.2.6"
+ * @see "https://tools.ietf.org/html/rfc7230#section-7"
+ */
+public abstract class QuotedCSVParser
+{
+ private enum State
+ {
+ VALUE, PARAM_NAME, PARAM_VALUE
+ }
+
+ protected final boolean _keepQuotes;
+
+ public QuotedCSVParser(boolean keepQuotes)
+ {
+ _keepQuotes = keepQuotes;
+ }
+
+ public static String unquote(String s)
+ {
+ // handle trivial cases
+ int l = s.length();
+ if (s == null || l == 0)
+ return s;
+
+ // Look for any quotes
+ int i = 0;
+ for (; i < l; i++)
+ {
+ char c = s.charAt(i);
+ if (c == '"')
+ break;
+ }
+ if (i == l)
+ return s;
+
+ boolean quoted = true;
+ boolean sloshed = false;
+ StringBuffer buffer = new StringBuffer();
+ buffer.append(s, 0, i);
+ i++;
+ for (; i < l; i++)
+ {
+ char c = s.charAt(i);
+ if (quoted)
+ {
+ if (sloshed)
+ {
+ buffer.append(c);
+ sloshed = false;
+ }
+ else if (c == '"')
+ quoted = false;
+ else if (c == '\\')
+ sloshed = true;
+ else
+ buffer.append(c);
+ }
+ else if (c == '"')
+ quoted = true;
+ else
+ buffer.append(c);
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * Add and parse a value string(s)
+ *
+ * @param value A value that may contain one or more Quoted CSV items.
+ */
+ public void addValue(String value)
+ {
+ if (value == null)
+ return;
+
+ StringBuffer buffer = new StringBuffer();
+
+ int l = value.length();
+ State state = State.VALUE;
+ boolean quoted = false;
+ boolean sloshed = false;
+ int nwsLength = 0;
+ int lastLength = 0;
+ int valueLength = -1;
+ int paramName = -1;
+ int paramValue = -1;
+
+ for (int i = 0; i <= l; i++)
+ {
+ char c = i == l ? 0 : value.charAt(i);
+
+ // Handle quoting https://tools.ietf.org/html/rfc7230#section-3.2.6
+ if (quoted && c != 0)
+ {
+ if (sloshed)
+ sloshed = false;
+ else
+ {
+ switch (c)
+ {
+ case '\\':
+ sloshed = true;
+ if (!_keepQuotes)
+ continue;
+ break;
+ case '"':
+ quoted = false;
+ if (!_keepQuotes)
+ continue;
+ break;
+ }
+ }
+
+ buffer.append(c);
+ nwsLength = buffer.length();
+ continue;
+ }
+
+ // Handle common cases
+ switch (c)
+ {
+ case ' ':
+ case '\t':
+ if (buffer.length() > lastLength) // not leading OWS
+ buffer.append(c);
+ continue;
+
+ case '"':
+ quoted = true;
+ if (_keepQuotes)
+ {
+ if (state == State.PARAM_VALUE && paramValue < 0)
+ paramValue = nwsLength;
+ buffer.append(c);
+ }
+ else if (state == State.PARAM_VALUE && paramValue < 0)
+ paramValue = nwsLength;
+ nwsLength = buffer.length();
+ continue;
+
+ case ';':
+ buffer.setLength(nwsLength); // trim following OWS
+ if (state == State.VALUE)
+ {
+ parsedValue(buffer);
+ valueLength = buffer.length();
+ }
+ else
+ parsedParam(buffer, valueLength, paramName, paramValue);
+ nwsLength = buffer.length();
+ paramName = paramValue = -1;
+ buffer.append(c);
+ lastLength = ++nwsLength;
+ state = State.PARAM_NAME;
+ continue;
+
+ case ',':
+ case 0:
+ if (nwsLength > 0)
+ {
+ buffer.setLength(nwsLength); // trim following OWS
+ switch (state)
+ {
+ case VALUE:
+ parsedValue(buffer);
+ valueLength = buffer.length();
+ break;
+ case PARAM_NAME:
+ case PARAM_VALUE:
+ parsedParam(buffer, valueLength, paramName, paramValue);
+ break;
+ }
+ parsedValueAndParams(buffer);
+ }
+ buffer.setLength(0);
+ lastLength = 0;
+ nwsLength = 0;
+ valueLength = paramName = paramValue = -1;
+ state = State.VALUE;
+ continue;
+
+ case '=':
+ switch (state)
+ {
+ case VALUE:
+ // It wasn't really a value, it was a param name
+ valueLength = paramName = 0;
+ buffer.setLength(nwsLength); // trim following OWS
+ String param = buffer.toString();
+ buffer.setLength(0);
+ parsedValue(buffer);
+ valueLength = buffer.length();
+ buffer.append(param);
+ buffer.append(c);
+ lastLength = ++nwsLength;
+ state = State.PARAM_VALUE;
+ continue;
+
+ case PARAM_NAME:
+ buffer.setLength(nwsLength); // trim following OWS
+ buffer.append(c);
+ lastLength = ++nwsLength;
+ state = State.PARAM_VALUE;
+ continue;
+
+ case PARAM_VALUE:
+ if (paramValue < 0)
+ paramValue = nwsLength;
+ buffer.append(c);
+ nwsLength = buffer.length();
+ continue;
+ }
+ continue;
+
+ default:
+ {
+ switch (state)
+ {
+ case VALUE:
+ {
+ buffer.append(c);
+ nwsLength = buffer.length();
+ continue;
+ }
+
+ case PARAM_NAME:
+ {
+ if (paramName < 0)
+ paramName = nwsLength;
+ buffer.append(c);
+ nwsLength = buffer.length();
+ continue;
+ }
+
+ case PARAM_VALUE:
+ {
+ if (paramValue < 0)
+ paramValue = nwsLength;
+ buffer.append(c);
+ nwsLength = buffer.length();
+ continue;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when a value and it's parameters has been parsed
+ *
+ * @param buffer Containing the trimmed value and parameters
+ */
+ protected void parsedValueAndParams(StringBuffer buffer)
+ {
+ }
+
+ /**
+ * Called when a value has been parsed (prior to any parameters)
+ *
+ * @param buffer Containing the trimmed value, which may be mutated
+ */
+ protected void parsedValue(StringBuffer buffer)
+ {
+ }
+
+ /**
+ * Called when a parameter has been parsed
+ *
+ * @param buffer Containing the trimmed value and all parameters, which may be mutated
+ * @param valueLength The length of the value
+ * @param paramName The index of the start of the parameter just parsed
+ * @param paramValue The index of the start of the parameter value just parsed, or -1
+ */
+ protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
+ {
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java
new file mode 100644
index 0000000..5bc9985
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java
@@ -0,0 +1,249 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.ToIntFunction;
+import java.util.stream.Collectors;
+
+import org.eclipse.jetty.util.log.Log;
+
+/**
+ * Implements a quoted comma separated list of quality values
+ * in accordance with RFC7230 and RFC7231.
+ * Values are returned sorted in quality order, with OWS and the
+ * quality parameters removed.
+ *
+ * @see "https://tools.ietf.org/html/rfc7230#section-3.2.6"
+ * @see "https://tools.ietf.org/html/rfc7230#section-7"
+ * @see "https://tools.ietf.org/html/rfc7231#section-5.3.1"
+ */
+public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
+{
+ /**
+ * Lambda to apply a most specific MIME encoding secondary ordering.
+ *
+ * @see "https://tools.ietf.org/html/rfc7231#section-5.3.2"
+ */
+ public static ToIntFunction<String> MOST_SPECIFIC_MIME_ORDERING = s ->
+ {
+ if ("*/*".equals(s))
+ return 0;
+ if (s.endsWith("/*"))
+ return 1;
+ if (s.indexOf(';') < 0)
+ return 2;
+ return 3;
+ };
+
+ private final List<QualityValue> _qualities = new ArrayList<>();
+ private QualityValue _lastQuality;
+ private boolean _sorted = false;
+ private final ToIntFunction<String> _secondaryOrdering;
+
+ /**
+ * Sorts values with equal quality according to the length of the value String.
+ */
+ public QuotedQualityCSV()
+ {
+ this((ToIntFunction<String>)null);
+ }
+
+ /**
+ * Sorts values with equal quality according to given order.
+ *
+ * @param preferredOrder Array indicating the preferred order of known values
+ */
+ public QuotedQualityCSV(String[] preferredOrder)
+ {
+ this((s) ->
+ {
+ for (int i = 0; i < preferredOrder.length; ++i)
+ {
+ if (preferredOrder[i].equals(s))
+ return preferredOrder.length - i;
+ }
+
+ if ("*".equals(s))
+ return preferredOrder.length;
+
+ return 0;
+ });
+ }
+
+ /**
+ * Orders values with equal quality with the given function.
+ *
+ * @param secondaryOrdering Function to apply an ordering other than specified by quality, highest values are sorted first.
+ */
+ public QuotedQualityCSV(ToIntFunction<String> secondaryOrdering)
+ {
+ this._secondaryOrdering = secondaryOrdering == null ? s -> 0 : secondaryOrdering;
+ }
+
+ @Override
+ protected void parsedValueAndParams(StringBuffer buffer)
+ {
+ super.parsedValueAndParams(buffer);
+
+ // Collect full value with parameters
+ _lastQuality = new QualityValue(_lastQuality._quality, buffer.toString(), _lastQuality._index);
+ _qualities.set(_lastQuality._index, _lastQuality);
+ }
+
+ @Override
+ protected void parsedValue(StringBuffer buffer)
+ {
+ super.parsedValue(buffer);
+
+ _sorted = false;
+
+ // This is the just the value, without parameters.
+ // Assume a quality of ONE
+ _lastQuality = new QualityValue(1.0D, buffer.toString(), _qualities.size());
+ _qualities.add(_lastQuality);
+ }
+
+ @Override
+ protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
+ {
+ _sorted = false;
+
+ if (paramName < 0)
+ {
+ if (buffer.charAt(buffer.length() - 1) == ';')
+ buffer.setLength(buffer.length() - 1);
+ }
+ else if (paramValue >= 0 &&
+ buffer.charAt(paramName) == 'q' && paramValue > paramName &&
+ buffer.length() >= paramName && buffer.charAt(paramName + 1) == '=')
+ {
+ double q;
+ try
+ {
+ q = (_keepQuotes && buffer.charAt(paramValue) == '"')
+ ? Double.valueOf(buffer.substring(paramValue + 1, buffer.length() - 1))
+ : Double.valueOf(buffer.substring(paramValue));
+ }
+ catch (Exception e)
+ {
+ Log.getLogger(QuotedQualityCSV.class).ignore(e);
+ q = 0.0D;
+ }
+ buffer.setLength(Math.max(0, paramName - 1));
+
+ if (q != 1.0D)
+ {
+ _lastQuality = new QualityValue(q, buffer.toString(), _lastQuality._index);
+ _qualities.set(_lastQuality._index, _lastQuality);
+ }
+ }
+ }
+
+ @Override
+ public List<String> getValues()
+ {
+ if (!_sorted)
+ sort();
+ return _values;
+ }
+
+ @Override
+ public Iterator<String> iterator()
+ {
+ if (!_sorted)
+ sort();
+ return _values.iterator();
+ }
+
+ protected void sort()
+ {
+ _values.clear();
+ _qualities.stream()
+ .filter((qv) -> qv._quality != 0.0D)
+ .sorted()
+ .map(QualityValue::getValue)
+ .collect(Collectors.toCollection(() -> _values));
+ _sorted = true;
+ }
+
+ private class QualityValue implements Comparable<QualityValue>
+ {
+ private final double _quality;
+ private final String _value;
+ private final int _index;
+
+ private QualityValue(double quality, String value, int index)
+ {
+ _quality = quality;
+ _value = value;
+ _index = index;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Double.hashCode(_quality) ^ Objects.hash(_value, _index);
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (!(obj instanceof QualityValue))
+ return false;
+ QualityValue qv = (QualityValue)obj;
+ return _quality == qv._quality && Objects.equals(_value, qv._value) && Objects.equals(_index, qv._index);
+ }
+
+ private String getValue()
+ {
+ return _value;
+ }
+
+ @Override
+ public int compareTo(QualityValue o)
+ {
+ // sort highest quality first
+ int compare = Double.compare(o._quality, _quality);
+ if (compare == 0)
+ {
+ // then sort secondary order highest first
+ compare = Integer.compare(_secondaryOrdering.applyAsInt(o._value), _secondaryOrdering.applyAsInt(_value));
+ if (compare == 0)
+ // then sort index lowest first
+ compare = -Integer.compare(o._index, _index);
+ }
+ return compare;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x[%s,q=%f,i=%d]",
+ getClass().getSimpleName(),
+ hashCode(),
+ _value,
+ _quality,
+ _index);
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/ResourceHttpContent.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/ResourceHttpContent.java
new file mode 100644
index 0000000..f72d597
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/ResourceHttpContent.java
@@ -0,0 +1,215 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jetty.http.MimeTypes.Type;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.resource.Resource;
+
+/**
+ * HttpContent created from a {@link Resource}.
+ * <p>The HttpContent is used to server static content that is not
+ * cached. So fields and values are only generated as need be an not
+ * kept for reuse</p>
+ */
+public class ResourceHttpContent implements HttpContent
+{
+ final Resource _resource;
+ final String _contentType;
+ final int _maxBuffer;
+ Map<CompressedContentFormat, HttpContent> _precompressedContents;
+ String _etag;
+
+ public ResourceHttpContent(final Resource resource, final String contentType)
+ {
+ this(resource, contentType, -1, null);
+ }
+
+ public ResourceHttpContent(final Resource resource, final String contentType, int maxBuffer)
+ {
+ this(resource, contentType, maxBuffer, null);
+ }
+
+ public ResourceHttpContent(final Resource resource, final String contentType, int maxBuffer, Map<CompressedContentFormat, HttpContent> precompressedContents)
+ {
+ _resource = resource;
+ _contentType = contentType;
+ _maxBuffer = maxBuffer;
+ if (precompressedContents == null)
+ {
+ _precompressedContents = null;
+ }
+ else
+ {
+ _precompressedContents = new HashMap<>(precompressedContents.size());
+ for (Map.Entry<CompressedContentFormat, HttpContent> entry : precompressedContents.entrySet())
+ {
+ _precompressedContents.put(entry.getKey(), new PrecompressedHttpContent(this, entry.getValue(), entry.getKey()));
+ }
+ }
+ }
+
+ @Override
+ public String getContentTypeValue()
+ {
+ return _contentType;
+ }
+
+ @Override
+ public HttpField getContentType()
+ {
+ return _contentType == null ? null : new HttpField(HttpHeader.CONTENT_TYPE, _contentType);
+ }
+
+ @Override
+ public HttpField getContentEncoding()
+ {
+ return null;
+ }
+
+ @Override
+ public String getContentEncodingValue()
+ {
+ return null;
+ }
+
+ @Override
+ public String getCharacterEncoding()
+ {
+ return _contentType == null ? null : MimeTypes.getCharsetFromContentType(_contentType);
+ }
+
+ @Override
+ public Type getMimeType()
+ {
+ return _contentType == null ? null : MimeTypes.CACHE.get(MimeTypes.getContentTypeWithoutCharset(_contentType));
+ }
+
+ @Override
+ public HttpField getLastModified()
+ {
+ long lm = _resource.lastModified();
+ return lm >= 0 ? new HttpField(HttpHeader.LAST_MODIFIED, DateGenerator.formatDate(lm)) : null;
+ }
+
+ @Override
+ public String getLastModifiedValue()
+ {
+ long lm = _resource.lastModified();
+ return lm >= 0 ? DateGenerator.formatDate(lm) : null;
+ }
+
+ @Override
+ public ByteBuffer getDirectBuffer()
+ {
+ if (_resource.length() <= 0 || _maxBuffer > 0 && _resource.length() > _maxBuffer)
+ return null;
+ try
+ {
+ return BufferUtil.toBuffer(_resource, true);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public HttpField getETag()
+ {
+ return new HttpField(HttpHeader.ETAG, getETagValue());
+ }
+
+ @Override
+ public String getETagValue()
+ {
+ return _resource.getWeakETag();
+ }
+
+ @Override
+ public ByteBuffer getIndirectBuffer()
+ {
+ if (_resource.length() <= 0 || _maxBuffer > 0 && _resource.length() > _maxBuffer)
+ return null;
+ try
+ {
+ return BufferUtil.toBuffer(_resource, false);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public HttpField getContentLength()
+ {
+ long l = _resource.length();
+ return l == -1 ? null : new HttpField.LongValueHttpField(HttpHeader.CONTENT_LENGTH, l);
+ }
+
+ @Override
+ public long getContentLengthValue()
+ {
+ return _resource.length();
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException
+ {
+ return _resource.getInputStream();
+ }
+
+ @Override
+ public ReadableByteChannel getReadableByteChannel() throws IOException
+ {
+ return _resource.getReadableByteChannel();
+ }
+
+ @Override
+ public Resource getResource()
+ {
+ return _resource;
+ }
+
+ @Override
+ public void release()
+ {
+ _resource.close();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{r=%s,ct=%s,c=%b}", this.getClass().getSimpleName(), hashCode(), _resource, _contentType, _precompressedContents != null);
+ }
+
+ @Override
+ public Map<CompressedContentFormat, HttpContent> getPrecompressedContents()
+ {
+ return _precompressedContents;
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/Syntax.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/Syntax.java
new file mode 100644
index 0000000..e3ae05d
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/Syntax.java
@@ -0,0 +1,142 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.Objects;
+
+/**
+ * Collection of Syntax validation methods.
+ * <p>
+ * Use in a similar way as you would {@link java.util.Objects#requireNonNull(Object)}
+ * </p>
+ */
+public final class Syntax
+{
+
+ /**
+ * Per RFC2616: Section 2.2, a token follows these syntax rules
+ * <pre>
+ * token = 1*<any CHAR except CTLs or separators>
+ * CHAR = <any US-ASCII character (octets 0 - 127)>
+ * CTL = <any US-ASCII control character
+ * (octets 0 - 31) and DEL (127)>
+ * separators = "(" | ")" | "<" | ">" | "@"
+ * | "," | ";" | ":" | "\" | <">
+ * | "/" | "[" | "]" | "?" | "="
+ * | "{" | "}" | SP | HT
+ * </pre>
+ *
+ * @param value the value to test
+ * @param msg the message to be prefixed if an {@link IllegalArgumentException} is thrown.
+ * @throws IllegalArgumentException if the value is invalid per spec
+ */
+ public static void requireValidRFC2616Token(String value, String msg)
+ {
+ Objects.requireNonNull(msg, "msg cannot be null");
+
+ if (value == null)
+ {
+ return;
+ }
+
+ int valueLen = value.length();
+ if (valueLen == 0)
+ {
+ return;
+ }
+
+ for (int i = 0; i < valueLen; i++)
+ {
+ char c = value.charAt(i);
+
+ // 0x00 - 0x1F are low order control characters
+ // 0x7F is the DEL control character
+ if ((c <= 0x1F) || (c == 0x7F))
+ throw new IllegalArgumentException(msg + ": RFC2616 tokens may not contain control characters");
+ if (c == '(' || c == ')' || c == '<' || c == '>' || c == '@' ||
+ c == ',' || c == ';' || c == ':' || c == '\\' || c == '"' ||
+ c == '/' || c == '[' || c == ']' || c == '?' || c == '=' ||
+ c == '{' || c == '}' || c == ' ')
+ {
+ throw new IllegalArgumentException(msg + ": RFC2616 tokens may not contain separator character: [" + c + "]");
+ }
+ if (c >= 0x80)
+ throw new IllegalArgumentException(msg + ": RFC2616 tokens characters restricted to US-ASCII: 0x" + Integer.toHexString(c));
+ }
+ }
+
+ /**
+ * Per RFC6265, Cookie.value follows these syntax rules
+ * <pre>
+ * cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
+ * cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
+ * ; US-ASCII characters excluding CTLs,
+ * ; whitespace DQUOTE, comma, semicolon,
+ * ; and backslash
+ * </pre>
+ *
+ * @param value the value to test
+ * @throws IllegalArgumentException if the value is invalid per spec
+ */
+ public static void requireValidRFC6265CookieValue(String value)
+ {
+ if (value == null)
+ {
+ return;
+ }
+
+ int valueLen = value.length();
+ if (valueLen == 0)
+ {
+ return;
+ }
+
+ int i = 0;
+ if (value.charAt(0) == '"')
+ {
+ // Has starting DQUOTE
+ if (valueLen <= 1 || (value.charAt(valueLen - 1) != '"'))
+ {
+ throw new IllegalArgumentException("RFC6265 Cookie values must have balanced DQUOTES (if used)");
+ }
+
+ // adjust search range to exclude DQUOTES
+ i++;
+ valueLen--;
+ }
+ for (; i < valueLen; i++)
+ {
+ char c = value.charAt(i);
+
+ // 0x00 - 0x1F are low order control characters
+ // 0x7F is the DEL control character
+ if ((c <= 0x1F) || (c == 0x7F))
+ throw new IllegalArgumentException("RFC6265 Cookie values may not contain control characters");
+ if ((c == ' ' /* 0x20 */) ||
+ (c == '"' /* 0x2C */) ||
+ (c == ';' /* 0x3B */) ||
+ (c == '\\' /* 0x5C */))
+ {
+ throw new IllegalArgumentException("RFC6265 Cookie values may not contain character: [" + c + "]");
+ }
+ if (c >= 0x80)
+ throw new IllegalArgumentException("RFC6265 Cookie values characters restricted to US-ASCII: 0x" + Integer.toHexString(c));
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/package-info.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/package-info.java
new file mode 100644
index 0000000..0233b51
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Http : Tools for Http processing
+ */
+package org.eclipse.jetty.http;
+
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/AbstractPathSpec.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/AbstractPathSpec.java
new file mode 100644
index 0000000..d00f12b
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/AbstractPathSpec.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import java.util.Objects;
+
+public abstract class AbstractPathSpec implements PathSpec
+{
+ @Override
+ public int compareTo(PathSpec other)
+ {
+ // Grouping (increasing)
+ int diff = getGroup().ordinal() - other.getGroup().ordinal();
+ if (diff != 0)
+ return diff;
+
+ // Spec Length (decreasing)
+ diff = other.getSpecLength() - getSpecLength();
+ if (diff != 0)
+ return diff;
+
+ // Path Spec Name (alphabetical)
+ return getDeclaration().compareTo(other.getDeclaration());
+ }
+
+ @Override
+ public final boolean equals(Object obj)
+ {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+
+ return compareTo((AbstractPathSpec)obj) == 0;
+ }
+
+ @Override
+ public final int hashCode()
+ {
+ return Objects.hash(getDeclaration());
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%s{%s}", getClass().getSimpleName(), Integer.toHexString(hashCode()), getDeclaration());
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/MappedResource.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/MappedResource.java
new file mode 100644
index 0000000..ae3f920
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/MappedResource.java
@@ -0,0 +1,95 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+
+@ManagedObject("Mapped Resource")
+public class MappedResource<E> implements Comparable<MappedResource<E>>
+{
+ private final PathSpec pathSpec;
+ private final E resource;
+
+ public MappedResource(PathSpec pathSpec, E resource)
+ {
+ this.pathSpec = pathSpec;
+ this.resource = resource;
+ }
+
+ /**
+ * Comparison is based solely on the pathSpec
+ */
+ @Override
+ public int compareTo(MappedResource<E> other)
+ {
+ return this.pathSpec.compareTo(other.pathSpec);
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ {
+ return true;
+ }
+ if (obj == null)
+ {
+ return false;
+ }
+ if (getClass() != obj.getClass())
+ {
+ return false;
+ }
+ MappedResource<?> other = (MappedResource<?>)obj;
+ if (pathSpec == null)
+ {
+ return other.pathSpec == null;
+ }
+ else
+ return pathSpec.equals(other.pathSpec);
+ }
+
+ @ManagedAttribute(value = "path spec", readonly = true)
+ public PathSpec getPathSpec()
+ {
+ return pathSpec;
+ }
+
+ @ManagedAttribute(value = "resource", readonly = true)
+ public E getResource()
+ {
+ return resource;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = (prime * result) + ((pathSpec == null) ? 0 : pathSpec.hashCode());
+ return result;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("MappedResource[pathSpec=%s,resource=%s]", pathSpec, resource);
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathMappings.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathMappings.java
new file mode 100644
index 0000000..9980996
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathMappings.java
@@ -0,0 +1,303 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Predicate;
+
+import org.eclipse.jetty.util.ArrayTernaryTrie;
+import org.eclipse.jetty.util.Trie;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Path Mappings of PathSpec to Resource.
+ * <p>
+ * Sorted into search order upon entry into the Set
+ *
+ * @param <E> the type of mapping endpoint
+ */
+@ManagedObject("Path Mappings")
+public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
+{
+ private static final Logger LOG = Log.getLogger(PathMappings.class);
+ private final Set<MappedResource<E>> _mappings = new TreeSet<>(Comparator.comparing(MappedResource::getPathSpec));
+
+ private Trie<MappedResource<E>> _exactMap = new ArrayTernaryTrie<>(false);
+ private Trie<MappedResource<E>> _prefixMap = new ArrayTernaryTrie<>(false);
+ private Trie<MappedResource<E>> _suffixMap = new ArrayTernaryTrie<>(false);
+
+ @Override
+ public String dump()
+ {
+ return Dumpable.dump(this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ Dumpable.dumpObjects(out, indent, toString(), _mappings);
+ }
+
+ @ManagedAttribute(value = "mappings", readonly = true)
+ public List<MappedResource<E>> getMappings()
+ {
+ return new ArrayList<>(_mappings);
+ }
+
+ public int size()
+ {
+ return _mappings.size();
+ }
+
+ public void reset()
+ {
+ _mappings.clear();
+ _prefixMap.clear();
+ _suffixMap.clear();
+ }
+
+ public void removeIf(Predicate<MappedResource<E>> predicate)
+ {
+ _mappings.removeIf(predicate);
+ }
+
+ /**
+ * Return a list of MappedResource matches for the specified path.
+ *
+ * @param path the path to return matches on
+ * @return the list of mapped resource the path matches on
+ */
+ public List<MappedResource<E>> getMatches(String path)
+ {
+ boolean isRootPath = "/".equals(path);
+
+ List<MappedResource<E>> ret = new ArrayList<>();
+ for (MappedResource<E> mr : _mappings)
+ {
+ switch (mr.getPathSpec().getGroup())
+ {
+ case ROOT:
+ if (isRootPath)
+ ret.add(mr);
+ break;
+ case DEFAULT:
+ if (isRootPath || mr.getPathSpec().matches(path))
+ ret.add(mr);
+ break;
+ default:
+ if (mr.getPathSpec().matches(path))
+ ret.add(mr);
+ break;
+ }
+ }
+ return ret;
+ }
+
+ public MappedResource<E> getMatch(String path)
+ {
+ PathSpecGroup lastGroup = null;
+
+ // Search all the mappings
+ for (MappedResource<E> mr : _mappings)
+ {
+ PathSpecGroup group = mr.getPathSpec().getGroup();
+ if (group != lastGroup)
+ {
+ // New group in list, so let's look for an optimization
+ switch (group)
+ {
+ case EXACT:
+ {
+ int i = path.length();
+ final Trie<MappedResource<E>> exact_map = _exactMap;
+ while (i >= 0)
+ {
+ MappedResource<E> candidate = exact_map.getBest(path, 0, i);
+ if (candidate == null)
+ break;
+ if (candidate.getPathSpec().matches(path))
+ return candidate;
+ i = candidate.getPathSpec().getPrefix().length() - 1;
+ }
+ break;
+ }
+
+ case PREFIX_GLOB:
+ {
+ int i = path.length();
+ final Trie<MappedResource<E>> prefix_map = _prefixMap;
+ while (i >= 0)
+ {
+ MappedResource<E> candidate = prefix_map.getBest(path, 0, i);
+ if (candidate == null)
+ break;
+ if (candidate.getPathSpec().matches(path))
+ return candidate;
+ i = candidate.getPathSpec().getPrefix().length() - 1;
+ }
+ break;
+ }
+
+ case SUFFIX_GLOB:
+ {
+ int i = 0;
+ final Trie<MappedResource<E>> suffix_map = _suffixMap;
+ while ((i = path.indexOf('.', i + 1)) > 0)
+ {
+ MappedResource<E> candidate = suffix_map.get(path, i + 1, path.length() - i - 1);
+ if (candidate != null && candidate.getPathSpec().matches(path))
+ return candidate;
+ }
+ break;
+ }
+
+ default:
+ }
+ }
+
+ if (mr.getPathSpec().matches(path))
+ return mr;
+
+ lastGroup = group;
+ }
+
+ return null;
+ }
+
+ @Override
+ public Iterator<MappedResource<E>> iterator()
+ {
+ return _mappings.iterator();
+ }
+
+ public static PathSpec asPathSpec(String pathSpecString)
+ {
+ if ((pathSpecString == null) || (pathSpecString.length() < 1))
+ {
+ throw new RuntimeException("Path Spec String must start with '^', '/', or '*.': got [" + pathSpecString + "]");
+ }
+ return pathSpecString.charAt(0) == '^' ? new RegexPathSpec(pathSpecString) : new ServletPathSpec(pathSpecString);
+ }
+
+ public E get(PathSpec spec)
+ {
+ Optional<E> optionalResource = _mappings.stream()
+ .filter(mappedResource -> mappedResource.getPathSpec().equals(spec))
+ .map(mappedResource -> mappedResource.getResource())
+ .findFirst();
+ if (!optionalResource.isPresent())
+ return null;
+
+ return optionalResource.get();
+ }
+
+ public boolean put(String pathSpecString, E resource)
+ {
+ return put(asPathSpec(pathSpecString), resource);
+ }
+
+ public boolean put(PathSpec pathSpec, E resource)
+ {
+ MappedResource<E> entry = new MappedResource<>(pathSpec, resource);
+ switch (pathSpec.getGroup())
+ {
+ case EXACT:
+ String exact = pathSpec.getPrefix();
+ while (exact != null && !_exactMap.put(exact, entry))
+ {
+ _exactMap = new ArrayTernaryTrie<>((ArrayTernaryTrie<MappedResource<E>>)_exactMap, 1.5);
+ }
+ break;
+ case PREFIX_GLOB:
+ String prefix = pathSpec.getPrefix();
+ while (prefix != null && !_prefixMap.put(prefix, entry))
+ {
+ _prefixMap = new ArrayTernaryTrie<>((ArrayTernaryTrie<MappedResource<E>>)_prefixMap, 1.5);
+ }
+ break;
+ case SUFFIX_GLOB:
+ String suffix = pathSpec.getSuffix();
+ while (suffix != null && !_suffixMap.put(suffix, entry))
+ {
+ _suffixMap = new ArrayTernaryTrie<>((ArrayTernaryTrie<MappedResource<E>>)_prefixMap, 1.5);
+ }
+ break;
+ default:
+ }
+
+ boolean added = _mappings.add(entry);
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} {} to {}", added ? "Added" : "Ignored", entry, this);
+ return added;
+ }
+
+ @SuppressWarnings("incomplete-switch")
+ public boolean remove(PathSpec pathSpec)
+ {
+ String prefix = pathSpec.getPrefix();
+ String suffix = pathSpec.getSuffix();
+ switch (pathSpec.getGroup())
+ {
+ case EXACT:
+ if (prefix != null)
+ _exactMap.remove(prefix);
+ break;
+ case PREFIX_GLOB:
+ if (prefix != null)
+ _prefixMap.remove(prefix);
+ break;
+ case SUFFIX_GLOB:
+ if (suffix != null)
+ _suffixMap.remove(suffix);
+ break;
+ }
+
+ Iterator<MappedResource<E>> iter = _mappings.iterator();
+ boolean removed = false;
+ while (iter.hasNext())
+ {
+ if (iter.next().getPathSpec().equals(pathSpec))
+ {
+ removed = true;
+ iter.remove();
+ break;
+ }
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} {} to {}", removed ? "Removed" : "Ignored", pathSpec, this);
+ return removed;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[size=%d]", this.getClass().getSimpleName(), _mappings.size());
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathSpec.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathSpec.java
new file mode 100644
index 0000000..c5aec1b
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathSpec.java
@@ -0,0 +1,95 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+/**
+ * A path specification is a URI path template that can be matched against.
+ * <p>
+ * Implementors <i>must</i> override {@link Object#equals(Object)} and {@link Object#hashCode()}.
+ */
+public interface PathSpec extends Comparable<PathSpec>
+{
+ /**
+ * The length of the spec.
+ *
+ * @return the length of the spec.
+ */
+ int getSpecLength();
+
+ /**
+ * The spec group.
+ *
+ * @return the spec group.
+ */
+ PathSpecGroup getGroup();
+
+ /**
+ * Get the number of path elements that this path spec declares.
+ * <p>
+ * This is used to determine longest match logic.
+ *
+ * @return the depth of the path segments that this spec declares
+ */
+ int getPathDepth();
+
+ /**
+ * Return the portion of the path that is after the path spec.
+ *
+ * @param path the path to match against
+ * @return the path info portion of the string
+ */
+ String getPathInfo(String path);
+
+ /**
+ * Return the portion of the path that matches a path spec.
+ *
+ * @param path the path to match against
+ * @return the match, or null if no match at all
+ */
+ String getPathMatch(String path);
+
+ /**
+ * The as-provided path spec.
+ *
+ * @return the as-provided path spec
+ */
+ String getDeclaration();
+
+ /**
+ * A simple prefix match for the pathspec or null
+ *
+ * @return A simple prefix match for the pathspec or null
+ */
+ String getPrefix();
+
+ /**
+ * A simple suffix match for the pathspec or null
+ *
+ * @return A simple suffix match for the pathspec or null
+ */
+ String getSuffix();
+
+ /**
+ * Test to see if the provided path matches this path spec
+ *
+ * @param path the path to test
+ * @return true if the path matches this path spec, false otherwise
+ */
+ boolean matches(String path);
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathSpecGroup.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathSpecGroup.java
new file mode 100644
index 0000000..28b569b
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathSpecGroup.java
@@ -0,0 +1,100 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+/**
+ * Types of path spec groups.
+ * <p>
+ * This is used to facilitate proper pathspec search order.
+ * <p>
+ * Search Order:
+ * <ol>
+ * <li>{@link PathSpecGroup#ordinal()} [increasing]</li>
+ * <li>{@link PathSpec#getSpecLength()} [decreasing]</li>
+ * <li>{@link PathSpec#getDeclaration()} [natural sort order]</li>
+ * </ol>
+ */
+public enum PathSpecGroup
+{
+ // NOTE: Order of enums determines order of Groups.
+
+ /**
+ * The root spec for accessing the Root behavior.
+ *
+ * <pre>
+ * "" - servlet spec (Root Servlet)
+ * null - legacy (Root Servlet)
+ * </pre>
+ *
+ * Note: there is no known uri-template spec variant of this kind of path spec
+ */
+ ROOT,
+ /**
+ * For exactly defined path specs, no glob.
+ */
+ EXACT,
+ /**
+ * For path specs that have a hardcoded prefix and suffix with wildcard glob in the middle.
+ *
+ * <pre>
+ * "^/downloads/[^/]*.zip$" - regex spec
+ * "/a/{var}/c" - uri-template spec
+ * </pre>
+ *
+ * Note: there is no known servlet spec variant of this kind of path spec
+ */
+ MIDDLE_GLOB,
+ /**
+ * For path specs that have a hardcoded prefix and a trailing wildcard glob.
+ *
+ * <pre>
+ * "/downloads/*" - servlet spec
+ * "/api/*" - servlet spec
+ * "^/rest/.*$" - regex spec
+ * "/bookings/{guest-id}" - uri-template spec
+ * "/rewards/{vip-level}" - uri-template spec
+ * </pre>
+ */
+ PREFIX_GLOB,
+ /**
+ * For path specs that have a wildcard glob with a hardcoded suffix
+ *
+ * <pre>
+ * "*.do" - servlet spec
+ * "*.css" - servlet spec
+ * "^.*\.zip$" - regex spec
+ * </pre>
+ *
+ * Note: there is no known uri-template spec variant of this kind of path spec
+ */
+ SUFFIX_GLOB,
+ /**
+ * The default spec for accessing the Default path behavior.
+ *
+ * <pre>
+ * "/" - servlet spec (Default Servlet)
+ * "/" - uri-template spec (Root Context)
+ * "^/$" - regex spec (Root Context)
+ * </pre>
+ *
+ * Per Servlet Spec, pathInfo is always null for these specs.
+ * If nothing above matches, then default will match.
+ */
+ DEFAULT,
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathSpecSet.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathSpecSet.java
new file mode 100644
index 0000000..dcf5b86
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathSpecSet.java
@@ -0,0 +1,100 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import java.util.AbstractSet;
+import java.util.Iterator;
+import java.util.function.Predicate;
+
+/**
+ * A Set of PathSpec strings.
+ * <p>
+ * Used by {@link org.eclipse.jetty.util.IncludeExclude} logic
+ */
+public class PathSpecSet extends AbstractSet<String> implements Predicate<String>
+{
+ private final PathMappings<Boolean> specs = new PathMappings<>();
+
+ @Override
+ public boolean test(String s)
+ {
+ return specs.getMatch(s) != null;
+ }
+
+ @Override
+ public int size()
+ {
+ return specs.size();
+ }
+
+ private PathSpec asPathSpec(Object o)
+ {
+ if (o == null)
+ {
+ return null;
+ }
+ if (o instanceof PathSpec)
+ {
+ return (PathSpec)o;
+ }
+ if (o instanceof String)
+ {
+ return PathMappings.asPathSpec((String)o);
+ }
+ return PathMappings.asPathSpec(o.toString());
+ }
+
+ @Override
+ public boolean add(String s)
+ {
+ return specs.put(PathMappings.asPathSpec(s), Boolean.TRUE);
+ }
+
+ @Override
+ public boolean remove(Object o)
+ {
+ return specs.remove(asPathSpec(o));
+ }
+
+ @Override
+ public void clear()
+ {
+ specs.reset();
+ }
+
+ @Override
+ public Iterator<String> iterator()
+ {
+ final Iterator<MappedResource<Boolean>> iterator = specs.iterator();
+ return new Iterator<String>()
+ {
+ @Override
+ public boolean hasNext()
+ {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public String next()
+ {
+ return iterator.next().getPathSpec().getDeclaration();
+ }
+ };
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/RegexPathSpec.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/RegexPathSpec.java
new file mode 100644
index 0000000..de3f01c
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/RegexPathSpec.java
@@ -0,0 +1,196 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class RegexPathSpec extends AbstractPathSpec
+{
+ private final String _declaration;
+ private final PathSpecGroup _group;
+ private final int _pathDepth;
+ private final int _specLength;
+ private final Pattern _pattern;
+
+ public RegexPathSpec(String regex)
+ {
+ String declaration;
+ if (regex.startsWith("regex|"))
+ declaration = regex.substring("regex|".length());
+ else
+ declaration = regex;
+ int specLength = declaration.length();
+ // build up a simple signature we can use to identify the grouping
+ boolean inGrouping = false;
+ StringBuilder signature = new StringBuilder();
+
+ int pathDepth = 0;
+ for (int i = 0; i < declaration.length(); i++)
+ {
+ char c = declaration.charAt(i);
+ switch (c)
+ {
+ case '[':
+ inGrouping = true;
+ break;
+ case ']':
+ inGrouping = false;
+ signature.append('g'); // glob
+ break;
+ case '*':
+ signature.append('g'); // glob
+ break;
+ case '/':
+ if (!inGrouping)
+ pathDepth++;
+ break;
+ default:
+ if (!inGrouping && Character.isLetterOrDigit(c))
+ signature.append('l'); // literal (exact)
+ break;
+ }
+ }
+ Pattern pattern = Pattern.compile(declaration);
+
+ // Figure out the grouping based on the signature
+ String sig = signature.toString();
+
+ PathSpecGroup group;
+ if (Pattern.matches("^l*$", sig))
+ group = PathSpecGroup.EXACT;
+ else if (Pattern.matches("^l*g+", sig))
+ group = PathSpecGroup.PREFIX_GLOB;
+ else if (Pattern.matches("^g+l+$", sig))
+ group = PathSpecGroup.SUFFIX_GLOB;
+ else
+ group = PathSpecGroup.MIDDLE_GLOB;
+
+ _declaration = declaration;
+ _group = group;
+ _pathDepth = pathDepth;
+ _specLength = specLength;
+ _pattern = pattern;
+ }
+
+ protected Matcher getMatcher(String path)
+ {
+ return _pattern.matcher(path);
+ }
+
+ @Override
+ public int getSpecLength()
+ {
+ return _specLength;
+ }
+
+ @Override
+ public PathSpecGroup getGroup()
+ {
+ return _group;
+ }
+
+ @Override
+ public int getPathDepth()
+ {
+ return _pathDepth;
+ }
+
+ @Override
+ public String getPathInfo(String path)
+ {
+ // Path Info only valid for PREFIX_GLOB types
+ if (_group == PathSpecGroup.PREFIX_GLOB)
+ {
+ Matcher matcher = getMatcher(path);
+ if (matcher.matches())
+ {
+ if (matcher.groupCount() >= 1)
+ {
+ String pathInfo = matcher.group(1);
+ if ("".equals(pathInfo))
+ return "/";
+ else
+ return pathInfo;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getPathMatch(String path)
+ {
+ Matcher matcher = getMatcher(path);
+ if (matcher.matches())
+ {
+ if (matcher.groupCount() >= 1)
+ {
+ int idx = matcher.start(1);
+ if (idx > 0)
+ {
+ if (path.charAt(idx - 1) == '/')
+ idx--;
+ return path.substring(0, idx);
+ }
+ }
+ return path;
+ }
+ return null;
+ }
+
+ @Override
+ public String getDeclaration()
+ {
+ return _declaration;
+ }
+
+ @Override
+ public String getPrefix()
+ {
+ return null;
+ }
+
+ @Override
+ public String getSuffix()
+ {
+ return null;
+ }
+
+ public Pattern getPattern()
+ {
+ return _pattern;
+ }
+
+ @Override
+ public boolean matches(final String path)
+ {
+ int idx = path.indexOf('?');
+ if (idx >= 0)
+ {
+ // match only non-query part
+ return getMatcher(path.substring(0, idx)).matches();
+ }
+ else
+ {
+ // match entire path
+ return getMatcher(path).matches();
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java
new file mode 100644
index 0000000..cf3457b
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java
@@ -0,0 +1,294 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class ServletPathSpec extends AbstractPathSpec
+{
+ private static final Logger LOG = Log.getLogger(ServletPathSpec.class);
+
+ private final String _declaration;
+ private final PathSpecGroup _group;
+ private final int _pathDepth;
+ private final int _specLength;
+ private final String _prefix;
+ private final String _suffix;
+
+ /**
+ * If a servlet or filter path mapping isn't a suffix mapping, ensure
+ * it starts with '/'
+ *
+ * @param pathSpec the servlet or filter mapping pattern
+ * @return the pathSpec prefixed by '/' if appropriate
+ */
+ public static String normalize(String pathSpec)
+ {
+ if (StringUtil.isNotBlank(pathSpec) && !pathSpec.startsWith("/") && !pathSpec.startsWith("*"))
+ return "/" + pathSpec;
+ return pathSpec;
+ }
+
+ public ServletPathSpec(String servletPathSpec)
+ {
+ if (servletPathSpec == null)
+ servletPathSpec = "";
+ if (servletPathSpec.startsWith("servlet|"))
+ servletPathSpec = servletPathSpec.substring("servlet|".length());
+ assertValidServletPathSpec(servletPathSpec);
+
+ // The Root Path Spec
+ if (servletPathSpec.isEmpty())
+ {
+ _declaration = "";
+ _group = PathSpecGroup.ROOT;
+ _pathDepth = -1; // Set pathDepth to -1 to force this to be at the end of the sort order.
+ _specLength = 1;
+ _prefix = null;
+ _suffix = null;
+ return;
+ }
+
+ // The Default Path Spec
+ if ("/".equals(servletPathSpec))
+ {
+ _declaration = "/";
+ _group = PathSpecGroup.DEFAULT;
+ _pathDepth = -1; // Set pathDepth to -1 to force this to be at the end of the sort order.
+ _specLength = 1;
+ _prefix = null;
+ _suffix = null;
+ return;
+ }
+
+ int specLength = servletPathSpec.length();
+ PathSpecGroup group;
+ String prefix;
+ String suffix;
+
+ // prefix based
+ if (servletPathSpec.charAt(0) == '/' && servletPathSpec.endsWith("/*"))
+ {
+ group = PathSpecGroup.PREFIX_GLOB;
+ prefix = servletPathSpec.substring(0, specLength - 2);
+ suffix = null;
+ }
+ // suffix based
+ else if (servletPathSpec.charAt(0) == '*' && servletPathSpec.length() > 1)
+ {
+ group = PathSpecGroup.SUFFIX_GLOB;
+ prefix = null;
+ suffix = servletPathSpec.substring(2, specLength);
+ }
+ else
+ {
+ group = PathSpecGroup.EXACT;
+ prefix = servletPathSpec;
+ suffix = null;
+ if (servletPathSpec.endsWith("*"))
+ {
+ LOG.warn("Suspicious URL pattern: '{}'; see sections 12.1 and 12.2 of the Servlet specification",
+ servletPathSpec);
+ }
+ }
+
+ int pathDepth = 0;
+ for (int i = 0; i < specLength; i++)
+ {
+ int cp = servletPathSpec.codePointAt(i);
+ if (cp < 128)
+ {
+ char c = (char)cp;
+ if (c == '/')
+ pathDepth++;
+ }
+ }
+
+ _declaration = servletPathSpec;
+ _group = group;
+ _pathDepth = pathDepth;
+ _specLength = specLength;
+ _prefix = prefix;
+ _suffix = suffix;
+ }
+
+ private static void assertValidServletPathSpec(String servletPathSpec)
+ {
+ if ((servletPathSpec == null) || servletPathSpec.equals(""))
+ {
+ return; // empty path spec
+ }
+
+ int len = servletPathSpec.length();
+ // path spec must either start with '/' or '*.'
+ if (servletPathSpec.charAt(0) == '/')
+ {
+ // Prefix Based
+ if (len == 1)
+ {
+ return; // simple '/' path spec
+ }
+ int idx = servletPathSpec.indexOf('*');
+ if (idx < 0)
+ {
+ return; // no hit on glob '*'
+ }
+ // only allowed to have '*' at the end of the path spec
+ if (idx != (len - 1))
+ {
+ throw new IllegalArgumentException("Servlet Spec 12.2 violation: glob '*' can only exist at end of prefix based matches: bad spec \"" + servletPathSpec + "\"");
+ }
+ }
+ else if (servletPathSpec.startsWith("*."))
+ {
+ // Suffix Based
+ int idx = servletPathSpec.indexOf('/');
+ // cannot have path separator
+ if (idx >= 0)
+ throw new IllegalArgumentException("Servlet Spec 12.2 violation: suffix based path spec cannot have path separators: bad spec \"" + servletPathSpec + "\"");
+
+ idx = servletPathSpec.indexOf('*', 2);
+ // only allowed to have 1 glob '*', at the start of the path spec
+ if (idx >= 1)
+ throw new IllegalArgumentException("Servlet Spec 12.2 violation: suffix based path spec cannot have multiple glob '*': bad spec \"" + servletPathSpec + "\"");
+ }
+ else
+ {
+ throw new IllegalArgumentException("Servlet Spec 12.2 violation: path spec must start with \"/\" or \"*.\": bad spec \"" + servletPathSpec + "\"");
+ }
+ }
+
+ @Override
+ public int getSpecLength()
+ {
+ return _specLength;
+ }
+
+ @Override
+ public PathSpecGroup getGroup()
+ {
+ return _group;
+ }
+
+ @Override
+ public int getPathDepth()
+ {
+ return _pathDepth;
+ }
+
+ @Override
+ public String getPathInfo(String path)
+ {
+ switch (_group)
+ {
+ case ROOT:
+ return path;
+
+ case PREFIX_GLOB:
+ if (path.length() == (_specLength - 2))
+ return null;
+ return path.substring(_specLength - 2);
+
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public String getPathMatch(String path)
+ {
+ switch (_group)
+ {
+ case ROOT:
+ return "";
+
+ case EXACT:
+ if (_declaration.equals(path))
+ return path;
+ return null;
+
+ case PREFIX_GLOB:
+ if (isWildcardMatch(path))
+ return path.substring(0, _specLength - 2);
+ return null;
+
+ case SUFFIX_GLOB:
+ if (path.regionMatches(path.length() - (_specLength - 1), _declaration, 1, _specLength - 1))
+ return path;
+ return null;
+
+ case DEFAULT:
+ return path;
+
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public String getDeclaration()
+ {
+ return _declaration;
+ }
+
+ @Override
+ public String getPrefix()
+ {
+ return _prefix;
+ }
+
+ @Override
+ public String getSuffix()
+ {
+ return _suffix;
+ }
+
+ private boolean isWildcardMatch(String path)
+ {
+ // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
+ int cpl = _specLength - 2;
+ if ((_group == PathSpecGroup.PREFIX_GLOB) && (path.regionMatches(0, _declaration, 0, cpl)))
+ return (path.length() == cpl) || ('/' == path.charAt(cpl));
+ return false;
+ }
+
+ @Override
+ public boolean matches(String path)
+ {
+ switch (_group)
+ {
+ case EXACT:
+ return _declaration.equals(path);
+ case PREFIX_GLOB:
+ return isWildcardMatch(path);
+ case SUFFIX_GLOB:
+ return path.regionMatches((path.length() - _specLength) + 1, _declaration, 1, _specLength - 1);
+ case ROOT:
+ // Only "/" matches
+ return ("/".equals(path));
+ case DEFAULT:
+ // If we reached this point, then everything matches
+ return true;
+ default:
+ return false;
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/UriTemplatePathSpec.java b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/UriTemplatePathSpec.java
new file mode 100644
index 0000000..3a34c26
--- /dev/null
+++ b/third_party/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/UriTemplatePathSpec.java
@@ -0,0 +1,424 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * PathSpec for URI Template based declarations
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc6570">URI Templates (Level 1)</a>
+ */
+public class UriTemplatePathSpec extends AbstractPathSpec
+{
+ private static final Logger LOG = Log.getLogger(UriTemplatePathSpec.class);
+
+ private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{(.*)\\}");
+ /**
+ * Reserved Symbols in URI Template variable
+ */
+ private static final String VARIABLE_RESERVED = ":/?#[]@" + // gen-delims
+ "!$&'()*+,;="; // sub-delims
+ /**
+ * Allowed Symbols in a URI Template variable
+ */
+ private static final String VARIABLE_SYMBOLS = "-._";
+ private static final Set<String> FORBIDDEN_SEGMENTS;
+
+ static
+ {
+ FORBIDDEN_SEGMENTS = new HashSet<>();
+ FORBIDDEN_SEGMENTS.add("/./");
+ FORBIDDEN_SEGMENTS.add("/../");
+ FORBIDDEN_SEGMENTS.add("//");
+ }
+
+ private final String _declaration;
+ private final PathSpecGroup _group;
+ private final int _pathDepth;
+ private final int _specLength;
+ private final Pattern _pattern;
+ private final String[] _variables;
+ /**
+ * The logical (simplified) declaration
+ */
+ private final String _logicalDeclaration;
+
+ public UriTemplatePathSpec(String rawSpec)
+ {
+ Objects.requireNonNull(rawSpec, "Path Param Spec cannot be null");
+
+ if ("".equals(rawSpec) || "/".equals(rawSpec))
+ {
+ _declaration = "/";
+ _group = PathSpecGroup.EXACT;
+ _pathDepth = 1;
+ _specLength = 1;
+ _pattern = Pattern.compile("^/$");
+ _variables = new String[0];
+ _logicalDeclaration = "/";
+ return;
+ }
+
+ if (rawSpec.charAt(0) != '/')
+ {
+ // path specs must start with '/'
+ throw new IllegalArgumentException("Syntax Error: path spec \"" + rawSpec + "\" must start with '/'");
+ }
+
+ for (String forbidden : FORBIDDEN_SEGMENTS)
+ {
+ if (rawSpec.contains(forbidden))
+ throw new IllegalArgumentException("Syntax Error: segment " + forbidden + " is forbidden in path spec: " + rawSpec);
+ }
+
+ String declaration = rawSpec;
+ StringBuilder regex = new StringBuilder();
+ regex.append('^');
+
+ List<String> varNames = new ArrayList<>();
+ // split up into path segments (ignoring the first slash that will always be empty)
+ String[] segments = rawSpec.substring(1).split("/");
+ char[] segmentSignature = new char[segments.length];
+ StringBuilder logicalSignature = new StringBuilder();
+ int pathDepth = segments.length;
+ for (int i = 0; i < segments.length; i++)
+ {
+ String segment = segments[i];
+ Matcher mat = VARIABLE_PATTERN.matcher(segment);
+
+ if (mat.matches())
+ {
+ // entire path segment is a variable.
+ String variable = mat.group(1);
+ if (varNames.contains(variable))
+ {
+ // duplicate variable names
+ throw new IllegalArgumentException("Syntax Error: variable " + variable + " is duplicated in path spec: " + rawSpec);
+ }
+
+ assertIsValidVariableLiteral(variable, declaration);
+
+ segmentSignature[i] = 'v'; // variable
+ logicalSignature.append("/*");
+ // valid variable name
+ varNames.add(variable);
+ // build regex
+ regex.append("/([^/]+)");
+ }
+ else if (mat.find(0))
+ {
+ // variable exists as partial segment
+ throw new IllegalArgumentException("Syntax Error: variable " + mat.group() + " must exist as entire path segment: " + rawSpec);
+ }
+ else if ((segment.indexOf('{') >= 0) || (segment.indexOf('}') >= 0))
+ {
+ // variable is split with a path separator
+ throw new IllegalArgumentException("Syntax Error: invalid path segment /" + segment + "/ variable declaration incomplete: " + rawSpec);
+ }
+ else if (segment.indexOf('*') >= 0)
+ {
+ // glob segment
+ throw new IllegalArgumentException("Syntax Error: path segment /" + segment + "/ contains a wildcard symbol (not supported by this uri-template implementation): " + rawSpec);
+ }
+ else
+ {
+ // valid path segment
+ segmentSignature[i] = 'e'; // exact
+ logicalSignature.append('/').append(segment);
+ // build regex
+ regex.append('/');
+ // escape regex special characters
+ for (int j = 0; j < segment.length(); j++)
+ {
+ char c = segment.charAt(j);
+ if ((c == '.') || (c == '[') || (c == ']') || (c == '\\'))
+ regex.append('\\');
+ regex.append(c);
+ }
+ }
+ }
+
+ // Handle trailing slash (which is not picked up during split)
+ if (rawSpec.charAt(rawSpec.length() - 1) == '/')
+ {
+ regex.append('/');
+ logicalSignature.append('/');
+ }
+
+ regex.append('$');
+
+ Pattern pattern = Pattern.compile(regex.toString());
+
+ int varcount = varNames.size();
+ String[] variables = varNames.toArray(new String[varcount]);
+
+ // Convert signature to group
+ String sig = String.valueOf(segmentSignature);
+
+ PathSpecGroup group;
+ if (Pattern.matches("^e*$", sig))
+ group = PathSpecGroup.EXACT;
+ else if (Pattern.matches("^e*v+", sig))
+ group = PathSpecGroup.PREFIX_GLOB;
+ else if (Pattern.matches("^v+e+", sig))
+ group = PathSpecGroup.SUFFIX_GLOB;
+ else
+ group = PathSpecGroup.MIDDLE_GLOB;
+
+ _declaration = declaration;
+ _group = group;
+ _pathDepth = pathDepth;
+ _specLength = declaration.length();
+ _pattern = pattern;
+ _variables = variables;
+ _logicalDeclaration = logicalSignature.toString();
+ }
+
+ /**
+ * Validate variable literal name, per RFC6570, Section 2.1 Literals
+ */
+ private static void assertIsValidVariableLiteral(String variable, String declaration)
+ {
+ int len = variable.length();
+
+ int i = 0;
+ int codepoint;
+ boolean valid = (len > 0); // must not be zero length
+
+ while (valid && i < len)
+ {
+ codepoint = variable.codePointAt(i);
+ i += Character.charCount(codepoint);
+
+ // basic letters, digits, or symbols
+ if (isValidBasicLiteralCodepoint(codepoint, declaration))
+ continue;
+
+ // The ucschar and iprivate pieces
+ if (Character.isSupplementaryCodePoint(codepoint))
+ continue;
+
+ // pct-encoded
+ if (codepoint == '%')
+ {
+ if (i + 2 > len)
+ {
+ // invalid percent encoding, missing extra 2 chars
+ valid = false;
+ continue;
+ }
+ codepoint = TypeUtil.convertHexDigit(variable.codePointAt(i++)) << 4;
+ codepoint |= TypeUtil.convertHexDigit(variable.codePointAt(i++));
+
+ // validate basic literal
+ if (isValidBasicLiteralCodepoint(codepoint, declaration))
+ continue;
+ }
+
+ valid = false;
+ }
+
+ if (!valid)
+ {
+ // invalid variable name
+ throw new IllegalArgumentException("Syntax Error: variable {" + variable + "} an invalid variable name: " + declaration);
+ }
+ }
+
+ private static boolean isValidBasicLiteralCodepoint(int codepoint, String declaration)
+ {
+ // basic letters or digits
+ if ((codepoint >= 'a' && codepoint <= 'z') ||
+ (codepoint >= 'A' && codepoint <= 'Z') ||
+ (codepoint >= '0' && codepoint <= '9'))
+ return true;
+
+ // basic allowed symbols
+ if (VARIABLE_SYMBOLS.indexOf(codepoint) >= 0)
+ return true; // valid simple value
+
+ // basic reserved symbols
+ if (VARIABLE_RESERVED.indexOf(codepoint) >= 0)
+ {
+ LOG.warn("Detected URI Template reserved symbol [{}] in path spec \"{}\"", (char)codepoint, declaration);
+ return false; // valid simple value
+ }
+
+ return false;
+ }
+
+ @Override
+ public int compareTo(PathSpec other)
+ {
+ if (other instanceof UriTemplatePathSpec)
+ {
+ UriTemplatePathSpec otherUriPathSpec = (UriTemplatePathSpec)other;
+ return otherUriPathSpec._logicalDeclaration.compareTo(this._logicalDeclaration);
+ }
+ else
+ {
+ return super.compareTo(other);
+ }
+ }
+
+ public Map<String, String> getPathParams(String path)
+ {
+ Matcher matcher = getMatcher(path);
+ if (matcher.matches())
+ {
+ if (_group == PathSpecGroup.EXACT)
+ return Collections.emptyMap();
+ Map<String, String> ret = new HashMap<>();
+ int groupCount = matcher.groupCount();
+ for (int i = 1; i <= groupCount; i++)
+ ret.put(_variables[i - 1], matcher.group(i));
+ return ret;
+ }
+ return null;
+ }
+
+ protected Matcher getMatcher(String path)
+ {
+ return _pattern.matcher(path);
+ }
+
+ @Override
+ public int getSpecLength()
+ {
+ return _specLength;
+ }
+
+ @Override
+ public PathSpecGroup getGroup()
+ {
+ return _group;
+ }
+
+ @Override
+ public int getPathDepth()
+ {
+ return _pathDepth;
+ }
+
+ @Override
+ public String getPathInfo(String path)
+ {
+ // Path Info only valid for PREFIX_GLOB types
+ if (_group == PathSpecGroup.PREFIX_GLOB)
+ {
+ Matcher matcher = getMatcher(path);
+ if (matcher.matches())
+ {
+ if (matcher.groupCount() >= 1)
+ {
+ String pathInfo = matcher.group(1);
+ if ("".equals(pathInfo))
+ return "/";
+ else
+ return pathInfo;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getPathMatch(String path)
+ {
+ Matcher matcher = getMatcher(path);
+ if (matcher.matches())
+ {
+ if (matcher.groupCount() >= 1)
+ {
+ int idx = matcher.start(1);
+ if (idx > 0)
+ {
+ if (path.charAt(idx - 1) == '/')
+ idx--;
+ return path.substring(0, idx);
+ }
+ }
+ return path;
+ }
+ return null;
+ }
+
+ @Override
+ public String getDeclaration()
+ {
+ return _declaration;
+ }
+
+ @Override
+ public String getPrefix()
+ {
+ return null;
+ }
+
+ @Override
+ public String getSuffix()
+ {
+ return null;
+ }
+
+ public Pattern getPattern()
+ {
+ return _pattern;
+ }
+
+ @Override
+ public boolean matches(final String path)
+ {
+ int idx = path.indexOf('?');
+ if (idx >= 0)
+ {
+ // match only non-query part
+ return getMatcher(path.substring(0, idx)).matches();
+ }
+ else
+ {
+ // match entire path
+ return getMatcher(path).matches();
+ }
+ }
+
+ public int getVariableCount()
+ {
+ return _variables.length;
+ }
+
+ public String[] getVariables()
+ {
+ return _variables;
+ }
+}
diff --git a/third_party/jetty-http/src/main/resources/META-INF/services/org.eclipse.jetty.http.HttpFieldPreEncoder b/third_party/jetty-http/src/main/resources/META-INF/services/org.eclipse.jetty.http.HttpFieldPreEncoder
new file mode 100644
index 0000000..92640bd
--- /dev/null
+++ b/third_party/jetty-http/src/main/resources/META-INF/services/org.eclipse.jetty.http.HttpFieldPreEncoder
@@ -0,0 +1 @@
+org.eclipse.jetty.http.Http1FieldPreEncoder
\ No newline at end of file
diff --git a/third_party/jetty-http/src/main/resources/org/eclipse/jetty/http/encoding.properties b/third_party/jetty-http/src/main/resources/org/eclipse/jetty/http/encoding.properties
new file mode 100644
index 0000000..3f2d2dd
--- /dev/null
+++ b/third_party/jetty-http/src/main/resources/org/eclipse/jetty/http/encoding.properties
@@ -0,0 +1,12 @@
+# Mapping of mime type to inferred or assumed charset
+# inferred charsets are used for encoding/decoding and explicitly set in associated metadata
+# assumed charsets are used for encoding/decoding, but are not set in associated metadata
+# In this file, assumed charsets are indicated with a leading '-'
+
+text/html=utf-8
+text/plain=iso-8859-1
+text/xml=utf-8
+application/xhtml+xml=utf-8
+text/json=-utf-8
+application/json=-utf-8
+application/vnd.api+json=-utf-8
\ No newline at end of file
diff --git a/third_party/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties b/third_party/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties
new file mode 100644
index 0000000..fe22dea
--- /dev/null
+++ b/third_party/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties
@@ -0,0 +1,199 @@
+ai=application/postscript
+aif=audio/x-aiff
+aifc=audio/x-aiff
+aiff=audio/x-aiff
+apk=application/vnd.android.package-archive
+apng=image/apng
+asc=text/plain
+asf=video/x.ms.asf
+asx=video/x.ms.asx
+au=audio/basic
+avi=video/x-msvideo
+avif=image/avif
+bcpio=application/x-bcpio
+bin=application/octet-stream
+bmp=image/bmp
+br=application/brotli
+cab=application/x-cabinet
+cdf=application/x-netcdf
+chm=application/vnd.ms-htmlhelp
+class=application/java-vm
+cpio=application/x-cpio
+cpt=application/mac-compactpro
+crt=application/x-x509-ca-cert
+csh=application/x-csh
+css=text/css
+csv=text/csv
+dcr=application/x-director
+dir=application/x-director
+dll=application/x-msdownload
+dms=application/octet-stream
+doc=application/msword
+dtd=application/xml-dtd
+dvi=application/x-dvi
+dxr=application/x-director
+eot=application/vnd.ms-fontobject
+eps=application/postscript
+etx=text/x-setext
+exe=application/octet-stream
+ez=application/andrew-inset
+gif=image/gif
+gtar=application/x-gtar
+gz=application/gzip
+gzip=application/gzip
+hdf=application/x-hdf
+hqx=application/mac-binhex40
+htc=text/x-component
+htm=text/html
+html=text/html
+ice=x-conference/x-cooltalk
+ico=image/x-icon
+ief=image/ief
+iges=model/iges
+igs=model/iges
+jad=text/vnd.sun.j2me.app-descriptor
+jar=application/java-archive
+java=text/plain
+jnlp=application/x-java-jnlp-file
+jpe=image/jpeg
+jp2=image/jpeg2000
+jpeg=image/jpeg
+jpg=image/jpeg
+js=application/javascript
+json=application/json
+jsp=text/html
+kar=audio/midi
+latex=application/x-latex
+lha=application/octet-stream
+lzh=application/octet-stream
+man=application/x-troff-man
+mathml=application/mathml+xml
+me=application/x-troff-me
+mesh=model/mesh
+mid=audio/midi
+midi=audio/midi
+mif=application/vnd.mif
+mol=chemical/x-mdl-molfile
+mov=video/quicktime
+movie=video/x-sgi-movie
+mp2=audio/mpeg
+mp3=audio/mpeg
+mp4=video/mp4
+mpe=video/mpeg
+mpeg=video/mpeg
+mpg=video/mpeg
+mpga=audio/mpeg
+ms=application/x-troff-ms
+msh=model/mesh
+msi=application/octet-stream
+nc=application/x-netcdf
+oda=application/oda
+odb=application/vnd.oasis.opendocument.database
+odc=application/vnd.oasis.opendocument.chart
+odf=application/vnd.oasis.opendocument.formula
+odg=application/vnd.oasis.opendocument.graphics
+odi=application/vnd.oasis.opendocument.image
+odm=application/vnd.oasis.opendocument.text-master
+odp=application/vnd.oasis.opendocument.presentation
+ods=application/vnd.oasis.opendocument.spreadsheet
+odt=application/vnd.oasis.opendocument.text
+ogg=application/ogg
+otc=application/vnd.oasis.opendocument.chart-template
+otf=application/vnd.oasis.opendocument.formula-template
+otg=application/vnd.oasis.opendocument.graphics-template
+oth=application/vnd.oasis.opendocument.text-web
+oti=application/vnd.oasis.opendocument.image-template
+otp=application/vnd.oasis.opendocument.presentation-template
+ots=application/vnd.oasis.opendocument.spreadsheet-template
+ott=application/vnd.oasis.opendocument.text-template
+pbm=image/x-portable-bitmap
+pdb=chemical/x-pdb
+pdf=application/pdf
+pgm=image/x-portable-graymap
+pgn=application/x-chess-pgn
+png=image/png
+pnm=image/x-portable-anymap
+ppm=image/x-portable-pixmap
+pps=application/vnd.ms-powerpoint
+ppt=application/vnd.ms-powerpoint
+ps=application/postscript
+qml=text/x-qml
+qt=video/quicktime
+ra=audio/x-pn-realaudio
+rar=application/x-rar-compressed
+ram=audio/x-pn-realaudio
+ras=image/x-cmu-raster
+rdf=application/rdf+xml
+rgb=image/x-rgb
+rm=audio/x-pn-realaudio
+roff=application/x-troff
+rpm=application/x-rpm
+rtf=application/rtf
+rtx=text/richtext
+rv=video/vnd.rn-realvideo
+ser=application/java-serialized-object
+sgm=text/sgml
+sgml=text/sgml
+sh=application/x-sh
+shar=application/x-shar
+silo=model/mesh
+sit=application/x-stuffit
+skd=application/x-koan
+skm=application/x-koan
+skp=application/x-koan
+skt=application/x-koan
+smi=application/smil
+smil=application/smil
+snd=audio/basic
+spl=application/x-futuresplash
+src=application/x-wais-source
+sv4cpio=application/x-sv4cpio
+sv4crc=application/x-sv4crc
+svg=image/svg+xml
+svgz=image/svg+xml
+swf=application/x-shockwave-flash
+t=application/x-troff
+tar=application/x-tar
+tar.gz=application/x-gtar
+tcl=application/x-tcl
+tex=application/x-tex
+texi=application/x-texinfo
+texinfo=application/x-texinfo
+tgz=application/x-gtar
+tif=image/tiff
+tiff=image/tiff
+tr=application/x-troff
+tsv=text/tab-separated-values
+txt=text/plain
+ustar=application/x-ustar
+vcd=application/x-cdlink
+vrml=model/vrml
+vxml=application/voicexml+xml
+wasm=application/wasm
+wav=audio/x-wav
+wbmp=image/vnd.wap.wbmp
+webp=image/webp
+wml=text/vnd.wap.wml
+wmlc=application/vnd.wap.wmlc
+wmls=text/vnd.wap.wmlscript
+wmlsc=application/vnd.wap.wmlscriptc
+woff=application/font-woff
+woff2=font/woff2
+wrl=model/vrml
+wtls-ca-certificate=application/vnd.wap.wtls-ca-certificate
+xbm=image/x-xbitmap
+xcf=image/xcf
+xht=application/xhtml+xml
+xhtml=application/xhtml+xml
+xls=application/vnd.ms-excel
+xml=application/xml
+xpm=image/x-xpixmap
+xsd=application/xml
+xsl=application/xml
+xslt=application/xslt+xml
+xul=application/vnd.mozilla.xul+xml
+xwd=image/x-xwindowdump
+xyz=chemical/x-xyz
+xz=application/x-xz
+z=application/compress
+zip=application/zip
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/DateParserTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/DateParserTest.java
new file mode 100644
index 0000000..5d53cc0
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/DateParserTest.java
@@ -0,0 +1,44 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Unit tests for class {@link DateParser}.
+ *
+ * @see DateParser
+ */
+public class DateParserTest
+{
+
+ @Test
+ public void testParseDateWithNonDateString()
+ {
+ assertEquals((-1L), DateParser.parseDate("3%~"));
+ }
+
+ @Test
+ public void testParseDateWithNonDateStringEndingWithGMT()
+ {
+ assertEquals((-1L), DateParser.parseDate("3%~ GMT"));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/GZIPContentDecoderTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/GZIPContentDecoderTest.java
new file mode 100644
index 0000000..9598a47
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/GZIPContentDecoderTest.java
@@ -0,0 +1,455 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+import org.eclipse.jetty.io.ArrayByteBufferPool;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.eclipse.jetty.http.CompressedContentFormat.BR;
+import static org.eclipse.jetty.http.CompressedContentFormat.GZIP;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class GZIPContentDecoderTest
+{
+ private ArrayByteBufferPool pool;
+ private AtomicInteger buffers = new AtomicInteger(0);
+
+ @BeforeEach
+ public void before()
+ {
+ buffers.set(0);
+ pool = new ArrayByteBufferPool()
+ {
+
+ @Override
+ public ByteBuffer acquire(int size, boolean direct)
+ {
+ buffers.incrementAndGet();
+ return super.acquire(size, direct);
+ }
+
+ @Override
+ public void release(ByteBuffer buffer)
+ {
+ buffers.decrementAndGet();
+ super.release(buffer);
+ }
+ };
+ }
+
+ @AfterEach
+ public void after()
+ {
+ assertEquals(0, buffers.get());
+ }
+
+ @Test
+ public void testCompressedContentFormat()
+ {
+ assertTrue(CompressedContentFormat.tagEquals("tag", "tag"));
+ assertTrue(CompressedContentFormat.tagEquals("\"tag\"", "\"tag\""));
+ assertTrue(CompressedContentFormat.tagEquals("\"tag\"", "\"tag" + GZIP.getEtagSuffix() + "\""));
+ assertTrue(CompressedContentFormat.tagEquals("\"tag\"", "\"tag" + BR.getEtagSuffix() + "\""));
+ assertTrue(CompressedContentFormat.tagEquals("W/\"1234567\"", "W/\"1234567\""));
+ assertTrue(CompressedContentFormat.tagEquals("W/\"1234567\"", "W/\"1234567" + GZIP.getEtagSuffix() + "\""));
+
+ assertFalse(CompressedContentFormat.tagEquals("Zag", "Xag" + GZIP.getEtagSuffix()));
+ assertFalse(CompressedContentFormat.tagEquals("xtag", "tag"));
+ assertFalse(CompressedContentFormat.tagEquals("W/\"1234567\"", "W/\"1234111\""));
+ assertFalse(CompressedContentFormat.tagEquals("W/\"1234567\"", "W/\"1234111" + GZIP.getEtagSuffix() + "\""));
+
+ assertTrue(CompressedContentFormat.tagEquals("12345", "\"12345\""));
+ assertTrue(CompressedContentFormat.tagEquals("\"12345\"", "12345"));
+ assertTrue(CompressedContentFormat.tagEquals("12345", "\"12345" + GZIP.getEtagSuffix() + "\""));
+ assertTrue(CompressedContentFormat.tagEquals("\"12345\"", "12345" + GZIP.getEtagSuffix()));
+
+ assertThat(GZIP.stripSuffixes("12345"), is("12345"));
+ assertThat(GZIP.stripSuffixes("12345, 666" + GZIP.getEtagSuffix()), is("12345, 666"));
+ assertThat(GZIP.stripSuffixes("12345, 666" + GZIP.getEtagSuffix() + ",W/\"9999" + GZIP.getEtagSuffix() + "\""),
+ is("12345, 666,W/\"9999\""));
+ }
+
+ @Test
+ public void testStreamNoBlocks() throws Exception
+ {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ GZIPOutputStream output = new GZIPOutputStream(baos);
+ output.close();
+ byte[] bytes = baos.toByteArray();
+
+ GZIPInputStream input = new GZIPInputStream(new ByteArrayInputStream(bytes), 1);
+ int read = input.read();
+ assertEquals(-1, read);
+ }
+
+ @Test
+ public void testStreamBigBlockOneByteAtATime() throws Exception
+ {
+ String data = "0123456789ABCDEF";
+ for (int i = 0; i < 10; ++i)
+ {
+ data += data;
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ GZIPOutputStream output = new GZIPOutputStream(baos);
+ output.write(data.getBytes(StandardCharsets.UTF_8));
+ output.close();
+ byte[] bytes = baos.toByteArray();
+
+ baos = new ByteArrayOutputStream();
+ GZIPInputStream input = new GZIPInputStream(new ByteArrayInputStream(bytes), 1);
+ int read;
+ while ((read = input.read()) >= 0)
+ {
+ baos.write(read);
+ }
+ assertEquals(data, new String(baos.toByteArray(), StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void testNoBlocks() throws Exception
+ {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ GZIPOutputStream output = new GZIPOutputStream(baos);
+ output.close();
+ byte[] bytes = baos.toByteArray();
+
+ GZIPContentDecoder decoder = new GZIPContentDecoder(pool, 2048);
+ ByteBuffer decoded = decoder.decode(ByteBuffer.wrap(bytes));
+ assertEquals(0, decoded.remaining());
+ }
+
+ @Test
+ public void testSmallBlock() throws Exception
+ {
+ String data = "0";
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ GZIPOutputStream output = new GZIPOutputStream(baos);
+ output.write(data.getBytes(StandardCharsets.UTF_8));
+ output.close();
+ byte[] bytes = baos.toByteArray();
+
+ GZIPContentDecoder decoder = new GZIPContentDecoder(pool, 2048);
+ ByteBuffer decoded = decoder.decode(ByteBuffer.wrap(bytes));
+ assertEquals(data, StandardCharsets.UTF_8.decode(decoded).toString());
+ decoder.release(decoded);
+ }
+
+ @Test
+ public void testSmallBlockWithGZIPChunkedAtBegin() throws Exception
+ {
+ String data = "0";
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ GZIPOutputStream output = new GZIPOutputStream(baos);
+ output.write(data.getBytes(StandardCharsets.UTF_8));
+ output.close();
+ byte[] bytes = baos.toByteArray();
+
+ // The header is 10 bytes, chunk at 11 bytes
+ byte[] bytes1 = new byte[11];
+ System.arraycopy(bytes, 0, bytes1, 0, bytes1.length);
+ byte[] bytes2 = new byte[bytes.length - bytes1.length];
+ System.arraycopy(bytes, bytes1.length, bytes2, 0, bytes2.length);
+
+ GZIPContentDecoder decoder = new GZIPContentDecoder(pool, 2048);
+ ByteBuffer decoded = decoder.decode(ByteBuffer.wrap(bytes1));
+ assertEquals(0, decoded.capacity());
+ decoded = decoder.decode(ByteBuffer.wrap(bytes2));
+ assertEquals(data, StandardCharsets.UTF_8.decode(decoded).toString());
+ decoder.release(decoded);
+ }
+
+ @Test
+ public void testSmallBlockWithGZIPChunkedAtEnd() throws Exception
+ {
+ String data = "0";
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ GZIPOutputStream output = new GZIPOutputStream(baos);
+ output.write(data.getBytes(StandardCharsets.UTF_8));
+ output.close();
+ byte[] bytes = baos.toByteArray();
+
+ // The trailer is 8 bytes, chunk the last 9 bytes
+ byte[] bytes1 = new byte[bytes.length - 9];
+ System.arraycopy(bytes, 0, bytes1, 0, bytes1.length);
+ byte[] bytes2 = new byte[bytes.length - bytes1.length];
+ System.arraycopy(bytes, bytes1.length, bytes2, 0, bytes2.length);
+
+ GZIPContentDecoder decoder = new GZIPContentDecoder(pool, 2048);
+ ByteBuffer decoded = decoder.decode(ByteBuffer.wrap(bytes1));
+ assertEquals(data, StandardCharsets.UTF_8.decode(decoded).toString());
+ assertFalse(decoder.isFinished());
+ decoder.release(decoded);
+ decoded = decoder.decode(ByteBuffer.wrap(bytes2));
+ assertEquals(0, decoded.remaining());
+ assertTrue(decoder.isFinished());
+ decoder.release(decoded);
+ }
+
+ @Test
+ public void testSmallBlockWithGZIPTrailerChunked() throws Exception
+ {
+ String data = "0";
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ GZIPOutputStream output = new GZIPOutputStream(baos);
+ output.write(data.getBytes(StandardCharsets.UTF_8));
+ output.close();
+ byte[] bytes = baos.toByteArray();
+
+ // The trailer is 4+4 bytes, chunk the last 3 bytes
+ byte[] bytes1 = new byte[bytes.length - 3];
+ System.arraycopy(bytes, 0, bytes1, 0, bytes1.length);
+ byte[] bytes2 = new byte[bytes.length - bytes1.length];
+ System.arraycopy(bytes, bytes1.length, bytes2, 0, bytes2.length);
+
+ GZIPContentDecoder decoder = new GZIPContentDecoder(pool, 2048);
+ ByteBuffer decoded = decoder.decode(ByteBuffer.wrap(bytes1));
+ assertEquals(0, decoded.capacity());
+ decoder.release(decoded);
+ decoded = decoder.decode(ByteBuffer.wrap(bytes2));
+ assertEquals(data, StandardCharsets.UTF_8.decode(decoded).toString());
+ decoder.release(decoded);
+ }
+
+ @Test
+ public void testTwoSmallBlocks() throws Exception
+ {
+ String data1 = "0";
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ GZIPOutputStream output = new GZIPOutputStream(baos);
+ output.write(data1.getBytes(StandardCharsets.UTF_8));
+ output.close();
+ byte[] bytes1 = baos.toByteArray();
+
+ String data2 = "1";
+ baos = new ByteArrayOutputStream();
+ output = new GZIPOutputStream(baos);
+ output.write(data2.getBytes(StandardCharsets.UTF_8));
+ output.close();
+ byte[] bytes2 = baos.toByteArray();
+
+ byte[] bytes = new byte[bytes1.length + bytes2.length];
+ System.arraycopy(bytes1, 0, bytes, 0, bytes1.length);
+ System.arraycopy(bytes2, 0, bytes, bytes1.length, bytes2.length);
+
+ GZIPContentDecoder decoder = new GZIPContentDecoder(pool, 2048);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes);
+ ByteBuffer decoded = decoder.decode(buffer);
+ assertEquals(data1, StandardCharsets.UTF_8.decode(decoded).toString());
+ assertTrue(decoder.isFinished());
+ assertTrue(buffer.hasRemaining());
+ decoder.release(decoded);
+ decoded = decoder.decode(buffer);
+ assertEquals(data2, StandardCharsets.UTF_8.decode(decoded).toString());
+ assertTrue(decoder.isFinished());
+ assertFalse(buffer.hasRemaining());
+ decoder.release(decoded);
+ }
+
+ @Test
+ public void testBigBlock() throws Exception
+ {
+ String data = "0123456789ABCDEF";
+ for (int i = 0; i < 10; ++i)
+ {
+ data += data;
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ GZIPOutputStream output = new GZIPOutputStream(baos);
+ output.write(data.getBytes(StandardCharsets.UTF_8));
+ output.close();
+ byte[] bytes = baos.toByteArray();
+
+ String result = "";
+ GZIPContentDecoder decoder = new GZIPContentDecoder(pool, 2048);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes);
+ while (buffer.hasRemaining())
+ {
+ ByteBuffer decoded = decoder.decode(buffer);
+ result += StandardCharsets.UTF_8.decode(decoded).toString();
+ decoder.release(decoded);
+ }
+ assertEquals(data, result);
+ }
+
+ @Test
+ public void testBigBlockOneByteAtATime() throws Exception
+ {
+ String data = "0123456789ABCDEF";
+ for (int i = 0; i < 10; ++i)
+ {
+ data += data;
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ GZIPOutputStream output = new GZIPOutputStream(baos);
+ output.write(data.getBytes(StandardCharsets.UTF_8));
+ output.close();
+ byte[] bytes = baos.toByteArray();
+
+ String result = "";
+ GZIPContentDecoder decoder = new GZIPContentDecoder(64);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes);
+ while (buffer.hasRemaining())
+ {
+ ByteBuffer decoded = decoder.decode(ByteBuffer.wrap(new byte[]{buffer.get()}));
+ if (decoded.hasRemaining())
+ result += StandardCharsets.UTF_8.decode(decoded).toString();
+ decoder.release(decoded);
+ }
+ assertEquals(data, result);
+ assertTrue(decoder.isFinished());
+ }
+
+ @Test
+ public void testBigBlockWithExtraBytes() throws Exception
+ {
+ String data1 = "0123456789ABCDEF";
+ for (int i = 0; i < 10; ++i)
+ {
+ data1 += data1;
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ GZIPOutputStream output = new GZIPOutputStream(baos);
+ output.write(data1.getBytes(StandardCharsets.UTF_8));
+ output.close();
+ byte[] bytes1 = baos.toByteArray();
+
+ String data2 = "HELLO";
+ byte[] bytes2 = data2.getBytes(StandardCharsets.UTF_8);
+
+ byte[] bytes = new byte[bytes1.length + bytes2.length];
+ System.arraycopy(bytes1, 0, bytes, 0, bytes1.length);
+ System.arraycopy(bytes2, 0, bytes, bytes1.length, bytes2.length);
+
+ String result = "";
+ GZIPContentDecoder decoder = new GZIPContentDecoder(64);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes);
+ while (buffer.hasRemaining())
+ {
+ ByteBuffer decoded = decoder.decode(buffer);
+ if (decoded.hasRemaining())
+ result += StandardCharsets.UTF_8.decode(decoded).toString();
+ decoder.release(decoded);
+ if (decoder.isFinished())
+ break;
+ }
+ assertEquals(data1, result);
+ assertTrue(buffer.hasRemaining());
+ assertEquals(data2, StandardCharsets.UTF_8.decode(buffer).toString());
+ }
+
+ // Signed Integer Max
+ static final long INT_MAX = Integer.MAX_VALUE;
+
+ // Unsigned Integer Max == 2^32
+ static final long UINT_MAX = 0xFFFFFFFFL;
+
+ @ParameterizedTest
+ @ValueSource(longs = {INT_MAX, INT_MAX + 1, UINT_MAX, UINT_MAX + 1})
+ public void testLargeGzipStream(long origSize) throws IOException
+ {
+ // Size chosen for trade off between speed of I/O vs speed of Gzip
+ final int BUFSIZE = 1024 * 1024;
+
+ // Create a buffer to use over and over again to produce the uncompressed input
+ byte[] cbuf = "0123456789ABCDEFGHIJKLMOPQRSTUVWXYZ".getBytes(StandardCharsets.UTF_8);
+ byte[] buf = new byte[BUFSIZE];
+ for (int off = 0; off < buf.length; )
+ {
+ int len = Math.min(cbuf.length, buf.length - off);
+ System.arraycopy(cbuf, 0, buf, off, len);
+ off += len;
+ }
+
+ GZIPDecoderOutputStream out = new GZIPDecoderOutputStream(new GZIPContentDecoder(BUFSIZE));
+ GZIPOutputStream outputStream = new GZIPOutputStream(out, BUFSIZE);
+
+ for (long bytesLeft = origSize; bytesLeft > 0; )
+ {
+ int len = buf.length;
+ if (bytesLeft < buf.length)
+ {
+ len = (int)bytesLeft;
+ }
+ outputStream.write(buf, 0, len);
+ bytesLeft -= len;
+ }
+
+ // Close GZIPOutputStream to have it generate gzip trailer.
+ // This can cause more writes of unflushed gzip buffers
+ outputStream.close();
+
+ // out.decodedByteCount is only valid after close
+ assertThat("Decoded byte count", out.decodedByteCount, is(origSize));
+ }
+
+ public static class GZIPDecoderOutputStream extends OutputStream
+ {
+ private final GZIPContentDecoder decoder;
+ public long decodedByteCount = 0L;
+
+ public GZIPDecoderOutputStream(GZIPContentDecoder decoder)
+ {
+ this.decoder = decoder;
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException
+ {
+ ByteBuffer buf = ByteBuffer.wrap(b, off, len);
+ while (buf.hasRemaining())
+ {
+ ByteBuffer decoded = decoder.decode(buf);
+ if (decoded.hasRemaining())
+ {
+ decodedByteCount += decoded.remaining();
+ }
+ decoder.release(decoded);
+ }
+ }
+
+ @Override
+ public void write(int b) throws IOException
+ {
+ write(new byte[]{(byte)b}, 0, 1);
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieTest.java
new file mode 100644
index 0000000..7d7224d
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieTest.java
@@ -0,0 +1,710 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.EventListener;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+import javax.servlet.Filter;
+import javax.servlet.FilterRegistration;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.Servlet;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRegistration;
+import javax.servlet.ServletRegistration.Dynamic;
+import javax.servlet.SessionCookieConfig;
+import javax.servlet.SessionTrackingMode;
+import javax.servlet.descriptor.JspConfigDescriptor;
+
+import org.eclipse.jetty.http.HttpCookie.SameSite;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HttpCookieTest
+{
+ public static class TestServletContext implements ServletContext
+ {
+ private Map<String, Object> _attributes = new HashMap<>();
+
+ @Override
+ public String getContextPath()
+ {
+ return null;
+ }
+
+ @Override
+ public ServletContext getContext(String uripath)
+ {
+ return null;
+ }
+
+ @Override
+ public int getMajorVersion()
+ {
+ return 0;
+ }
+
+ @Override
+ public int getMinorVersion()
+ {
+ return 0;
+ }
+
+ @Override
+ public int getEffectiveMajorVersion()
+ {
+ return 0;
+ }
+
+ @Override
+ public int getEffectiveMinorVersion()
+ {
+ return 0;
+ }
+
+ @Override
+ public String getMimeType(String file)
+ {
+ return null;
+ }
+
+ @Override
+ public Set<String> getResourcePaths(String path)
+ {
+ return null;
+ }
+
+ @Override
+ public URL getResource(String path) throws MalformedURLException
+ {
+ return null;
+ }
+
+ @Override
+ public InputStream getResourceAsStream(String path)
+ {
+ return null;
+ }
+
+ @Override
+ public RequestDispatcher getRequestDispatcher(String path)
+ {
+ return null;
+ }
+
+ @Override
+ public RequestDispatcher getNamedDispatcher(String name)
+ {
+ return null;
+ }
+
+ @Override
+ public Servlet getServlet(String name) throws ServletException
+ {
+ return null;
+ }
+
+ @Override
+ public Enumeration<Servlet> getServlets()
+ {
+ return null;
+ }
+
+ @Override
+ public Enumeration<String> getServletNames()
+ {
+ return null;
+ }
+
+ @Override
+ public void log(String msg)
+ {
+ }
+
+ @Override
+ public void log(Exception exception, String msg)
+ {
+ }
+
+ @Override
+ public void log(String message, Throwable throwable)
+ {
+ }
+
+ @Override
+ public String getRealPath(String path)
+ {
+ return null;
+ }
+
+ @Override
+ public String getServerInfo()
+ {
+ return null;
+ }
+
+ @Override
+ public String getInitParameter(String name)
+ {
+ return null;
+ }
+
+ @Override
+ public Enumeration<String> getInitParameterNames()
+ {
+ return null;
+ }
+
+ @Override
+ public boolean setInitParameter(String name, String value)
+ {
+ return false;
+ }
+
+ @Override
+ public Object getAttribute(String name)
+ {
+ return _attributes.get(name);
+ }
+
+ @Override
+ public Enumeration<String> getAttributeNames()
+ {
+ return Collections.enumeration(_attributes.keySet());
+ }
+
+ @Override
+ public void setAttribute(String name, Object object)
+ {
+ _attributes.put(name, object);
+ }
+
+ @Override
+ public void removeAttribute(String name)
+ {
+ _attributes.remove(name);
+ }
+
+ @Override
+ public String getServletContextName()
+ {
+ return null;
+ }
+
+ @Override
+ public Dynamic addServlet(String servletName, String className)
+ {
+ return null;
+ }
+
+ @Override
+ public Dynamic addServlet(String servletName, Servlet servlet)
+ {
+ return null;
+ }
+
+ @Override
+ public Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass)
+ {
+ return null;
+ }
+
+ @Override
+ public <T extends Servlet> T createServlet(Class<T> clazz) throws ServletException
+ {
+ return null;
+ }
+
+ @Override
+ public ServletRegistration getServletRegistration(String servletName)
+ {
+ return null;
+ }
+
+ @Override
+ public Map<String, ? extends ServletRegistration> getServletRegistrations()
+ {
+ return null;
+ }
+
+ @Override
+ public javax.servlet.FilterRegistration.Dynamic addFilter(String filterName, String className)
+ {
+ return null;
+ }
+
+ @Override
+ public javax.servlet.FilterRegistration.Dynamic addFilter(String filterName, Filter filter)
+ {
+ return null;
+ }
+
+ @Override
+ public javax.servlet.FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass)
+ {
+ return null;
+ }
+
+ @Override
+ public <T extends Filter> T createFilter(Class<T> clazz) throws ServletException
+ {
+ return null;
+ }
+
+ @Override
+ public FilterRegistration getFilterRegistration(String filterName)
+ {
+ return null;
+ }
+
+ @Override
+ public Map<String, ? extends FilterRegistration> getFilterRegistrations()
+ {
+ return null;
+ }
+
+ @Override
+ public SessionCookieConfig getSessionCookieConfig()
+ {
+ return null;
+ }
+
+ @Override
+ public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes)
+ {
+ }
+
+ @Override
+ public Set<SessionTrackingMode> getDefaultSessionTrackingModes()
+ {
+ return null;
+ }
+
+ @Override
+ public Set<SessionTrackingMode> getEffectiveSessionTrackingModes()
+ {
+ return null;
+ }
+
+ @Override
+ public void addListener(String className)
+ {
+ }
+
+ @Override
+ public <T extends EventListener> void addListener(T t)
+ {
+ }
+
+ @Override
+ public void addListener(Class<? extends EventListener> listenerClass)
+ {
+ }
+
+ @Override
+ public <T extends EventListener> T createListener(Class<T> clazz) throws ServletException
+ {
+ return null;
+ }
+
+ @Override
+ public JspConfigDescriptor getJspConfigDescriptor()
+ {
+ return null;
+ }
+
+ @Override
+ public ClassLoader getClassLoader()
+ {
+ return null;
+ }
+
+ @Override
+ public void declareRoles(String... roleNames)
+ {
+ }
+
+ @Override
+ public String getVirtualServerName()
+ {
+ return null;
+ }
+ }
+
+ @Test
+ public void testDefaultSameSite()
+ {
+ TestServletContext context = new TestServletContext();
+ //test null value for default
+ assertNull(HttpCookie.getSameSiteDefault(context));
+
+ //test good value for default as SameSite enum
+ context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, SameSite.LAX);
+ assertEquals(SameSite.LAX, HttpCookie.getSameSiteDefault(context));
+
+ //test good value for default as String
+ context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "NONE");
+ assertEquals(SameSite.NONE, HttpCookie.getSameSiteDefault(context));
+
+ //test case for default as String
+ context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "sTrIcT");
+ assertEquals(SameSite.STRICT, HttpCookie.getSameSiteDefault(context));
+
+ //test bad value for default as String
+ context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "fooBAR");
+ assertThrows(IllegalStateException.class,
+ () -> HttpCookie.getSameSiteDefault(context));
+ }
+
+ @Test
+ public void testConstructFromSetCookie()
+ {
+ HttpCookie cookie = new HttpCookie("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly");
+ }
+
+ @Test
+ public void testSetRFC2965Cookie() throws Exception
+ {
+ HttpCookie httpCookie;
+
+ httpCookie = new HttpCookie("null", null, null, null, -1, false, false, null, -1);
+ assertEquals("null=", httpCookie.getRFC2965SetCookie());
+
+ httpCookie = new HttpCookie("minimal", "value", null, null, -1, false, false, null, -1);
+ assertEquals("minimal=value", httpCookie.getRFC2965SetCookie());
+
+ httpCookie = new HttpCookie("everything", "something", "domain", "path", 0, true, true, "noncomment", 0);
+ assertEquals("everything=something;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=noncomment", httpCookie.getRFC2965SetCookie());
+
+ httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, "comment", 0);
+ assertEquals("everything=value;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment", httpCookie.getRFC2965SetCookie());
+
+ httpCookie = new HttpCookie("ev erything", "va lue", "do main", "pa th", 1, true, true, "co mment", 1);
+ String setCookie = httpCookie.getRFC2965SetCookie();
+ assertThat(setCookie, Matchers.startsWith("\"ev erything\"=\"va lue\";Version=1;Path=\"pa th\";Domain=\"do main\";Expires="));
+ assertThat(setCookie, Matchers.endsWith(" GMT;Max-Age=1;Secure;HttpOnly;Comment=\"co mment\""));
+
+ httpCookie = new HttpCookie("name", "value", null, null, -1, false, false, null, 0);
+ setCookie = httpCookie.getRFC2965SetCookie();
+ assertEquals(-1, setCookie.indexOf("Version="));
+ httpCookie = new HttpCookie("name", "v a l u e", null, null, -1, false, false, null, 0);
+ setCookie = httpCookie.getRFC2965SetCookie();
+
+ httpCookie = new HttpCookie("json", "{\"services\":[\"cwa\", \"aa\"]}", null, null, -1, false, false, null, -1);
+ assertEquals("json=\"{\\\"services\\\":[\\\"cwa\\\", \\\"aa\\\"]}\"", httpCookie.getRFC2965SetCookie());
+
+ httpCookie = new HttpCookie("name", "value%=", null, null, -1, false, false, null, 0);
+ setCookie = httpCookie.getRFC2965SetCookie();
+ assertEquals("name=value%=", setCookie);
+ }
+
+ @Test
+ public void testSetRFC6265Cookie() throws Exception
+ {
+ HttpCookie httpCookie;
+
+ httpCookie = new HttpCookie("null", null, null, null, -1, false, false, null, -1);
+ assertEquals("null=", httpCookie.getRFC6265SetCookie());
+
+ httpCookie = new HttpCookie("minimal", "value", null, null, -1, false, false, null, -1);
+ assertEquals("minimal=value", httpCookie.getRFC6265SetCookie());
+
+ //test cookies with same name, domain and path
+ httpCookie = new HttpCookie("everything", "something", "domain", "path", 0, true, true, null, -1);
+ assertEquals("everything=something; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly", httpCookie.getRFC6265SetCookie());
+
+ httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1);
+ assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly", httpCookie.getRFC6265SetCookie());
+
+ httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1, HttpCookie.SameSite.NONE);
+ assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=None", httpCookie.getRFC6265SetCookie());
+
+ httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1, HttpCookie.SameSite.LAX);
+ assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax", httpCookie.getRFC6265SetCookie());
+
+ httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1, HttpCookie.SameSite.STRICT);
+ assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Strict", httpCookie.getRFC6265SetCookie());
+ }
+
+ public static Stream<String> rfc6265BadNameSource()
+ {
+ return Stream.of(
+ "\"name\"",
+ "name\t",
+ "na me",
+ "name\u0082",
+ "na\tme",
+ "na;me",
+ "{name}",
+ "[name]",
+ "\""
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("rfc6265BadNameSource")
+ public void testSetRFC6265CookieBadName(String badNameExample)
+ {
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+ () ->
+ {
+ HttpCookie httpCookie = new HttpCookie(badNameExample, "value", null, "/", 1, true, true, null, -1);
+ httpCookie.getRFC6265SetCookie();
+ });
+ // make sure that exception mentions just how mad of a name it truly is
+ assertThat("message", ex.getMessage(),
+ allOf(
+ // violation of Cookie spec
+ containsString("RFC6265"),
+ // violation of HTTP spec
+ containsString("RFC2616")
+ ));
+ }
+
+ public static Stream<String> rfc6265BadValueSource()
+ {
+ return Stream.of(
+ "va\tlue",
+ "\t",
+ "value\u0000",
+ "val\u0082ue",
+ "va lue",
+ "va;lue",
+ "\"value",
+ "value\"",
+ "val\\ue",
+ "val\"ue",
+ "\""
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("rfc6265BadValueSource")
+ public void testSetRFC6265CookieBadValue(String badValueExample)
+ {
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+ () ->
+ {
+ HttpCookie httpCookie = new HttpCookie("name", badValueExample, null, "/", 1, true, true, null, -1);
+ httpCookie.getRFC6265SetCookie();
+ });
+ assertThat("message", ex.getMessage(), containsString("RFC6265"));
+ }
+
+ public static Stream<String> rfc6265GoodNameSource()
+ {
+ return Stream.of(
+ "name",
+ "n.a.m.e",
+ "na-me",
+ "+name",
+ "na*me",
+ "na$me",
+ "#name");
+ }
+
+ @ParameterizedTest
+ @MethodSource("rfc6265GoodNameSource")
+ public void testSetRFC6265CookieGoodName(String goodNameExample)
+ {
+ new HttpCookie(goodNameExample, "value", null, "/", 1, true, true, null, -1);
+ // should not throw an exception
+ }
+
+ public static Stream<String> rfc6265GoodValueSource()
+ {
+ String[] goodValueExamples = {
+ "value",
+ "",
+ null,
+ "val=ue",
+ "val-ue",
+ "val/ue",
+ "v.a.l.u.e"
+ };
+ return Stream.of(goodValueExamples);
+ }
+
+ @ParameterizedTest
+ @MethodSource("rfc6265GoodValueSource")
+ public void testSetRFC6265CookieGoodValue(String goodValueExample)
+ {
+ new HttpCookie("name", goodValueExample, null, "/", 1, true, true, null, -1);
+ // should not throw an exception
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "__HTTP_ONLY__",
+ "__HTTP_ONLY__comment",
+ "comment__HTTP_ONLY__"
+ })
+ public void testIsHttpOnlyInCommentTrue(String comment)
+ {
+ assertTrue(HttpCookie.isHttpOnlyInComment(comment), "Comment \"" + comment + "\"");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "comment",
+ "",
+ "__",
+ "__HTTP__ONLY__",
+ "__http_only__",
+ "HTTP_ONLY",
+ "__HTTP__comment__ONLY__"
+ })
+ public void testIsHttpOnlyInCommentFalse(String comment)
+ {
+ assertFalse(HttpCookie.isHttpOnlyInComment(comment), "Comment \"" + comment + "\"");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "__SAME_SITE_NONE__",
+ "__SAME_SITE_NONE____SAME_SITE_NONE__"
+ })
+ public void testGetSameSiteFromCommentNONE(String comment)
+ {
+ assertEquals(HttpCookie.getSameSiteFromComment(comment), HttpCookie.SameSite.NONE, "Comment \"" + comment + "\"");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "__SAME_SITE_LAX__",
+ "__SAME_SITE_LAX____SAME_SITE_NONE__",
+ "__SAME_SITE_NONE____SAME_SITE_LAX__",
+ "__SAME_SITE_LAX____SAME_SITE_NONE__"
+ })
+ public void testGetSameSiteFromCommentLAX(String comment)
+ {
+ assertEquals(HttpCookie.getSameSiteFromComment(comment), HttpCookie.SameSite.LAX, "Comment \"" + comment + "\"");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "__SAME_SITE_STRICT__",
+ "__SAME_SITE_NONE____SAME_SITE_STRICT____SAME_SITE_LAX__",
+ "__SAME_SITE_STRICT____SAME_SITE_LAX____SAME_SITE_NONE__",
+ "__SAME_SITE_STRICT____SAME_SITE_STRICT__"
+ })
+ public void testGetSameSiteFromCommentSTRICT(String comment)
+ {
+ assertEquals(HttpCookie.getSameSiteFromComment(comment), HttpCookie.SameSite.STRICT, "Comment \"" + comment + "\"");
+ }
+
+ /**
+ * A comment that does not have a declared SamesSite attribute defined
+ */
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "__HTTP_ONLY__",
+ "comment",
+ // not jetty attributes
+ "SameSite=None",
+ "SameSite=Lax",
+ "SameSite=Strict",
+ // incomplete jetty attributes
+ "SAME_SITE_NONE",
+ "SAME_SITE_LAX",
+ "SAME_SITE_STRICT",
+ })
+ public void testGetSameSiteFromCommentUndefined(String comment)
+ {
+ assertNull(HttpCookie.getSameSiteFromComment(comment), "Comment \"" + comment + "\"");
+ }
+
+ public static Stream<Arguments> getCommentWithoutAttributesSource()
+ {
+ return Stream.of(
+ // normal - only attribute comment
+ Arguments.of("__SAME_SITE_LAX__", null),
+ // normal - no attribute comment
+ Arguments.of("comment", "comment"),
+ // mixed - attributes at end
+ Arguments.of("comment__SAME_SITE_NONE__", "comment"),
+ Arguments.of("comment__HTTP_ONLY____SAME_SITE_NONE__", "comment"),
+ // mixed - attributes at start
+ Arguments.of("__SAME_SITE_NONE__comment", "comment"),
+ Arguments.of("__HTTP_ONLY____SAME_SITE_NONE__comment", "comment"),
+ // mixed - attributes at start and end
+ Arguments.of("__SAME_SITE_NONE__comment__HTTP_ONLY__", "comment"),
+ Arguments.of("__HTTP_ONLY__comment__SAME_SITE_NONE__", "comment")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("getCommentWithoutAttributesSource")
+ public void testGetCommentWithoutAttributes(String rawComment, String expectedComment)
+ {
+ String actualComment = HttpCookie.getCommentWithoutAttributes(rawComment);
+ if (expectedComment == null)
+ {
+ assertNull(actualComment);
+ }
+ else
+ {
+ assertEquals(actualComment, expectedComment);
+ }
+ }
+
+ @Test
+ public void testGetCommentWithAttributes()
+ {
+ assertThat(HttpCookie.getCommentWithAttributes(null, false, null), nullValue());
+ assertThat(HttpCookie.getCommentWithAttributes("", false, null), nullValue());
+ assertThat(HttpCookie.getCommentWithAttributes("hello", false, null), is("hello"));
+
+ assertThat(HttpCookie.getCommentWithAttributes(null, true, HttpCookie.SameSite.STRICT),
+ is("__HTTP_ONLY____SAME_SITE_STRICT__"));
+ assertThat(HttpCookie.getCommentWithAttributes("", true, HttpCookie.SameSite.NONE),
+ is("__HTTP_ONLY____SAME_SITE_NONE__"));
+ assertThat(HttpCookie.getCommentWithAttributes("hello", true, HttpCookie.SameSite.LAX),
+ is("hello__HTTP_ONLY____SAME_SITE_LAX__"));
+
+ assertThat(HttpCookie.getCommentWithAttributes("__HTTP_ONLY____SAME_SITE_LAX__", false, null), nullValue());
+ assertThat(HttpCookie.getCommentWithAttributes("__HTTP_ONLY____SAME_SITE_LAX__", true, HttpCookie.SameSite.NONE),
+ is("__HTTP_ONLY____SAME_SITE_NONE__"));
+ assertThat(HttpCookie.getCommentWithAttributes("__HTTP_ONLY____SAME_SITE_LAX__hello", true, HttpCookie.SameSite.LAX),
+ is("hello__HTTP_ONLY____SAME_SITE_LAX__"));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldTest.java
new file mode 100644
index 0000000..0a376c4
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldTest.java
@@ -0,0 +1,197 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HttpFieldTest
+{
+ @Test
+ public void testContainsSimple()
+ {
+ HttpField field = new HttpField("name", "SomeValue");
+ assertTrue(field.contains("somevalue"));
+ assertTrue(field.contains("sOmEvAlUe"));
+ assertTrue(field.contains("SomeValue"));
+ assertFalse(field.contains("other"));
+ assertFalse(field.contains("some"));
+ assertFalse(field.contains("Some"));
+ assertFalse(field.contains("value"));
+ assertFalse(field.contains("v"));
+ assertFalse(field.contains(""));
+ assertFalse(field.contains(null));
+ }
+
+ @Test
+ public void testCaseInsensitiveHashcodeKnownField()
+ {
+ HttpField fieldFoo1 = new HttpField("Cookie", "foo");
+ HttpField fieldFoo2 = new HttpField("cookie", "foo");
+
+ assertThat("Field hashcodes are case insensitive", fieldFoo1.hashCode(), is(fieldFoo2.hashCode()));
+ }
+
+ @Test
+ public void testCaseInsensitiveHashcodeUnknownField()
+ {
+ HttpField fieldFoo1 = new HttpField("X-Foo", "bar");
+ HttpField fieldFoo2 = new HttpField("x-foo", "bar");
+
+ assertThat("Field hashcodes are case insensitive", fieldFoo1.hashCode(), is(fieldFoo2.hashCode()));
+ }
+
+ @Test
+ public void testContainsList()
+ {
+ HttpField field = new HttpField("name", ",aaa,Bbb,CCC, ddd , e e, \"\\\"f,f\\\"\", ");
+ assertTrue(field.contains("aaa"));
+ assertTrue(field.contains("bbb"));
+ assertTrue(field.contains("ccc"));
+ assertTrue(field.contains("Aaa"));
+ assertTrue(field.contains("Bbb"));
+ assertTrue(field.contains("Ccc"));
+ assertTrue(field.contains("AAA"));
+ assertTrue(field.contains("BBB"));
+ assertTrue(field.contains("CCC"));
+ assertTrue(field.contains("ddd"));
+ assertTrue(field.contains("e e"));
+ assertTrue(field.contains("\"f,f\""));
+ assertFalse(field.contains(""));
+ assertFalse(field.contains("aa"));
+ assertFalse(field.contains("bb"));
+ assertFalse(field.contains("cc"));
+ assertFalse(field.contains(null));
+ }
+
+ @Test
+ public void testQualityContainsList()
+ {
+ HttpField field;
+
+ field = new HttpField("name", "yes");
+ assertTrue(field.contains("yes"));
+ assertFalse(field.contains("no"));
+
+ field = new HttpField("name", ",yes,");
+ assertTrue(field.contains("yes"));
+ assertFalse(field.contains("no"));
+
+ field = new HttpField("name", "other,yes,other");
+ assertTrue(field.contains("yes"));
+ assertFalse(field.contains("no"));
+
+ field = new HttpField("name", "other, yes ,other");
+ assertTrue(field.contains("yes"));
+ assertFalse(field.contains("no"));
+
+ field = new HttpField("name", "other, y s ,other");
+ assertTrue(field.contains("y s"));
+ assertFalse(field.contains("no"));
+
+ field = new HttpField("name", "other, \"yes\" ,other");
+ assertTrue(field.contains("yes"));
+ assertFalse(field.contains("no"));
+
+ field = new HttpField("name", "other, \"\\\"yes\\\"\" ,other");
+ assertTrue(field.contains("\"yes\""));
+ assertFalse(field.contains("no"));
+
+ field = new HttpField("name", ";no,yes,;no");
+ assertTrue(field.contains("yes"));
+ assertFalse(field.contains("no"));
+
+ field = new HttpField("name", "no;q=0,yes;q=1,no; q = 0");
+ assertTrue(field.contains("yes"));
+ assertFalse(field.contains("no"));
+
+ field = new HttpField("name", "no;q=0.0000,yes;q=0.0001,no; q = 0.00000");
+ assertTrue(field.contains("yes"));
+ assertFalse(field.contains("no"));
+
+ field = new HttpField("name", "no;q=0.0000,Yes;Q=0.0001,no; Q = 0.00000");
+ assertTrue(field.contains("yes"));
+ assertFalse(field.contains("no"));
+ }
+
+ @Test
+ public void testValues()
+ {
+ String[] values = new HttpField("name", "value").getValues();
+ assertEquals(1, values.length);
+ assertEquals("value", values[0]);
+
+ values = new HttpField("name", "a,b,c").getValues();
+ assertEquals(3, values.length);
+ assertEquals("a", values[0]);
+ assertEquals("b", values[1]);
+ assertEquals("c", values[2]);
+
+ values = new HttpField("name", "a,\"x,y,z\",c").getValues();
+ assertEquals(3, values.length);
+ assertEquals("a", values[0]);
+ assertEquals("x,y,z", values[1]);
+ assertEquals("c", values[2]);
+
+ values = new HttpField("name", "a,\"x,\\\"p,q\\\",z\",c").getValues();
+ assertEquals(3, values.length);
+ assertEquals("a", values[0]);
+ assertEquals("x,\"p,q\",z", values[1]);
+ assertEquals("c", values[2]);
+ }
+
+ @Test
+ public void testFieldNameNull()
+ {
+ assertThrows(NullPointerException.class, () -> new HttpField((String)null, null));
+ }
+
+ @Test
+ public void testCachedField()
+ {
+ PreEncodedHttpField field = new PreEncodedHttpField(HttpHeader.ACCEPT, "something");
+ ByteBuffer buf = BufferUtil.allocate(256);
+ BufferUtil.clearToFill(buf);
+ field.putTo(buf, HttpVersion.HTTP_1_0);
+ BufferUtil.flipToFlush(buf, 0);
+ String s = BufferUtil.toString(buf);
+
+ assertEquals("Accept: something\r\n", s);
+ }
+
+ @Test
+ public void testCachedFieldWithHeaderName()
+ {
+ PreEncodedHttpField field = new PreEncodedHttpField("X-My-Custom-Header", "something");
+
+ assertNull(field.getHeader());
+ assertEquals("X-My-Custom-Header", field.getName());
+ assertEquals("something", field.getValue());
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsMatchers.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsMatchers.java
new file mode 100644
index 0000000..3685d05
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsMatchers.java
@@ -0,0 +1,52 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import org.eclipse.jetty.http.matchers.HttpFieldsContainsHeaderKey;
+import org.eclipse.jetty.http.matchers.HttpFieldsContainsHeaderValue;
+import org.eclipse.jetty.http.matchers.HttpFieldsHeaderValue;
+import org.hamcrest.Matcher;
+
+public class HttpFieldsMatchers
+{
+ public static Matcher<HttpFields> containsHeader(String keyName)
+ {
+ return new HttpFieldsContainsHeaderKey(keyName);
+ }
+
+ public static Matcher<HttpFields> containsHeader(HttpHeader header)
+ {
+ return new HttpFieldsContainsHeaderKey(header);
+ }
+
+ public static Matcher<HttpFields> headerValue(String keyName, String value)
+ {
+ return new HttpFieldsHeaderValue(keyName, value);
+ }
+
+ public static Matcher<HttpFields> containsHeaderValue(String keyName, String value)
+ {
+ return new HttpFieldsContainsHeaderValue(keyName, value);
+ }
+
+ public static Matcher<HttpFields> containsHeaderValue(HttpHeader header, String value)
+ {
+ return new HttpFieldsContainsHeaderValue(header, value);
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java
new file mode 100644
index 0000000..a12a178
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java
@@ -0,0 +1,898 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Locale;
+import java.util.NoSuchElementException;
+import java.util.stream.Collectors;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HttpFieldsTest
+{
+ @Test
+ public void testPut()
+ {
+ HttpFields header = new HttpFields();
+
+ header.add("name0", "wrong");
+ header.add(HttpHeader.ACCEPT, "nothing");
+ header.add("name0", "still wrong");
+ header.add(HttpHeader.ACCEPT, "money");
+
+ header.put("name0", "value0");
+ header.put(HttpHeader.ACCEPT, "praise");
+
+ assertEquals(2, header.size());
+ assertEquals("value0", header.get("name0"));
+ assertEquals("praise", header.get("accept"));
+ assertNull(header.get("name2"));
+
+ header.add("name0", "wrong");
+ header.add(HttpHeader.ACCEPT, "nothing");
+
+ header.put("name0", (String)null);
+ header.put(HttpHeader.ACCEPT, (String)null);
+ assertEquals(0, header.size());
+ }
+
+ @Test
+ public void testPutTo()
+ {
+ HttpFields header = new HttpFields();
+
+ header.put("name0", "value0");
+ header.put("name1", "value:A");
+ header.add("name1", "value:B");
+ header.add("name2", "");
+
+ ByteBuffer buffer = BufferUtil.allocate(1024);
+ BufferUtil.flipToFill(buffer);
+ HttpGenerator.putTo(header, buffer);
+ BufferUtil.flipToFlush(buffer, 0);
+ String result = BufferUtil.toString(buffer);
+
+ assertThat(result, Matchers.containsString("name0: value0"));
+ assertThat(result, Matchers.containsString("name1: value:A"));
+ assertThat(result, Matchers.containsString("name1: value:B"));
+ }
+
+ @Test
+ public void testGet()
+ {
+ HttpFields header = new HttpFields();
+
+ header.put("name0", "value0");
+ header.put("name1", "value1");
+
+ assertEquals("value0", header.get("name0"));
+ assertEquals("value0", header.get("Name0"));
+ assertEquals("value1", header.get("name1"));
+ assertEquals("value1", header.get("Name1"));
+ assertNull(header.get("Name2"));
+
+ assertEquals("value0", header.getField("name0").getValue());
+ assertEquals("value0", header.getField("Name0").getValue());
+ assertEquals("value1", header.getField("name1").getValue());
+ assertEquals("value1", header.getField("Name1").getValue());
+ assertNull(header.getField("Name2"));
+
+ assertEquals("value0", header.getField(0).getValue());
+ assertEquals("value1", header.getField(1).getValue());
+ assertThrows(NoSuchElementException.class, () -> header.getField(2));
+ }
+
+ @Test
+ public void testGetValuesList()
+ {
+ HttpFields header = new HttpFields();
+
+ header.add("name0", "value0");
+ header.add("name1", "value1a");
+ header.add(HttpHeader.ACCEPT, "something");
+ header.add("name2", "value2");
+ header.add("name1", "value1b");
+ header.add(HttpHeader.ACCEPT, "everything");
+
+ assertThat(header.getValuesList("unknown").size(), is(0));
+ assertThat(header.getValuesList(HttpHeader.CONNECTION).size(), is(0));
+ assertThat(header.getValuesList("name0"), contains("value0"));
+ assertThat(header.getValuesList("name1"), contains("value1a", "value1b"));
+ assertThat(header.getValuesList(HttpHeader.ACCEPT), contains("something", "everything"));
+
+ assertThat(header.getFields(HttpHeader.CONNECTION).size(), is(0));
+ assertThat(header.getFields(HttpHeader.ACCEPT).stream().map(HttpField::getValue).collect(Collectors.toList()),
+ contains("something", "everything"));
+ }
+
+ @Test
+ public void testGetKnown()
+ {
+ HttpFields header = new HttpFields();
+
+ header.put("Connection", "value0");
+ header.put(HttpHeader.ACCEPT, "value1");
+
+ assertEquals("value0", header.get(HttpHeader.CONNECTION));
+ assertEquals("value1", header.get(HttpHeader.ACCEPT));
+
+ assertEquals("value0", header.getField(HttpHeader.CONNECTION).getValue());
+ assertEquals("value1", header.getField(HttpHeader.ACCEPT).getValue());
+
+ assertNull(header.getField(HttpHeader.AGE));
+ assertNull(header.get(HttpHeader.AGE));
+ }
+
+ @Test
+ public void testCRLF()
+ {
+ HttpFields header = new HttpFields();
+
+ header.put("name0", "value\r\n0");
+ header.put("name\r\n1", "value1");
+ header.put("name:2", "value:\r\n2");
+
+ ByteBuffer buffer = BufferUtil.allocate(1024);
+ BufferUtil.flipToFill(buffer);
+ HttpGenerator.putTo(header, buffer);
+ BufferUtil.flipToFlush(buffer, 0);
+ String out = BufferUtil.toString(buffer);
+ assertThat(out, containsString("name0: value 0"));
+ assertThat(out, containsString("name??1: value1"));
+ assertThat(out, containsString("name?2: value: 2"));
+ }
+
+ @Test
+ public void testCachedPut()
+ {
+ HttpFields header = new HttpFields();
+
+ header.put("Connection", "Keep-Alive");
+ header.put("tRansfer-EncOding", "CHUNKED");
+ header.put("CONTENT-ENCODING", "gZIP");
+
+ ByteBuffer buffer = BufferUtil.allocate(1024);
+ BufferUtil.flipToFill(buffer);
+ HttpGenerator.putTo(header, buffer);
+ BufferUtil.flipToFlush(buffer, 0);
+ String out = BufferUtil.toString(buffer).toLowerCase(Locale.ENGLISH);
+
+ assertThat(out, Matchers.containsString((HttpHeader.CONNECTION + ": " + HttpHeaderValue.KEEP_ALIVE).toLowerCase(Locale.ENGLISH)));
+ assertThat(out, Matchers.containsString((HttpHeader.TRANSFER_ENCODING + ": " + HttpHeaderValue.CHUNKED).toLowerCase(Locale.ENGLISH)));
+ assertThat(out, Matchers.containsString((HttpHeader.CONTENT_ENCODING + ": " + HttpHeaderValue.GZIP).toLowerCase(Locale.ENGLISH)));
+ }
+
+ @Test
+ public void testRePut()
+ {
+ HttpFields header = new HttpFields();
+
+ header.put("name0", "value0");
+ header.put("name1", "xxxxxx");
+ header.put("name2", "value2");
+
+ assertEquals("value0", header.get("name0"));
+ assertEquals("xxxxxx", header.get("name1"));
+ assertEquals("value2", header.get("name2"));
+
+ header.put("name1", "value1");
+
+ assertEquals("value0", header.get("name0"));
+ assertEquals("value1", header.get("name1"));
+ assertEquals("value2", header.get("name2"));
+ assertNull(header.get("name3"));
+
+ int matches = 0;
+ Enumeration<String> e = header.getFieldNames();
+ while (e.hasMoreElements())
+ {
+ String o = e.nextElement();
+ if ("name0".equals(o))
+ matches++;
+ if ("name1".equals(o))
+ matches++;
+ if ("name2".equals(o))
+ matches++;
+ }
+ assertEquals(3, matches);
+
+ e = header.getValues("name1");
+ assertTrue(e.hasMoreElements());
+ assertEquals(e.nextElement(), "value1");
+ assertFalse(e.hasMoreElements());
+ }
+
+ @Test
+ public void testRemovePut()
+ {
+ HttpFields header = new HttpFields(1);
+
+ header.put("name0", "value0");
+ header.put("name1", "value1");
+ header.put("name2", "value2");
+
+ assertEquals("value0", header.get("name0"));
+ assertEquals("value1", header.get("name1"));
+ assertEquals("value2", header.get("name2"));
+
+ header.remove("name1");
+
+ assertEquals("value0", header.get("name0"));
+ assertNull(header.get("name1"));
+ assertEquals("value2", header.get("name2"));
+ assertNull(header.get("name3"));
+
+ int matches = 0;
+ Enumeration<String> e = header.getFieldNames();
+ while (e.hasMoreElements())
+ {
+ Object o = e.nextElement();
+ if ("name0".equals(o))
+ matches++;
+ if ("name1".equals(o))
+ matches++;
+ if ("name2".equals(o))
+ matches++;
+ }
+ assertEquals(2, matches);
+
+ e = header.getValues("name1");
+ assertFalse(e.hasMoreElements());
+ }
+
+ @Test
+ public void testAdd()
+ {
+ HttpFields fields = new HttpFields();
+
+ fields.add("name0", "value0");
+ fields.add("name1", "valueA");
+ fields.add(HttpHeader.ACCEPT, "everything");
+
+ assertEquals("value0", fields.get("name0"));
+ assertEquals("valueA", fields.get("name1"));
+ assertEquals("everything", fields.get("accept"));
+
+ fields.add("name1", "valueB");
+ fields.add(HttpHeader.ACCEPT, "nothing");
+ fields.add("name1", null);
+ // fields.add(HttpHeader.ACCEPT, (String)null); TODO this one throws IAE. Should make the same as the others.
+
+ fields.add("name2", "value2");
+
+ assertEquals("value0", fields.get("name0"));
+ assertEquals("valueA", fields.get("name1"));
+ assertEquals("value2", fields.get("name2"));
+ assertNull(fields.get("name3"));
+
+ assertThat(fields.getValuesList("name1"), contains("valueA", "valueB"));
+ assertThat(fields.getValuesList(HttpHeader.ACCEPT), contains("everything", "nothing"));
+
+ fields.add(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE);
+ fields.add(HttpHeader.CONNECTION, (HttpHeaderValue)null);
+ assertThat(fields.getValuesList("Connection"), contains("close"));
+ }
+
+ @Test
+ public void testAddAll()
+ {
+ HttpFields fields0 = new HttpFields();
+ assertThat(fields0.size(), is(0));
+ HttpFields fields1 = new HttpFields(fields0);
+ assertThat(fields1.size(), is(0));
+
+ fields0.add("name0", "value0");
+ fields0.add("name1", "valueA");
+ fields0.add("name2", "value2");
+
+ fields1.addAll(fields0);
+ assertThat(fields1.size(), is(3));
+ assertThat(fields0, is(fields1));
+ }
+
+ @Test
+ public void testPreEncodedField()
+ {
+ ByteBuffer buffer = BufferUtil.allocate(1024);
+
+ PreEncodedHttpField known = new PreEncodedHttpField(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString());
+ BufferUtil.clearToFill(buffer);
+ known.putTo(buffer, HttpVersion.HTTP_1_1);
+ BufferUtil.flipToFlush(buffer, 0);
+ assertThat(BufferUtil.toString(buffer), is("Connection: close\r\n"));
+
+ PreEncodedHttpField unknown = new PreEncodedHttpField(null, "Header", "Value");
+ BufferUtil.clearToFill(buffer);
+ unknown.putTo(buffer, HttpVersion.HTTP_1_1);
+ BufferUtil.flipToFlush(buffer, 0);
+ assertThat(BufferUtil.toString(buffer), is("Header: Value\r\n"));
+ }
+
+ @Test
+ public void testAddPreEncodedField()
+ {
+ final PreEncodedHttpField X_XSS_PROTECTION_FIELD = new PreEncodedHttpField("X-XSS-Protection", "1; mode=block");
+
+ HttpFields fields = new HttpFields();
+ fields.add(X_XSS_PROTECTION_FIELD);
+
+ assertThat("Fields output", fields.toString(), containsString("X-XSS-Protection: 1; mode=block"));
+ }
+
+ @Test
+ public void testAddFinalHttpField()
+ {
+ final HttpField X_XSS_PROTECTION_FIELD = new HttpField("X-XSS-Protection", "1; mode=block");
+
+ HttpFields fields = new HttpFields();
+ fields.add(X_XSS_PROTECTION_FIELD);
+
+ assertThat("Fields output", fields.toString(), containsString("X-XSS-Protection: 1; mode=block"));
+ }
+
+ @Test
+ public void testGetValues()
+ {
+ HttpFields fields = new HttpFields();
+
+ fields.put("name0", "value0A,value0B");
+ fields.add("name0", "value0C,value0D");
+ fields.put("name1", "value1A, \"value\t, 1B\" ");
+ fields.add("name1", "\"value1C\",\tvalue1D");
+
+ Enumeration<String> e = fields.getValues("name0");
+ assertTrue(e.hasMoreElements());
+ assertEquals(e.nextElement(), "value0A,value0B");
+ assertTrue(e.hasMoreElements());
+ assertEquals(e.nextElement(), "value0C,value0D");
+ assertFalse(e.hasMoreElements());
+
+ //noinspection deprecation
+ e = fields.getValues("name0", ",");
+ assertTrue(e.hasMoreElements());
+ assertEquals(e.nextElement(), "value0A");
+ assertTrue(e.hasMoreElements());
+ assertEquals(e.nextElement(), "value0B");
+ assertTrue(e.hasMoreElements());
+ assertEquals(e.nextElement(), "value0C");
+ assertTrue(e.hasMoreElements());
+ assertEquals(e.nextElement(), "value0D");
+ assertFalse(e.hasMoreElements());
+
+ //noinspection deprecation
+ e = fields.getValues("name1", ",");
+ assertTrue(e.hasMoreElements());
+ assertEquals(e.nextElement(), "value1A");
+ assertTrue(e.hasMoreElements());
+ assertEquals(e.nextElement(), "value\t, 1B");
+ assertTrue(e.hasMoreElements());
+ assertEquals(e.nextElement(), "value1C");
+ assertTrue(e.hasMoreElements());
+ assertEquals(e.nextElement(), "value1D");
+ assertFalse(e.hasMoreElements());
+ }
+
+ @Test
+ public void testAddCSV()
+ {
+ HttpFields fields = new HttpFields();
+ fields.addCSV(HttpHeader.CONNECTION);
+ fields.addCSV("name");
+ assertThat(fields.size(), is(0));
+
+ fields.addCSV(HttpHeader.CONNECTION, "one");
+ fields.addCSV("name", "one");
+ assertThat(fields.getValuesList("name"), contains("one"));
+ assertThat(fields.getValuesList(HttpHeader.CONNECTION), contains("one"));
+
+ fields.addCSV(HttpHeader.CONNECTION, "two");
+ fields.addCSV("name", "two");
+ assertThat(fields.getValuesList("name"), contains("one", "two"));
+ assertThat(fields.getValuesList(HttpHeader.CONNECTION), contains("one", "two"));
+
+ fields.addCSV(HttpHeader.CONNECTION, "one", "three", "four");
+ fields.addCSV("name", "one", "three", "four");
+ assertThat(fields.getValuesList("name"), contains("one", "two", "three, four"));
+ assertThat(fields.getValuesList(HttpHeader.CONNECTION), contains("one", "two", "three, four"));
+ }
+
+ @Test
+ public void testGetCSV()
+ {
+ HttpFields fields = new HttpFields();
+
+ fields.put(HttpHeader.ACCEPT, "valueA, \"value, B\"");
+ fields.add(HttpHeader.ACCEPT, "\"valueC\",valueD");
+ fields.put("name1", "value1A, \"value\t, 1B\" ");
+ fields.add("name1", "\"value1C\",\tvalue1D");
+
+ assertThat(fields.getCSV(HttpHeader.ACCEPT, false), contains("valueA", "value, B", "valueC", "valueD"));
+ assertThat(fields.getCSV(HttpHeader.ACCEPT, true), contains("valueA", "\"value, B\"", "\"valueC\"", "valueD"));
+ assertThat(fields.getCSV("name1", false), contains("value1A", "value\t, 1B", "value1C", "value1D"));
+ assertThat(fields.getCSV("name1", true), contains("value1A", "\"value\t, 1B\"", "\"value1C\"", "value1D"));
+ }
+
+ @Test
+ public void testAddQuotedCSV()
+ {
+ HttpFields fields = new HttpFields();
+
+ fields.put("some", "value");
+ fields.add("name", "\"zero\"");
+ fields.add("name", "one, \"1 + 1\"");
+ fields.put("other", "value");
+ fields.add("name", "three");
+ fields.add("name", "four, I V");
+
+ List<String> list = fields.getCSV("name", false);
+ assertEquals(HttpFields.valueParameters(list.get(0), null), "zero");
+ assertEquals(HttpFields.valueParameters(list.get(1), null), "one");
+ assertEquals(HttpFields.valueParameters(list.get(2), null), "1 + 1");
+ assertEquals(HttpFields.valueParameters(list.get(3), null), "three");
+ assertEquals(HttpFields.valueParameters(list.get(4), null), "four");
+ assertEquals(HttpFields.valueParameters(list.get(5), null), "I V");
+
+ fields.addCSV("name", "six");
+ list = fields.getCSV("name", false);
+ assertEquals(HttpFields.valueParameters(list.get(0), null), "zero");
+ assertEquals(HttpFields.valueParameters(list.get(1), null), "one");
+ assertEquals(HttpFields.valueParameters(list.get(2), null), "1 + 1");
+ assertEquals(HttpFields.valueParameters(list.get(3), null), "three");
+ assertEquals(HttpFields.valueParameters(list.get(4), null), "four");
+ assertEquals(HttpFields.valueParameters(list.get(5), null), "I V");
+ assertEquals(HttpFields.valueParameters(list.get(6), null), "six");
+
+ fields.addCSV("name", "1 + 1", "7", "zero");
+ list = fields.getCSV("name", false);
+ assertEquals(HttpFields.valueParameters(list.get(0), null), "zero");
+ assertEquals(HttpFields.valueParameters(list.get(1), null), "one");
+ assertEquals(HttpFields.valueParameters(list.get(2), null), "1 + 1");
+ assertEquals(HttpFields.valueParameters(list.get(3), null), "three");
+ assertEquals(HttpFields.valueParameters(list.get(4), null), "four");
+ assertEquals(HttpFields.valueParameters(list.get(5), null), "I V");
+ assertEquals(HttpFields.valueParameters(list.get(6), null), "six");
+ assertEquals(HttpFields.valueParameters(list.get(7), null), "7");
+ }
+
+ @Test
+ public void testGetQualityCSV()
+ {
+ HttpFields fields = new HttpFields();
+
+ fields.put("some", "value");
+ fields.add("name", "zero;q=0.9,four;q=0.1");
+ fields.put("other", "value");
+ fields.add("name", "nothing;q=0");
+ fields.add("name", "one;q=0.4");
+ fields.add("name", "three;x=y;q=0.2;a=b,two;q=0.3");
+ fields.add("name", "first;");
+
+ List<String> list = fields.getQualityCSV("name");
+ assertEquals(HttpFields.valueParameters(list.get(0), null), "first");
+ assertEquals(HttpFields.valueParameters(list.get(1), null), "zero");
+ assertEquals(HttpFields.valueParameters(list.get(2), null), "one");
+ assertEquals(HttpFields.valueParameters(list.get(3), null), "two");
+ assertEquals(HttpFields.valueParameters(list.get(4), null), "three");
+ assertEquals(HttpFields.valueParameters(list.get(5), null), "four");
+ }
+
+ @Test
+ public void testGetQualityCSVHeader()
+ {
+ HttpFields fields = new HttpFields();
+
+ fields.put("some", "value");
+ fields.add("Accept", "zero;q=0.9,four;q=0.1");
+ fields.put("other", "value");
+ fields.add("Accept", "nothing;q=0");
+ fields.add("Accept", "one;q=0.4");
+ fields.add("Accept", "three;x=y;q=0.2;a=b,two;q=0.3");
+ fields.add("Accept", "first;");
+
+ List<String> list = fields.getQualityCSV(HttpHeader.ACCEPT);
+ assertEquals(HttpFields.valueParameters(list.get(0), null), "first");
+ assertEquals(HttpFields.valueParameters(list.get(1), null), "zero");
+ assertEquals(HttpFields.valueParameters(list.get(2), null), "one");
+ assertEquals(HttpFields.valueParameters(list.get(3), null), "two");
+ assertEquals(HttpFields.valueParameters(list.get(4), null), "three");
+ assertEquals(HttpFields.valueParameters(list.get(5), null), "four");
+ }
+
+ @Test
+ public void testDateFields()
+ {
+ HttpFields fields = new HttpFields();
+
+ fields.put("D0", "Wed, 31 Dec 1969 23:59:59 GMT");
+ fields.put("D1", "Fri, 31 Dec 1999 23:59:59 GMT");
+ fields.put("D2", "Friday, 31-Dec-99 23:59:59 GMT");
+ fields.put("D3", "Fri Dec 31 23:59:59 1999");
+ fields.put("D4", "Mon Jan 1 2000 00:00:01");
+ fields.put("D5", "Tue Feb 29 2000 12:00:00");
+
+ long d1 = fields.getDateField("D1");
+ long d0 = fields.getDateField("D0");
+ long d2 = fields.getDateField("D2");
+ long d3 = fields.getDateField("D3");
+ long d4 = fields.getDateField("D4");
+ long d5 = fields.getDateField("D5");
+ assertTrue(d0 != -1);
+ assertTrue(d1 > 0);
+ assertTrue(d2 > 0);
+ assertEquals(d1, d2);
+ assertEquals(d2, d3);
+ assertEquals(d3 + 2000, d4);
+ assertEquals(951825600000L, d5);
+
+ d1 = fields.getDateField("D1");
+ d2 = fields.getDateField("D2");
+ d3 = fields.getDateField("D3");
+ d4 = fields.getDateField("D4");
+ d5 = fields.getDateField("D5");
+ assertTrue(d1 > 0);
+ assertTrue(d2 > 0);
+ assertEquals(d1, d2);
+ assertEquals(d2, d3);
+ assertEquals(d3 + 2000, d4);
+ assertEquals(951825600000L, d5);
+
+ fields.putDateField("D2", d1);
+ assertEquals("Fri, 31 Dec 1999 23:59:59 GMT", fields.get("D2"));
+ }
+
+ @Test
+ public void testNegDateFields()
+ {
+ HttpFields fields = new HttpFields();
+
+ fields.putDateField("Dzero", 0);
+ assertEquals("Thu, 01 Jan 1970 00:00:00 GMT", fields.get("Dzero"));
+
+ fields.putDateField("Dminus", -1);
+ assertEquals("Wed, 31 Dec 1969 23:59:59 GMT", fields.get("Dminus"));
+
+ fields.putDateField("Dminus", -1000);
+ assertEquals("Wed, 31 Dec 1969 23:59:59 GMT", fields.get("Dminus"));
+
+ fields.putDateField("Dancient", Long.MIN_VALUE);
+ assertEquals("Sun, 02 Dec 55 16:47:04 GMT", fields.get("Dancient"));
+ }
+
+ @Test
+ public void testLongFields()
+ {
+ HttpFields header = new HttpFields();
+
+ header.put("I1", "42");
+ header.put("I2", " 43 99");
+ header.put("I3", "-44");
+ header.put("I4", " - 45abc");
+ header.put("N1", " - ");
+ header.put("N2", "xx");
+
+ long i1 = header.getLongField("I1");
+ assertThrows(NumberFormatException.class, () -> header.getLongField("I2"));
+
+ long i3 = header.getLongField("I3");
+ assertThrows(NumberFormatException.class, () -> header.getLongField("I4"));
+ assertThrows(NumberFormatException.class, () -> header.getLongField("N1"));
+ assertThrows(NumberFormatException.class, () -> header.getLongField("N2"));
+
+ assertEquals(42, i1);
+ assertEquals(-44, i3);
+
+ header.putLongField("I5", 46);
+ header.putLongField("I6", -47);
+ assertEquals("46", header.get("I5"));
+ assertEquals("-47", header.get("I6"));
+ }
+
+ @Test
+ public void testContains()
+ {
+ HttpFields header = new HttpFields();
+
+ header.add("n0", "");
+ header.add("n1", ",");
+ header.add("n2", ",,");
+ header.add("N3", "abc");
+ header.add("N4", "def");
+ header.add("n5", "abc,def,hig");
+ header.add("N6", "abc");
+ header.add("n6", "def");
+ header.add("N6", "hig");
+ header.add("n7", "abc , def;q=0.9 , hig");
+ header.add("n8", "abc , def;q=0 , hig");
+ header.add(HttpHeader.ACCEPT, "abc , def;q=0 , hig");
+
+ for (int i = 0; i < 8; i++)
+ {
+ assertTrue(header.containsKey("n" + i));
+ assertTrue(header.containsKey("N" + i));
+ assertFalse(header.contains("n" + i, "xyz"), "" + i);
+ assertEquals(i >= 4, header.contains("n" + i, "def"), "" + i);
+ }
+
+ assertTrue(header.contains(new HttpField("N5", "def")));
+ assertTrue(header.contains(new HttpField("accept", "abc")));
+ assertTrue(header.contains(HttpHeader.ACCEPT));
+ assertTrue(header.contains(HttpHeader.ACCEPT, "abc"));
+ assertFalse(header.contains(new HttpField("N5", "xyz")));
+ assertFalse(header.contains(new HttpField("N8", "def")));
+ assertFalse(header.contains(HttpHeader.ACCEPT, "def"));
+ assertFalse(header.contains(HttpHeader.AGE));
+ assertFalse(header.contains(HttpHeader.AGE, "abc"));
+
+ assertFalse(header.containsKey("n11"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"Host", "host", "HOST", "HoSt", "Connection", "CONNECTION", "connection", "CoNnEcTiOn"})
+ public void testContainsKeyTrue(String keyName)
+ {
+ HttpFields fields = new HttpFields();
+ fields.put("Host", "localhost");
+ HttpField namelessField = new HttpField(HttpHeader.CONNECTION, null, "bogus");
+ fields.put(namelessField);
+
+ assertTrue(fields.containsKey(keyName), "containsKey('" + keyName + "')");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"Content-Type", "Content-Length", "X-Bogus", ""})
+ public void testContainsKeyFalse(String keyName)
+ {
+ HttpFields fields = new HttpFields();
+ fields.add("Host", "localhost");
+ HttpField namelessField = new HttpField(HttpHeader.CONNECTION, null, "bogus");
+ fields.put(namelessField);
+
+ assertFalse(fields.containsKey(keyName), "containsKey('" + keyName + "')");
+ }
+
+ @Test
+ public void testPreventNullFieldName()
+ {
+ HttpFields fields = new HttpFields();
+ assertThrows(NullPointerException.class, () ->
+ {
+ HttpField nullNullField = new HttpField(null, null, "bogus");
+ fields.put(nullNullField);
+ });
+ }
+
+ @Test
+ public void testAddNullName()
+ {
+ HttpFields fields = new HttpFields();
+ assertThrows(NullPointerException.class, () -> fields.add((String)null, "bogus"));
+ assertThat(fields.size(), is(0));
+
+ assertThrows(NullPointerException.class, () -> fields.add((HttpHeader)null, "bogus"));
+ assertThat(fields.size(), is(0));
+ }
+
+ @Test
+ public void testPutNullName()
+ {
+ HttpFields fields = new HttpFields();
+ assertThrows(NullPointerException.class, () -> fields.put((String)null, "bogus"));
+ assertThat(fields.size(), is(0));
+
+ assertThrows(NullPointerException.class, () -> fields.put(null, (List<String>)null));
+ assertThat(fields.size(), is(0));
+
+ List<String> emptyList = new ArrayList<>();
+ assertThrows(NullPointerException.class, () -> fields.put(null, emptyList));
+ assertThat(fields.size(), is(0));
+
+ assertThrows(NullPointerException.class, () -> fields.put((HttpHeader)null, "bogus"));
+ assertThat(fields.size(), is(0));
+ }
+
+ @Test
+ public void testPutNullValueList()
+ {
+ HttpFields fields = new HttpFields();
+
+ fields.put("name", (List<String>)null);
+ assertThat(fields.size(), is(0));
+ }
+
+ @Test
+ public void testPreventNullField()
+ {
+ // Attempt various ways that may have put a null field in the array that
+ // previously caused a NPE in put.
+ HttpFields fields = new HttpFields();
+ fields.add((HttpField)null); // should not result in field being added
+ assertThat(fields.size(), is(0));
+ fields.put(null); // should not result in field being added
+ assertThat(fields.size(), is(0));
+ fields.put("something", "else");
+ assertThat(fields.size(), is(1));
+ ListIterator<HttpField> iter = fields.listIterator();
+ iter.next();
+ iter.set(null); // set field to null - should result in noop
+ assertThat(fields.size(), is(0));
+ iter.add(null); // attempt to add null entry
+ assertThat(fields.size(), is(0));
+ fields.put("something", "other");
+ assertThat(fields.size(), is(1));
+ iter = fields.listIterator();
+ iter.next();
+ iter.remove(); // remove only entry
+ assertThat(fields.size(), is(0));
+ fields.put("something", "other");
+ assertThat(fields.size(), is(1));
+ fields.clear();
+ }
+
+ @Test
+ public void testIteration()
+ {
+ HttpFields header = new HttpFields(5);
+ Iterator<HttpField> i = header.iterator();
+ assertThat(i.hasNext(), is(false));
+
+ header.put("name1", "valueA");
+ header.put("name2", "valueB");
+ header.add("name3", "valueC");
+
+ i = header.iterator();
+ assertThat(i.hasNext(), is(true));
+ assertThat(i.next().getName(), is("name1"));
+ assertThat(i.next().getName(), is("name2"));
+ i.remove();
+ assertThat(i.next().getName(), is("name3"));
+ assertThat(i.hasNext(), is(false));
+
+ i = header.iterator();
+ assertThat(i.hasNext(), is(true));
+ assertThat(i.next().getName(), is("name1"));
+ assertThat(i.next().getName(), is("name3"));
+ assertThat(i.hasNext(), is(false));
+
+ ListIterator<HttpField> l = header.listIterator();
+ assertThat(l.hasNext(), is(true));
+ assertThat(l.nextIndex(), is(0));
+ assertThat(l.previousIndex(), is(-1));
+
+ l.add(new HttpField("name0", "value"));
+ assertThat(l.hasNext(), is(true));
+ assertThat(l.nextIndex(), is(1));
+ assertThat(l.hasPrevious(), is(true));
+ assertThat(l.previousIndex(), is(0));
+
+ assertThat(l.next().getName(), is("name1"));
+ assertThat(l.hasNext(), is(true));
+ assertThat(l.nextIndex(), is(2));
+ assertThat(l.hasPrevious(), is(true));
+ assertThat(l.previousIndex(), is(1));
+
+ l.set(new HttpField("NAME1", "value"));
+ assertThat(l.hasNext(), is(true));
+ assertThat(l.nextIndex(), is(2));
+ assertThat(l.hasPrevious(), is(true));
+ assertThat(l.previousIndex(), is(1));
+
+ assertThat(l.previous().getName(), is("NAME1"));
+ assertThat(l.hasNext(), is(true));
+ assertThat(l.nextIndex(), is(1));
+ assertThat(l.hasPrevious(), is(true));
+ assertThat(l.previousIndex(), is(0));
+
+ assertThat(l.previous().getName(), is("name0"));
+ assertThat(l.hasNext(), is(true));
+ assertThat(l.nextIndex(), is(0));
+ assertThat(l.hasPrevious(), is(false));
+ assertThat(l.previousIndex(), is(-1));
+
+ assertThat(l.next().getName(), is("name0"));
+ assertThat(l.hasNext(), is(true));
+ assertThat(l.nextIndex(), is(1));
+ assertThat(l.hasPrevious(), is(true));
+ assertThat(l.previousIndex(), is(0));
+
+ assertThat(l.next().getName(), is("NAME1"));
+ l.add(new HttpField("name2", "value"));
+ assertThat(l.next().getName(), is("name3"));
+ assertThat(l.hasNext(), is(false));
+ assertThat(l.nextIndex(), is(4));
+ assertThat(l.hasPrevious(), is(true));
+ assertThat(l.previousIndex(), is(3));
+
+ l.add(new HttpField("name4", "value"));
+ assertThat(l.hasNext(), is(false));
+ assertThat(l.nextIndex(), is(5));
+ assertThat(l.hasPrevious(), is(true));
+ assertThat(l.previousIndex(), is(4));
+ assertThat(l.previous().getName(), is("name4"));
+
+ i = header.iterator();
+ assertThat(i.hasNext(), is(true));
+ assertThat(i.next().getName(), is("name0"));
+ assertThat(i.next().getName(), is("NAME1"));
+ assertThat(i.next().getName(), is("name2"));
+ assertThat(i.next().getName(), is("name3"));
+ assertThat(i.next().getName(), is("name4"));
+ assertThat(i.hasNext(), is(false));
+ }
+
+ @Test
+ public void testStream()
+ {
+ HttpFields header = new HttpFields();
+ assertThat(header.stream().count(), is(0L));
+ header.put("name1", "valueA");
+ header.put("name2", "valueB");
+ header.add("name3", "valueC");
+ assertThat(header.stream().count(), is(3L));
+ assertThat(header.stream().map(HttpField::getName).filter("name2"::equalsIgnoreCase).count(), is(1L));
+ }
+
+ @Test
+ public void testComputeField()
+ {
+ HttpFields header = new HttpFields();
+ assertThat(header.size(), is(0));
+
+ header.computeField("Test", (n, f) -> null);
+ assertThat(header.size(), is(0));
+
+ header.add(new HttpField("Before", "value"));
+ assertThat(header.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value"));
+
+ header.computeField("Test", (n, f) -> new HttpField(n, "one"));
+ assertThat(header.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "Test: one"));
+
+ header.add(new HttpField("After", "value"));
+ assertThat(header.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "Test: one", "After: value"));
+
+ header.add(new HttpField("Test", "two"));
+ header.add(new HttpField("Test", "three"));
+ assertThat(header.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "Test: one", "After: value", "Test: two", "Test: three"));
+
+ header.computeField("Test", (n, f) -> new HttpField("TEST", "count=" + f.size()));
+ assertThat(header.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "TEST: count=3", "After: value"));
+
+ header.computeField("TEST", (n, f) -> null);
+ assertThat(header.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "After: value"));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorClientTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorClientTest.java
new file mode 100644
index 0000000..dc8e37b
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorClientTest.java
@@ -0,0 +1,370 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HttpGeneratorClientTest
+{
+ public static final String[] connect = {null, "keep-alive", "close"};
+
+ class Info extends MetaData.Request
+ {
+ Info(String method, String uri)
+ {
+ super(method, new HttpURI(uri), HttpVersion.HTTP_1_1, new HttpFields(), -1);
+ }
+
+ public Info(String method, String uri, int contentLength)
+ {
+ super(method, new HttpURI(uri), HttpVersion.HTTP_1_1, new HttpFields(), contentLength);
+ }
+ }
+
+ @Test
+ public void testGETRequestNoContent() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(2048);
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result
+ result = gen.generateRequest(null, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ Info info = new Info("GET", "/index.html");
+ info.getFields().add("Host", "something");
+ info.getFields().add("User-Agent", "test");
+ assertTrue(!gen.isChunking());
+
+ result = gen.generateRequest(info, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateRequest(info, header, null, null, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ assertTrue(!gen.isChunking());
+ String out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+
+ result = gen.generateResponse(null, false, null, null, null, false);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+ assertTrue(!gen.isChunking());
+
+ assertEquals(0, gen.getContentPrepared());
+ assertThat(out, Matchers.containsString("GET /index.html HTTP/1.1"));
+ assertThat(out, Matchers.not(Matchers.containsString("Content-Length")));
+ }
+
+ @Test
+ public void testEmptyHeaders() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(2048);
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result
+ result = gen.generateRequest(null, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ Info info = new Info("GET", "/index.html");
+ info.getFields().add("Host", "something");
+ info.getFields().add("Null", null);
+ info.getFields().add("Empty", "");
+ assertTrue(!gen.isChunking());
+
+ result = gen.generateRequest(info, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateRequest(info, header, null, null, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ assertTrue(!gen.isChunking());
+ String out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+
+ result = gen.generateResponse(null, false, null, null, null, false);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+ assertTrue(!gen.isChunking());
+
+ assertEquals(0, gen.getContentPrepared());
+ assertThat(out, Matchers.containsString("GET /index.html HTTP/1.1"));
+ assertThat(out, Matchers.not(Matchers.containsString("Content-Length")));
+ assertThat(out, Matchers.containsString("Empty:"));
+ assertThat(out, Matchers.not(Matchers.containsString("Null:")));
+ }
+
+ @Test
+ public void testHeaderOverflow() throws Exception
+ {
+ HttpGenerator gen = new HttpGenerator();
+
+ Info info = new Info("GET", "/index.html");
+ info.getFields().add("Host", "localhost");
+ info.getFields().add("Field", "SomeWhatLongValue");
+ info.setHttpVersion(HttpVersion.HTTP_1_0);
+
+ HttpGenerator.Result result = gen.generateRequest(info, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+
+ ByteBuffer header = BufferUtil.allocate(16);
+ result = gen.generateRequest(info, header, null, null, true);
+ assertEquals(HttpGenerator.Result.HEADER_OVERFLOW, result);
+
+ header = BufferUtil.allocate(2048);
+ result = gen.generateRequest(info, header, null, null, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ assertFalse(gen.isChunking());
+ String out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+
+ result = gen.generateResponse(null, false, null, null, null, false);
+ assertEquals(HttpGenerator.Result.SHUTDOWN_OUT, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+ assertFalse(gen.isChunking());
+
+ assertEquals(0, gen.getContentPrepared());
+ assertThat(out, Matchers.containsString("GET /index.html HTTP/1.0"));
+ assertThat(out, Matchers.not(Matchers.containsString("Content-Length")));
+ assertThat(out, Matchers.containsString("Field: SomeWhatLongValue"));
+ }
+
+ @Test
+ public void testPOSTRequestNoContent() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(2048);
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result
+ result = gen.generateRequest(null, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ Info info = new Info("POST", "/index.html");
+ info.getFields().add("Host", "something");
+ info.getFields().add("User-Agent", "test");
+ assertTrue(!gen.isChunking());
+
+ result = gen.generateRequest(info, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateRequest(info, header, null, null, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ assertTrue(!gen.isChunking());
+ String out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+
+ result = gen.generateResponse(null, false, null, null, null, false);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+ assertTrue(!gen.isChunking());
+
+ assertEquals(0, gen.getContentPrepared());
+ assertThat(out, Matchers.containsString("POST /index.html HTTP/1.1"));
+ assertThat(out, Matchers.containsString("Content-Length: 0"));
+ }
+
+ @Test
+ public void testRequestWithContent() throws Exception
+ {
+ String out;
+ ByteBuffer header = BufferUtil.allocate(4096);
+ ByteBuffer content0 = BufferUtil.toBuffer("Hello World. The quick brown fox jumped over the lazy dog.");
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result
+ result = gen.generateRequest(null, null, null, content0, true);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ Info info = new Info("POST", "/index.html");
+ info.getFields().add("Host", "something");
+ info.getFields().add("User-Agent", "test");
+
+ result = gen.generateRequest(info, null, null, content0, true);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateRequest(info, header, null, content0, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ assertTrue(!gen.isChunking());
+ out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ out += BufferUtil.toString(content0);
+ BufferUtil.clear(content0);
+
+ result = gen.generateResponse(null, false, null, null, null, false);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+ assertTrue(!gen.isChunking());
+
+ assertThat(out, Matchers.containsString("POST /index.html HTTP/1.1"));
+ assertThat(out, Matchers.containsString("Host: something"));
+ assertThat(out, Matchers.containsString("Content-Length: 58"));
+ assertThat(out, Matchers.containsString("Hello World. The quick brown fox jumped over the lazy dog."));
+
+ assertEquals(58, gen.getContentPrepared());
+ }
+
+ @Test
+ public void testRequestWithChunkedContent() throws Exception
+ {
+ String out;
+ ByteBuffer header = BufferUtil.allocate(4096);
+ ByteBuffer chunk = BufferUtil.allocate(HttpGenerator.CHUNK_SIZE);
+ ByteBuffer content0 = BufferUtil.toBuffer("Hello World. ");
+ ByteBuffer content1 = BufferUtil.toBuffer("The quick brown fox jumped over the lazy dog.");
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result
+ result = gen.generateRequest(null, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ Info info = new Info("POST", "/index.html");
+ info.getFields().add("Host", "something");
+ info.getFields().add("User-Agent", "test");
+
+ result = gen.generateRequest(info, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateRequest(info, header, null, content0, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+ assertTrue(gen.isChunking());
+ out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ out += BufferUtil.toString(content0);
+ BufferUtil.clear(content0);
+
+ result = gen.generateRequest(null, header, null, content1, false);
+ assertEquals(HttpGenerator.Result.NEED_CHUNK, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+
+ result = gen.generateRequest(null, null, chunk, content1, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+ assertTrue(gen.isChunking());
+ out += BufferUtil.toString(chunk);
+ BufferUtil.clear(chunk);
+ out += BufferUtil.toString(content1);
+ BufferUtil.clear(content1);
+
+ result = gen.generateResponse(null, false, null, chunk, null, true);
+ assertEquals(HttpGenerator.Result.CONTINUE, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ assertTrue(gen.isChunking());
+
+ result = gen.generateResponse(null, false, null, chunk, null, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ out += BufferUtil.toString(chunk);
+ BufferUtil.clear(chunk);
+ assertTrue(!gen.isChunking());
+
+ result = gen.generateResponse(null, false, null, chunk, null, true);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertThat(out, Matchers.containsString("POST /index.html HTTP/1.1"));
+ assertThat(out, Matchers.containsString("Host: something"));
+ assertThat(out, Matchers.containsString("Transfer-Encoding: chunked"));
+ assertThat(out, Matchers.containsString("\r\nD\r\nHello World. \r\n"));
+ assertThat(out, Matchers.containsString("\r\n2D\r\nThe quick brown fox jumped over the lazy dog.\r\n"));
+ assertThat(out, Matchers.containsString("\r\n0\r\n\r\n"));
+
+ assertEquals(58, gen.getContentPrepared());
+ }
+
+ @Test
+ public void testRequestWithKnownContent() throws Exception
+ {
+ String out;
+ ByteBuffer header = BufferUtil.allocate(4096);
+ ByteBuffer chunk = BufferUtil.allocate(HttpGenerator.CHUNK_SIZE);
+ ByteBuffer content0 = BufferUtil.toBuffer("Hello World. ");
+ ByteBuffer content1 = BufferUtil.toBuffer("The quick brown fox jumped over the lazy dog.");
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result
+ result = gen.generateRequest(null, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ Info info = new Info("POST", "/index.html", 58);
+ info.getFields().add("Host", "something");
+ info.getFields().add("User-Agent", "test");
+
+ result = gen.generateRequest(info, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateRequest(info, header, null, content0, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+ assertTrue(!gen.isChunking());
+ out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ out += BufferUtil.toString(content0);
+ BufferUtil.clear(content0);
+
+ result = gen.generateRequest(null, null, null, content1, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+ assertTrue(!gen.isChunking());
+ out += BufferUtil.toString(content1);
+ BufferUtil.clear(content1);
+
+ result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.CONTINUE, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ assertTrue(!gen.isChunking());
+
+ result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+ out += BufferUtil.toString(chunk);
+ BufferUtil.clear(chunk);
+
+ assertThat(out, Matchers.containsString("POST /index.html HTTP/1.1"));
+ assertThat(out, Matchers.containsString("Host: something"));
+ assertThat(out, Matchers.containsString("Content-Length: 58"));
+ assertThat(out, Matchers.containsString("\r\n\r\nHello World. The quick brown fox jumped over the lazy dog."));
+
+ assertEquals(58, gen.getContentPrepared());
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorServerHTTPTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorServerHTTPTest.java
new file mode 100644
index 0000000..1e26969
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorServerHTTPTest.java
@@ -0,0 +1,367 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.either;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HttpGeneratorServerHTTPTest
+{
+ private String _content;
+ private String _reason;
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testHTTP(Run run) throws Exception
+ {
+ Handler handler = new Handler();
+
+ HttpGenerator gen = new HttpGenerator();
+
+ String msg = run.toString();
+
+ run.result.getHttpFields().clear();
+
+ String response = run.result.build(run.httpVersion, gen, "OK\r\nTest", run.connection.val, null, run.chunks);
+
+ HttpParser parser = new HttpParser(handler);
+ parser.setHeadResponse(run.result._head);
+
+ parser.parseNext(BufferUtil.toBuffer(response));
+
+ if (run.result._body != null)
+ assertEquals(run.result._body, this._content, msg);
+
+ // TODO: Break down rationale more clearly, these should be separate checks and/or assertions
+ if (run.httpVersion == 10)
+ assertTrue(gen.isPersistent() || run.result._contentLength >= 0 || EnumSet.of(ConnectionType.CLOSE, ConnectionType.KEEP_ALIVE, ConnectionType.NONE).contains(run.connection), msg);
+ else
+ assertTrue(gen.isPersistent() || EnumSet.of(ConnectionType.CLOSE, ConnectionType.TE_CLOSE).contains(run.connection), msg);
+
+ assertEquals("OK??Test", _reason);
+
+ if (_content == null)
+ assertTrue(run.result._body == null, msg);
+ else
+ assertThat(msg, run.result._contentLength, either(equalTo(_content.length())).or(equalTo(-1)));
+ }
+
+ private static class Result
+ {
+ private HttpFields _fields = new HttpFields();
+ private final String _body;
+ private final int _code;
+ private String _connection;
+ private int _contentLength;
+ private String _contentType;
+ private final boolean _head;
+ private String _other;
+ private String _te;
+
+ private Result(int code, String contentType, int contentLength, String content, boolean head)
+ {
+ _code = code;
+ _contentType = contentType;
+ _contentLength = contentLength;
+ _other = "value";
+ _body = content;
+ _head = head;
+ }
+
+ private String build(int version, HttpGenerator gen, String reason, String connection, String te, int nchunks) throws Exception
+ {
+ String response = "";
+ _connection = connection;
+ _te = te;
+
+ if (_contentType != null)
+ _fields.put("Content-Type", _contentType);
+ if (_contentLength >= 0)
+ _fields.put("Content-Length", "" + _contentLength);
+ if (_connection != null)
+ _fields.put("Connection", _connection);
+ if (_te != null)
+ _fields.put("Transfer-Encoding", _te);
+ if (_other != null)
+ _fields.put("Other", _other);
+
+ ByteBuffer source = _body == null ? null : BufferUtil.toBuffer(_body);
+ ByteBuffer[] chunks = new ByteBuffer[nchunks];
+ ByteBuffer content = null;
+ int c = 0;
+ if (source != null)
+ {
+ for (int i = 0; i < nchunks; i++)
+ {
+ chunks[i] = source.duplicate();
+ chunks[i].position(i * (source.capacity() / nchunks));
+ if (i > 0)
+ chunks[i - 1].limit(chunks[i].position());
+ }
+ content = chunks[c++];
+ }
+ ByteBuffer header = null;
+ ByteBuffer chunk = null;
+ MetaData.Response info = null;
+
+ loop:
+ while (true)
+ {
+ // if we have unwritten content
+ if (source != null && content != null && content.remaining() == 0 && c < nchunks)
+ content = chunks[c++];
+
+ // Generate
+ boolean last = !BufferUtil.hasContent(content);
+
+ HttpGenerator.Result result = gen.generateResponse(info, _head, header, chunk, content, last);
+
+ switch (result)
+ {
+ case NEED_INFO:
+ info = new MetaData.Response(HttpVersion.fromVersion(version), _code, reason, _fields, _contentLength);
+ continue;
+
+ case NEED_HEADER:
+ header = BufferUtil.allocate(2048);
+ continue;
+
+ case HEADER_OVERFLOW:
+ if (header.capacity() >= 8192)
+ throw new BadMessageException(500, "Header too large");
+ header = BufferUtil.allocate(8192);
+ continue;
+
+ case NEED_CHUNK:
+ chunk = BufferUtil.allocate(HttpGenerator.CHUNK_SIZE);
+ continue;
+
+ case NEED_CHUNK_TRAILER:
+ chunk = BufferUtil.allocate(2048);
+ continue;
+
+ case FLUSH:
+ if (BufferUtil.hasContent(header))
+ {
+ response += BufferUtil.toString(header);
+ header.position(header.limit());
+ }
+ if (BufferUtil.hasContent(chunk))
+ {
+ response += BufferUtil.toString(chunk);
+ chunk.position(chunk.limit());
+ }
+ if (BufferUtil.hasContent(content))
+ {
+ response += BufferUtil.toString(content);
+ content.position(content.limit());
+ }
+ break;
+
+ case CONTINUE:
+ continue;
+
+ case SHUTDOWN_OUT:
+ break;
+
+ case DONE:
+ break loop;
+ }
+ }
+ return response;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "[" + _code + "," + _contentType + "," + _contentLength + "," + (_body == null ? "null" : "content") + "]";
+ }
+
+ public HttpFields getHttpFields()
+ {
+ return _fields;
+ }
+ }
+
+ private class Handler implements HttpParser.ResponseHandler
+ {
+ @Override
+ public boolean content(ByteBuffer ref)
+ {
+ if (_content == null)
+ _content = "";
+ _content += BufferUtil.toString(ref);
+ ref.position(ref.limit());
+ return false;
+ }
+
+ @Override
+ public void earlyEOF()
+ {
+ }
+
+ @Override
+ public boolean headerComplete()
+ {
+ _content = null;
+ return false;
+ }
+
+ @Override
+ public boolean contentComplete()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean messageComplete()
+ {
+ return true;
+ }
+
+ @Override
+ public void parsedHeader(HttpField field)
+ {
+ }
+
+ @Override
+ public boolean startResponse(HttpVersion version, int status, String reason)
+ {
+ _reason = reason;
+ return false;
+ }
+
+ @Override
+ public void badMessage(BadMessageException failure)
+ {
+ throw failure;
+ }
+
+ @Override
+ public int getHeaderCacheSize()
+ {
+ return 1024;
+ }
+ }
+
+ public static final String CONTENT = "The quick brown fox jumped over the lazy dog.\nNow is the time for all good men to come to the aid of the party\nThe moon is blue to a fish in love.\n";
+
+ private static class Run
+ {
+ private Result result;
+ private ConnectionType connection;
+ private int httpVersion;
+ private int chunks;
+
+ public Run(Result result, int ver, int chunks, ConnectionType connection)
+ {
+ this.result = result;
+ this.httpVersion = ver;
+ this.chunks = chunks;
+ this.connection = connection;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("result=%s,version=%d,chunks=%d,connection=%s", result, httpVersion, chunks, connection.name());
+ }
+ }
+
+ private enum ConnectionType
+ {
+ NONE(null, 9, 10, 11),
+ KEEP_ALIVE("keep-alive", 9, 10, 11),
+ CLOSE("close", 9, 10, 11),
+ TE_CLOSE("TE, close", 11);
+
+ private String val;
+ private int[] supportedHttpVersions;
+
+ private ConnectionType(String val, int... supportedHttpVersions)
+ {
+ this.val = val;
+ this.supportedHttpVersions = supportedHttpVersions;
+ }
+
+ public boolean isSupportedByHttp(int version)
+ {
+ for (int supported : supportedHttpVersions)
+ {
+ if (supported == version)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ public static Stream<Arguments> data()
+ {
+ Result[] results = {
+ new Result(200, null, -1, null, false),
+ new Result(200, null, -1, CONTENT, false),
+ new Result(200, null, CONTENT.length(), null, true),
+ new Result(200, null, CONTENT.length(), CONTENT, false),
+ new Result(200, "text/html", -1, null, true),
+ new Result(200, "text/html", -1, CONTENT, false),
+ new Result(200, "text/html", CONTENT.length(), null, true),
+ new Result(200, "text/html", CONTENT.length(), CONTENT, false)
+ };
+
+ ArrayList<Arguments> data = new ArrayList<>();
+
+ // For each test result
+ for (Result result : results)
+ {
+ // Loop over HTTP versions
+ for (int v = 10; v <= 11; v++)
+ {
+ // Loop over chunks
+ for (int chunks = 1; chunks <= 6; chunks++)
+ {
+ // Loop over Connection values
+ for (ConnectionType connection : ConnectionType.values())
+ {
+ if (connection.isSupportedByHttp(v))
+ {
+ data.add(Arguments.of(new Run(result, v, chunks, connection)));
+ }
+ }
+ }
+ }
+ }
+
+ return data.stream();
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorServerTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorServerTest.java
new file mode 100644
index 0000000..9338fb1
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorServerTest.java
@@ -0,0 +1,881 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+import java.util.function.Supplier;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class HttpGeneratorServerTest
+{
+ @Test
+ public void test09() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(8096);
+ ByteBuffer content = BufferUtil.toBuffer("0123456789");
+
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content, true);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_0_9, 200, null, new HttpFields(), 10);
+ info.getFields().add("Content-Type", "test/data");
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+
+ result = gen.generateResponse(info, false, null, null, content, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ String response = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ response += BufferUtil.toString(content);
+ BufferUtil.clear(content);
+
+ result = gen.generateResponse(null, false, null, null, content, false);
+ assertEquals(HttpGenerator.Result.SHUTDOWN_OUT, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertEquals(10, gen.getContentPrepared());
+
+ assertThat(response, not(containsString("200 OK")));
+ assertThat(response, not(containsString("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")));
+ assertThat(response, not(containsString("Content-Length: 10")));
+ assertThat(response, containsString("0123456789"));
+ }
+
+ @Test
+ public void testSimple() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(8096);
+ ByteBuffer content = BufferUtil.toBuffer("0123456789");
+
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content, true);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), 10);
+ info.getFields().add("Content-Type", "test/data");
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+
+ result = gen.generateResponse(info, false, null, null, content, true);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+
+ result = gen.generateResponse(info, false, header, null, content, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ String response = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ response += BufferUtil.toString(content);
+ BufferUtil.clear(content);
+
+ result = gen.generateResponse(null, false, null, null, content, false);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertEquals(10, gen.getContentPrepared());
+
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT"));
+ assertThat(response, containsString("Content-Length: 10"));
+ assertThat(response, containsString("\r\n0123456789"));
+ }
+
+ @Test
+ public void testHeaderOverflow() throws Exception
+ {
+ HttpGenerator gen = new HttpGenerator();
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 302, null, new HttpFields(), 0);
+ info.getFields().add("Location", "http://somewhere/else");
+
+ HttpGenerator.Result result = gen.generateResponse(info, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+
+ ByteBuffer header = BufferUtil.allocate(16);
+ result = gen.generateResponse(info, false, header, null, null, true);
+ assertEquals(HttpGenerator.Result.HEADER_OVERFLOW, result);
+
+ header = BufferUtil.allocate(8096);
+ result = gen.generateResponse(info, false, header, null, null, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ String response = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+
+ result = gen.generateResponse(null, false, null, null, null, false);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertEquals(0, gen.getContentPrepared());
+
+ assertThat(response, containsString("HTTP/1.1 302 Found"));
+ assertThat(response, containsString("Location: http://somewhere/else"));
+ }
+
+ @Test
+ public void test204() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(8096);
+ ByteBuffer content = BufferUtil.toBuffer("0123456789");
+
+ HttpGenerator gen = new HttpGenerator();
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 204, "Foo", new HttpFields(), 10);
+ info.getFields().add("Content-Type", "test/data");
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+
+ HttpGenerator.Result result = gen.generateResponse(info, false, header, null, content, true);
+
+ assertEquals(gen.isNoContent(), true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ String responseheaders = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+
+ result = gen.generateResponse(null, false, null, null, content, false);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertThat(responseheaders, containsString("HTTP/1.1 204 Foo"));
+ assertThat(responseheaders, containsString("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT"));
+ assertThat(responseheaders, not(containsString("Content-Length: 10")));
+
+ //Note: the HttpConnection.process() method is responsible for actually
+ //excluding the content from the response based on generator.isNoContent()==true
+ }
+
+ @Test
+ public void testComplexChars() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(8096);
+ ByteBuffer content = BufferUtil.toBuffer("0123456789");
+
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content, true);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, "ØÆ", new HttpFields(), 10);
+ info.getFields().add("Content-Type", "test/data;\r\nextra=value");
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+
+ result = gen.generateResponse(info, false, null, null, content, true);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+
+ result = gen.generateResponse(info, false, header, null, content, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ String response = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ response += BufferUtil.toString(content);
+ BufferUtil.clear(content);
+
+ result = gen.generateResponse(null, false, null, null, content, false);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertEquals(10, gen.getContentPrepared());
+
+ assertThat(response, containsString("HTTP/1.1 200 ØÆ"));
+ assertThat(response, containsString("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT"));
+ assertThat(response, containsString("Content-Type: test/data; extra=value"));
+ assertThat(response, containsString("Content-Length: 10"));
+ assertThat(response, containsString("\r\n0123456789"));
+ }
+
+ @Test
+ public void testSendServerXPoweredBy() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(8096);
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), -1);
+ HttpFields fields = new HttpFields();
+ fields.add(HttpHeader.SERVER, "SomeServer");
+ fields.add(HttpHeader.X_POWERED_BY, "SomePower");
+ MetaData.Response infoF = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, fields, -1);
+ String head;
+
+ HttpGenerator gen = new HttpGenerator(true, true);
+ gen.generateResponse(info, false, header, null, null, true);
+ head = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ assertThat(head, containsString("HTTP/1.1 200 OK"));
+ assertThat(head, containsString("Server: Jetty(9.x.x)"));
+ assertThat(head, containsString("X-Powered-By: Jetty(9.x.x)"));
+ gen.reset();
+ gen.generateResponse(infoF, false, header, null, null, true);
+ head = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ assertThat(head, containsString("HTTP/1.1 200 OK"));
+ assertThat(head, not(containsString("Server: Jetty(9.x.x)")));
+ assertThat(head, containsString("Server: SomeServer"));
+ assertThat(head, containsString("X-Powered-By: Jetty(9.x.x)"));
+ assertThat(head, containsString("X-Powered-By: SomePower"));
+ gen.reset();
+
+ gen = new HttpGenerator(false, false);
+ gen.generateResponse(info, false, header, null, null, true);
+ head = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ assertThat(head, containsString("HTTP/1.1 200 OK"));
+ assertThat(head, not(containsString("Server: Jetty(9.x.x)")));
+ assertThat(head, not(containsString("X-Powered-By: Jetty(9.x.x)")));
+ gen.reset();
+ gen.generateResponse(infoF, false, header, null, null, true);
+ head = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ assertThat(head, containsString("HTTP/1.1 200 OK"));
+ assertThat(head, not(containsString("Server: Jetty(9.x.x)")));
+ assertThat(head, containsString("Server: SomeServer"));
+ assertThat(head, not(containsString("X-Powered-By: Jetty(9.x.x)")));
+ assertThat(head, containsString("X-Powered-By: SomePower"));
+ gen.reset();
+ }
+
+ @Test
+ public void testResponseIncorrectContentLength() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(8096);
+
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), 10);
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+ info.getFields().add("Content-Length", "11");
+
+ result = gen.generateResponse(info, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+
+ BadMessageException e = assertThrows(BadMessageException.class, () ->
+ {
+ gen.generateResponse(info, false, header, null, null, true);
+ });
+ assertEquals(500, e._code);
+ }
+
+ @Test
+ public void testResponseNoContentPersistent() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(8096);
+
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), 0);
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+
+ result = gen.generateResponse(info, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+
+ result = gen.generateResponse(info, false, header, null, null, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ String head = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+
+ result = gen.generateResponse(null, false, null, null, null, false);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertEquals(0, gen.getContentPrepared());
+ assertThat(head, containsString("HTTP/1.1 200 OK"));
+ assertThat(head, containsString("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT"));
+ assertThat(head, containsString("Content-Length: 0"));
+ }
+
+ @Test
+ public void testResponseKnownNoContentNotPersistent() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(8096);
+
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), 0);
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+ info.getFields().add("Connection", "close");
+
+ result = gen.generateResponse(info, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+
+ result = gen.generateResponse(info, false, header, null, null, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ String head = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+
+ result = gen.generateResponse(null, false, null, null, null, false);
+ assertEquals(HttpGenerator.Result.SHUTDOWN_OUT, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertEquals(0, gen.getContentPrepared());
+ assertThat(head, containsString("HTTP/1.1 200 OK"));
+ assertThat(head, containsString("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT"));
+ assertThat(head, containsString("Connection: close"));
+ }
+
+ @Test
+ public void testResponseUpgrade() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(8096);
+
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 101, null, new HttpFields(), -1);
+ info.getFields().add("Upgrade", "WebSocket");
+ info.getFields().add("Connection", "Upgrade");
+ info.getFields().add("Sec-WebSocket-Accept", "123456789==");
+
+ result = gen.generateResponse(info, false, header, null, null, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ String head = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+
+ result = gen.generateResponse(info, false, null, null, null, false);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertEquals(0, gen.getContentPrepared());
+
+ assertThat(head, startsWith("HTTP/1.1 101 Switching Protocols"));
+ assertThat(head, containsString("Upgrade: WebSocket\r\n"));
+ assertThat(head, containsString("Connection: Upgrade\r\n"));
+ }
+
+ @Test
+ public void testResponseWithChunkedContent() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(4096);
+ ByteBuffer chunk = BufferUtil.allocate(HttpGenerator.CHUNK_SIZE);
+ ByteBuffer content0 = BufferUtil.toBuffer("Hello World! ");
+ ByteBuffer content1 = BufferUtil.toBuffer("The quick brown fox jumped over the lazy dog. ");
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), -1);
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+ result = gen.generateResponse(info, false, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateResponse(info, false, header, null, content0, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+
+ String out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ out += BufferUtil.toString(content0);
+ BufferUtil.clear(content0);
+
+ result = gen.generateResponse(null, false, null, chunk, content1, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+ out += BufferUtil.toString(chunk);
+ BufferUtil.clear(chunk);
+ out += BufferUtil.toString(content1);
+ BufferUtil.clear(content1);
+
+ result = gen.generateResponse(null, false, null, chunk, null, true);
+ assertEquals(HttpGenerator.Result.CONTINUE, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+
+ result = gen.generateResponse(null, false, null, chunk, null, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ out += BufferUtil.toString(chunk);
+ BufferUtil.clear(chunk);
+
+ result = gen.generateResponse(null, false, null, chunk, null, true);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertThat(out, containsString("HTTP/1.1 200 OK"));
+ assertThat(out, containsString("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT"));
+ assertThat(out, not(containsString("Content-Length")));
+ assertThat(out, containsString("Transfer-Encoding: chunked"));
+
+ assertThat(out, endsWith(
+ "\r\n\r\nD\r\n" +
+ "Hello World! \r\n" +
+ "2E\r\n" +
+ "The quick brown fox jumped over the lazy dog. \r\n" +
+ "0\r\n" +
+ "\r\n"));
+ }
+
+ @Test
+ public void testResponseWithHintedChunkedContent() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(4096);
+ ByteBuffer chunk = BufferUtil.allocate(HttpGenerator.CHUNK_SIZE);
+ ByteBuffer content0 = BufferUtil.toBuffer("Hello World! ");
+ ByteBuffer content1 = BufferUtil.toBuffer("The quick brown fox jumped over the lazy dog. ");
+ HttpGenerator gen = new HttpGenerator();
+ gen.setPersistent(false);
+
+ HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), -1);
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+ info.getFields().add(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED);
+ result = gen.generateResponse(info, false, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateResponse(info, false, header, null, content0, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+
+ String out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ out += BufferUtil.toString(content0);
+ BufferUtil.clear(content0);
+
+ result = gen.generateResponse(null, false, null, null, content1, false);
+ assertEquals(HttpGenerator.Result.NEED_CHUNK, result);
+
+ result = gen.generateResponse(null, false, null, chunk, content1, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+ out += BufferUtil.toString(chunk);
+ BufferUtil.clear(chunk);
+ out += BufferUtil.toString(content1);
+ BufferUtil.clear(content1);
+
+ result = gen.generateResponse(null, false, null, chunk, null, true);
+ assertEquals(HttpGenerator.Result.CONTINUE, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+
+ result = gen.generateResponse(null, false, null, chunk, null, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ out += BufferUtil.toString(chunk);
+ BufferUtil.clear(chunk);
+
+ result = gen.generateResponse(null, false, null, chunk, null, true);
+ assertEquals(HttpGenerator.Result.SHUTDOWN_OUT, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertThat(out, containsString("HTTP/1.1 200 OK"));
+ assertThat(out, containsString("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT"));
+ assertThat(out, not(containsString("Content-Length")));
+ assertThat(out, containsString("Transfer-Encoding: chunked"));
+
+ assertThat(out, endsWith(
+ "\r\n\r\nD\r\n" +
+ "Hello World! \r\n" +
+ "2E\r\n" +
+ "The quick brown fox jumped over the lazy dog. \r\n" +
+ "0\r\n" +
+ "\r\n"));
+ }
+
+ @Test
+ public void testResponseWithContentAndTrailer() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(4096);
+ ByteBuffer chunk = BufferUtil.allocate(HttpGenerator.CHUNK_SIZE);
+ ByteBuffer trailer = BufferUtil.allocate(4096);
+ ByteBuffer content0 = BufferUtil.toBuffer("Hello World! ");
+ ByteBuffer content1 = BufferUtil.toBuffer("The quick brown fox jumped over the lazy dog. ");
+ HttpGenerator gen = new HttpGenerator();
+ gen.setPersistent(false);
+
+ HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), -1);
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+ info.getFields().add(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED);
+ info.setTrailerSupplier(new Supplier<HttpFields>()
+ {
+ @Override
+ public HttpFields get()
+ {
+ HttpFields trailer = new HttpFields();
+ trailer.add("T-Name0", "T-ValueA");
+ trailer.add("T-Name0", "T-ValueB");
+ trailer.add("T-Name1", "T-ValueC");
+ return trailer;
+ }
+ });
+
+ result = gen.generateResponse(info, false, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateResponse(info, false, header, null, content0, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+
+ String out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ out += BufferUtil.toString(content0);
+ BufferUtil.clear(content0);
+
+ result = gen.generateResponse(null, false, null, null, content1, false);
+ assertEquals(HttpGenerator.Result.NEED_CHUNK, result);
+
+ result = gen.generateResponse(null, false, null, chunk, content1, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+ out += BufferUtil.toString(chunk);
+ BufferUtil.clear(chunk);
+ out += BufferUtil.toString(content1);
+ BufferUtil.clear(content1);
+
+ result = gen.generateResponse(null, false, null, chunk, null, true);
+ assertEquals(HttpGenerator.Result.CONTINUE, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+
+ result = gen.generateResponse(null, false, null, chunk, null, true);
+
+ assertEquals(HttpGenerator.Result.NEED_CHUNK_TRAILER, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+
+ result = gen.generateResponse(null, false, null, trailer, null, true);
+
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ out += BufferUtil.toString(trailer);
+ BufferUtil.clear(trailer);
+
+ result = gen.generateResponse(null, false, null, trailer, null, true);
+ assertEquals(HttpGenerator.Result.SHUTDOWN_OUT, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertThat(out, containsString("HTTP/1.1 200 OK"));
+ assertThat(out, containsString("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT"));
+ assertThat(out, not(containsString("Content-Length")));
+ assertThat(out, containsString("Transfer-Encoding: chunked"));
+
+ assertThat(out, endsWith(
+ "\r\n\r\nD\r\n" +
+ "Hello World! \r\n" +
+ "2E\r\n" +
+ "The quick brown fox jumped over the lazy dog. \r\n" +
+ "0\r\n" +
+ "T-Name0: T-ValueA\r\n" +
+ "T-Name0: T-ValueB\r\n" +
+ "T-Name1: T-ValueC\r\n" +
+ "\r\n"));
+ }
+
+ @Test
+ public void testResponseWithTrailer() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(4096);
+ ByteBuffer chunk = BufferUtil.allocate(HttpGenerator.CHUNK_SIZE);
+ ByteBuffer trailer = BufferUtil.allocate(4096);
+ HttpGenerator gen = new HttpGenerator();
+ gen.setPersistent(false);
+
+ HttpGenerator.Result result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), -1);
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+ info.getFields().add(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED);
+ info.setTrailerSupplier(new Supplier<HttpFields>()
+ {
+ @Override
+ public HttpFields get()
+ {
+ HttpFields trailer = new HttpFields();
+ trailer.add("T-Name0", "T-ValueA");
+ trailer.add("T-Name0", "T-ValueB");
+ trailer.add("T-Name1", "T-ValueC");
+ return trailer;
+ }
+ });
+
+ result = gen.generateResponse(info, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateResponse(info, false, header, null, null, true);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+
+ String out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+
+ result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.NEED_CHUNK_TRAILER, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+
+ result = gen.generateResponse(null, false, null, chunk, null, true);
+ assertEquals(HttpGenerator.Result.NEED_CHUNK_TRAILER, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+
+ result = gen.generateResponse(null, false, null, trailer, null, true);
+
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+ out += BufferUtil.toString(trailer);
+ BufferUtil.clear(trailer);
+
+ result = gen.generateResponse(null, false, null, trailer, null, true);
+ assertEquals(HttpGenerator.Result.SHUTDOWN_OUT, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertThat(out, containsString("HTTP/1.1 200 OK"));
+ assertThat(out, containsString("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT"));
+ assertThat(out, not(containsString("Content-Length")));
+ assertThat(out, containsString("Transfer-Encoding: chunked"));
+
+ assertThat(out, endsWith(
+ "\r\n\r\n" +
+ "0\r\n" +
+ "T-Name0: T-ValueA\r\n" +
+ "T-Name0: T-ValueB\r\n" +
+ "T-Name1: T-ValueC\r\n" +
+ "\r\n"));
+ }
+
+ @Test
+ public void testResponseWithKnownContentLengthFromMetaData() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(4096);
+ ByteBuffer content0 = BufferUtil.toBuffer("Hello World! ");
+ ByteBuffer content1 = BufferUtil.toBuffer("The quick brown fox jumped over the lazy dog. ");
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), 59);
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+ result = gen.generateResponse(info, false, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateResponse(info, false, header, null, content0, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+
+ String out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ out += BufferUtil.toString(content0);
+ BufferUtil.clear(content0);
+
+ result = gen.generateResponse(null, false, null, null, content1, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+ out += BufferUtil.toString(content1);
+ BufferUtil.clear(content1);
+
+ result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.CONTINUE, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+
+ result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertThat(out, containsString("HTTP/1.1 200 OK"));
+ assertThat(out, containsString("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT"));
+ assertThat(out, not(containsString("chunked")));
+ assertThat(out, containsString("Content-Length: 59"));
+ assertThat(out, containsString("\r\n\r\nHello World! The quick brown fox jumped over the lazy dog. "));
+ }
+
+ @Test
+ public void testResponseWithKnownContentLengthFromHeader() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(4096);
+ ByteBuffer content0 = BufferUtil.toBuffer("Hello World! ");
+ ByteBuffer content1 = BufferUtil.toBuffer("The quick brown fox jumped over the lazy dog. ");
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), -1);
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+ info.getFields().add("Content-Length", "" + (content0.remaining() + content1.remaining()));
+ result = gen.generateResponse(info, false, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateResponse(info, false, header, null, content0, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+
+ String out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ out += BufferUtil.toString(content0);
+ BufferUtil.clear(content0);
+
+ result = gen.generateResponse(null, false, null, null, content1, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+ out += BufferUtil.toString(content1);
+ BufferUtil.clear(content1);
+
+ result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.CONTINUE, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+
+ result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertThat(out, containsString("HTTP/1.1 200 OK"));
+ assertThat(out, containsString("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT"));
+ assertThat(out, not(containsString("chunked")));
+ assertThat(out, containsString("Content-Length: 59"));
+ assertThat(out, containsString("\r\n\r\nHello World! The quick brown fox jumped over the lazy dog. "));
+ }
+
+ @Test
+ public void test100ThenResponseWithContent() throws Exception
+ {
+ ByteBuffer header = BufferUtil.allocate(4096);
+ ByteBuffer content0 = BufferUtil.toBuffer("Hello World! ");
+ ByteBuffer content1 = BufferUtil.toBuffer("The quick brown fox jumped over the lazy dog. ");
+ HttpGenerator gen = new HttpGenerator();
+
+ HttpGenerator.Result result = gen.generateResponse(HttpGenerator.CONTINUE_100_INFO, false, null, null, null, false);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateResponse(HttpGenerator.CONTINUE_100_INFO, false, header, null, null, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMPLETING_1XX, gen.getState());
+ String out = BufferUtil.toString(header);
+
+ result = gen.generateResponse(null, false, null, null, null, false);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ assertThat(out, containsString("HTTP/1.1 100 Continue"));
+
+ result = gen.generateResponse(null, false, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_INFO, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), BufferUtil.length(content0) + BufferUtil.length(content1));
+ info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
+ result = gen.generateResponse(info, false, null, null, content0, false);
+ assertEquals(HttpGenerator.Result.NEED_HEADER, result);
+ assertEquals(HttpGenerator.State.START, gen.getState());
+
+ result = gen.generateResponse(info, false, header, null, content0, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+
+ out = BufferUtil.toString(header);
+ BufferUtil.clear(header);
+ out += BufferUtil.toString(content0);
+ BufferUtil.clear(content0);
+
+ result = gen.generateResponse(null, false, null, null, content1, false);
+ assertEquals(HttpGenerator.Result.FLUSH, result);
+ assertEquals(HttpGenerator.State.COMMITTED, gen.getState());
+ out += BufferUtil.toString(content1);
+ BufferUtil.clear(content1);
+
+ result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.CONTINUE, result);
+ assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
+
+ result = gen.generateResponse(null, false, null, null, null, true);
+ assertEquals(HttpGenerator.Result.DONE, result);
+ assertEquals(HttpGenerator.State.END, gen.getState());
+
+ assertThat(out, containsString("HTTP/1.1 200 OK"));
+ assertThat(out, containsString("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT"));
+ assertThat(out, not(containsString("chunked")));
+ assertThat(out, containsString("Content-Length: 59"));
+ assertThat(out, containsString("\r\n\r\nHello World! The quick brown fox jumped over the lazy dog. "));
+ }
+
+ @Test
+ public void testConnectionKeepAliveWithAdditionalCustomValue() throws Exception
+ {
+ HttpGenerator generator = new HttpGenerator();
+
+ HttpFields fields = new HttpFields();
+ fields.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE);
+ String customValue = "test";
+ fields.add(HttpHeader.CONNECTION, customValue);
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_0, 200, "OK", fields, -1);
+ ByteBuffer header = BufferUtil.allocate(4096);
+ HttpGenerator.Result result = generator.generateResponse(info, false, header, null, null, true);
+ assertSame(HttpGenerator.Result.FLUSH, result);
+ String headers = BufferUtil.toString(header);
+ assertThat(headers, containsString(HttpHeaderValue.KEEP_ALIVE.asString()));
+ assertThat(headers, containsString(customValue));
+ }
+
+ @Test
+ public void testKeepAliveWithClose() throws Exception
+ {
+ HttpGenerator generator = new HttpGenerator();
+ HttpFields fields = new HttpFields();
+ fields.put(HttpHeader.CONNECTION,
+ HttpHeaderValue.KEEP_ALIVE.asString() + ", other, " + HttpHeaderValue.CLOSE.asString());
+ MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_0, 200, "OK", fields, -1);
+ ByteBuffer header = BufferUtil.allocate(4096);
+ HttpGenerator.Result result = generator.generateResponse(info, false, header, null, null, true);
+ assertSame(HttpGenerator.Result.FLUSH, result);
+ String headers = BufferUtil.toString(header);
+ assertThat(headers, containsString("Connection: other, close\r\n"));
+ assertThat(headers, not(containsString("keep-alive")));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java
new file mode 100644
index 0000000..f886ad6
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java
@@ -0,0 +1,3058 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+import org.eclipse.jetty.http.HttpParser.State;
+import org.eclipse.jetty.toolchain.test.Net;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.eclipse.jetty.http.HttpComplianceSection.NO_FIELD_FOLDING;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HttpParserTest
+{
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+
+ static
+ {
+ HttpCompliance.CUSTOM0.sections().remove(HttpComplianceSection.NO_WS_AFTER_FIELD_NAME);
+ }
+
+ /**
+ * Parse until {@link State#END} state.
+ * If the parser is already in the END state, then it is {@link HttpParser#reset()} and re-parsed.
+ *
+ * @param parser The parser to test
+ * @param buffer the buffer to parse
+ * @throws IllegalStateException If the buffers have already been partially parsed.
+ */
+ public static void parseAll(HttpParser parser, ByteBuffer buffer)
+ {
+ if (parser.isState(State.END))
+ parser.reset();
+ if (!parser.isState(State.START))
+ throw new IllegalStateException("!START");
+
+ // continue parsing
+ int remaining = buffer.remaining();
+ while (!parser.isState(State.END) && remaining > 0)
+ {
+ int wasRemaining = remaining;
+ parser.parseNext(buffer);
+ remaining = buffer.remaining();
+ if (remaining == wasRemaining)
+ break;
+ }
+ }
+
+ @Test
+ public void httpMethodTest()
+ {
+ for (HttpMethod m : HttpMethod.values())
+ {
+ assertNull(HttpMethod.lookAheadGet(BufferUtil.toBuffer(m.asString().substring(0, 2))));
+ assertNull(HttpMethod.lookAheadGet(BufferUtil.toBuffer(m.asString())));
+ assertNull(HttpMethod.lookAheadGet(BufferUtil.toBuffer(m.asString() + "FOO")));
+ assertEquals(m, HttpMethod.lookAheadGet(BufferUtil.toBuffer(m.asString() + " ")));
+ assertEquals(m, HttpMethod.lookAheadGet(BufferUtil.toBuffer(m.asString() + " /foo/bar")));
+
+ assertNull(HttpMethod.lookAheadGet(m.asString().substring(0, 2).getBytes(), 0, 2));
+ assertNull(HttpMethod.lookAheadGet(m.asString().getBytes(), 0, m.asString().length()));
+ assertNull(HttpMethod.lookAheadGet((m.asString() + "FOO").getBytes(), 0, m.asString().length() + 3));
+ assertEquals(m, HttpMethod.lookAheadGet(("\n" + m.asString() + " ").getBytes(), 1, m.asString().length() + 2));
+ assertEquals(m, HttpMethod.lookAheadGet(("\n" + m.asString() + " /foo").getBytes(), 1, m.asString().length() + 6));
+ }
+
+ ByteBuffer b = BufferUtil.allocateDirect(128);
+ BufferUtil.append(b, BufferUtil.toBuffer("GET"));
+ assertNull(HttpMethod.lookAheadGet(b));
+
+ BufferUtil.append(b, BufferUtil.toBuffer(" "));
+ assertEquals(HttpMethod.GET, HttpMethod.lookAheadGet(b));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"GET", "POST", "VERSION-CONTROL"})
+ public void httpMethodNameTest(String methodName)
+ {
+ HttpMethod method = HttpMethod.fromString(methodName);
+ assertNotNull(method, "Method should have been found: " + methodName);
+ assertEquals(methodName.toUpperCase(Locale.US), method.toString());
+ }
+
+ @Test
+ public void testLineParseMockIP()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer("POST /mock/127.0.0.1 HTTP/1.1\r\n" + "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertEquals("POST", _methodOrVersion);
+ assertEquals("/mock/127.0.0.1", _uriOrStatus);
+ assertEquals("HTTP/1.1", _versionOrReason);
+ assertEquals(-1, _headers);
+ }
+
+ @Test
+ public void testLineParse0()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer("POST /foo HTTP/1.0\r\n" + "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertEquals("POST", _methodOrVersion);
+ assertEquals("/foo", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(-1, _headers);
+ }
+
+ @Test
+ public void testLineParse1RFC2616()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer("GET /999\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY);
+ parseAll(parser, buffer);
+
+ assertNull(_bad);
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/999", _uriOrStatus);
+ assertEquals("HTTP/0.9", _versionOrReason);
+ assertEquals(-1, _headers);
+ assertThat(_complianceViolation, contains(HttpComplianceSection.NO_HTTP_0_9));
+ }
+
+ @Test
+ public void testLineParse1()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer("GET /999\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertEquals("HTTP/0.9 not supported", _bad);
+ assertThat(_complianceViolation, Matchers.empty());
+ }
+
+ @Test
+ public void testLineParse2RFC2616()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer("POST /222 \r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY);
+ parseAll(parser, buffer);
+
+ assertNull(_bad);
+ assertEquals("POST", _methodOrVersion);
+ assertEquals("/222", _uriOrStatus);
+ assertEquals("HTTP/0.9", _versionOrReason);
+ assertEquals(-1, _headers);
+ assertThat(_complianceViolation, contains(HttpComplianceSection.NO_HTTP_0_9));
+ }
+
+ @Test
+ public void testLineParse2()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer("POST /222 \r\n");
+
+ _versionOrReason = null;
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertEquals("HTTP/0.9 not supported", _bad);
+ assertThat(_complianceViolation, Matchers.empty());
+ }
+
+ @Test
+ public void testLineParse3()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer("POST /fo\u0690 HTTP/1.0\r\n" + "\r\n", StandardCharsets.UTF_8);
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertEquals("POST", _methodOrVersion);
+ assertEquals("/fo\u0690", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(-1, _headers);
+ }
+
+ @Test
+ public void testLineParse4()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer("POST /foo?param=\u0690 HTTP/1.0\r\n" + "\r\n", StandardCharsets.UTF_8);
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertEquals("POST", _methodOrVersion);
+ assertEquals("/foo?param=\u0690", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(-1, _headers);
+ }
+
+ @Test
+ public void testLongURLParse()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer("POST /123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/ HTTP/1.0\r\n" + "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertEquals("POST", _methodOrVersion);
+ assertEquals("/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(-1, _headers);
+ }
+
+ @Test
+ public void testAllowedLinePreamble()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer("\r\n\r\nGET / HTTP/1.0\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(-1, _headers);
+ }
+
+ @Test
+ public void testDisallowedLinePreamble()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer("\r\n \r\nGET / HTTP/1.0\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertEquals("Illegal character SPACE=' '", _bad);
+ }
+
+ @Test
+ public void testConnect()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer("CONNECT 192.168.1.2:80 HTTP/1.1\r\n" + "\r\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertEquals("CONNECT", _methodOrVersion);
+ assertEquals("192.168.1.2:80", _uriOrStatus);
+ assertEquals("HTTP/1.1", _versionOrReason);
+ assertEquals(-1, _headers);
+ }
+
+ @Test
+ public void testSimple()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals("Host", _hdr[0]);
+ assertEquals("localhost", _val[0]);
+ assertEquals("Connection", _hdr[1]);
+ assertEquals("close", _val[1]);
+ assertEquals(1, _headers);
+ }
+
+ @Test
+ public void testFoldedField2616()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Name: value\r\n" +
+ " extra\r\n" +
+ "Name2: \r\n" +
+ "\tvalue2\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY);
+ parseAll(parser, buffer);
+
+ assertThat(_bad, Matchers.nullValue());
+ assertEquals("Host", _hdr[0]);
+ assertEquals("localhost", _val[0]);
+ assertEquals(2, _headers);
+ assertEquals("Name", _hdr[1]);
+ assertEquals("value extra", _val[1]);
+ assertEquals("Name2", _hdr[2]);
+ assertEquals("value2", _val[2]);
+ assertThat(_complianceViolation, contains(NO_FIELD_FOLDING, NO_FIELD_FOLDING));
+ }
+
+ @Test
+ public void testFoldedField7230()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Name: value\r\n" +
+ " extra\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, 4096, HttpCompliance.RFC7230_LEGACY);
+ parseAll(parser, buffer);
+
+ assertThat(_bad, Matchers.notNullValue());
+ assertThat(_bad, containsString("Header Folding"));
+ assertThat(_complianceViolation, Matchers.empty());
+ }
+
+ @Test
+ public void testWhiteSpaceInName()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "N ame: value\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, 4096, HttpCompliance.RFC7230_LEGACY);
+ parseAll(parser, buffer);
+
+ assertThat(_bad, Matchers.notNullValue());
+ assertThat(_bad, containsString("Illegal character"));
+ }
+
+ @Test
+ public void testWhiteSpaceAfterName()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Name : value\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, 4096, HttpCompliance.RFC7230_LEGACY);
+ parseAll(parser, buffer);
+
+ assertThat(_bad, Matchers.notNullValue());
+ assertThat(_bad, containsString("Illegal character"));
+ }
+
+ @Test // TODO: Parameterize Test
+ public void testWhiteSpaceBeforeRequest()
+ {
+ HttpCompliance[] compliances = new HttpCompliance[]
+ {
+ HttpCompliance.RFC7230, HttpCompliance.RFC2616
+ };
+
+ String[][] whitespaces = new String[][]
+ {
+ {" ", "Illegal character SPACE"},
+ {"\t", "Illegal character HTAB"},
+ {"\n", null},
+ {"\r", "Bad EOL"},
+ {"\r\n", null},
+ {"\r\n\r\n", null},
+ {"\r\n \r\n", "Illegal character SPACE"},
+ {"\r\n\t\r\n", "Illegal character HTAB"},
+ {"\r\t\n", "Bad EOL"},
+ {"\r\r\n", "Bad EOL"},
+ {"\t\r\t\r\n", "Illegal character HTAB"},
+ {" \t \r \t \n\n", "Illegal character SPACE"},
+ {" \r \t \r\n\r\n\r\n", "Illegal character SPACE"}
+ };
+
+ for (HttpCompliance compliance : compliances)
+ {
+ for (int j = 0; j < whitespaces.length; j++)
+ {
+ String request =
+ whitespaces[j][0] +
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Name: value" + j + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+
+ ByteBuffer buffer = BufferUtil.toBuffer(request);
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, 4096, compliance);
+ _bad = null;
+ parseAll(parser, buffer);
+
+ String test = "whitespace.[" + compliance + "].[" + j + "]";
+ String expected = whitespaces[j][1];
+ if (expected == null)
+ assertThat(test, _bad, is(nullValue()));
+ else
+ assertThat(test, _bad, containsString(expected));
+ }
+ }
+ }
+
+ @Test
+ public void testNoValue()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Name0: \r\n" +
+ "Name1:\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals("Host", _hdr[0]);
+ assertEquals("localhost", _val[0]);
+ assertEquals("Name0", _hdr[1]);
+ assertEquals("", _val[1]);
+ assertEquals("Name1", _hdr[2]);
+ assertEquals("", _val[2]);
+ assertEquals(2, _headers);
+ }
+
+ @Test
+ public void testSpaceinNameCustom0()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Name with space: value\r\n" +
+ "Other: value\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, HttpCompliance.CUSTOM0);
+ parseAll(parser, buffer);
+
+ assertThat(_bad, containsString("Illegal character"));
+ assertThat(_complianceViolation, contains(HttpComplianceSection.NO_WS_AFTER_FIELD_NAME));
+ }
+
+ @Test
+ public void testNoColonCustom0()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Name \r\n" +
+ "Other: value\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, HttpCompliance.CUSTOM0);
+ parseAll(parser, buffer);
+
+ assertThat(_bad, containsString("Illegal character"));
+ assertThat(_complianceViolation, contains(HttpComplianceSection.NO_WS_AFTER_FIELD_NAME));
+ }
+
+ @Test
+ public void testTrailingSpacesInHeaderNameInCustom0Mode()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 204 No Content\r\n" +
+ "Access-Control-Allow-Headers : Origin\r\n" +
+ "Other\t : value\r\n" +
+ "\r\n");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, -1, HttpCompliance.CUSTOM0);
+ parseAll(parser, buffer);
+
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("204", _uriOrStatus);
+ assertEquals("No Content", _versionOrReason);
+ assertNull(_content);
+
+ assertEquals(1, _headers);
+ System.out.println(Arrays.asList(_hdr));
+ System.out.println(Arrays.asList(_val));
+ assertEquals("Access-Control-Allow-Headers", _hdr[0]);
+ assertEquals("Origin", _val[0]);
+ assertEquals("Other", _hdr[1]);
+ assertEquals("value", _val[1]);
+
+ assertThat(_complianceViolation, contains(HttpComplianceSection.NO_WS_AFTER_FIELD_NAME, HttpComplianceSection.NO_WS_AFTER_FIELD_NAME));
+ }
+
+ @Test
+ public void testTrailingSpacesInHeaderNameNoCustom0()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 204 No Content\r\n" +
+ "Access-Control-Allow-Headers : Origin\r\n" +
+ "Other: value\r\n" +
+ "\r\n");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("204", _uriOrStatus);
+ assertEquals("No Content", _versionOrReason);
+ assertThat(_bad, containsString("Illegal character "));
+ }
+
+ @Test
+ public void testNoColon7230()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Name\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, HttpCompliance.RFC7230_LEGACY);
+ parseAll(parser, buffer);
+ assertThat(_bad, containsString("Illegal character"));
+ assertThat(_complianceViolation, Matchers.empty());
+ }
+
+ @Test
+ public void testHeaderParseDirect()
+ {
+ ByteBuffer b0 = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Header1: value1\r\n" +
+ "Header2: value 2a \r\n" +
+ "Header3: 3\r\n" +
+ "Header4:value4\r\n" +
+ "Server5: notServer\r\n" +
+ "HostHeader: notHost\r\n" +
+ "Connection: close\r\n" +
+ "Accept-Encoding: gzip, deflated\r\n" +
+ "Accept: unknown\r\n" +
+ "\r\n");
+ ByteBuffer buffer = BufferUtil.allocateDirect(b0.capacity());
+ int pos = BufferUtil.flipToFill(buffer);
+ BufferUtil.put(b0, buffer);
+ BufferUtil.flipToFlush(buffer, pos);
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals("Host", _hdr[0]);
+ assertEquals("localhost", _val[0]);
+ assertEquals("Header1", _hdr[1]);
+ assertEquals("value1", _val[1]);
+ assertEquals("Header2", _hdr[2]);
+ assertEquals("value 2a", _val[2]);
+ assertEquals("Header3", _hdr[3]);
+ assertEquals("3", _val[3]);
+ assertEquals("Header4", _hdr[4]);
+ assertEquals("value4", _val[4]);
+ assertEquals("Server5", _hdr[5]);
+ assertEquals("notServer", _val[5]);
+ assertEquals("HostHeader", _hdr[6]);
+ assertEquals("notHost", _val[6]);
+ assertEquals("Connection", _hdr[7]);
+ assertEquals("close", _val[7]);
+ assertEquals("Accept-Encoding", _hdr[8]);
+ assertEquals("gzip, deflated", _val[8]);
+ assertEquals("Accept", _hdr[9]);
+ assertEquals("unknown", _val[9]);
+ assertEquals(9, _headers);
+ }
+
+ @Test
+ public void testHeaderParseCRLF()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Header1: value1\r\n" +
+ "Header2: value 2a \r\n" +
+ "Header3: 3\r\n" +
+ "Header4:value4\r\n" +
+ "Server5: notServer\r\n" +
+ "HostHeader: notHost\r\n" +
+ "Connection: close\r\n" +
+ "Accept-Encoding: gzip, deflated\r\n" +
+ "Accept: unknown\r\n" +
+ "\r\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals("Host", _hdr[0]);
+ assertEquals("localhost", _val[0]);
+ assertEquals("Header1", _hdr[1]);
+ assertEquals("value1", _val[1]);
+ assertEquals("Header2", _hdr[2]);
+ assertEquals("value 2a", _val[2]);
+ assertEquals("Header3", _hdr[3]);
+ assertEquals("3", _val[3]);
+ assertEquals("Header4", _hdr[4]);
+ assertEquals("value4", _val[4]);
+ assertEquals("Server5", _hdr[5]);
+ assertEquals("notServer", _val[5]);
+ assertEquals("HostHeader", _hdr[6]);
+ assertEquals("notHost", _val[6]);
+ assertEquals("Connection", _hdr[7]);
+ assertEquals("close", _val[7]);
+ assertEquals("Accept-Encoding", _hdr[8]);
+ assertEquals("gzip, deflated", _val[8]);
+ assertEquals("Accept", _hdr[9]);
+ assertEquals("unknown", _val[9]);
+ assertEquals(9, _headers);
+ }
+
+ @Test
+ public void testHeaderParseLF()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\n" +
+ "Host: localhost\n" +
+ "Header1: value1\n" +
+ "Header2: value 2a value 2b \n" +
+ "Header3: 3\n" +
+ "Header4:value4\n" +
+ "Server5: notServer\n" +
+ "HostHeader: notHost\n" +
+ "Connection: close\n" +
+ "Accept-Encoding: gzip, deflated\n" +
+ "Accept: unknown\n" +
+ "\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals("Host", _hdr[0]);
+ assertEquals("localhost", _val[0]);
+ assertEquals("Header1", _hdr[1]);
+ assertEquals("value1", _val[1]);
+ assertEquals("Header2", _hdr[2]);
+ assertEquals("value 2a value 2b", _val[2]);
+ assertEquals("Header3", _hdr[3]);
+ assertEquals("3", _val[3]);
+ assertEquals("Header4", _hdr[4]);
+ assertEquals("value4", _val[4]);
+ assertEquals("Server5", _hdr[5]);
+ assertEquals("notServer", _val[5]);
+ assertEquals("HostHeader", _hdr[6]);
+ assertEquals("notHost", _val[6]);
+ assertEquals("Connection", _hdr[7]);
+ assertEquals("close", _val[7]);
+ assertEquals("Accept-Encoding", _hdr[8]);
+ assertEquals("gzip, deflated", _val[8]);
+ assertEquals("Accept", _hdr[9]);
+ assertEquals("unknown", _val[9]);
+ assertEquals(9, _headers);
+ }
+
+ @Test
+ public void testQuoted()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\n" +
+ "Name0: \"value0\"\t\n" +
+ "Name1: \"value\t1\"\n" +
+ "Name2: \"value\t2A\",\"value,2B\"\t\n" +
+ "\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals("Name0", _hdr[0]);
+ assertEquals("\"value0\"", _val[0]);
+ assertEquals("Name1", _hdr[1]);
+ assertEquals("\"value\t1\"", _val[1]);
+ assertEquals("Name2", _hdr[2]);
+ assertEquals("\"value\t2A\",\"value,2B\"", _val[2]);
+ assertEquals(2, _headers);
+ }
+
+ @Test
+ public void testEncodedHeader()
+ {
+ ByteBuffer buffer = BufferUtil.allocate(4096);
+ BufferUtil.flipToFill(buffer);
+ BufferUtil.put(BufferUtil.toBuffer("GET "), buffer);
+ buffer.put("/foo/\u0690/".getBytes(StandardCharsets.UTF_8));
+ BufferUtil.put(BufferUtil.toBuffer(" HTTP/1.0\r\n"), buffer);
+ BufferUtil.put(BufferUtil.toBuffer("Header1: "), buffer);
+ buffer.put("\u00e6 \u00e6".getBytes(StandardCharsets.ISO_8859_1));
+ BufferUtil.put(BufferUtil.toBuffer(" \r\nHeader2: "), buffer);
+ buffer.put((byte)-1);
+ BufferUtil.put(BufferUtil.toBuffer("\r\n\r\n"), buffer);
+ BufferUtil.flipToFlush(buffer, 0);
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/foo/\u0690/", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals("Header1", _hdr[0]);
+ assertEquals("\u00e6 \u00e6", _val[0]);
+ assertEquals("Header2", _hdr[1]);
+ assertEquals("" + (char)255, _val[1]);
+ assertEquals(1, _headers);
+ assertNull(_bad);
+ }
+
+ @Test
+ public void testResponseBufferUpgradeFrom()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 101 Upgrade\r\n" +
+ "Connection: upgrade\r\n" +
+ "Content-Length: 0\r\n" +
+ "Sec-WebSocket-Accept: 4GnyoUP4Sc1JD+2pCbNYAhFYVVA\r\n" +
+ "\r\n" +
+ "FOOGRADE");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ while (!parser.isState(State.END))
+ {
+ parser.parseNext(buffer);
+ }
+
+ assertThat(BufferUtil.toUTF8String(buffer), Matchers.is("FOOGRADE"));
+ }
+
+ @Test
+ public void testBadMethodEncoding()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "G\u00e6T / HTTP/1.0\r\nHeader0: value0\r\n\n\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertThat(_bad, Matchers.notNullValue());
+ }
+
+ @Test
+ public void testBadVersionEncoding()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / H\u00e6P/1.0\r\nHeader0: value0\r\n\n\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertThat(_bad, Matchers.notNullValue());
+ }
+
+ @Test
+ public void testBadHeaderEncoding()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "H\u00e6der0: value0\r\n" +
+ "\n\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertThat(_bad, Matchers.notNullValue());
+ }
+
+ @Test // TODO: Parameterize Test
+ public void testBadHeaderNames()
+ {
+ String[] bad = new String[]
+ {
+ "Foo\\Bar: value\r\n",
+ "Foo@Bar: value\r\n",
+ "Foo,Bar: value\r\n",
+ "Foo}Bar: value\r\n",
+ "Foo{Bar: value\r\n",
+ "Foo=Bar: value\r\n",
+ "Foo>Bar: value\r\n",
+ "Foo<Bar: value\r\n",
+ "Foo)Bar: value\r\n",
+ "Foo(Bar: value\r\n",
+ "Foo?Bar: value\r\n",
+ "Foo\"Bar: value\r\n",
+ "Foo/Bar: value\r\n",
+ "Foo]Bar: value\r\n",
+ "Foo[Bar: value\r\n"
+ };
+
+ for (String s : bad)
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" + s + "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertThat(s, _bad, Matchers.notNullValue());
+ }
+ }
+
+ @Test
+ public void testHeaderTab()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Header: value\talternate\r\n" +
+ "\n\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/", _uriOrStatus);
+ assertEquals("HTTP/1.1", _versionOrReason);
+ assertEquals("Host", _hdr[0]);
+ assertEquals("localhost", _val[0]);
+ assertEquals("Header", _hdr[1]);
+ assertEquals("value\talternate", _val[1]);
+ }
+
+ @Test
+ public void testCaseSensitiveMethod()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "gEt / http/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, -1, HttpCompliance.RFC7230_LEGACY);
+ parseAll(parser, buffer);
+ assertNull(_bad);
+ assertEquals("GET", _methodOrVersion);
+ assertThat(_complianceViolation, contains(HttpComplianceSection.METHOD_CASE_SENSITIVE));
+ }
+
+ @Test
+ public void testCaseSensitiveMethodLegacy()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "gEt / http/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, -1, HttpCompliance.LEGACY);
+ parseAll(parser, buffer);
+ assertNull(_bad);
+ assertEquals("gEt", _methodOrVersion);
+ assertThat(_complianceViolation, Matchers.empty());
+ }
+
+ @Test
+ public void testCaseInsensitiveHeader()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / http/1.0\r\n" +
+ "HOST: localhost\r\n" +
+ "cOnNeCtIoN: ClOsE\r\n" +
+ "\r\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, -1, HttpCompliance.RFC7230_LEGACY);
+ parseAll(parser, buffer);
+ assertNull(_bad);
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals("Host", _hdr[0]);
+ assertEquals("localhost", _val[0]);
+ assertEquals("Connection", _hdr[1]);
+ assertEquals("close", _val[1]);
+ assertEquals(1, _headers);
+ assertThat(_complianceViolation, Matchers.empty());
+ }
+
+ @Test
+ public void testCaseInSensitiveHeaderLegacy()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / http/1.0\r\n" +
+ "HOST: localhost\r\n" +
+ "cOnNeCtIoN: ClOsE\r\n" +
+ "\r\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, -1, HttpCompliance.LEGACY);
+ parseAll(parser, buffer);
+ assertNull(_bad);
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals("HOST", _hdr[0]);
+ assertEquals("localhost", _val[0]);
+ assertEquals("cOnNeCtIoN", _hdr[1]);
+ assertEquals("ClOsE", _val[1]);
+ assertEquals(1, _headers);
+ assertThat(_complianceViolation, contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE, HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE, HttpComplianceSection.CASE_INSENSITIVE_FIELD_VALUE_CACHE));
+ }
+
+ @Test
+ public void testSplitHeaderParse()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "XXXXSPLIT / HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Header1: value1\r\n" +
+ "Header2: value 2a \r\n" +
+ "Header3: 3\r\n" +
+ "Header4:value4\r\n" +
+ "Server5: notServer\r\n" +
+ "\r\nZZZZ");
+ buffer.position(2);
+ buffer.limit(buffer.capacity() - 2);
+ buffer = buffer.slice();
+
+ for (int i = 0; i < buffer.capacity() - 4; i++)
+ {
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ buffer.position(2);
+ buffer.limit(2 + i);
+
+ if (!parser.parseNext(buffer))
+ {
+ // consumed all
+ assertEquals(0, buffer.remaining());
+
+ // parse the rest
+ buffer.limit(buffer.capacity() - 2);
+ parser.parseNext(buffer);
+ }
+
+ assertEquals("SPLIT", _methodOrVersion);
+ assertEquals("/", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals("Host", _hdr[0]);
+ assertEquals("localhost", _val[0]);
+ assertEquals("Header1", _hdr[1]);
+ assertEquals("value1", _val[1]);
+ assertEquals("Header2", _hdr[2]);
+ assertEquals("value 2a", _val[2]);
+ assertEquals("Header3", _hdr[3]);
+ assertEquals("3", _val[3]);
+ assertEquals("Header4", _hdr[4]);
+ assertEquals("value4", _val[4]);
+ assertEquals("Server5", _hdr[5]);
+ assertEquals("notServer", _val[5]);
+ assertEquals(5, _headers);
+ }
+ }
+
+ @Test
+ public void testChunkParse()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET /chunk HTTP/1.0\r\n" +
+ "Header1: value1\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "a;\r\n" +
+ "0123456789\r\n" +
+ "1a\r\n" +
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" +
+ "0\r\n" +
+ "\r\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/chunk", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(1, _headers);
+ assertEquals("Header1", _hdr[0]);
+ assertEquals("value1", _val[0]);
+ assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", _content);
+
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testBadChunkParse()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET /chunk HTTP/1.0\r\n" +
+ "Header1: value1\r\n" +
+ "Transfer-Encoding: chunked, identity\r\n" +
+ "\r\n" +
+ "a;\r\n" +
+ "0123456789\r\n" +
+ "1a\r\n" +
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" +
+ "0\r\n" +
+ "\r\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/chunk", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertThat(_bad, containsString("Bad Transfer-Encoding"));
+ }
+
+ @Test
+ public void testChunkParseTrailer()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET /chunk HTTP/1.0\r\n" +
+ "Header1: value1\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "a;\r\n" +
+ "0123456789\r\n" +
+ "1a\r\n" +
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" +
+ "0\r\n" +
+ "Trailer: value\r\n" +
+ "\r\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/chunk", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(1, _headers);
+ assertEquals("Header1", _hdr[0]);
+ assertEquals("value1", _val[0]);
+ assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", _content);
+ assertEquals(1, _trailers.size());
+ HttpField trailer1 = _trailers.get(0);
+ assertEquals("Trailer", trailer1.getName());
+ assertEquals("value", trailer1.getValue());
+
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testChunkParseTrailers()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET /chunk HTTP/1.0\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "a;\r\n" +
+ "0123456789\r\n" +
+ "1a\r\n" +
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" +
+ "0\r\n" +
+ "Trailer: value\r\n" +
+ "Foo: bar\r\n" +
+ "\r\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/chunk", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(0, _headers);
+ assertEquals("Transfer-Encoding", _hdr[0]);
+ assertEquals("chunked", _val[0]);
+ assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", _content);
+ assertEquals(2, _trailers.size());
+ HttpField trailer1 = _trailers.get(0);
+ assertEquals("Trailer", trailer1.getName());
+ assertEquals("value", trailer1.getValue());
+ HttpField trailer2 = _trailers.get(1);
+ assertEquals("Foo", trailer2.getName());
+ assertEquals("bar", trailer2.getValue());
+
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testChunkParseBadTrailer()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET /chunk HTTP/1.0\r\n" +
+ "Header1: value1\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "a;\r\n" +
+ "0123456789\r\n" +
+ "1a\r\n" +
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" +
+ "0\r\n" +
+ "Trailer: value");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/chunk", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(1, _headers);
+ assertEquals("Header1", _hdr[0]);
+ assertEquals("value1", _val[0]);
+ assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", _content);
+
+ assertTrue(_headerCompleted);
+ assertTrue(_early);
+ }
+
+ @Test
+ public void testChunkParseNoTrailer()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET /chunk HTTP/1.0\r\n" +
+ "Header1: value1\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "a;\r\n" +
+ "0123456789\r\n" +
+ "1a\r\n" +
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" +
+ "0\r\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/chunk", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(1, _headers);
+ assertEquals("Header1", _hdr[0]);
+ assertEquals("value1", _val[0]);
+ assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", _content);
+
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testStartEOF()
+ {
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+
+ assertTrue(_early);
+ assertNull(_bad);
+ }
+
+ @Test
+ public void testEarlyEOF()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET /uri HTTP/1.0\r\n" +
+ "Content-Length: 20\r\n" +
+ "\r\n" +
+ "0123456789");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.atEOF();
+ parseAll(parser, buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/uri", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals("0123456789", _content);
+
+ assertTrue(_early);
+ }
+
+ @Test
+ public void testChunkEarlyEOF()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET /chunk HTTP/1.0\r\n" +
+ "Header1: value1\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "a;\r\n" +
+ "0123456789\r\n");
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.atEOF();
+ parseAll(parser, buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/chunk", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(1, _headers);
+ assertEquals("Header1", _hdr[0]);
+ assertEquals("value1", _val[0]);
+ assertEquals("0123456789", _content);
+
+ assertTrue(_early);
+ }
+
+ @Test
+ public void testMultiParse()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET /mp HTTP/1.0\r\n" +
+ "Connection: Keep-Alive\r\n" +
+ "Header1: value1\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "a;\r\n" +
+ "0123456789\r\n" +
+ "1a\r\n" +
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" +
+ "0\r\n" +
+
+ "\r\n" +
+
+ "POST /foo HTTP/1.0\r\n" +
+ "Connection: Keep-Alive\r\n" +
+ "Header2: value2\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n" +
+
+ "PUT /doodle HTTP/1.0\r\n" +
+ "Connection: close\r\n" +
+ "Header3: value3\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "0123456789\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/mp", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(2, _headers);
+ assertEquals("Header1", _hdr[1]);
+ assertEquals("value1", _val[1]);
+ assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", _content);
+
+ parser.reset();
+ init();
+ parser.parseNext(buffer);
+ assertEquals("POST", _methodOrVersion);
+ assertEquals("/foo", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(2, _headers);
+ assertEquals("Header2", _hdr[1]);
+ assertEquals("value2", _val[1]);
+ assertNull(_content);
+
+ parser.reset();
+ init();
+ parser.parseNext(buffer);
+ parser.atEOF();
+ assertEquals("PUT", _methodOrVersion);
+ assertEquals("/doodle", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(2, _headers);
+ assertEquals("Header3", _hdr[1]);
+ assertEquals("value3", _val[1]);
+ assertEquals("0123456789", _content);
+ }
+
+ @Test
+ public void testMultiParseEarlyEOF()
+ {
+ ByteBuffer buffer0 = BufferUtil.toBuffer(
+ "GET /mp HTTP/1.0\r\n" +
+ "Connection: Keep-Alive\r\n");
+
+ ByteBuffer buffer1 = BufferUtil.toBuffer("Header1: value1\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "a;\r\n" +
+ "0123456789\r\n" +
+ "1a\r\n" +
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" +
+ "0\r\n" +
+
+ "\r\n" +
+
+ "POST /foo HTTP/1.0\r\n" +
+ "Connection: Keep-Alive\r\n" +
+ "Header2: value2\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n" +
+
+ "PUT /doodle HTTP/1.0\r\n" +
+ "Connection: close\r\n" + "Header3: value3\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "0123456789\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer0);
+ parser.atEOF();
+ parser.parseNext(buffer1);
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/mp", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(2, _headers);
+ assertEquals("Header1", _hdr[1]);
+ assertEquals("value1", _val[1]);
+ assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", _content);
+
+ parser.reset();
+ init();
+ parser.parseNext(buffer1);
+ assertEquals("POST", _methodOrVersion);
+ assertEquals("/foo", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(2, _headers);
+ assertEquals("Header2", _hdr[1]);
+ assertEquals("value2", _val[1]);
+ assertNull(_content);
+
+ parser.reset();
+ init();
+ parser.parseNext(buffer1);
+ assertEquals("PUT", _methodOrVersion);
+ assertEquals("/doodle", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(2, _headers);
+ assertEquals("Header3", _hdr[1]);
+ assertEquals("value3", _val[1]);
+ assertEquals("0123456789", _content);
+ }
+
+ @Test
+ public void testResponseParse0()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 Correct\r\n" +
+ "Content-Length: 10\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "0123456789\r\n");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("200", _uriOrStatus);
+ assertEquals("Correct", _versionOrReason);
+ assertEquals(10, _content.length());
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testResponseParse1()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 304 Not-Modified\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("304", _uriOrStatus);
+ assertEquals("Not-Modified", _versionOrReason);
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testResponseParse2()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 204 No-Content\r\n" +
+ "Header: value\r\n" +
+ "\r\n" +
+
+ "HTTP/1.1 200 Correct\r\n" +
+ "Content-Length: 10\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "0123456789\r\n");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("204", _uriOrStatus);
+ assertEquals("No-Content", _versionOrReason);
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+
+ parser.reset();
+ init();
+
+ parser.parseNext(buffer);
+ parser.atEOF();
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("200", _uriOrStatus);
+ assertEquals("Correct", _versionOrReason);
+ assertEquals(_content.length(), 10);
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testResponseParse3()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200\r\n" +
+ "Content-Length: 10\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "0123456789\r\n");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("200", _uriOrStatus);
+ assertNull(_versionOrReason);
+ assertEquals(_content.length(), 10);
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testResponseParse4()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 \r\n" +
+ "Content-Length: 10\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "0123456789\r\n");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("200", _uriOrStatus);
+ assertNull(_versionOrReason);
+ assertEquals(_content.length(), 10);
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testResponseEOFContent()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 \r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "0123456789\r\n");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.atEOF();
+ parser.parseNext(buffer);
+
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("200", _uriOrStatus);
+ assertNull(_versionOrReason);
+ assertEquals(12, _content.length());
+ assertEquals("0123456789\r\n", _content);
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testResponse304WithContentLength()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 304 found\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("304", _uriOrStatus);
+ assertEquals("found", _versionOrReason);
+ assertNull(_content);
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testResponse101WithTransferEncoding()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 101 switching protocols\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("101", _uriOrStatus);
+ assertEquals("switching protocols", _versionOrReason);
+ assertNull(_content);
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testResponseReasonIso88591()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 302 déplacé temporairement\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n", StandardCharsets.ISO_8859_1);
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("302", _uriOrStatus);
+ assertEquals("déplacé temporairement", _versionOrReason);
+ }
+
+ @Test
+ public void testSeekEOF()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "\r\n" + // extra CRLF ignored
+ "HTTP/1.1 400 OK\r\n"); // extra data causes close ??
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("200", _uriOrStatus);
+ assertEquals("OK", _versionOrReason);
+ assertNull(_content);
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+
+ parser.close();
+ parser.reset();
+ parser.parseNext(buffer);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+ }
+
+ @Test
+ public void testNoURI()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET\r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertNull(_methodOrVersion);
+ assertEquals("No URI", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+ }
+
+ @Test
+ public void testNoURI2()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET \r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertNull(_methodOrVersion);
+ assertEquals("No URI", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+ }
+
+ @Test
+ public void testUnknownReponseVersion()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HPPT/7.7 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertNull(_methodOrVersion);
+ assertEquals("Unknown Version", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+ }
+
+ @Test
+ public void testNoStatus()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1\r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertNull(_methodOrVersion);
+ assertEquals("No Status", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+ }
+
+ @Test
+ public void testNoStatus2()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 \r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.ResponseHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertNull(_methodOrVersion);
+ assertEquals("No Status", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+ }
+
+ @Test
+ public void testBadRequestVersion()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HPPT/7.7\r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertNull(_methodOrVersion);
+ assertEquals("Unknown Version", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+
+ buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.01\r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ handler = new Handler();
+ parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertNull(_methodOrVersion);
+ assertEquals("Unknown Version", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+ }
+
+ @Test
+ public void testBadCR()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Content-Length: 0\r" +
+ "Connection: close\r" +
+ "\r");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertEquals("Bad EOL", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+
+ buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r" +
+ "Content-Length: 0\r" +
+ "Connection: close\r" +
+ "\r");
+
+ handler = new Handler();
+ parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertEquals("Bad EOL", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+ }
+
+ @Test
+ public void testBadContentLength0()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Content-Length: abc\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("Invalid Content-Length Value", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+ }
+
+ @Test
+ public void testBadContentLength1()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Content-Length: 9999999999999999999999999999999999999999999999\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("Invalid Content-Length Value", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+ }
+
+ @Test
+ public void testBadContentLength2()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.0\r\n" +
+ "Content-Length: 1.5\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("Invalid Content-Length Value", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+ }
+
+ @Test
+ public void testMultipleContentLengthWithLargerThenCorrectValue()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "POST / HTTP/1.1\r\n" +
+ "Content-Length: 2\r\n" +
+ "Content-Length: 1\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "X");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertEquals("POST", _methodOrVersion);
+ assertEquals("Multiple Content-Lengths", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+ }
+
+ @Test
+ public void testMultipleContentLengthWithCorrectThenLargerValue()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "POST / HTTP/1.1\r\n" +
+ "Content-Length: 1\r\n" +
+ "Content-Length: 2\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "X");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+
+ parser.parseNext(buffer);
+ assertEquals("POST", _methodOrVersion);
+ assertEquals("Multiple Content-Lengths", _bad);
+ assertFalse(buffer.hasRemaining());
+ assertEquals(HttpParser.State.CLOSE, parser.getState());
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ assertEquals(HttpParser.State.CLOSED, parser.getState());
+ }
+
+ @Test
+ public void testTransferEncodingChunkedThenContentLength()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "POST /chunk HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Length: 1\r\n" +
+ "\r\n" +
+ "1\r\n" +
+ "X\r\n" +
+ "0\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY);
+ parseAll(parser, buffer);
+
+ assertEquals("POST", _methodOrVersion);
+ assertEquals("/chunk", _uriOrStatus);
+ assertEquals("HTTP/1.1", _versionOrReason);
+ assertEquals("X", _content);
+
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+
+ assertThat(_complianceViolation, contains(HttpComplianceSection.TRANSFER_ENCODING_WITH_CONTENT_LENGTH));
+ }
+
+ @Test
+ public void testContentLengthThenTransferEncodingChunked()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "POST /chunk HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: 1\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "1\r\n" +
+ "X\r\n" +
+ "0\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY);
+ parseAll(parser, buffer);
+
+ assertEquals("POST", _methodOrVersion);
+ assertEquals("/chunk", _uriOrStatus);
+ assertEquals("HTTP/1.1", _versionOrReason);
+ assertEquals("X", _content);
+
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+
+ assertThat(_complianceViolation, contains(HttpComplianceSection.TRANSFER_ENCODING_WITH_CONTENT_LENGTH));
+ }
+
+ @Test
+ public void testHost()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.1\r\n" +
+ "Host: host\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("host", _host);
+ assertEquals(0, _port);
+ }
+
+ @Test
+ public void testUriHost11()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET http://host/ HTTP/1.1\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("No Host", _bad);
+ assertEquals("http://host/", _uriOrStatus);
+ assertEquals(0, _port);
+ }
+
+ @Test
+ public void testUriHost10()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET http://host/ HTTP/1.0\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertNull(_bad);
+ assertEquals("http://host/", _uriOrStatus);
+ assertEquals(0, _port);
+ }
+
+ @Test
+ public void testNoHost()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.1\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("No Host", _bad);
+ }
+
+ @Test
+ public void testIPHost()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.1\r\n" +
+ "Host: 192.168.0.1\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("192.168.0.1", _host);
+ assertEquals(0, _port);
+ }
+
+ @Test
+ public void testIPv6Host()
+ {
+ Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable());
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.1\r\n" +
+ "Host: [::1]\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("[::1]", _host);
+ assertEquals(0, _port);
+ }
+
+ @Test
+ public void testBadIPv6Host()
+ {
+ try (StacklessLogging s = new StacklessLogging(HttpParser.class))
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.1\r\n" +
+ "Host: [::1\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertThat(_bad, containsString("Bad"));
+ }
+ }
+
+ @Test
+ public void testHostPort()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.1\r\n" +
+ "Host: myhost:8888\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("myhost", _host);
+ assertEquals(8888, _port);
+ }
+
+ @Test
+ public void testHostBadPort()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.1\r\n" +
+ "Host: myhost:testBadPort\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertThat(_bad, containsString("Bad Host"));
+ }
+
+ @Test
+ public void testIPHostPort()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.1\r\n" +
+ "Host: 192.168.0.1:8888\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("192.168.0.1", _host);
+ assertEquals(8888, _port);
+ }
+
+ @Test
+ public void testIPv6HostPort()
+ {
+ Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable());
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.1\r\n" +
+ "Host: [::1]:8888\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertEquals("[::1]", _host);
+ assertEquals(8888, _port);
+ }
+
+ @Test
+ public void testEmptyHostPort()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.1\r\n" +
+ "Host:\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+ assertNull(_host);
+ assertNull(_bad);
+ }
+
+ @Test
+ @SuppressWarnings("ReferenceEquality")
+ public void testCachedField()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.1\r\n" +
+ "Host: www.smh.com.au\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+ assertEquals("www.smh.com.au", parser.getFieldCache().get("Host: www.smh.com.au").getValue());
+ HttpField field = _fields.get(0);
+
+ buffer.position(0);
+ parseAll(parser, buffer);
+ assertSame(field, _fields.get(0));
+ }
+
+ @Test
+ public void testParseRequest()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Header1: value1\r\n" +
+ "Connection: close\r\n" +
+ "Accept-Encoding: gzip, deflated\r\n" +
+ "Accept: unknown\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parser.parseNext(buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/", _uriOrStatus);
+ assertEquals("HTTP/1.1", _versionOrReason);
+ assertEquals("Host", _hdr[0]);
+ assertEquals("localhost", _val[0]);
+ assertEquals("Connection", _hdr[2]);
+ assertEquals("close", _val[2]);
+ assertEquals("Accept-Encoding", _hdr[3]);
+ assertEquals("gzip, deflated", _val[3]);
+ assertEquals("Accept", _hdr[4]);
+ assertEquals("unknown", _val[4]);
+ }
+
+ @Test
+ public void testHTTP2Preface()
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "PRI * HTTP/2.0\r\n" +
+ "\r\n" +
+ "SM\r\n" +
+ "\r\n");
+
+ HttpParser.RequestHandler handler = new Handler();
+ HttpParser parser = new HttpParser(handler);
+ parseAll(parser, buffer);
+
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ assertEquals("PRI", _methodOrVersion);
+ assertEquals("*", _uriOrStatus);
+ assertEquals("HTTP/2.0", _versionOrReason);
+ assertEquals(-1, _headers);
+ assertNull(_bad);
+ }
+
+ @Test
+ public void testForHTTP09HeaderCompleteTrueDoesNotEmitContentComplete()
+ {
+ HttpParser.RequestHandler handler = new Handler()
+ {
+ @Override
+ public boolean headerComplete()
+ {
+ super.headerComplete();
+ return true;
+ }
+ };
+
+ HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY);
+ ByteBuffer buffer = BufferUtil.toBuffer("GET /path\r\n");
+ boolean handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertFalse(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/path", _uriOrStatus);
+ assertEquals("HTTP/0.9", _versionOrReason);
+ assertEquals(-1, _headers);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(_contentCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testForContentLengthZeroHeaderCompleteTrueDoesNotEmitContentComplete()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean headerComplete()
+ {
+ super.headerComplete();
+ return true;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n");
+ boolean handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertFalse(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(_contentCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testForEmptyChunkedContentHeaderCompleteTrueDoesNotEmitContentComplete()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean headerComplete()
+ {
+ super.headerComplete();
+ return true;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 OK\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "0\r\n" +
+ "\r\n");
+ boolean handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertFalse(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(_contentCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testForContentLengthZeroContentCompleteTrueDoesNotEmitMessageComplete()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean contentComplete()
+ {
+ super.contentComplete();
+ return true;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n");
+ boolean handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testForEmptyChunkedContentContentCompleteTrueDoesNotEmitMessageComplete()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean contentComplete()
+ {
+ super.contentComplete();
+ return true;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 OK\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "0\r\n" +
+ "\r\n");
+ boolean handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testHeaderAfterContentLengthZeroContentCompleteTrue()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean contentComplete()
+ {
+ super.contentComplete();
+ return true;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+
+ String header = "Header: Foobar\r\n";
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n" +
+ header);
+ boolean handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertEquals(header, BufferUtil.toString(buffer));
+ assertTrue(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertEquals(header, BufferUtil.toString(buffer));
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testSmallContentLengthContentCompleteTrue()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean contentComplete()
+ {
+ super.contentComplete();
+ return true;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+
+ String header = "Header: Foobar\r\n";
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 1\r\n" +
+ "\r\n" +
+ "0" +
+ header);
+ boolean handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertEquals(header, BufferUtil.toString(buffer));
+ assertTrue(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertEquals(header, BufferUtil.toString(buffer));
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testHeaderAfterSmallContentLengthContentCompleteTrue()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean contentComplete()
+ {
+ super.contentComplete();
+ return true;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 1\r\n" +
+ "\r\n" +
+ "0");
+ boolean handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertTrue(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testEOFContentContentCompleteTrue()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean contentComplete()
+ {
+ super.contentComplete();
+ return true;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 OK\r\n" +
+ "\r\n" +
+ "0");
+ boolean handle = parser.parseNext(buffer);
+ assertFalse(handle);
+ assertFalse(buffer.hasRemaining());
+ assertEquals("0", _content);
+ assertFalse(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ parser.atEOF();
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertTrue(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testHEADRequestHeaderCompleteTrue()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean headerComplete()
+ {
+ super.headerComplete();
+ return true;
+ }
+
+ @Override
+ public boolean contentComplete()
+ {
+ super.contentComplete();
+ return true;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+ parser.setHeadResponse(true);
+
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 OK\r\n" +
+ "\r\n");
+ boolean handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertFalse(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertTrue(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testNoContentHeaderCompleteTrue()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean headerComplete()
+ {
+ super.headerComplete();
+ return true;
+ }
+
+ @Override
+ public boolean contentComplete()
+ {
+ super.contentComplete();
+ return true;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+
+ // HTTP 304 does not have a body.
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 304 Not Modified\r\n" +
+ "\r\n");
+ boolean handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertFalse(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertTrue(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testCRLFAfterResponseHeaderCompleteTrue()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean headerComplete()
+ {
+ super.headerComplete();
+ return true;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 304 Not Modified\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "HTTP/1.1 303 See Other\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n");
+ boolean handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertEquals("304", _uriOrStatus);
+ assertFalse(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertTrue(_contentCompleted);
+ assertTrue(_messageCompleted);
+
+ // Parse next response.
+ parser.reset();
+ init();
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertEquals("200", _uriOrStatus);
+ assertFalse(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertTrue(_contentCompleted);
+ assertTrue(_messageCompleted);
+
+ // Parse next response.
+ parser.reset();
+ init();
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertEquals("303", _uriOrStatus);
+ assertFalse(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertTrue(_contentCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testCRLFAfterResponseContentCompleteTrue()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean contentComplete()
+ {
+ super.contentComplete();
+ return true;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 304 Not Modified\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "HTTP/1.1 303 See Other\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n");
+ boolean handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertEquals("304", _uriOrStatus);
+ assertTrue(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertTrue(_messageCompleted);
+
+ // Parse next response.
+ parser.reset();
+ init();
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertEquals("200", _uriOrStatus);
+ assertTrue(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertTrue(buffer.hasRemaining());
+ assertTrue(_messageCompleted);
+
+ // Parse next response.
+ parser.reset();
+ init();
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertEquals("303", _uriOrStatus);
+ assertTrue(_contentCompleted);
+ assertFalse(_messageCompleted);
+
+ // Need to parse more to advance the parser.
+ handle = parser.parseNext(buffer);
+ assertTrue(handle);
+ assertFalse(buffer.hasRemaining());
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testCRLFAfterResponseMessageCompleteFalse()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean messageComplete()
+ {
+ super.messageComplete();
+ return false;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 304 Not Modified\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "HTTP/1.1 303 See Other\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n");
+ boolean handle = parser.parseNext(buffer);
+ assertFalse(handle);
+ assertTrue(buffer.hasRemaining());
+ assertEquals("304", _uriOrStatus);
+ assertTrue(_contentCompleted);
+ assertTrue(_messageCompleted);
+
+ // Parse next response.
+ parser.reset();
+ init();
+ handle = parser.parseNext(buffer);
+ assertFalse(handle);
+ assertTrue(buffer.hasRemaining());
+ assertEquals("200", _uriOrStatus);
+ assertTrue(_contentCompleted);
+ assertTrue(_messageCompleted);
+
+ // Parse next response.
+ parser.reset();
+ init();
+ handle = parser.parseNext(buffer);
+ assertFalse(handle);
+ assertFalse(buffer.hasRemaining());
+ assertEquals("303", _uriOrStatus);
+ assertTrue(_contentCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
+ public void testSPAfterResponseMessageCompleteFalse()
+ {
+ HttpParser.ResponseHandler handler = new Handler()
+ {
+ @Override
+ public boolean messageComplete()
+ {
+ super.messageComplete();
+ return false;
+ }
+ };
+ HttpParser parser = new HttpParser(handler);
+
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 304 Not Modified\r\n" +
+ "\r\n" +
+ " " + // Single SP.
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n");
+ boolean handle = parser.parseNext(buffer);
+ assertFalse(handle);
+ assertTrue(buffer.hasRemaining());
+ assertEquals("304", _uriOrStatus);
+ assertTrue(_contentCompleted);
+ assertTrue(_messageCompleted);
+
+ // Parse next response.
+ parser.reset();
+ init();
+ handle = parser.parseNext(buffer);
+ assertFalse(handle);
+ assertFalse(buffer.hasRemaining());
+ assertNotNull(_bad);
+
+ buffer = BufferUtil.toBuffer(
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n" +
+ " " + // Single SP.
+ "HTTP/1.1 303 See Other\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n");
+ parser = new HttpParser(handler);
+ handle = parser.parseNext(buffer);
+ assertFalse(handle);
+ assertTrue(buffer.hasRemaining());
+ assertEquals("200", _uriOrStatus);
+ assertTrue(_contentCompleted);
+ assertTrue(_messageCompleted);
+
+ // Parse next response.
+ parser.reset();
+ init();
+ handle = parser.parseNext(buffer);
+ assertFalse(handle);
+ assertFalse(buffer.hasRemaining());
+ assertNotNull(_bad);
+ }
+
+ @BeforeEach
+ public void init()
+ {
+ _bad = null;
+ _content = null;
+ _methodOrVersion = null;
+ _uriOrStatus = null;
+ _versionOrReason = null;
+ _hdr = null;
+ _val = null;
+ _headers = 0;
+ _headerCompleted = false;
+ _contentCompleted = false;
+ _messageCompleted = false;
+ _complianceViolation.clear();
+ }
+
+ private String _host;
+ private int _port;
+ private String _bad;
+ private String _content;
+ private String _methodOrVersion;
+ private String _uriOrStatus;
+ private String _versionOrReason;
+ private List<HttpField> _fields = new ArrayList<>();
+ private List<HttpField> _trailers = new ArrayList<>();
+ private String[] _hdr;
+ private String[] _val;
+ private int _headers;
+ private boolean _early;
+ private boolean _headerCompleted;
+ private boolean _contentCompleted;
+ private boolean _messageCompleted;
+ private final List<HttpComplianceSection> _complianceViolation = new ArrayList<>();
+
+ private class Handler implements HttpParser.RequestHandler, HttpParser.ResponseHandler, HttpParser.ComplianceHandler
+ {
+ @Override
+ public boolean content(ByteBuffer ref)
+ {
+ if (_content == null)
+ _content = "";
+ String c = BufferUtil.toString(ref, StandardCharsets.UTF_8);
+ _content = _content + c;
+ ref.position(ref.limit());
+ return false;
+ }
+
+ @Override
+ public boolean startRequest(String method, String uri, HttpVersion version)
+ {
+ _fields.clear();
+ _trailers.clear();
+ _headers = -1;
+ _hdr = new String[10];
+ _val = new String[10];
+ _methodOrVersion = method;
+ _uriOrStatus = uri;
+ _versionOrReason = version == null ? null : version.asString();
+ _messageCompleted = false;
+ _headerCompleted = false;
+ _early = false;
+ return false;
+ }
+
+ @Override
+ public void parsedHeader(HttpField field)
+ {
+ _fields.add(field);
+ _hdr[++_headers] = field.getName();
+ _val[_headers] = field.getValue();
+
+ if (field instanceof HostPortHttpField)
+ {
+ HostPortHttpField hpfield = (HostPortHttpField)field;
+ _host = hpfield.getHost();
+ _port = hpfield.getPort();
+ }
+ }
+
+ @Override
+ public boolean headerComplete()
+ {
+ _content = null;
+ _headerCompleted = true;
+ return false;
+ }
+
+ @Override
+ public void parsedTrailer(HttpField field)
+ {
+ _trailers.add(field);
+ }
+
+ @Override
+ public boolean contentComplete()
+ {
+ _contentCompleted = true;
+ return false;
+ }
+
+ @Override
+ public boolean messageComplete()
+ {
+ _messageCompleted = true;
+ return true;
+ }
+
+ @Override
+ public void badMessage(BadMessageException failure)
+ {
+ String reason = failure.getReason();
+ _bad = reason == null ? String.valueOf(failure.getCode()) : reason;
+ }
+
+ @Override
+ public boolean startResponse(HttpVersion version, int status, String reason)
+ {
+ _fields.clear();
+ _trailers.clear();
+ _methodOrVersion = version.asString();
+ _uriOrStatus = Integer.toString(status);
+ _versionOrReason = reason;
+ _headers = -1;
+ _hdr = new String[10];
+ _val = new String[10];
+ _messageCompleted = false;
+ _headerCompleted = false;
+ return false;
+ }
+
+ @Override
+ public void earlyEOF()
+ {
+ _early = true;
+ }
+
+ @Override
+ public int getHeaderCacheSize()
+ {
+ return 1024;
+ }
+
+ @Override
+ public void onComplianceViolation(HttpCompliance compliance, HttpComplianceSection violation, String reason)
+ {
+ _complianceViolation.add(violation);
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpSchemeTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpSchemeTest.java
new file mode 100644
index 0000000..9451d29
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpSchemeTest.java
@@ -0,0 +1,80 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for class {@link HttpScheme}.
+ *
+ * @see HttpScheme
+ */
+public class HttpSchemeTest
+{
+
+ @Test
+ public void testIsReturningTrue()
+ {
+ HttpScheme httpScheme = HttpScheme.HTTPS;
+
+ assertTrue(httpScheme.is("https"));
+ assertEquals("https", httpScheme.asString());
+ assertEquals("https", httpScheme.toString());
+ }
+
+ @Test
+ public void testIsReturningFalse()
+ {
+ HttpScheme httpScheme = HttpScheme.HTTP;
+
+ assertFalse(httpScheme.is(",CPL@@4'U4p"));
+ }
+
+ @Test
+ public void testIsWithNull()
+ {
+ HttpScheme httpScheme = HttpScheme.HTTPS;
+
+ assertFalse(httpScheme.is(null));
+ }
+
+ @Test
+ public void testAsByteBuffer()
+ {
+ HttpScheme httpScheme = HttpScheme.WS;
+ ByteBuffer byteBuffer = httpScheme.asByteBuffer();
+
+ assertEquals("ws", httpScheme.asString());
+ assertEquals("ws", httpScheme.toString());
+ assertEquals(2, byteBuffer.capacity());
+ assertEquals(2, byteBuffer.remaining());
+ assertEquals(2, byteBuffer.limit());
+ assertFalse(byteBuffer.hasArray());
+ assertEquals(0, byteBuffer.position());
+ assertTrue(byteBuffer.isReadOnly());
+ assertFalse(byteBuffer.isDirect());
+ assertTrue(byteBuffer.hasRemaining());
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpStatusCodeTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpStatusCodeTest.java
new file mode 100644
index 0000000..6f38967
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpStatusCodeTest.java
@@ -0,0 +1,46 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class HttpStatusCodeTest
+{
+ @Test
+ public void testInvalidGetCode()
+ {
+ assertNull(HttpStatus.getCode(800), "Invalid code: 800");
+ assertNull(HttpStatus.getCode(190), "Invalid code: 190");
+ }
+
+ @Test
+ public void testImATeapot()
+ {
+ assertEquals("I'm a Teapot", HttpStatus.getMessage(418));
+ assertEquals("Expectation Failed", HttpStatus.getMessage(417));
+ }
+
+ public void testHttpMethod()
+ {
+ assertEquals("GET", HttpMethod.GET.toString());
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTester.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTester.java
new file mode 100644
index 0000000..beed160
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTester.java
@@ -0,0 +1,599 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * An HTTP Testing helper class.
+ *
+ * Example usage:
+ * <pre>
+ * try(Socket socket = new Socket("www.google.com",80))
+ * {
+ * HttpTester.Request request = HttpTester.newRequest();
+ * request.setMethod("POST");
+ * request.setURI("/search");
+ * request.setVersion(HttpVersion.HTTP_1_0);
+ * request.put(HttpHeader.HOST,"www.google.com");
+ * request.put("Content-Type","application/x-www-form-urlencoded");
+ * request.setContent("q=jetty%20server");
+ * ByteBuffer output = request.generate();
+ *
+ * socket.getOutputStream().write(output.array(),output.arrayOffset()+output.position(),output.remaining());
+ * HttpTester.Input input = HttpTester.from(socket.getInputStream());
+ * HttpTester.Response response = HttpTester.parseResponse(input);
+ * System.err.printf("%s %s %s%n",response.getVersion(),response.getStatus(),response.getReason());
+ * for (HttpField field:response)
+ * System.err.printf("%s: %s%n",field.getName(),field.getValue());
+ * System.err.printf("%n%s%n",response.getContent());
+ * }
+ * </pre>
+ */
+public class HttpTester
+{
+ private static final Logger LOG = Log.getLogger(HttpTester.class);
+
+ private HttpTester()
+ {
+ }
+
+ public static Request newRequest()
+ {
+ Request r = new Request();
+ r.setMethod(HttpMethod.GET.asString());
+ r.setURI("/");
+ r.setVersion(HttpVersion.HTTP_1_1);
+ return r;
+ }
+
+ public static Request parseRequest(String request)
+ {
+ Request r = new Request();
+ HttpParser parser = new HttpParser(r);
+ parser.parseNext(BufferUtil.toBuffer(request));
+ return r;
+ }
+
+ public static Request parseRequest(ByteBuffer request)
+ {
+ Request r = new Request();
+ HttpParser parser = new HttpParser(r);
+ parser.parseNext(request);
+ return r;
+ }
+
+ public static Response parseResponse(String response)
+ {
+ Response r = new Response();
+ HttpParser parser = new HttpParser(r);
+ parser.parseNext(BufferUtil.toBuffer(response));
+ return r;
+ }
+
+ public static Response parseResponse(ByteBuffer response)
+ {
+ Response r = new Response();
+ HttpParser parser = new HttpParser(r);
+ parser.parseNext(response);
+ return r;
+ }
+
+ public static Response parseResponse(InputStream responseStream) throws IOException
+ {
+ Response r = new Response();
+ HttpParser parser = new HttpParser(r);
+
+ // Read and parse a character at a time so we never can read more than we should.
+ byte[] array = new byte[1];
+ ByteBuffer buffer = ByteBuffer.wrap(array);
+ buffer.limit(1);
+
+ while (true)
+ {
+ buffer.position(1);
+ int l = responseStream.read(array);
+ if (l < 0)
+ parser.atEOF();
+ else
+ buffer.position(0);
+
+ if (parser.parseNext(buffer))
+ return r;
+ else if (l < 0)
+ return null;
+ }
+ }
+
+ public abstract static class Input
+ {
+ protected final ByteBuffer _buffer;
+ protected boolean _eof = false;
+ protected HttpParser _parser;
+
+ public Input()
+ {
+ this(BufferUtil.allocate(8192));
+ }
+
+ Input(ByteBuffer buffer)
+ {
+ _buffer = buffer;
+ }
+
+ public ByteBuffer getBuffer()
+ {
+ return _buffer;
+ }
+
+ public void setHttpParser(HttpParser parser)
+ {
+ _parser = parser;
+ }
+
+ public HttpParser getHttpParser()
+ {
+ return _parser;
+ }
+
+ public HttpParser takeHttpParser()
+ {
+ HttpParser p = _parser;
+ _parser = null;
+ return p;
+ }
+
+ public boolean isEOF()
+ {
+ return BufferUtil.isEmpty(_buffer) && _eof;
+ }
+
+ public abstract int fillBuffer() throws IOException;
+ }
+
+ public static Input from(final ByteBuffer data)
+ {
+ return new Input(data.slice())
+ {
+ @Override
+ public int fillBuffer() throws IOException
+ {
+ _eof = true;
+ return -1;
+ }
+ };
+ }
+
+ public static Input from(final InputStream in)
+ {
+ return new Input()
+ {
+ @Override
+ public int fillBuffer() throws IOException
+ {
+ BufferUtil.compact(_buffer);
+ int len = in.read(_buffer.array(), _buffer.arrayOffset() + _buffer.limit(), BufferUtil.space(_buffer));
+ if (len < 0)
+ _eof = true;
+ else
+ _buffer.limit(_buffer.limit() + len);
+ return len;
+ }
+ };
+ }
+
+ public static Input from(final ReadableByteChannel in)
+ {
+ return new Input()
+ {
+ @Override
+ public int fillBuffer() throws IOException
+ {
+ BufferUtil.compact(_buffer);
+ int pos = BufferUtil.flipToFill(_buffer);
+ int len = in.read(_buffer);
+ if (len < 0)
+ _eof = true;
+ BufferUtil.flipToFlush(_buffer, pos);
+ return len;
+ }
+ };
+ }
+
+ public static Response parseResponse(Input in) throws IOException
+ {
+ Response r;
+ HttpParser parser = in.takeHttpParser();
+ if (parser == null)
+ {
+ r = new Response();
+ parser = new HttpParser(r);
+ }
+ else
+ r = (Response)parser.getHandler();
+
+ parseResponse(in, parser, r);
+
+ if (r.isComplete())
+ return r;
+
+ in.setHttpParser(parser);
+ return null;
+ }
+
+ public static void parseResponse(Input in, Response response) throws IOException
+ {
+ HttpParser parser = in.takeHttpParser();
+ if (parser == null)
+ {
+ parser = new HttpParser(response);
+ }
+ parseResponse(in, parser, response);
+
+ if (!response.isComplete())
+ in.setHttpParser(parser);
+ }
+
+ private static void parseResponse(Input in, HttpParser parser, Response r) throws IOException
+ {
+ ByteBuffer buffer = in.getBuffer();
+
+ while (true)
+ {
+ if (BufferUtil.hasContent(buffer))
+ if (parser.parseNext(buffer))
+ break;
+ int len = in.fillBuffer();
+ if (len == 0)
+ break;
+ if (len <= 0)
+ {
+ parser.atEOF();
+ parser.parseNext(buffer);
+ break;
+ }
+ }
+ }
+
+ public abstract static class Message extends HttpFields implements HttpParser.HttpHandler
+ {
+ boolean _earlyEOF;
+ boolean _complete = false;
+ ByteArrayOutputStream _content;
+ HttpVersion _version = HttpVersion.HTTP_1_0;
+
+ public boolean isComplete()
+ {
+ return _complete;
+ }
+
+ public HttpVersion getVersion()
+ {
+ return _version;
+ }
+
+ public void setVersion(String version)
+ {
+ setVersion(HttpVersion.CACHE.get(version));
+ }
+
+ public void setVersion(HttpVersion version)
+ {
+ _version = version;
+ }
+
+ public void setContent(byte[] bytes)
+ {
+ try
+ {
+ _content = new ByteArrayOutputStream();
+ _content.write(bytes);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void setContent(String content)
+ {
+ try
+ {
+ _content = new ByteArrayOutputStream();
+ _content.write(StringUtil.getBytes(content));
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void setContent(ByteBuffer content)
+ {
+ try
+ {
+ _content = new ByteArrayOutputStream();
+ _content.write(BufferUtil.toArray(content));
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public byte[] getContentBytes()
+ {
+ if (_content == null)
+ return null;
+ return _content.toByteArray();
+ }
+
+ public String getContent()
+ {
+ if (_content == null)
+ return null;
+ byte[] bytes = _content.toByteArray();
+
+ String contentType = get(HttpHeader.CONTENT_TYPE);
+ String encoding = MimeTypes.getCharsetFromContentType(contentType);
+ Charset charset = encoding == null ? StandardCharsets.UTF_8 : Charset.forName(encoding);
+
+ return new String(bytes, charset);
+ }
+
+ @Override
+ public void parsedHeader(HttpField field)
+ {
+ add(field.getName(), field.getValue());
+ }
+
+ @Override
+ public boolean contentComplete()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean messageComplete()
+ {
+ _complete = true;
+ return true;
+ }
+
+ @Override
+ public boolean headerComplete()
+ {
+ _content = new ByteArrayOutputStream();
+ return false;
+ }
+
+ @Override
+ public void earlyEOF()
+ {
+ _earlyEOF = true;
+ }
+
+ public boolean isEarlyEOF()
+ {
+ return _earlyEOF;
+ }
+
+ @Override
+ public boolean content(ByteBuffer ref)
+ {
+ try
+ {
+ _content.write(BufferUtil.toArray(ref));
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ return false;
+ }
+
+ @Override
+ public void badMessage(BadMessageException failure)
+ {
+ throw failure;
+ }
+
+ public ByteBuffer generate()
+ {
+ try
+ {
+ HttpGenerator generator = new HttpGenerator();
+ MetaData info = getInfo();
+ // System.err.println(info.getClass());
+ // System.err.println(info);
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteBuffer header = null;
+ ByteBuffer chunk = null;
+ ByteBuffer content = _content == null ? null : ByteBuffer.wrap(_content.toByteArray());
+
+ loop:
+ while (!generator.isEnd())
+ {
+ HttpGenerator.Result result = info instanceof MetaData.Request
+ ? generator.generateRequest((MetaData.Request)info, header, chunk, content, true)
+ : generator.generateResponse((MetaData.Response)info, false, header, chunk, content, true);
+ switch (result)
+ {
+ case NEED_HEADER:
+ header = BufferUtil.allocate(8192);
+ continue;
+
+ case HEADER_OVERFLOW:
+ if (header.capacity() >= 32 * 1024)
+ throw new BadMessageException(500, "Header too large");
+ header = BufferUtil.allocate(32 * 1024);
+ continue;
+
+ case NEED_CHUNK:
+ chunk = BufferUtil.allocate(HttpGenerator.CHUNK_SIZE);
+ continue;
+
+ case NEED_CHUNK_TRAILER:
+ chunk = BufferUtil.allocate(8192);
+ continue;
+
+ case NEED_INFO:
+ throw new IllegalStateException();
+
+ case FLUSH:
+ if (BufferUtil.hasContent(header))
+ {
+ out.write(BufferUtil.toArray(header));
+ BufferUtil.clear(header);
+ }
+ if (BufferUtil.hasContent(chunk))
+ {
+ out.write(BufferUtil.toArray(chunk));
+ BufferUtil.clear(chunk);
+ }
+ if (BufferUtil.hasContent(content))
+ {
+ out.write(BufferUtil.toArray(content));
+ BufferUtil.clear(content);
+ }
+ break;
+
+ case SHUTDOWN_OUT:
+ break loop;
+ }
+ }
+
+ return ByteBuffer.wrap(out.toByteArray());
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public abstract MetaData getInfo();
+
+ @Override
+ public int getHeaderCacheSize()
+ {
+ return 0;
+ }
+ }
+
+ public static class Request extends Message implements HttpParser.RequestHandler
+ {
+ private String _method;
+ private String _uri;
+
+ @Override
+ public boolean startRequest(String method, String uri, HttpVersion version)
+ {
+ _method = method;
+ _uri = uri.toString();
+ _version = version;
+ return false;
+ }
+
+ public String getMethod()
+ {
+ return _method;
+ }
+
+ public String getUri()
+ {
+ return _uri;
+ }
+
+ public void setMethod(String method)
+ {
+ _method = method;
+ }
+
+ public void setURI(String uri)
+ {
+ _uri = uri;
+ }
+
+ @Override
+ public MetaData.Request getInfo()
+ {
+ return new MetaData.Request(_method, new HttpURI(_uri), _version, this, _content == null ? 0 : _content.size());
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s %s %s\n%s\n", _method, _uri, _version, super.toString());
+ }
+
+ public void setHeader(String name, String value)
+ {
+ put(name, value);
+ }
+ }
+
+ public static class Response extends Message implements HttpParser.ResponseHandler
+ {
+ private int _status;
+ private String _reason;
+
+ @Override
+ public boolean startResponse(HttpVersion version, int status, String reason)
+ {
+ _version = version;
+ _status = status;
+ _reason = reason;
+ return false;
+ }
+
+ public int getStatus()
+ {
+ return _status;
+ }
+
+ public String getReason()
+ {
+ return _reason;
+ }
+
+ @Override
+ public MetaData.Response getInfo()
+ {
+ return new MetaData.Response(_version, _status, _reason, this, _content == null ? -1 : _content.size());
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s %s %s\n%s\n", _version, _status, _reason, super.toString());
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTesterTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTesterTest.java
new file mode 100644
index 0000000..4336fa2
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTesterTest.java
@@ -0,0 +1,314 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+
+public class HttpTesterTest
+{
+
+ public void testExampleUsage() throws Exception
+ {
+ try (Socket socket = new Socket("www.google.com", 80))
+ {
+ HttpTester.Request request = HttpTester.newRequest();
+ request.setMethod("POST");
+ request.setURI("/search");
+ request.setVersion(HttpVersion.HTTP_1_0);
+ request.put(HttpHeader.HOST, "www.google.com");
+ request.put("Content-Type", "application/x-www-form-urlencoded");
+ request.setContent("q=jetty%20server");
+ ByteBuffer output = request.generate();
+
+ socket.getOutputStream().write(output.array(), output.arrayOffset() + output.position(), output.remaining());
+ HttpTester.Input input = HttpTester.from(socket.getInputStream());
+ HttpTester.Response response = HttpTester.parseResponse(input);
+ System.err.printf("%s %s %s%n", response.getVersion(), response.getStatus(), response.getReason());
+ for (HttpField field : response)
+ {
+ System.err.printf("%s: %s%n", field.getName(), field.getValue());
+ }
+ System.err.printf("%n%s%n", response.getContent());
+ }
+ }
+
+ @Test
+ public void testGetRequestBuffer10()
+ {
+ HttpTester.Request request = HttpTester.parseRequest(
+ "GET /uri HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: keep-alive\r\n" +
+ "\r\n" +
+ "GET /some/other/request /HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n"
+ );
+ assertThat(request.getMethod(), is("GET"));
+ assertThat(request.getUri(), is("/uri"));
+ assertThat(request.getVersion(), is(HttpVersion.HTTP_1_0));
+ assertThat(request.get(HttpHeader.HOST), is("localhost"));
+ assertThat(request.getContent(), is(""));
+ }
+
+ @Test
+ public void testGetRequestBuffer11()
+ {
+ HttpTester.Request request = HttpTester.parseRequest(
+ "GET /uri HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ "GET /some/other/request /HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n"
+ );
+ assertThat(request.getMethod(), is("GET"));
+ assertThat(request.getUri(), is("/uri"));
+ assertThat(request.getVersion(), is(HttpVersion.HTTP_1_1));
+ assertThat(request.get(HttpHeader.HOST), is("localhost"));
+ assertThat(request.getContent(), is(""));
+ }
+
+ @Test
+ public void testPostRequestBuffer10()
+ {
+ HttpTester.Request request = HttpTester.parseRequest(
+ "POST /uri HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Content-Length: 16\r\n" +
+ "\r\n" +
+ "0123456789ABCDEF" +
+ "\r\n" +
+ "GET /some/other/request /HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n"
+ );
+ assertThat(request.getMethod(), is("POST"));
+ assertThat(request.getUri(), is("/uri"));
+ assertThat(request.getVersion(), is(HttpVersion.HTTP_1_0));
+ assertThat(request.get(HttpHeader.HOST), is("localhost"));
+ assertThat(request.getContent(), is("0123456789ABCDEF"));
+ }
+
+ @Test
+ public void testPostRequestBuffer11()
+ {
+ HttpTester.Request request = HttpTester.parseRequest(
+ "POST /uri HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "A\r\n" +
+ "0123456789\r\n" +
+ "6\r\n" +
+ "ABCDEF\r\n" +
+ "0\r\n" +
+ "\r\n" +
+ "GET /some/other/request /HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n"
+ );
+ assertThat(request.getMethod(), is("POST"));
+ assertThat(request.getUri(), is("/uri"));
+ assertThat(request.getVersion(), is(HttpVersion.HTTP_1_1));
+ assertThat(request.get(HttpHeader.HOST), is("localhost"));
+ assertThat(request.getContent(), is("0123456789ABCDEF"));
+ }
+
+ @Test
+ public void testResponseEOFBuffer()
+ {
+ HttpTester.Response response = HttpTester.parseResponse(
+ "HTTP/1.1 200 OK\r\n" +
+ "Header: value\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "0123456789ABCDEF"
+ );
+
+ assertThat(response.getVersion(), is(HttpVersion.HTTP_1_1));
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getReason(), is("OK"));
+ assertThat(response.get("Header"), is("value"));
+ assertThat(response.getContent(), is("0123456789ABCDEF"));
+ }
+
+ @Test
+ public void testResponseLengthBuffer()
+ {
+ HttpTester.Response response = HttpTester.parseResponse(
+ "HTTP/1.1 200 OK\r\n" +
+ "Header: value\r\n" +
+ "Content-Length: 16\r\n" +
+ "\r\n" +
+ "0123456789ABCDEF" +
+ "HTTP/1.1 200 OK\r\n" +
+ "\r\n"
+ );
+
+ assertThat(response.getVersion(), is(HttpVersion.HTTP_1_1));
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getReason(), is("OK"));
+ assertThat(response.get("Header"), is("value"));
+ assertThat(response.getContent(), is("0123456789ABCDEF"));
+ }
+
+ @Test
+ public void testResponseChunkedBuffer()
+ {
+ HttpTester.Response response = HttpTester.parseResponse(
+ "HTTP/1.1 200 OK\r\n" +
+ "Header: value\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "A\r\n" +
+ "0123456789\r\n" +
+ "6\r\n" +
+ "ABCDEF\r\n" +
+ "0\r\n" +
+ "\r\n" +
+ "HTTP/1.1 200 OK\r\n" +
+ "\r\n"
+ );
+
+ assertThat(response.getVersion(), is(HttpVersion.HTTP_1_1));
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getReason(), is("OK"));
+ assertThat(response.get("Header"), is("value"));
+ assertThat(response.getContent(), is("0123456789ABCDEF"));
+ }
+
+ @Test
+ public void testResponsesInput() throws Exception
+ {
+ ByteArrayInputStream stream = new ByteArrayInputStream((
+ "HTTP/1.1 200 OK\r\n" +
+ "Header: value\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "A\r\n" +
+ "0123456789\r\n" +
+ "6\r\n" +
+ "ABCDEF\r\n" +
+ "0\r\n" +
+ "\r\n" +
+ "HTTP/1.1 400 OK\r\n" +
+ "Next: response\r\n" +
+ "Content-Length: 16\r\n" +
+ "\r\n" +
+ "0123456789ABCDEF").getBytes(StandardCharsets.ISO_8859_1)
+ );
+
+ HttpTester.Input in = HttpTester.from(stream);
+
+ HttpTester.Response response = HttpTester.parseResponse(in);
+
+ assertThat(response.getVersion(), is(HttpVersion.HTTP_1_1));
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getReason(), is("OK"));
+ assertThat(response.get("Header"), is("value"));
+ assertThat(response.getContent(), is("0123456789ABCDEF"));
+
+ response = HttpTester.parseResponse(in);
+ assertThat(response.getVersion(), is(HttpVersion.HTTP_1_1));
+ assertThat(response.getStatus(), is(400));
+ assertThat(response.getReason(), is("OK"));
+ assertThat(response.get("Next"), is("response"));
+ assertThat(response.getContent(), is("0123456789ABCDEF"));
+ }
+
+ @Test
+ public void testResponsesSplitInput() throws Exception
+ {
+ PipedOutputStream src = new PipedOutputStream();
+ PipedInputStream stream = new PipedInputStream(src)
+ {
+ @Override
+ public synchronized int read(byte[] b, int off, int len) throws IOException
+ {
+ if (available() == 0)
+ return 0;
+ return super.read(b, off, len);
+ }
+ };
+
+ HttpTester.Input in = HttpTester.from(stream);
+
+ src.write((
+ "HTTP/1.1 200 OK\r\n" +
+ "Header: value\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "A\r\n" +
+ "0123456789\r\n" +
+ "6\r\n" +
+ "ABC"
+ ).getBytes(StandardCharsets.ISO_8859_1)
+ );
+
+ HttpTester.Response response = HttpTester.parseResponse(in);
+ assertThat(response, nullValue());
+ src.write((
+ "DEF\r\n" +
+ "0\r\n" +
+ "\r\n" +
+ "HTTP/1.1 400 OK\r\n" +
+ "Next: response\r\n" +
+ "Content-Length: 16\r\n" +
+ "\r\n" +
+ "0123456789"
+ ).getBytes(StandardCharsets.ISO_8859_1)
+ );
+
+ response = HttpTester.parseResponse(in);
+ assertThat(response.getVersion(), is(HttpVersion.HTTP_1_1));
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getReason(), is("OK"));
+ assertThat(response.get("Header"), is("value"));
+ assertThat(response.getContent(), is("0123456789ABCDEF"));
+
+ response = HttpTester.parseResponse(in);
+ assertThat(response, nullValue());
+
+ src.write((
+ "ABCDEF"
+ ).getBytes(StandardCharsets.ISO_8859_1)
+ );
+
+ response = HttpTester.parseResponse(in);
+ assertThat(response.getVersion(), is(HttpVersion.HTTP_1_1));
+ assertThat(response.getStatus(), is(400));
+ assertThat(response.getReason(), is("OK"));
+ assertThat(response.get("Next"), is("response"));
+ assertThat(response.getContent(), is("0123456789ABCDEF"));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
new file mode 100644
index 0000000..28be9c4
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
@@ -0,0 +1,792 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.http.HttpURI.Violation;
+import org.eclipse.jetty.util.MultiMap;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+public class HttpURITest
+{
+ @Test
+ public void testInvalidAddress() throws Exception
+ {
+ assertInvalidURI("http://[ffff::1:8080/", "Invalid URL; no closing ']' -- should throw exception");
+ assertInvalidURI("**", "only '*', not '**'");
+ assertInvalidURI("*/", "only '*', not '*/'");
+ }
+
+ private void assertInvalidURI(String invalidURI, String message)
+ {
+ HttpURI uri = new HttpURI();
+ try
+ {
+ uri.parse(invalidURI);
+ fail(message);
+ }
+ catch (IllegalArgumentException e)
+ {
+ assertTrue(true);
+ }
+ }
+
+ @Test
+ public void testParse()
+ {
+ HttpURI uri = new HttpURI();
+
+ uri.parse("*");
+ assertThat(uri.getHost(), nullValue());
+ assertThat(uri.getPath(), is("*"));
+
+ uri.parse("/foo/bar");
+ assertThat(uri.getHost(), nullValue());
+ assertThat(uri.getPath(), is("/foo/bar"));
+
+ uri.parse("//foo/bar");
+ assertThat(uri.getHost(), is("foo"));
+ assertThat(uri.getPath(), is("/bar"));
+
+ uri.parse("http://foo/bar");
+ assertThat(uri.getHost(), is("foo"));
+ assertThat(uri.getPath(), is("/bar"));
+
+ // We do allow nulls if not encoded. This can be used for testing 2nd line of defence.
+ uri.parse("http://fo\000/bar");
+ assertThat(uri.getHost(), is("fo\000"));
+ assertThat(uri.getPath(), is("/bar"));
+ }
+
+ @Test
+ public void testParseRequestTarget()
+ {
+ HttpURI uri = new HttpURI();
+
+ uri.parseRequestTarget("GET", "*");
+ assertThat(uri.getHost(), nullValue());
+ assertThat(uri.getPath(), is("*"));
+
+ uri.parseRequestTarget("GET", "/foo/bar");
+ assertThat(uri.getHost(), nullValue());
+ assertThat(uri.getPath(), is("/foo/bar"));
+
+ uri.parseRequestTarget("GET", "//foo/bar");
+ assertThat(uri.getHost(), nullValue());
+ assertThat(uri.getPath(), is("//foo/bar"));
+
+ uri.parseRequestTarget("GET", "http://foo/bar");
+ assertThat(uri.getHost(), is("foo"));
+ assertThat(uri.getPath(), is("/bar"));
+ }
+
+ @Test
+ public void testExtB() throws Exception
+ {
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+ for (String value : new String[]{"a", "abcdABCD", "\u00C0", "\u697C", "\uD869\uDED5", "\uD840\uDC08"})
+ {
+ HttpURI uri = new HttpURI("/path?value=" + URLEncoder.encode(value, "UTF-8"));
+
+ MultiMap<String> parameters = new MultiMap<>();
+ uri.decodeQueryTo(parameters, StandardCharsets.UTF_8);
+ assertEquals(value, parameters.getString("value"));
+ }
+ }
+
+ @Test
+ public void testAt() throws Exception
+ {
+ HttpURI uri = new HttpURI("/@foo/bar");
+ assertEquals("/@foo/bar", uri.getPath());
+ }
+
+ @Test
+ public void testParams() throws Exception
+ {
+ HttpURI uri = new HttpURI("/foo/bar");
+ assertEquals("/foo/bar", uri.getPath());
+ assertEquals("/foo/bar", uri.getDecodedPath());
+ assertEquals(null, uri.getParam());
+
+ uri = new HttpURI("/foo/bar;jsessionid=12345");
+ assertEquals("/foo/bar;jsessionid=12345", uri.getPath());
+ assertEquals("/foo/bar", uri.getDecodedPath());
+ assertEquals("jsessionid=12345", uri.getParam());
+
+ uri = new HttpURI("/foo;abc=123/bar;jsessionid=12345");
+ assertEquals("/foo;abc=123/bar;jsessionid=12345", uri.getPath());
+ assertEquals("/foo/bar", uri.getDecodedPath());
+ assertEquals("jsessionid=12345", uri.getParam());
+
+ uri = new HttpURI("/foo;abc=123/bar;jsessionid=12345?name=value");
+ assertEquals("/foo;abc=123/bar;jsessionid=12345", uri.getPath());
+ assertEquals("/foo/bar", uri.getDecodedPath());
+ assertEquals("jsessionid=12345", uri.getParam());
+
+ uri = new HttpURI("/foo;abc=123/bar;jsessionid=12345#target");
+ assertEquals("/foo;abc=123/bar;jsessionid=12345", uri.getPath());
+ assertEquals("/foo/bar", uri.getDecodedPath());
+ assertEquals("jsessionid=12345", uri.getParam());
+ }
+
+ @Test
+ public void testMutableURI()
+ {
+ HttpURI uri = new HttpURI("/foo/bar");
+ assertEquals("/foo/bar", uri.toString());
+ assertEquals("/foo/bar", uri.getPath());
+ assertEquals("/foo/bar", uri.getDecodedPath());
+
+ uri.setScheme("http");
+ assertEquals("http:/foo/bar", uri.toString());
+ assertEquals("/foo/bar", uri.getPath());
+ assertEquals("/foo/bar", uri.getDecodedPath());
+
+ uri.setAuthority("host", 0);
+ assertEquals("http://host/foo/bar", uri.toString());
+ assertEquals("/foo/bar", uri.getPath());
+ assertEquals("/foo/bar", uri.getDecodedPath());
+
+ uri.setAuthority("host", 8888);
+ assertEquals("http://host:8888/foo/bar", uri.toString());
+ assertEquals("/foo/bar", uri.getPath());
+ assertEquals("/foo/bar", uri.getDecodedPath());
+
+ uri.setPathQuery("/f%30%30;p0/bar;p1;p2");
+ assertEquals("http://host:8888/f%30%30;p0/bar;p1;p2", uri.toString());
+ assertEquals("/f%30%30;p0/bar;p1;p2", uri.getPath());
+ assertEquals("/f00/bar", uri.getDecodedPath());
+ assertEquals("p2", uri.getParam());
+ assertEquals(null, uri.getQuery());
+
+ uri.setPathQuery("/f%30%30;p0/bar;p1;p2?name=value");
+ assertEquals("http://host:8888/f%30%30;p0/bar;p1;p2?name=value", uri.toString());
+ assertEquals("/f%30%30;p0/bar;p1;p2", uri.getPath());
+ assertEquals("/f00/bar", uri.getDecodedPath());
+ assertEquals("p2", uri.getParam());
+ assertEquals("name=value", uri.getQuery());
+
+ uri.setQuery("other=123456");
+ assertEquals("http://host:8888/f%30%30;p0/bar;p1;p2?other=123456", uri.toString());
+ assertEquals("/f%30%30;p0/bar;p1;p2", uri.getPath());
+ assertEquals("/f00/bar", uri.getDecodedPath());
+ assertEquals("p2", uri.getParam());
+ assertEquals("other=123456", uri.getQuery());
+ }
+
+ @Test
+ public void testSchemeAndOrAuthority() throws Exception
+ {
+ HttpURI uri = new HttpURI("/path/info");
+ assertEquals("/path/info", uri.toString());
+
+ uri.setAuthority("host", 0);
+ assertEquals("//host/path/info", uri.toString());
+
+ uri.setAuthority("host", 8888);
+ assertEquals("//host:8888/path/info", uri.toString());
+
+ uri.setScheme("http");
+ assertEquals("http://host:8888/path/info", uri.toString());
+
+ uri.setAuthority(null, 0);
+ assertEquals("http:/path/info", uri.toString());
+ }
+
+ @Test
+ public void testSetters() throws Exception
+ {
+ HttpURI uri = new HttpURI();
+ assertEquals("", uri.toString());
+
+ uri = new HttpURI(null, null, 0, null, null, null, null);
+ assertEquals("", uri.toString());
+
+ uri.setPath("/path/info");
+ assertEquals("/path/info", uri.toString());
+
+ uri.setAuthority("host", 8080);
+ assertEquals("//host:8080/path/info", uri.toString());
+
+ uri.setParam("param");
+ assertEquals("//host:8080/path/info;param", uri.toString());
+
+ uri.setQuery("a=b");
+ assertEquals("//host:8080/path/info;param?a=b", uri.toString());
+
+ uri.setScheme("http");
+ assertEquals("http://host:8080/path/info;param?a=b", uri.toString());
+
+ uri.setPathQuery("/other;xxx/path;ppp?query");
+ assertEquals("http://host:8080/other;xxx/path;ppp?query", uri.toString());
+
+ assertThat(uri.getScheme(), is("http"));
+ assertThat(uri.getAuthority(), is("host:8080"));
+ assertThat(uri.getHost(), is("host"));
+ assertThat(uri.getPort(), is(8080));
+ assertThat(uri.getPath(), is("/other;xxx/path;ppp"));
+ assertThat(uri.getDecodedPath(), is("/other/path"));
+ assertThat(uri.getParam(), is("ppp"));
+ assertThat(uri.getQuery(), is("query"));
+ assertThat(uri.getPathQuery(), is("/other;xxx/path;ppp?query"));
+
+ uri.setPathQuery(null);
+ assertEquals("http://host:8080?query", uri.toString()); // Yes silly result!
+
+ uri.setQuery(null);
+ assertEquals("http://host:8080", uri.toString());
+
+ uri.setPathQuery("/other;xxx/path;ppp?query");
+ assertEquals("http://host:8080/other;xxx/path;ppp?query", uri.toString());
+
+ uri.setScheme(null);
+ assertEquals("//host:8080/other;xxx/path;ppp?query", uri.toString());
+
+ uri.setAuthority(null, -1);
+ assertEquals("/other;xxx/path;ppp?query", uri.toString());
+
+ uri.setParam(null);
+ assertEquals("/other;xxx/path?query", uri.toString());
+
+ uri.setQuery(null);
+ assertEquals("/other;xxx/path", uri.toString());
+
+ uri.setPath(null);
+ assertEquals("", uri.toString());
+ }
+
+ public static Stream<Arguments> decodePathTests()
+ {
+ return Arrays.stream(new Object[][]
+ {
+ // Simple path example
+ {"http://host/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
+ {"//host/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
+ {"/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
+
+ // Scheme & host containing unusual valid characters
+ {"ht..tp://host/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
+ {"ht1.2+..-3.4tp://127.0.0.1:8080/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
+ {"http://h%2est/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
+ {"http://h..est/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
+
+ // legal non ambiguous relative paths
+ {"http://host/../path/info", null, EnumSet.noneOf(Violation.class)},
+ {"http://host/path/../info", "/info", EnumSet.noneOf(Violation.class)},
+ {"http://host/path/./info", "/path/info", EnumSet.noneOf(Violation.class)},
+ {"//host/path/../info", "/info", EnumSet.noneOf(Violation.class)},
+ {"//host/path/./info", "/path/info", EnumSet.noneOf(Violation.class)},
+ {"/path/../info", "/info", EnumSet.noneOf(Violation.class)},
+ {"/path/./info", "/path/info", EnumSet.noneOf(Violation.class)},
+ {"path/../info", "info", EnumSet.noneOf(Violation.class)},
+ {"path/./info", "path/info", EnumSet.noneOf(Violation.class)},
+
+ // encoded paths
+ {"/f%6f%6F/bar", "/foo/bar", EnumSet.noneOf(Violation.class)},
+ {"/f%u006f%u006F/bar", "/foo/bar", EnumSet.of(Violation.UTF16)},
+ {"/f%u0001%u0001/bar", "/f\001\001/bar", EnumSet.of(Violation.UTF16)},
+ {"/foo/%u20AC/bar", "/foo/\u20AC/bar", EnumSet.of(Violation.UTF16)},
+
+ // illegal paths
+ {"//host/../path/info", null, EnumSet.noneOf(Violation.class)},
+ {"/../path/info", null, EnumSet.noneOf(Violation.class)},
+ {"../path/info", null, EnumSet.noneOf(Violation.class)},
+ {"/path/%XX/info", null, EnumSet.noneOf(Violation.class)},
+ {"/path/%2/F/info", null, EnumSet.noneOf(Violation.class)},
+ {"/path/%/info", null, EnumSet.noneOf(Violation.class)},
+ {"/path/%u000X/info", null, EnumSet.noneOf(Violation.class)},
+ {"/path/Fo%u0000/info", null, EnumSet.noneOf(Violation.class)},
+ {"/path/Fo%00/info", null, EnumSet.noneOf(Violation.class)},
+ {"/path/Foo/info%u0000", null, EnumSet.noneOf(Violation.class)},
+ {"/path/Foo/info%00", null, EnumSet.noneOf(Violation.class)},
+ {"/path/%U20AC", null, EnumSet.noneOf(Violation.class)},
+ {"%2e%2e/info", null, EnumSet.noneOf(Violation.class)},
+ {"%u002e%u002e/info", null, EnumSet.noneOf(Violation.class)},
+ {"%2e%2e;/info", null, EnumSet.noneOf(Violation.class)},
+ {"%u002e%u002e;/info", null, EnumSet.noneOf(Violation.class)},
+ {"%2e.", null, EnumSet.noneOf(Violation.class)},
+ {"%u002e.", null, EnumSet.noneOf(Violation.class)},
+ {".%2e", null, EnumSet.noneOf(Violation.class)},
+ {".%u002e", null, EnumSet.noneOf(Violation.class)},
+ {"%2e%2e", null, EnumSet.noneOf(Violation.class)},
+ {"%u002e%u002e", null, EnumSet.noneOf(Violation.class)},
+ {"%2e%u002e", null, EnumSet.noneOf(Violation.class)},
+ {"%u002e%2e", null, EnumSet.noneOf(Violation.class)},
+ {"..;/info", null, EnumSet.noneOf(Violation.class)},
+ {"..;param/info", null, EnumSet.noneOf(Violation.class)},
+
+ // ambiguous dot encodings
+ {"scheme://host/path/%2e/info", "/path/info", EnumSet.of(Violation.SEGMENT)},
+ {"scheme:/path/%2e/info", "/path/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e/info", "/path/info", EnumSet.of(Violation.SEGMENT)},
+ {"path/%2e/info/", "path/info/", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e/info", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e;/info", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e;param/info", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e;param;other/info;other", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"%2e/info", "info", EnumSet.of(Violation.SEGMENT)},
+ {"%u002e/info", "info", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
+
+ {"%2e", "", EnumSet.of(Violation.SEGMENT)},
+ {"%u002e", "", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
+
+ // empty segment treated as ambiguous
+ {"/foo//bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
+ {"/foo//../bar", "/foo/bar", EnumSet.of(Violation.EMPTY)},
+ {"/foo///../../../bar", "/bar", EnumSet.of(Violation.EMPTY)},
+ {"/foo/./../bar", "/bar", EnumSet.noneOf(Violation.class)},
+ {"/foo//./bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
+ {"foo/bar", "foo/bar", EnumSet.noneOf(Violation.class)},
+ {"foo;/bar", "foo/bar", EnumSet.noneOf(Violation.class)},
+ {";/bar", "/bar", EnumSet.of(Violation.EMPTY)},
+ {";?n=v", "", EnumSet.of(Violation.EMPTY)},
+ {"?n=v", "", EnumSet.noneOf(Violation.class)},
+ {"#n=v", "", EnumSet.noneOf(Violation.class)},
+ {"", "", EnumSet.noneOf(Violation.class)},
+ {"http:/foo", "/foo", EnumSet.noneOf(Violation.class)},
+
+ // ambiguous parameter inclusions
+ {"/path/.;/info", "/path/info", EnumSet.of(Violation.PARAM)},
+ {"/path/.;param/info", "/path/info", EnumSet.of(Violation.PARAM)},
+ {"/path/..;/info", "/info", EnumSet.of(Violation.PARAM)},
+ {"/path/..;param/info", "/info", EnumSet.of(Violation.PARAM)},
+ {".;/info", "info", EnumSet.of(Violation.PARAM)},
+ {".;param/info", "info", EnumSet.of(Violation.PARAM)},
+
+ // ambiguous segment separators
+ {"/path/%2f/info", "/path///info", EnumSet.of(Violation.SEPARATOR)},
+ {"%2f/info", "//info", EnumSet.of(Violation.SEPARATOR)},
+ {"%2F/info", "//info", EnumSet.of(Violation.SEPARATOR)},
+ {"/path/%2f../info", "/path/info", EnumSet.of(Violation.SEPARATOR)},
+
+ // ambiguous encoding
+ {"/path/%25/info", "/path/%/info", EnumSet.of(Violation.ENCODING)},
+ {"/path/%u0025/info", "/path/%/info", EnumSet.of(Violation.ENCODING, Violation.UTF16)},
+ {"%25/info", "%/info", EnumSet.of(Violation.ENCODING)},
+ {"/path/%25../info", "/path/%../info", EnumSet.of(Violation.ENCODING)},
+ {"/path/%u0025../info", "/path/%../info", EnumSet.of(Violation.ENCODING, Violation.UTF16)},
+
+ // combinations
+ {"/path/%2f/..;/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM)},
+ {"/path/%u002f/..;/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.UTF16)},
+ {"/path/%2f/..;/%2e/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT)},
+
+ // Non ascii characters
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+ {"http://localhost:9000/x\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32", "/x\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32", EnumSet.noneOf(Violation.class)},
+ {"http://localhost:9000/\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32", "/\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32\uD83C\uDF32", EnumSet.noneOf(Violation.class)},
+ // @checkstyle-enable-check : AvoidEscapedUnicodeCharactersCheck
+ }).map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("decodePathTests")
+ public void testDecodedPath(String input, String decodedPath, EnumSet<Violation> expected)
+ {
+ try
+ {
+ HttpURI uri = new HttpURI(input);
+ assertThat(uri.getDecodedPath(), is(decodedPath));
+ EnumSet<Violation> ambiguous = EnumSet.copyOf(expected);
+ ambiguous.retainAll(EnumSet.complementOf(EnumSet.of(Violation.UTF16)));
+
+ assertThat(uri.isAmbiguous(), is(!ambiguous.isEmpty()));
+ assertThat(uri.hasAmbiguousSegment(), is(ambiguous.contains(Violation.SEGMENT)));
+ assertThat(uri.hasAmbiguousSeparator(), is(ambiguous.contains(Violation.SEPARATOR)));
+ assertThat(uri.hasAmbiguousParameter(), is(ambiguous.contains(Violation.PARAM)));
+ assertThat(uri.hasAmbiguousEncoding(), is(ambiguous.contains(Violation.ENCODING)));
+
+ assertThat(uri.hasUtf16Encoding(), is(expected.contains(Violation.UTF16)));
+ }
+ catch (Exception e)
+ {
+ if (decodedPath != null)
+ e.printStackTrace();
+ assertThat(decodedPath, nullValue());
+ }
+ }
+
+ public static Stream<Arguments> testPathQueryTests()
+ {
+ return Arrays.stream(new Object[][]
+ {
+ // Simple path example
+ {"/path/info", "/path/info", EnumSet.noneOf(Violation.class)},
+
+ // legal non ambiguous relative paths
+ {"/path/../info", "/info", EnumSet.noneOf(Violation.class)},
+ {"/path/./info", "/path/info", EnumSet.noneOf(Violation.class)},
+ {"path/../info", "info", EnumSet.noneOf(Violation.class)},
+ {"path/./info", "path/info", EnumSet.noneOf(Violation.class)},
+
+ // illegal paths
+ {"/../path/info", null, null},
+ {"../path/info", null, null},
+ {"/path/%XX/info", null, null},
+ {"/path/%2/F/info", null, null},
+ {"%2e%2e/info", null, null},
+ {"%2e%2e;/info", null, null},
+ {"%2e.", null, null},
+ {".%2e", null, null},
+ {"%2e%2e", null, null},
+ {"..;/info", null, null},
+ {"..;param/info", null, null},
+
+ // ambiguous dot encodings
+ {"/path/%2e/info", "/path/info", EnumSet.of(Violation.SEGMENT)},
+ {"path/%2e/info/", "path/info/", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e/info", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e;/info", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e;param/info", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e;param;other/info;other", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"%2e/info", "info", EnumSet.of(Violation.SEGMENT)},
+ {"%2e", "", EnumSet.of(Violation.SEGMENT)},
+
+ // empty segment treated as ambiguous
+ {"/", "/", EnumSet.noneOf(Violation.class)},
+ {"/#", "/", EnumSet.noneOf(Violation.class)},
+ {"/path", "/path", EnumSet.noneOf(Violation.class)},
+ {"/path/", "/path/", EnumSet.noneOf(Violation.class)},
+ {"//", "//", EnumSet.of(Violation.EMPTY)},
+ {"/foo//", "/foo//", EnumSet.of(Violation.EMPTY)},
+ {"/foo//bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
+ {"//foo/bar", "//foo/bar", EnumSet.of(Violation.EMPTY)},
+ {"/foo?bar", "/foo", EnumSet.noneOf(Violation.class)},
+ {"/foo#bar", "/foo", EnumSet.noneOf(Violation.class)},
+ {"/foo;bar", "/foo", EnumSet.noneOf(Violation.class)},
+ {"/foo/?bar", "/foo/", EnumSet.noneOf(Violation.class)},
+ {"/foo/#bar", "/foo/", EnumSet.noneOf(Violation.class)},
+ {"/foo/;param", "/foo/", EnumSet.noneOf(Violation.class)},
+ {"/foo/;param/bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
+ {"/foo//bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
+ {"/foo//bar//", "/foo//bar//", EnumSet.of(Violation.EMPTY)},
+ {"//foo//bar//", "//foo//bar//", EnumSet.of(Violation.EMPTY)},
+ {"/foo//../bar", "/foo/bar", EnumSet.of(Violation.EMPTY)},
+ {"/foo///../../../bar", "/bar", EnumSet.of(Violation.EMPTY)},
+ {"/foo/./../bar", "/bar", EnumSet.noneOf(Violation.class)},
+ {"/foo//./bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
+ {"foo/bar", "foo/bar", EnumSet.noneOf(Violation.class)},
+ {"foo;/bar", "foo/bar", EnumSet.noneOf(Violation.class)},
+ {";/bar", "/bar", EnumSet.of(Violation.EMPTY)},
+ {";?n=v", "", EnumSet.of(Violation.EMPTY)},
+ {"?n=v", "", EnumSet.noneOf(Violation.class)},
+ {"#n=v", "", EnumSet.noneOf(Violation.class)},
+ {"", "", EnumSet.noneOf(Violation.class)},
+
+ // ambiguous parameter inclusions
+ {"/path/.;/info", "/path/info", EnumSet.of(Violation.PARAM)},
+ {"/path/.;param/info", "/path/info", EnumSet.of(Violation.PARAM)},
+ {"/path/..;/info", "/info", EnumSet.of(Violation.PARAM)},
+ {"/path/..;param/info", "/info", EnumSet.of(Violation.PARAM)},
+ {".;/info", "info", EnumSet.of(Violation.PARAM)},
+ {".;param/info", "info", EnumSet.of(Violation.PARAM)},
+
+ // ambiguous segment separators
+ {"/path/%2f/info", "/path///info", EnumSet.of(Violation.SEPARATOR)},
+ {"%2f/info", "//info", EnumSet.of(Violation.SEPARATOR)},
+ {"%2F/info", "//info", EnumSet.of(Violation.SEPARATOR)},
+ {"/path/%2f../info", "/path/info", EnumSet.of(Violation.SEPARATOR)},
+
+ // ambiguous encoding
+ {"/path/%25/info", "/path/%/info", EnumSet.of(Violation.ENCODING)},
+ {"%25/info", "%/info", EnumSet.of(Violation.ENCODING)},
+ {"/path/%25../info", "/path/%../info", EnumSet.of(Violation.ENCODING)},
+
+ // combinations
+ {"/path/%2f/..;/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM)},
+ {"/path/%2f/..;/%2e/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT)},
+ {"/path/%2f/%25/..;/%2e//info", "/path////info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT, Violation.ENCODING, Violation.EMPTY)},
+ }).map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("testPathQueryTests")
+ public void testPathQuery(String input, String decodedPath, EnumSet<Violation> expected)
+ {
+ HttpURI uri = new HttpURI();
+
+ // If expected is null then it is a bad URI and should throw.
+ if (expected == null)
+ {
+ assertThrows(Throwable.class, () -> uri.parseRequestTarget(HttpMethod.GET.asString(), input));
+ return;
+ }
+
+ uri.parseRequestTarget(HttpMethod.GET.asString(), input);
+ assertThat(uri.getDecodedPath(), is(decodedPath));
+ assertThat(uri.isAmbiguous(), is(!expected.isEmpty()));
+ assertThat(uri.hasAmbiguousEmptySegment(), is(expected.contains(Violation.EMPTY)));
+ assertThat(uri.hasAmbiguousSegment(), is(expected.contains(Violation.SEGMENT)));
+ assertThat(uri.hasAmbiguousSeparator(), is(expected.contains(Violation.SEPARATOR)));
+ assertThat(uri.hasAmbiguousParameter(), is(expected.contains(Violation.PARAM)));
+ assertThat(uri.hasAmbiguousEncoding(), is(expected.contains(Violation.ENCODING)));
+ }
+
+ public static Stream<Arguments> parseData()
+ {
+ return Stream.of(
+ // Nothing but path
+ Arguments.of("path", null, null, "-1", "path", null, null, null),
+ Arguments.of("path/path", null, null, "-1", "path/path", null, null, null),
+ Arguments.of("%65ncoded/path", null, null, "-1", "%65ncoded/path", null, null, null),
+
+ // Basic path reference
+ Arguments.of("/path/to/context", null, null, "-1", "/path/to/context", null, null, null),
+
+ // Basic with encoded query
+ Arguments.of("http://example.com/path/to/context;param?query=%22value%22#fragment", "http", "example.com", "-1", "/path/to/context;param", "param", "query=%22value%22", "fragment"),
+ Arguments.of("http://[::1]/path/to/context;param?query=%22value%22#fragment", "http", "[::1]", "-1", "/path/to/context;param", "param", "query=%22value%22", "fragment"),
+
+ // Basic with parameters and query
+ Arguments.of("http://example.com:8080/path/to/context;param?query=%22value%22#fragment", "http", "example.com", "8080", "/path/to/context;param", "param", "query=%22value%22", "fragment"),
+ Arguments.of("http://[::1]:8080/path/to/context;param?query=%22value%22#fragment", "http", "[::1]", "8080", "/path/to/context;param", "param", "query=%22value%22", "fragment"),
+
+ // Path References
+ Arguments.of("/path/info", null, null, null, "/path/info", null, null, null),
+ Arguments.of("/path/info#fragment", null, null, null, "/path/info", null, null, "fragment"),
+ Arguments.of("/path/info?query", null, null, null, "/path/info", null, "query", null),
+ Arguments.of("/path/info?query#fragment", null, null, null, "/path/info", null, "query", "fragment"),
+ Arguments.of("/path/info;param", null, null, null, "/path/info;param", "param", null, null),
+ Arguments.of("/path/info;param#fragment", null, null, null, "/path/info;param", "param", null, "fragment"),
+ Arguments.of("/path/info;param?query", null, null, null, "/path/info;param", "param", "query", null),
+ Arguments.of("/path/info;param?query#fragment", null, null, null, "/path/info;param", "param", "query", "fragment"),
+ Arguments.of("/path/info;a=b/foo;c=d", null, null, null, "/path/info;a=b/foo;c=d", "c=d", null, null), // TODO #405
+
+ // Protocol Less (aka scheme-less) URIs
+ Arguments.of("//host/path/info", null, "host", null, "/path/info", null, null, null),
+ Arguments.of("//user@host/path/info", null, "host", null, "/path/info", null, null, null),
+ Arguments.of("//user@host:8080/path/info", null, "host", "8080", "/path/info", null, null, null),
+ Arguments.of("//host:8080/path/info", null, "host", "8080", "/path/info", null, null, null),
+
+ // Host Less
+ Arguments.of("http:/path/info", "http", null, null, "/path/info", null, null, null),
+ Arguments.of("http:/path/info#fragment", "http", null, null, "/path/info", null, null, "fragment"),
+ Arguments.of("http:/path/info?query", "http", null, null, "/path/info", null, "query", null),
+ Arguments.of("http:/path/info?query#fragment", "http", null, null, "/path/info", null, "query", "fragment"),
+ Arguments.of("http:/path/info;param", "http", null, null, "/path/info;param", "param", null, null),
+ Arguments.of("http:/path/info;param#fragment", "http", null, null, "/path/info;param", "param", null, "fragment"),
+ Arguments.of("http:/path/info;param?query", "http", null, null, "/path/info;param", "param", "query", null),
+ Arguments.of("http:/path/info;param?query#fragment", "http", null, null, "/path/info;param", "param", "query", "fragment"),
+
+ // Everything and the kitchen sink
+ Arguments.of("http://user@host:8080/path/info;param?query#fragment", "http", "host", "8080", "/path/info;param", "param", "query", "fragment"),
+ Arguments.of("xxxxx://user@host:8080/path/info;param?query#fragment", "xxxxx", "host", "8080", "/path/info;param", "param", "query", "fragment"),
+
+ // No host, parameter with no content
+ Arguments.of("http:///;?#", "http", null, null, "/;", "", "", ""),
+
+ // Path with query that has no value
+ Arguments.of("/path/info?a=?query", null, null, null, "/path/info", null, "a=?query", null),
+
+ // Path with query alt syntax
+ Arguments.of("/path/info?a=;query", null, null, null, "/path/info", null, "a=;query", null),
+
+ // URI with host character
+ Arguments.of("/@path/info", null, null, null, "/@path/info", null, null, null),
+ Arguments.of("/user@path/info", null, null, null, "/user@path/info", null, null, null),
+ Arguments.of("//user@host/info", null, "host", null, "/info", null, null, null),
+ Arguments.of("//@host/info", null, "host", null, "/info", null, null, null),
+ Arguments.of("@host/info", null, null, null, "@host/info", null, null, null),
+
+ // Scheme-less, with host and port (overlapping with path)
+ Arguments.of("//host:8080//", null, "host", "8080", "//", null, null, null),
+
+ // File reference
+ Arguments.of("file:///path/info", "file", null, null, "/path/info", null, null, null),
+ Arguments.of("file:/path/info", "file", null, null, "/path/info", null, null, null),
+
+ // Bad URI (no scheme, no host, no path)
+ Arguments.of("//", null, null, null, null, null, null, null),
+
+ // Simple localhost references
+ Arguments.of("http://localhost/", "http", "localhost", null, "/", null, null, null),
+ Arguments.of("http://localhost:8080/", "http", "localhost", "8080", "/", null, null, null),
+ Arguments.of("http://localhost/?x=y", "http", "localhost", null, "/", null, "x=y", null),
+
+ // Simple path with parameter
+ Arguments.of("/;param", null, null, null, "/;param", "param", null, null),
+ Arguments.of(";param", null, null, null, ";param", "param", null, null),
+
+ // Simple path with query
+ Arguments.of("/?x=y", null, null, null, "/", null, "x=y", null),
+ Arguments.of("/?abc=test", null, null, null, "/", null, "abc=test", null),
+
+ // Simple path with fragment
+ Arguments.of("/#fragment", null, null, null, "/", null, null, "fragment"),
+
+ // Simple IPv4 host with port (default path)
+ Arguments.of("http://192.0.0.1:8080/", "http", "192.0.0.1", "8080", "/", null, null, null),
+
+ // Simple IPv6 host with port (default path)
+
+ Arguments.of("http://[2001:db8::1]:8080/", "http", "[2001:db8::1]", "8080", "/", null, null, null),
+ // IPv6 authenticated host with port (default path)
+
+ Arguments.of("http://user@[2001:db8::1]:8080/", "http", "[2001:db8::1]", "8080", "/", null, null, null),
+
+ // Simple IPv6 host no port (default path)
+ Arguments.of("http://[2001:db8::1]/", "http", "[2001:db8::1]", null, "/", null, null, null),
+
+ // Scheme-less IPv6, host with port (default path)
+ Arguments.of("//[2001:db8::1]:8080/", null, "[2001:db8::1]", "8080", "/", null, null, null),
+
+ // Interpreted as relative path of "*" (no host/port/scheme/query/fragment)
+ Arguments.of("*", null, null, null, "*", null, null, null),
+
+ // Path detection Tests (seen from JSP/JSTL and <c:url> use)
+ Arguments.of("http://host:8080/path/info?q1=v1&q2=v2", "http", "host", "8080", "/path/info", null, "q1=v1&q2=v2", null),
+ Arguments.of("/path/info?q1=v1&q2=v2", null, null, null, "/path/info", null, "q1=v1&q2=v2", null),
+ Arguments.of("/info?q1=v1&q2=v2", null, null, null, "/info", null, "q1=v1&q2=v2", null),
+ Arguments.of("info?q1=v1&q2=v2", null, null, null, "info", null, "q1=v1&q2=v2", null),
+ Arguments.of("info;q1=v1?q2=v2", null, null, null, "info;q1=v1", "q1=v1", "q2=v2", null),
+
+ // Path-less, query only (seen from JSP/JSTL and <c:url> use)
+ Arguments.of("?q1=v1&q2=v2", null, null, null, "", null, "q1=v1&q2=v2", null)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("parseData")
+ public void testParseString(String input, String scheme, String host, Integer port, String path, String param, String query, String fragment)
+ {
+ HttpURI httpUri = new HttpURI(input);
+
+ try
+ {
+ new URI(input);
+ // URI is valid (per java.net.URI parsing)
+
+ // Test case sanity check
+ assertThat("[" + input + "] expected path (test case) cannot be null", path, notNullValue());
+
+ // Assert expectations
+ assertThat("[" + input + "] .scheme", httpUri.getScheme(), is(scheme));
+ assertThat("[" + input + "] .host", httpUri.getHost(), is(host));
+ assertThat("[" + input + "] .port", httpUri.getPort(), is(port == null ? -1 : port));
+ assertThat("[" + input + "] .path", httpUri.getPath(), is(path));
+ assertThat("[" + input + "] .param", httpUri.getParam(), is(param));
+ assertThat("[" + input + "] .query", httpUri.getQuery(), is(query));
+ assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(fragment));
+ assertThat("[" + input + "] .toString", httpUri.toString(), is(input));
+ }
+ catch (URISyntaxException e)
+ {
+ // Assert HttpURI values for invalid URI (such as "//")
+ assertThat("[" + input + "] .scheme", httpUri.getScheme(), is(nullValue()));
+ assertThat("[" + input + "] .host", httpUri.getHost(), is(nullValue()));
+ assertThat("[" + input + "] .port", httpUri.getPort(), is(-1));
+ assertThat("[" + input + "] .path", httpUri.getPath(), is(nullValue()));
+ assertThat("[" + input + "] .param", httpUri.getParam(), is(nullValue()));
+ assertThat("[" + input + "] .query", httpUri.getQuery(), is(nullValue()));
+ assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(nullValue()));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("parseData")
+ public void testParseURI(String input, String scheme, String host, Integer port, String path, String param, String query, String fragment) throws Exception
+ {
+ URI javaUri = null;
+ try
+ {
+ javaUri = new URI(input);
+ }
+ catch (URISyntaxException ignore)
+ {
+ // Ignore, as URI is invalid anyway
+ }
+ assumeTrue(javaUri != null, "Skipping, not a valid input URI: " + input);
+
+ HttpURI httpUri = new HttpURI(input);
+
+ assertThat("[" + input + "] .scheme", httpUri.getScheme(), is(scheme));
+ assertThat("[" + input + "] .host", httpUri.getHost(), is(host));
+ assertThat("[" + input + "] .port", httpUri.getPort(), is(port == null ? -1 : port));
+ assertThat("[" + input + "] .path", httpUri.getPath(), is(path));
+ assertThat("[" + input + "] .param", httpUri.getParam(), is(param));
+ assertThat("[" + input + "] .query", httpUri.getQuery(), is(query));
+ assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(fragment));
+
+ assertThat("[" + input + "] .toString", httpUri.toString(), is(input));
+ }
+
+ @ParameterizedTest
+ @MethodSource("parseData")
+ public void testCompareToJavaNetURI(String input, String scheme, String host, Integer port, String path, String param, String query, String fragment) throws Exception
+ {
+ URI javaUri = null;
+ try
+ {
+ javaUri = new URI(input);
+ }
+ catch (URISyntaxException ignore)
+ {
+ // Ignore, as URI is invalid anyway
+ }
+ assumeTrue(javaUri != null, "Skipping, not a valid input URI");
+
+ HttpURI httpUri = new HttpURI(input);
+
+ assertThat("[" + input + "] .scheme", httpUri.getScheme(), is(javaUri.getScheme()));
+ assertThat("[" + input + "] .host", httpUri.getHost(), is(javaUri.getHost()));
+ assertThat("[" + input + "] .port", httpUri.getPort(), is(javaUri.getPort()));
+ assertThat("[" + input + "] .path", httpUri.getPath(), is(javaUri.getRawPath()));
+ // Not Relevant for java.net.URI -- assertThat("["+input+"] .param", httpUri.getParam(), is(param));
+ assertThat("[" + input + "] .query", httpUri.getQuery(), is(javaUri.getRawQuery()));
+ assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(javaUri.getFragment()));
+ assertThat("[" + input + "] .toString", httpUri.toString(), is(javaUri.toASCIIString()));
+ }
+
+ public static Stream<Arguments> queryData()
+ {
+ return Stream.of(
+ new String[]{"/path?p=%U20AC", "p=%U20AC"},
+ new String[]{"/path?p=%u20AC", "p=%u20AC"}
+ ).map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("queryData")
+ public void testEncodedQuery(String input, String expectedQuery)
+ {
+ HttpURI httpURI = new HttpURI(input);
+ assertThat("[" + input + "] .query", httpURI.getQuery(), is(expectedQuery));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/MimeTypesTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/MimeTypesTest.java
new file mode 100644
index 0000000..68b6f5b
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/MimeTypesTest.java
@@ -0,0 +1,132 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class MimeTypesTest
+{
+ public static Stream<Arguments> mimeTypesByExtensionCases()
+ {
+ return Stream.of(
+ Arguments.of("test.gz", "application/gzip"),
+ Arguments.of("foo.webp", "image/webp"),
+ Arguments.of("zed.avif", "image/avif"),
+ // make sure that filename case isn't an issue
+ Arguments.of("test.png", "image/png"),
+ Arguments.of("TEST.PNG", "image/png"),
+ Arguments.of("Test.Png", "image/png"),
+ Arguments.of("test.txt", "text/plain"),
+ Arguments.of("TEST.TXT", "text/plain"),
+ // Make sure that multiple dots don't interfere
+ Arguments.of("org.eclipse.jetty.Logo.png", "image/png"),
+ // Make sure that a deep path doesn't interfere
+ Arguments.of("org/eclipse/jetty/Logo.png", "image/png"),
+ // Make sure that path that looks like a filename doesn't interfere
+ Arguments.of("org/eclipse.jpg/jetty/Logo.png", "image/png")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("mimeTypesByExtensionCases")
+ public void testMimeTypesByExtension(String filename, String expectedMimeType)
+ {
+ MimeTypes mimetypes = new MimeTypes();
+ String contentType = mimetypes.getMimeByExtension(filename);
+ assertThat("MimeTypes.getMimeByExtension(\"" + filename + "\")",
+ contentType, is(expectedMimeType));
+ }
+
+ @Test
+ public void testGetMimeByExtensionNoExtension()
+ {
+ MimeTypes mimetypes = new MimeTypes();
+ String contentType = mimetypes.getMimeByExtension("README");
+ assertNull(contentType);
+ }
+
+ public static Stream<Arguments> charsetFromContentTypeCases()
+ {
+ return Stream.of(
+ Arguments.of("foo/bar;charset=abc;some=else", "abc"),
+ Arguments.of("foo/bar;charset=abc", "abc"),
+ Arguments.of("foo/bar ; charset = abc", "abc"),
+ Arguments.of("foo/bar ; charset = abc ; some=else", "abc"),
+ Arguments.of("foo/bar;other=param;charset=abc;some=else", "abc"),
+ Arguments.of("foo/bar;other=param;charset=abc", "abc"),
+ Arguments.of("foo/bar other = param ; charset = abc", "abc"),
+ Arguments.of("foo/bar other = param ; charset = abc ; some=else", "abc"),
+ Arguments.of("foo/bar other = param ; charset = abc", "abc"),
+ Arguments.of("foo/bar other = param ; charset = \"abc\" ; some=else", "abc"),
+ Arguments.of("foo/bar", null),
+ Arguments.of("foo/bar;charset=uTf8", "utf-8"),
+ Arguments.of("foo/bar;other=\"charset=abc\";charset=uTf8", "utf-8"),
+ Arguments.of("application/pdf; charset=UTF-8", "utf-8"),
+ Arguments.of("application/pdf;; charset=UTF-8", "utf-8"),
+ Arguments.of("application/pdf;;; charset=UTF-8", "utf-8"),
+ Arguments.of("application/pdf;;;; charset=UTF-8", "utf-8"),
+ Arguments.of("text/html;charset=utf-8", "utf-8")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("charsetFromContentTypeCases")
+ public void testCharsetFromContentType(String contentType, String expectedCharset)
+ {
+ assertThat("getCharsetFromContentType(\"" + contentType + "\")",
+ MimeTypes.getCharsetFromContentType(contentType), is(expectedCharset));
+ }
+
+ public static Stream<Arguments> contentTypeWithoutCharsetCases()
+ {
+ return Stream.of(
+ Arguments.of("foo/bar;charset=abc;some=else", "foo/bar;some=else"),
+ Arguments.of("foo/bar;charset=abc", "foo/bar"),
+ Arguments.of("foo/bar ; charset = abc", "foo/bar"),
+ Arguments.of("foo/bar ; charset = abc ; some=else", "foo/bar;some=else"),
+ Arguments.of("foo/bar;other=param;charset=abc;some=else", "foo/bar;other=param;some=else"),
+ Arguments.of("foo/bar;other=param;charset=abc", "foo/bar;other=param"),
+ Arguments.of("foo/bar ; other = param ; charset = abc", "foo/bar ; other = param"),
+ Arguments.of("foo/bar ; other = param ; charset = abc ; some=else", "foo/bar ; other = param;some=else"),
+ Arguments.of("foo/bar ; other = param ; charset = abc", "foo/bar ; other = param"),
+ Arguments.of("foo/bar ; other = param ; charset = \"abc\" ; some=else", "foo/bar ; other = param;some=else"),
+ Arguments.of("foo/bar", "foo/bar"),
+ Arguments.of("foo/bar;charset=uTf8", "foo/bar"),
+ Arguments.of("foo/bar;other=\"charset=abc\";charset=uTf8", "foo/bar;other=\"charset=abc\""),
+ Arguments.of("text/html;charset=utf-8", "text/html")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("contentTypeWithoutCharsetCases")
+ public void testContentTypeWithoutCharset(String contentTypeWithCharset, String expectedContentType)
+ {
+ assertThat("MimeTypes.getContentTypeWithoutCharset(\"" + contentTypeWithCharset + "\")",
+ MimeTypes.getContentTypeWithoutCharset(contentTypeWithCharset), is(expectedContentType));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartCaptureTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartCaptureTest.java
new file mode 100644
index 0000000..d429a1d
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartCaptureTest.java
@@ -0,0 +1,399 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.DigestOutputStream;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import javax.servlet.MultipartConfigElement;
+import javax.servlet.http.Part;
+
+import org.eclipse.jetty.toolchain.test.Hex;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.QuotedStringTokenizer;
+import org.eclipse.jetty.util.StringUtil;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+@ExtendWith(WorkDirExtension.class)
+public class MultiPartCaptureTest
+{
+ public static final int MAX_FILE_SIZE = 2 * 1024 * 1024;
+ public static final int MAX_REQUEST_SIZE = MAX_FILE_SIZE + (60 * 1024);
+ public static final int FILE_SIZE_THRESHOLD = 50;
+
+ public static Stream<Arguments> data()
+ {
+ return Stream.of(
+ // == Arbitrary / Non-Standard Examples ==
+
+ "multipart-uppercase",
+ // "multipart-base64", // base64 transfer encoding deprecated
+ // "multipart-base64-long", // base64 transfer encoding deprecated
+
+ // == Capture of raw request body contents from Apache HttpClient 4.5.5 ==
+
+ "browser-capture-company-urlencoded-apache-httpcomp",
+ "browser-capture-complex-apache-httpcomp",
+ "browser-capture-duplicate-names-apache-httpcomp",
+ "browser-capture-encoding-mess-apache-httpcomp",
+ "browser-capture-nested-apache-httpcomp",
+ "browser-capture-nested-binary-apache-httpcomp",
+ "browser-capture-number-only2-apache-httpcomp",
+ "browser-capture-number-only-apache-httpcomp",
+ "browser-capture-sjis-apache-httpcomp",
+ "browser-capture-strange-quoting-apache-httpcomp",
+ "browser-capture-text-files-apache-httpcomp",
+ "browser-capture-unicode-names-apache-httpcomp",
+ "browser-capture-zalgo-text-plain-apache-httpcomp",
+
+ // == Capture of raw request body contents from Eclipse Jetty Http Client 9.4.9 ==
+
+ "browser-capture-complex-jetty-client",
+ "browser-capture-duplicate-names-jetty-client",
+ "browser-capture-encoding-mess-jetty-client",
+ "browser-capture-nested-jetty-client",
+ "browser-capture-number-only-jetty-client",
+ "browser-capture-sjis-jetty-client",
+ "browser-capture-text-files-jetty-client",
+ "browser-capture-unicode-names-jetty-client",
+ "browser-capture-whitespace-only-jetty-client",
+
+ // == Capture of raw request body contents from various browsers ==
+
+ // simple form - 2 fields
+ "browser-capture-form1-android-chrome",
+ "browser-capture-form1-android-firefox",
+ "browser-capture-form1-chrome",
+ "browser-capture-form1-edge",
+ "browser-capture-form1-firefox",
+ "browser-capture-form1-ios-safari",
+ "browser-capture-form1-msie",
+ "browser-capture-form1-osx-safari",
+
+ // form submitted as shift-jis
+ "browser-capture-sjis-form-edge",
+ "browser-capture-sjis-form-msie",
+ // TODO: these might be addressable via Issue #2398
+ // "browser-capture-sjis-form-android-chrome", // contains html encoded character and unspecified charset defaults to utf-8
+ // "browser-capture-sjis-form-android-firefox", // contains html encoded character and unspecified charset defaults to utf-8
+ // "browser-capture-sjis-form-chrome", // contains html encoded character and unspecified charset defaults to utf-8
+ // "browser-capture-sjis-form-firefox", // contains html encoded character and unspecified charset defaults to utf-8
+ // "browser-capture-sjis-form-ios-safari", // contains html encoded character and unspecified charset defaults to utf-8
+ // "browser-capture-sjis-form-safari", // contains html encoded character and unspecified charset defaults to utf-8
+
+ // form submitted as shift-jis (with HTML5 specific hidden _charset_ field)
+ "browser-capture-sjis-charset-form-android-chrome", // contains html encoded character
+ "browser-capture-sjis-charset-form-android-firefox", // contains html encoded character
+ "browser-capture-sjis-charset-form-chrome", // contains html encoded character
+ "browser-capture-sjis-charset-form-edge",
+ "browser-capture-sjis-charset-form-firefox", // contains html encoded character
+ "browser-capture-sjis-charset-form-ios-safari", // contains html encoded character
+ "browser-capture-sjis-charset-form-msie",
+ "browser-capture-sjis-charset-form-safari", // contains html encoded character
+
+ // form submitted with simple file upload
+ "browser-capture-form-fileupload-android-chrome",
+ "browser-capture-form-fileupload-android-firefox",
+ "browser-capture-form-fileupload-chrome",
+ "browser-capture-form-fileupload-edge",
+ "browser-capture-form-fileupload-firefox",
+ "browser-capture-form-fileupload-ios-safari",
+ "browser-capture-form-fileupload-msie",
+ "browser-capture-form-fileupload-safari",
+
+ // form submitted with 2 files (1 binary, 1 text) and 2 text fields
+ "browser-capture-form-fileupload-alt-chrome",
+ "browser-capture-form-fileupload-alt-edge",
+ "browser-capture-form-fileupload-alt-firefox",
+ "browser-capture-form-fileupload-alt-msie",
+ "browser-capture-form-fileupload-alt-safari"
+ ).map(Arguments::of);
+ }
+
+ public WorkDir testingDir;
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testUtilParse(String rawPrefix) throws Exception
+ {
+ Path multipartRawFile = MavenTestingUtils.getTestResourcePathFile("multipart/" + rawPrefix + ".raw");
+ Path expectationPath = MavenTestingUtils.getTestResourcePathFile("multipart/" + rawPrefix + ".expected.txt");
+ MultipartExpectations multipartExpectations = new MultipartExpectations(expectationPath);
+
+ Path outputDir = testingDir.getEmptyPathDir();
+ MultipartConfigElement config = newMultipartConfigElement(outputDir);
+ try (InputStream in = Files.newInputStream(multipartRawFile))
+ {
+ org.eclipse.jetty.util.MultiPartInputStreamParser parser = new org.eclipse.jetty.util.MultiPartInputStreamParser(in, multipartExpectations.contentType, config, outputDir.toFile());
+
+ multipartExpectations.checkParts(parser.getParts(), s ->
+ {
+ try
+ {
+ return parser.getPart(s);
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testHttpParse(String rawPrefix) throws Exception
+ {
+ Path multipartRawFile = MavenTestingUtils.getTestResourcePathFile("multipart/" + rawPrefix + ".raw");
+ Path expectationPath = MavenTestingUtils.getTestResourcePathFile("multipart/" + rawPrefix + ".expected.txt");
+ MultipartExpectations multipartExpectations = new MultipartExpectations(expectationPath);
+
+ Path outputDir = testingDir.getEmptyPathDir();
+ MultipartConfigElement config = newMultipartConfigElement(outputDir);
+ try (InputStream in = Files.newInputStream(multipartRawFile))
+ {
+ MultiPartFormInputStream parser = new MultiPartFormInputStream(in, multipartExpectations.contentType, config, outputDir.toFile());
+
+ multipartExpectations.checkParts(parser.getParts(), s ->
+ {
+ try
+ {
+ return parser.getPart(s);
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+ }
+
+ private MultipartConfigElement newMultipartConfigElement(Path path)
+ {
+ return new MultipartConfigElement(path.toString(), MAX_FILE_SIZE, MAX_REQUEST_SIZE, FILE_SIZE_THRESHOLD);
+ }
+
+ public static class NameValue
+ {
+ public String name;
+ public String value;
+ }
+
+ public static class MultipartExpectations
+ {
+ public final String contentType;
+ public final int partCount;
+ public final List<NameValue> partFilenames = new ArrayList<>();
+ public final List<NameValue> partSha1sums = new ArrayList<>();
+ public final List<NameValue> partContainsContents = new ArrayList<>();
+
+ public MultipartExpectations(Path expectationsPath) throws IOException
+ {
+ String parsedContentType = null;
+ String parsedPartCount = "-1";
+
+ try (BufferedReader reader = Files.newBufferedReader(expectationsPath))
+ {
+ String line;
+ while ((line = reader.readLine()) != null)
+ {
+ line = line.trim();
+ if (StringUtil.isBlank(line) || line.startsWith("#"))
+ {
+ // skip blanks and comments
+ continue;
+ }
+
+ String[] split = line.split("\\|");
+ switch (split[0])
+ {
+ case "Request-Header":
+ if (split[1].equalsIgnoreCase("Content-Type"))
+ {
+ parsedContentType = split[2];
+ }
+ break;
+ case "Content-Type":
+ parsedContentType = split[1];
+ break;
+ case "Parts-Count":
+ parsedPartCount = split[1];
+ break;
+ case "Part-ContainsContents":
+ {
+ NameValue pair = new NameValue();
+ pair.name = split[1];
+ pair.value = split[2];
+ partContainsContents.add(pair);
+ break;
+ }
+ case "Part-Filename":
+ {
+ NameValue pair = new NameValue();
+ pair.name = split[1];
+ pair.value = split[2];
+ partFilenames.add(pair);
+ break;
+ }
+ case "Part-Sha1sum":
+ {
+ NameValue pair = new NameValue();
+ pair.name = split[1];
+ pair.value = split[2];
+ partSha1sums.add(pair);
+ break;
+ }
+ default:
+ throw new IOException("Bad Line in " + expectationsPath + ": " + line);
+ }
+ }
+ }
+
+ Objects.requireNonNull(parsedContentType, "Missing required 'Content-Type' declaration: " + expectationsPath);
+ this.contentType = parsedContentType;
+ this.partCount = Integer.parseInt(parsedPartCount);
+ }
+
+ private void checkParts(Collection<Part> parts, Function<String, Part> getPart) throws Exception
+ {
+ // Evaluate Count
+ if (partCount >= 0)
+ {
+ assertThat("Mulitpart.parts.size", parts.size(), is(partCount));
+ }
+
+ String defaultCharset = UTF_8.toString();
+ Part charSetPart = getPart.apply("_charset_");
+ if (charSetPart != null)
+ {
+ defaultCharset = IO.toString(charSetPart.getInputStream());
+ }
+
+ // Evaluate expected Contents
+ for (NameValue expected : partContainsContents)
+ {
+ Part part = getPart.apply(expected.name);
+ assertThat("Part[" + expected.name + "]", part, is(notNullValue()));
+ try (InputStream partInputStream = part.getInputStream())
+ {
+ String charset = getCharsetFromContentType(part.getContentType(), defaultCharset);
+ String contents = IO.toString(partInputStream, charset);
+ assertThat("Part[" + expected.name + "].contents", contents, containsString(expected.value));
+ }
+ }
+
+ // Evaluate expected filenames
+ for (NameValue expected : partFilenames)
+ {
+ Part part = getPart.apply(expected.name);
+ assertThat("Part[" + expected.name + "]", part, is(notNullValue()));
+ assertThat("Part[" + expected.name + "]", part.getSubmittedFileName(), is(expected.value));
+ }
+
+ // Evaluate expected contents checksums
+ for (NameValue expected : partSha1sums)
+ {
+ Part part = getPart.apply(expected.name);
+ assertThat("Part[" + expected.name + "]", part, is(notNullValue()));
+ MessageDigest digest = MessageDigest.getInstance("SHA1");
+ try (InputStream partInputStream = part.getInputStream();
+ NoOpOutputStream noop = new NoOpOutputStream();
+ DigestOutputStream digester = new DigestOutputStream(noop, digest))
+ {
+ IO.copy(partInputStream, digester);
+ String actualSha1sum = Hex.asHex(digest.digest()).toLowerCase(Locale.US);
+ assertThat("Part[" + expected.name + "].sha1sum", actualSha1sum, Matchers.equalToIgnoringCase(expected.value));
+ }
+ }
+ }
+
+ private String getCharsetFromContentType(String contentType, String defaultCharset)
+ {
+ if (StringUtil.isBlank(contentType))
+ {
+ return defaultCharset;
+ }
+
+ QuotedStringTokenizer tok = new QuotedStringTokenizer(contentType, ";", false, false);
+ while (tok.hasMoreTokens())
+ {
+ String str = tok.nextToken().trim();
+ if (str.startsWith("charset="))
+ {
+ return str.substring("charset=".length());
+ }
+ }
+
+ return defaultCharset;
+ }
+ }
+
+ static class NoOpOutputStream extends OutputStream
+ {
+ @Override
+ public void write(byte[] b) throws IOException
+ {
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException
+ {
+ }
+
+ @Override
+ public void flush() throws IOException
+ {
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ }
+
+ @Override
+ public void write(int b) throws IOException
+ {
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormInputStreamTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormInputStreamTest.java
new file mode 100644
index 0000000..f34acea
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormInputStreamTest.java
@@ -0,0 +1,1079 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.servlet.MultipartConfigElement;
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.Part;
+
+import org.eclipse.jetty.http.MultiPartFormInputStream.MultiPart;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.IO;
+import org.junit.jupiter.api.Test;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class MultiPartFormInputStreamTest
+{
+ private static final AtomicInteger testCounter = new AtomicInteger();
+ private static final String FILENAME = "stuff.txt";
+ protected String _contentType = "multipart/form-data, boundary=AaB03x";
+ protected String _multi = createMultipartRequestString(FILENAME);
+ protected File _tmpDir = MavenTestingUtils.getTargetTestingDir(String.valueOf(testCounter.incrementAndGet()));
+ protected String _dirname = _tmpDir.getAbsolutePath();
+
+ public MultiPartFormInputStreamTest()
+ {
+ _tmpDir.deleteOnExit();
+ }
+
+ @Test
+ public void testBadMultiPartRequest()
+ {
+ String boundary = "X0Y0";
+ String str = "--" + boundary + "\r\n" +
+ "Content-Disposition: form-data; name=\"fileup\"; filename=\"test.upload\"\r\n" +
+ "Content-Type: application/octet-stream\r\n\r\n" +
+ "How now brown cow." +
+ "\r\n--" + boundary + "-\r\n" +
+ "Content-Disposition: form-data; name=\"fileup\"; filename=\"test.upload\"\r\n" +
+ "\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(str.getBytes()),
+ "multipart/form-data, boundary=" + boundary,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+
+ IOException x = assertThrows(IOException.class,
+ mpis::getParts,
+ "Incomplete Multipart");
+ assertThat(x.getMessage(), startsWith("Incomplete"));
+ }
+
+ @Test
+ public void testFinalBoundaryOnly() throws Exception
+ {
+ String delimiter = "\r\n";
+ final String boundary = "MockMultiPartTestBoundary";
+
+ // Malformed multipart request body containing only an arbitrary string of text, followed by the final boundary marker, delimited by empty lines.
+ String str =
+ delimiter +
+ "Hello world" +
+ delimiter + // Two delimiter markers, which make an empty line.
+ delimiter +
+ "--" + boundary + "--" + delimiter;
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(str.getBytes()),
+ "multipart/form-data, boundary=" + boundary,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ assertTrue(mpis.getParts().isEmpty());
+ }
+
+ @Test
+ public void testEmpty() throws Exception
+ {
+ String delimiter = "\r\n";
+ final String boundary = "MockMultiPartTestBoundary";
+
+ String str =
+ delimiter +
+ "--" + boundary + "--" + delimiter;
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(str.getBytes()),
+ "multipart/form-data, boundary=" + boundary,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ assertTrue(mpis.getParts().isEmpty());
+ }
+
+ @Test
+ public void testNoBoundaryRequest() throws Exception
+ {
+ String str = "--\r\n" +
+ "Content-Disposition: form-data; name=\"fileName\"\r\n" +
+ "Content-Type: text/plain; charset=US-ASCII\r\n" +
+ "Content-Transfer-Encoding: 8bit\r\n" +
+ "\r\n" +
+ "abc\r\n" +
+ "--\r\n" +
+ "Content-Disposition: form-data; name=\"desc\"\r\n" +
+ "Content-Type: text/plain; charset=US-ASCII\r\n" +
+ "Content-Transfer-Encoding: 8bit\r\n" +
+ "\r\n" +
+ "123\r\n" +
+ "--\r\n" +
+ "Content-Disposition: form-data; name=\"title\"\r\n" +
+ "Content-Type: text/plain; charset=US-ASCII\r\n" +
+ "Content-Transfer-Encoding: 8bit\r\n" +
+ "\r\n" +
+ "ttt\r\n" +
+ "--\r\n" +
+ "Content-Disposition: form-data; name=\"datafile5239138112980980385.txt\"; filename=\"datafile5239138112980980385.txt\"\r\n" +
+ "Content-Type: application/octet-stream; charset=ISO-8859-1\r\n" +
+ "Content-Transfer-Encoding: binary\r\n" +
+ "\r\n" +
+ "000\r\n" +
+ "----\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(str.getBytes()),
+ "multipart/form-data",
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(4));
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ Part fileName = mpis.getPart("fileName");
+ assertThat(fileName, notNullValue());
+ assertThat(fileName.getSize(), is(3L));
+ IO.copy(fileName.getInputStream(), baos);
+ assertThat(baos.toString("US-ASCII"), is("abc"));
+
+ baos = new ByteArrayOutputStream();
+ Part desc = mpis.getPart("desc");
+ assertThat(desc, notNullValue());
+ assertThat(desc.getSize(), is(3L));
+ IO.copy(desc.getInputStream(), baos);
+ assertThat(baos.toString("US-ASCII"), is("123"));
+
+ baos = new ByteArrayOutputStream();
+ Part title = mpis.getPart("title");
+ assertThat(title, notNullValue());
+ assertThat(title.getSize(), is(3L));
+ IO.copy(title.getInputStream(), baos);
+ assertThat(baos.toString("US-ASCII"), is("ttt"));
+ }
+
+ @Test
+ public void testNonMultiPartRequest() throws Exception
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(_multi.getBytes()),
+ "Content-type: text/plain",
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ assertTrue(mpis.getParts().isEmpty());
+ }
+
+ @Test
+ public void testNoBody()
+ {
+ String body = "";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(body.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+
+ IOException x = assertThrows(IOException.class, mpis::getParts);
+ assertThat(x.getMessage(), containsString("Missing initial multi part boundary"));
+ }
+
+ @Test
+ public void testBodyAlreadyConsumed() throws Exception
+ {
+ ServletInputStream is = new ServletInputStream()
+ {
+
+ @Override
+ public boolean isFinished()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean isReady()
+ {
+ return false;
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener)
+ {
+ }
+
+ @Override
+ public int read()
+ {
+ return 0;
+ }
+ };
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(is,
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertEquals(0, parts.size());
+ }
+
+ @Test
+ public void testWhitespaceBodyWithCRLF()
+ {
+ String whitespace = " \n\n\n\r\n\r\n\r\n\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(whitespace.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ IOException x = assertThrows(IOException.class, mpis::getParts);
+ assertThat(x.getMessage(), containsString("Missing initial multi part boundary"));
+ }
+
+ @Test
+ public void testWhitespaceBody()
+ {
+ String whitespace = " ";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(whitespace.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ IOException x = assertThrows(IOException.class, mpis::getParts);
+ assertThat(x.getMessage(), containsString("Missing initial"));
+ }
+
+ @Test
+ public void testLeadingWhitespaceBodyWithCRLF() throws Exception
+ {
+ String body = " \n\n\n\r\n\r\n\r\n\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"field1\"\r\n" +
+ "\r\n" +
+ "Joe Blow\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"" + "foo.txt" + "\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + "aaaa" +
+ "bbbbb" + "\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(body.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts, notNullValue());
+ assertThat(parts.size(), is(2));
+
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
+ {
+ Part field1 = mpis.getPart("field1");
+ assertThat(field1, notNullValue());
+ IO.copy(field1.getInputStream(), baos);
+ assertThat(baos.toString("US-ASCII"), is("Joe Blow"));
+ }
+
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
+ {
+ Part stuff = mpis.getPart("stuff");
+ assertThat(stuff, notNullValue());
+ IO.copy(stuff.getInputStream(), baos);
+ assertThat(baos.toString("US-ASCII"), containsString("aaaa"));
+ }
+ }
+
+ @Test
+ public void testLeadingWhitespaceBodyWithoutCRLF() throws Exception
+ {
+ String body = " " +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"field1\"\r\n" +
+ "\r\n" +
+ "Joe Blow\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"" + "foo.txt" + "\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + "aaaa" +
+ "bbbbb" + "\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(body.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts, notNullValue());
+ assertThat(parts.size(), is(1));
+
+ Part stuff = mpis.getPart("stuff");
+ assertThat(stuff, notNullValue());
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ IO.copy(stuff.getInputStream(), baos);
+ assertThat(baos.toString("US-ASCII"), containsString("bbbbb"));
+ }
+
+ @Test
+ public void testNoLimits() throws Exception
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(_multi.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertFalse(parts.isEmpty());
+ }
+
+ @Test
+ public void testRequestTooBig()
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 60, 100, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(_multi.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+
+ IllegalStateException x = assertThrows(IllegalStateException.class, mpis::getParts);
+ assertThat(x.getMessage(), containsString("Request exceeds maxRequestSize"));
+ }
+
+ @Test
+ public void testRequestTooBigThrowsErrorOnGetParts()
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 60, 100, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(_multi.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+
+ //cause parsing
+ IllegalStateException x = assertThrows(IllegalStateException.class, mpis::getParts);
+ assertThat(x.getMessage(), containsString("Request exceeds maxRequestSize"));
+
+ //try again
+ x = assertThrows(IllegalStateException.class, mpis::getParts);
+ assertThat(x.getMessage(), containsString("Request exceeds maxRequestSize"));
+ }
+
+ @Test
+ public void testFileTooBig()
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 40, 1024, 30);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(_multi.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ IllegalStateException x = assertThrows(IllegalStateException.class,
+ mpis::getParts,
+ "stuff.txt should have been larger than maxFileSize");
+ assertThat(x.getMessage(), startsWith("Multipart Mime part"));
+ }
+
+ @Test
+ public void testFileTooBigThrowsErrorOnGetParts()
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 40, 1024, 30);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(_multi.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ // Caused parsing
+ IllegalStateException x = assertThrows(IllegalStateException.class,
+ mpis::getParts,
+ "stuff.txt should have been larger than maxFileSize");
+ assertThat(x.getMessage(), startsWith("Multipart Mime part"));
+
+ //test again after the parsing
+ x = assertThrows(IllegalStateException.class,
+ mpis::getParts,
+ "stuff.txt should have been larger than maxFileSize");
+ assertThat(x.getMessage(), startsWith("Multipart Mime part"));
+ }
+
+ @Test
+ public void testPartFileNotDeleted() throws Exception
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(createMultipartRequestString("tptfd").getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ mpis.getParts();
+
+ MultiPart part = (MultiPart)mpis.getPart("stuff");
+ File stuff = part.getFile();
+ assertThat(stuff, notNullValue()); // longer than 100 bytes, should already be a tmp file
+ part.write("tptfd.txt");
+ File tptfd = new File(_dirname + File.separator + "tptfd.txt");
+ assertThat(tptfd.exists(), is(true));
+ assertThat(stuff.exists(), is(false)); //got renamed
+ part.cleanUp();
+ assertThat(tptfd.exists(), is(true)); //explicitly written file did not get removed after cleanup
+ tptfd.deleteOnExit(); //clean up test
+ }
+
+ @Test
+ public void testPartTmpFileDeletion() throws Exception
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(createMultipartRequestString("tptfd").getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ mpis.getParts();
+
+ MultiPart part = (MultiPart)mpis.getPart("stuff");
+ File stuff = part.getFile();
+ assertThat(stuff, notNullValue()); // longer than 100 bytes, should already be a tmp file
+ assertThat(stuff.exists(), is(true));
+ part.cleanUp();
+ assertThat(stuff.exists(), is(false)); //tmp file was removed after cleanup
+ }
+
+ @Test
+ public void testDeleteNPE()
+ {
+ final InputStream input = new ByteArrayInputStream(createMultipartRequestString("myFile").getBytes());
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 1024, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(input, _contentType, config, _tmpDir);
+
+ mpis.deleteParts(); // this should not be an NPE
+ }
+
+ @Test
+ public void testLFOnlyRequest() throws Exception
+ {
+ String str = "--AaB03x\n" +
+ "content-disposition: form-data; name=\"field1\"\n" +
+ "\n" +
+ "Joe Blow" +
+ "\r\n--AaB03x\n" +
+ "content-disposition: form-data; name=\"field2\"\n" +
+ "\n" +
+ "Other" +
+ "\r\n--AaB03x--\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(str.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(2));
+ Part p1 = mpis.getPart("field1");
+ assertThat(p1, notNullValue());
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ IO.copy(p1.getInputStream(), baos);
+ assertThat(baos.toString("UTF-8"), is("Joe Blow"));
+
+ Part p2 = mpis.getPart("field2");
+ assertThat(p2, notNullValue());
+ baos = new ByteArrayOutputStream();
+ IO.copy(p2.getInputStream(), baos);
+ assertThat(baos.toString("UTF-8"), is("Other"));
+ }
+
+ @Test
+ public void testCROnlyRequest()
+ {
+ String str = "--AaB03x\r" +
+ "content-disposition: form-data; name=\"field1\"\r" +
+ "\r" +
+ "Joe Blow\r" +
+ "--AaB03x\r" +
+ "content-disposition: form-data; name=\"field2\"\r" +
+ "\r" +
+ "Other\r" +
+ "--AaB03x--\r";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(str.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+
+ IllegalStateException x = assertThrows(IllegalStateException.class,
+ mpis::getParts,
+ "Improper EOL");
+ assertThat(x.getMessage(), containsString("Bad EOL"));
+ }
+
+ @Test
+ public void testCRandLFMixRequest()
+ {
+ String str = "--AaB03x\r" +
+ "content-disposition: form-data; name=\"field1\"\r" +
+ "\r" +
+ "\nJoe Blow\n" +
+ "\r" +
+ "--AaB03x\r" +
+ "content-disposition: form-data; name=\"field2\"\r" +
+ "\r" +
+ "Other\r" +
+ "--AaB03x--\r";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(str.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+
+ IllegalStateException x = assertThrows(IllegalStateException.class,
+ mpis::getParts,
+ "Improper EOL");
+ assertThat(x.getMessage(), containsString("Bad EOL"));
+ }
+
+ @Test
+ public void testBufferOverflowNoCRLF() throws Exception
+ {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ baos.write("--AaB03x\r\n".getBytes());
+ for (int i = 0; i < 3000; i++) //create content that will overrun default buffer size of BufferedInputStream
+ {
+ baos.write('a');
+ }
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(baos.toByteArray()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ IllegalStateException x = assertThrows(IllegalStateException.class,
+ mpis::getParts,
+ "Header Line Exceeded Max Length");
+ assertThat(x.getMessage(), containsString("Header Line Exceeded Max Length"));
+ }
+
+ @Test
+ public void testCharsetEncoding() throws Exception
+ {
+ String contentType = "multipart/form-data; boundary=TheBoundary; charset=ISO-8859-1";
+ String str = "--TheBoundary\r\n" +
+ "content-disposition: form-data; name=\"field1\"\r\n" +
+ "\r\n" +
+ "\nJoe Blow\n" +
+ "\r\n" +
+ "--TheBoundary--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(str.getBytes()),
+ contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(1));
+ }
+
+ @Test
+ public void testBadlyEncodedFilename() throws Exception
+ {
+
+ String contents = "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"" + "Taken on Aug 22 \\ 2012.jpg" + "\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + "stuff" +
+ "aaa" + "\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(contents.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(1));
+ assertThat(parts.iterator().next().getSubmittedFileName(), is("Taken on Aug 22 \\ 2012.jpg"));
+ }
+
+ @Test
+ public void testBadlyEncodedMSFilename() throws Exception
+ {
+
+ String contents = "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"" + "c:\\this\\really\\is\\some\\path\\to\\a\\file.txt" + "\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + "stuff" +
+ "aaa" + "\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(contents.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(1));
+ assertThat(parts.iterator().next().getSubmittedFileName(), is("c:\\this\\really\\is\\some\\path\\to\\a\\file.txt"));
+ }
+
+ @Test
+ public void testCorrectlyEncodedMSFilename() throws Exception
+ {
+ String contents = "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"" + "c:\\\\this\\\\really\\\\is\\\\some\\\\path\\\\to\\\\a\\\\file.txt" + "\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + "stuff" +
+ "aaa" + "\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(contents.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(1));
+ assertThat(parts.iterator().next().getSubmittedFileName(), is("c:\\this\\really\\is\\some\\path\\to\\a\\file.txt"));
+ }
+
+ @Test
+ public void testMultiWithSpaceInFilename() throws Exception
+ {
+ testMulti("stuff with spaces.txt");
+ }
+
+ @Test
+ public void testWriteFilesIfContentDispositionFilename() throws Exception
+ {
+ String s = "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"field1\"; filename=\"frooble.txt\"\r\n" +
+ "\r\n" +
+ "Joe Blow\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + "sss" +
+ "aaa" + "\r\n" +
+ "--AaB03x--\r\n";
+ //all default values for multipartconfig, ie file size threshold 0
+ MultipartConfigElement config = new MultipartConfigElement(_dirname);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(s.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ mpis.setWriteFilesWithFilenames(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(2));
+ Part field1 = mpis.getPart("field1"); //has a filename, should be written to a file
+ File f = ((MultiPartFormInputStream.MultiPart)field1).getFile();
+ assertThat(f, notNullValue()); // longer than 100 bytes, should already be a tmp file
+
+ Part stuff = mpis.getPart("stuff");
+ f = ((MultiPartFormInputStream.MultiPart)stuff).getFile(); //should only be in memory, no filename
+ assertThat(f, nullValue());
+ }
+
+ private void testMulti(String filename) throws IOException
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(createMultipartRequestString(filename).getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(2));
+ Part field1 = mpis.getPart("field1"); //field 1 too small to go into tmp file, should be in internal buffer
+ assertThat(field1, notNullValue());
+ assertThat(field1.getName(), is("field1"));
+
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ try (InputStream is = field1.getInputStream())
+ {
+ IO.copy(is, os);
+ }
+ assertEquals("Joe Blow", new String(os.toByteArray()));
+ assertEquals(8, field1.getSize());
+
+ assertNotNull(((MultiPartFormInputStream.MultiPart)field1).getBytes()); //in internal buffer
+ field1.write("field1.txt");
+ assertNull(((MultiPartFormInputStream.MultiPart)field1).getBytes()); //no longer in internal buffer
+ File f = new File(_dirname + File.separator + "field1.txt");
+ assertTrue(f.exists());
+ field1.write("another_field1.txt"); //write after having already written
+ File f2 = new File(_dirname + File.separator + "another_field1.txt");
+ assertTrue(f2.exists());
+ assertFalse(f.exists()); //should have been renamed
+ field1.delete(); //file should be deleted
+ assertFalse(f.exists()); //original file was renamed
+ assertFalse(f2.exists()); //2nd written file was explicitly deleted
+
+ MultiPart stuff = (MultiPart)mpis.getPart("stuff");
+ assertThat(stuff.getSubmittedFileName(), is(filename));
+ assertThat(stuff.getContentType(), is("text/plain"));
+ assertThat(stuff.getHeader("Content-Type"), is("text/plain"));
+ assertThat(stuff.getHeaders("content-type").size(), is(1));
+ assertNotNull(stuff.getHeaders("non existing part"));
+ assertThat(stuff.getHeaders("non existing part").size(), is(0));
+ assertThat(stuff.getHeader("content-disposition"), is("form-data; name=\"stuff\"; filename=\"" + filename + "\""));
+ assertThat(stuff.getHeaderNames().size(), is(2));
+ assertThat(stuff.getSize(), is(51L));
+
+ File tmpfile = stuff.getFile();
+ assertThat(tmpfile, notNullValue()); // longer than 50 bytes, should already be a tmp file
+ assertThat(stuff.getBytes(), nullValue()); //not in an internal buffer
+ assertThat(tmpfile.exists(), is(true));
+ assertThat(tmpfile.getName(), is(not("stuff with space.txt")));
+ stuff.write(filename);
+ f = new File(_dirname + File.separator + filename);
+ assertThat(f.exists(), is(true));
+ assertThat(tmpfile.exists(), is(false));
+ try
+ {
+ stuff.getInputStream();
+ }
+ catch (Exception e)
+ {
+ fail("Part.getInputStream() after file rename operation", e);
+ }
+ f.deleteOnExit(); //clean up after test
+ }
+
+ @Test
+ public void testMultiSameNames() throws Exception
+ {
+ String sameNames = "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"stuff1.txt\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "00000\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"stuff2.txt\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "110000000000000000000000000000000000000000000000000\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(sameNames.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertEquals(2, parts.size());
+ for (Part p : parts)
+ {
+ assertEquals("stuff", p.getName());
+ }
+
+ //if they all have the name name, then only retrieve the first one
+ Part p = mpis.getPart("stuff");
+ assertNotNull(p);
+ assertEquals(5, p.getSize());
+ }
+
+ @Test
+ public void testBase64EncodedContent() throws Exception
+ {
+ String contentWithEncodedPart =
+ "--AaB03x\r\n" +
+ "Content-disposition: form-data; name=\"other\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "other" + "\r\n" +
+ "--AaB03x\r\n" +
+ "Content-disposition: form-data; name=\"stuff\"; filename=\"stuff.txt\"\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "Content-Type: application/octet-stream\r\n" +
+ "\r\n" +
+ Base64.getEncoder().encodeToString("hello jetty".getBytes(ISO_8859_1)) + "\r\n" +
+ "--AaB03x\r\n" +
+ "Content-disposition: form-data; name=\"final\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "the end" + "\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(contentWithEncodedPart.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertEquals(3, parts.size());
+
+ Part p1 = mpis.getPart("other");
+ assertNotNull(p1);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ IO.copy(p1.getInputStream(), baos);
+ assertEquals("other", baos.toString("US-ASCII"));
+
+ Part p2 = mpis.getPart("stuff");
+ assertNotNull(p2);
+ baos = new ByteArrayOutputStream();
+ IO.copy(p2.getInputStream(), baos);
+ assertEquals(Base64.getEncoder().encodeToString("hello jetty".getBytes(ISO_8859_1)), baos.toString("US-ASCII"));
+
+ Part p3 = mpis.getPart("final");
+ assertNotNull(p3);
+ baos = new ByteArrayOutputStream();
+ IO.copy(p3.getInputStream(), baos);
+ assertEquals("the end", baos.toString("US-ASCII"));
+ }
+
+ @Test
+ public void testFragmentation() throws IOException
+ {
+ String contentType = "multipart/form-data, boundary=----WebKitFormBoundaryhXfFAMfUnUKhmqT8";
+ String payload1 =
+ "------WebKitFormBoundaryhXfFAMfUnUKhmqT8\r\n" +
+ "Content-Disposition: form-data; name=\"field1\"\r\n\r\n" +
+ "value1" +
+ "\r\n--";
+ String payload2 = "----WebKitFormBoundaryhXfFAMfUnUKhmqT8\r\n" +
+ "Content-Disposition: form-data; name=\"field2\"\r\n\r\n" +
+ "value2" +
+ "\r\n------WebKitFormBoundaryhXfFAMfUnUKhmqT8--\r\n";
+
+ // Split the content into separate reads, with the content broken up on the boundary string.
+ AppendableInputStream stream = new AppendableInputStream();
+ stream.append(payload1);
+ stream.append("");
+ stream.append(payload2);
+ stream.endOfContent();
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(stream, contentType, config, _tmpDir);
+ mpis.setDeleteOnExit(true);
+
+ // Check size.
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(2));
+
+ // Check part content.
+ assertThat(IO.toString(mpis.getPart("field1").getInputStream()), is("value1"));
+ assertThat(IO.toString(mpis.getPart("field2").getInputStream()), is("value2"));
+ }
+
+ static class AppendableInputStream extends InputStream
+ {
+ private static final ByteBuffer EOF = ByteBuffer.allocate(0);
+ private final BlockingArrayQueue<ByteBuffer> buffers = new BlockingArrayQueue<>();
+ private ByteBuffer current;
+
+ public void append(String data)
+ {
+ append(data.getBytes(StandardCharsets.US_ASCII));
+ }
+
+ public void append(byte[] data)
+ {
+ buffers.add(BufferUtil.toBuffer(data));
+ }
+
+ public void endOfContent()
+ {
+ buffers.add(EOF);
+ }
+
+ @Override
+ public int read() throws IOException
+ {
+ byte[] buf = new byte[1];
+ while (true)
+ {
+ int len = read(buf, 0, 1);
+ if (len < 0)
+ return -1;
+ if (len > 0)
+ return buf[0];
+ }
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException
+ {
+ if (current == null)
+ current = buffers.poll();
+ if (current == EOF)
+ return -1;
+ if (BufferUtil.isEmpty(current))
+ {
+ current = null;
+ return 0;
+ }
+
+ ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
+ buffer.flip();
+ int read = BufferUtil.append(buffer, current);
+ if (BufferUtil.isEmpty(current))
+ current = buffers.poll();
+ return read;
+ }
+ }
+
+ @Test
+ public void testQuotedPrintableEncoding() throws Exception
+ {
+ String contentWithEncodedPart =
+ "--AaB03x\r\n" +
+ "Content-disposition: form-data; name=\"other\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "other" + "\r\n" +
+ "--AaB03x\r\n" +
+ "Content-disposition: form-data; name=\"stuff\"; filename=\"stuff.txt\"\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "truth=3Dbeauty" + "\r\n" +
+ "--AaB03x--\r\n";
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(contentWithEncodedPart.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertEquals(2, parts.size());
+
+ Part p1 = mpis.getPart("other");
+ assertNotNull(p1);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ IO.copy(p1.getInputStream(), baos);
+ assertEquals("other", baos.toString("US-ASCII"));
+
+ Part p2 = mpis.getPart("stuff");
+ assertNotNull(p2);
+ baos = new ByteArrayOutputStream();
+ IO.copy(p2.getInputStream(), baos);
+ assertEquals("truth=3Dbeauty", baos.toString("US-ASCII"));
+ }
+
+ @Test
+ public void testGeneratedForm() throws Exception
+ {
+ String contentType = "multipart/form-data, boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW";
+ String body = "Content-Type: multipart/form-data; boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
+ "\r\n" +
+ "--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
+ "Content-Disposition: form-data; name=\"part1\"\r\n" +
+ "\n" +
+ "wNfミxVamt\r\n" +
+ "--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\n" +
+ "Content-Disposition: form-data; name=\"part2\"\r\n" +
+ "\r\n" +
+ "&ᄈ취ᅢO\r\n" +
+ "--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW--";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(body.getBytes()),
+ contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts, notNullValue());
+ assertThat(parts.size(), is(2));
+
+ Part part1 = mpis.getPart("part1");
+ assertThat(part1, notNullValue());
+ Part part2 = mpis.getPart("part2");
+ assertThat(part2, notNullValue());
+ }
+
+ private static String createMultipartRequestString(String filename)
+ {
+ int length = filename.length();
+ String name = filename;
+ if (length > 10)
+ name = filename.substring(0, 10);
+ StringBuilder filler = new StringBuilder();
+ int i = name.length();
+ while (i < 51)
+ {
+ filler.append("0");
+ i++;
+ }
+
+ return "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"field1\"; filename=\"frooble.txt\"\r\n" +
+ "\r\n" +
+ "Joe Blow\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"" + filename + "\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + name +
+ filler.toString() + "\r\n" +
+ "--AaB03x--\r\n";
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartParserTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartParserTest.java
new file mode 100644
index 0000000..52e9986
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartParserTest.java
@@ -0,0 +1,736 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.eclipse.jetty.http.MultiPartParser.State;
+import org.eclipse.jetty.util.BufferUtil;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+public class MultiPartParserTest
+{
+
+ @Test
+ public void testEmptyPreamble()
+ {
+ MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler()
+ {
+ }, "BOUNDARY");
+ ByteBuffer data = BufferUtil.toBuffer("");
+
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.PREAMBLE));
+ }
+
+ @Test
+ public void testNoPreamble()
+ {
+ MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler()
+ {
+ }, "BOUNDARY");
+ ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY \r\n");
+
+ parser.parse(data, false);
+ assertTrue(parser.isState(State.BODY_PART));
+ assertThat(data.remaining(), is(0));
+ }
+
+ @Test
+ public void testPreamble()
+ {
+ MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler()
+ {
+ }, "BOUNDARY");
+ ByteBuffer data;
+
+ data = BufferUtil.toBuffer("This is not part of a part\r\n");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.PREAMBLE));
+ assertThat(data.remaining(), is(0));
+
+ data = BufferUtil.toBuffer("More data that almost includes \n--BOUNDARY but no CR before.");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.PREAMBLE));
+ assertThat(data.remaining(), is(0));
+
+ data = BufferUtil.toBuffer("Could be a boundary \r\n--BOUNDAR");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.PREAMBLE));
+ assertThat(data.remaining(), is(0));
+
+ data = BufferUtil.toBuffer("but not it isn't \r\n--BOUN");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.PREAMBLE));
+ assertThat(data.remaining(), is(0));
+
+ data = BufferUtil.toBuffer("DARX nor is this");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.PREAMBLE));
+ assertThat(data.remaining(), is(0));
+ }
+
+ @Test
+ public void testPreambleCompleteBoundary()
+ {
+ MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler()
+ {
+ }, "BOUNDARY");
+ ByteBuffer data = BufferUtil.toBuffer("This is not part of a part\r\n--BOUNDARY \r\n");
+
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.BODY_PART));
+ assertThat(data.remaining(), is(0));
+ }
+
+ @Test
+ public void testPreambleSplitBoundary()
+ {
+ MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler()
+ {
+ }, "BOUNDARY");
+ ByteBuffer data = BufferUtil.toBuffer("This is not part of a part\r\n");
+
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.PREAMBLE));
+ assertThat(data.remaining(), is(0));
+ data = BufferUtil.toBuffer("-");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.PREAMBLE));
+ assertThat(data.remaining(), is(0));
+ data = BufferUtil.toBuffer("-");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.PREAMBLE));
+ assertThat(data.remaining(), is(0));
+ data = BufferUtil.toBuffer("B");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.PREAMBLE));
+ assertThat(data.remaining(), is(0));
+ data = BufferUtil.toBuffer("OUNDARY-");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.DELIMITER_CLOSE));
+ assertThat(data.remaining(), is(0));
+ data = BufferUtil.toBuffer("ignore\r");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.DELIMITER_PADDING));
+ assertThat(data.remaining(), is(0));
+ data = BufferUtil.toBuffer("\n");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.BODY_PART));
+ assertThat(data.remaining(), is(0));
+ }
+
+ @Test
+ public void testFirstPartNoFields()
+ {
+ MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler()
+ {
+ }, "BOUNDARY");
+ ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\r\n\r\n");
+
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.FIRST_OCTETS));
+ assertThat(data.remaining(), is(0));
+ }
+
+ @Test
+ public void testFirstPartFields()
+ {
+ TestHandler handler = new TestHandler()
+ {
+ @Override
+ public boolean headerComplete()
+ {
+ super.headerComplete();
+ return true;
+ }
+ };
+ MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
+
+ ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\r\n" +
+ "name0: value0\r\n" +
+ "name1 :value1 \r\n" +
+ "name2:value\r\n" +
+ " 2\r\n" +
+ "\r\n" +
+ "Content");
+
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.FIRST_OCTETS));
+ assertThat(data.remaining(), is(7));
+ assertThat(handler.fields, Matchers.contains("name0: value0", "name1: value1", "name2: value 2", "<<COMPLETE>>"));
+ }
+
+ @Test
+ public void testFirstPartNoContent()
+ {
+ TestHandler handler = new TestHandler();
+ MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
+
+ ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\r\n" +
+ "name: value\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "--BOUNDARY");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.DELIMITER));
+ assertThat(data.remaining(), is(0));
+ assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>"));
+ assertThat(handler.content, Matchers.contains("<<LAST>>"));
+ }
+
+ @Test
+ public void testFirstPartNoContentNoCRLF()
+ {
+ TestHandler handler = new TestHandler();
+ MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
+
+ ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\r\n" +
+ "name: value\r\n" +
+ "\r\n" +
+ "--BOUNDARY");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.DELIMITER));
+ assertThat(data.remaining(), is(0));
+ assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>"));
+ assertThat(handler.content, Matchers.contains("<<LAST>>"));
+ }
+
+ @Test
+ public void testFirstPartContentLookingLikeNoCRLF()
+ {
+ TestHandler handler = new TestHandler();
+ MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
+
+ ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\r\n" +
+ "name: value\r\n" +
+ "\r\n" +
+ "-");
+ parser.parse(data, false);
+ data = BufferUtil.toBuffer("Content!");
+ parser.parse(data, false);
+
+ assertThat(parser.getState(), is(State.OCTETS));
+ assertThat(data.remaining(), is(0));
+ assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>"));
+ assertThat(handler.content, Matchers.contains("-", "Content!"));
+ }
+
+ @Test
+ public void testFirstPartPartialContent()
+ {
+ TestHandler handler = new TestHandler();
+ MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
+
+ ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\r\n" +
+ "name: value\n" +
+ "\r\n" +
+ "Hello\r\n");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.OCTETS));
+ assertThat(data.remaining(), is(0));
+ assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>"));
+ assertThat(handler.content, Matchers.contains("Hello"));
+
+ data = BufferUtil.toBuffer(
+ "Now is the time for all good ment to come to the aid of the party.\r\n" +
+ "How now brown cow.\r\n" +
+ "The quick brown fox jumped over the lazy dog.\r\n" +
+ "this is not a --BOUNDARY\r\n");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.OCTETS));
+ assertThat(data.remaining(), is(0));
+ assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>"));
+ assertThat(handler.content, Matchers.contains("Hello", "\r\n", "Now is the time for all good ment to come to the aid of the party.\r\n" +
+ "How now brown cow.\r\n" +
+ "The quick brown fox jumped over the lazy dog.\r\n" +
+ "this is not a --BOUNDARY"));
+ }
+
+ @Test
+ public void testFirstPartShortContent()
+ {
+ TestHandler handler = new TestHandler();
+ MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
+
+ ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\r\n" +
+ "name: value\n" +
+ "\r\n" +
+ "Hello\r\n" +
+ "--BOUNDARY");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.DELIMITER));
+ assertThat(data.remaining(), is(0));
+ assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>"));
+ assertThat(handler.content, Matchers.contains("Hello", "<<LAST>>"));
+ }
+
+ @Test
+ public void testFirstPartLongContent()
+ {
+ TestHandler handler = new TestHandler();
+ MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
+
+ ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\r\n" +
+ "name: value\n" +
+ "\r\n" +
+ "Now is the time for all good ment to come to the aid of the party.\r\n" +
+ "How now brown cow.\r\n" +
+ "The quick brown fox jumped over the lazy dog.\r\n" +
+ "\r\n" +
+ "--BOUNDARY");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.DELIMITER));
+ assertThat(data.remaining(), is(0));
+ assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>"));
+ assertThat(handler.content, Matchers.contains("Now is the time for all good ment to come to the aid of the party.\r\n" +
+ "How now brown cow.\r\n" +
+ "The quick brown fox jumped over the lazy dog.\r\n", "<<LAST>>"));
+ }
+
+ @Test
+ public void testFirstPartLongContentNoCarriageReturn()
+ {
+ TestHandler handler = new TestHandler();
+ MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
+
+ //boundary still requires carriage return
+ ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\n" +
+ "name: value\n" +
+ "\n" +
+ "Now is the time for all good men to come to the aid of the party.\n" +
+ "How now brown cow.\n" +
+ "The quick brown fox jumped over the lazy dog.\n" +
+ "\r\n" +
+ "--BOUNDARY");
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.DELIMITER));
+ assertThat(data.remaining(), is(0));
+ assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>"));
+ assertThat(handler.content, Matchers.contains("Now is the time for all good men to come to the aid of the party.\n" +
+ "How now brown cow.\n" +
+ "The quick brown fox jumped over the lazy dog.\n", "<<LAST>>"));
+ }
+
+ @Test
+ public void testBinaryPart()
+ {
+ byte[] random = new byte[8192];
+ final ByteBuffer bytes = BufferUtil.allocate(random.length);
+ ThreadLocalRandom.current().nextBytes(random);
+ // Arrays.fill(random,(byte)'X');
+
+ TestHandler handler = new TestHandler()
+ {
+ @Override
+ public boolean content(ByteBuffer buffer, boolean last)
+ {
+ BufferUtil.append(bytes, buffer);
+ return last;
+ }
+ };
+ MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
+
+ String preamble = "Blah blah blah\r\n--BOUNDARY\r\n\r\n";
+ String epilogue = "\r\n--BOUNDARY\r\nBlah blah blah!\r\n";
+
+ ByteBuffer data = BufferUtil.allocate(preamble.length() + random.length + epilogue.length());
+ BufferUtil.append(data, BufferUtil.toBuffer(preamble));
+ BufferUtil.append(data, ByteBuffer.wrap(random));
+ BufferUtil.append(data, BufferUtil.toBuffer(epilogue));
+
+ parser.parse(data, true);
+ assertThat(parser.getState(), is(State.DELIMITER));
+ assertThat(data.remaining(), is(19));
+ assertThat(bytes.array(), is(random));
+ }
+
+ @Test
+ public void testEpilogue()
+ {
+ TestHandler handler = new TestHandler();
+ MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
+
+ ByteBuffer data = BufferUtil.toBuffer("" +
+ "--BOUNDARY\r\n" +
+ "name: value\n" +
+ "\r\n" +
+ "Hello\r\n" +
+ "--BOUNDARY--" +
+ "epilogue here:" +
+ "\r\n" +
+ "--BOUNDARY--" +
+ "\r\n" +
+ "--BOUNDARY");
+
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.DELIMITER));
+ assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>"));
+ assertThat(handler.content, Matchers.contains("Hello", "<<LAST>>"));
+
+ parser.parse(data, true);
+ assertThat(parser.getState(), is(State.END));
+ }
+
+ @Test
+ public void testMultipleContent()
+ {
+ TestHandler handler = new TestHandler();
+ MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
+
+ ByteBuffer data = BufferUtil.toBuffer("" +
+ "--BOUNDARY\r\n" +
+ "name: value\n" +
+ "\r\n" +
+ "Hello" +
+ "\r\n" +
+ "--BOUNDARY\r\n" +
+ "powerLevel: 9001\n" +
+ "\r\n" +
+ "secondary" +
+ "\r\n" +
+ "content" +
+ "\r\n--BOUNDARY--" +
+ "epilogue here");
+
+ /* Test First Content Section */
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.DELIMITER));
+ assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>"));
+ assertThat(handler.content, Matchers.contains("Hello", "<<LAST>>"));
+
+ /* Test Second Content Section */
+ parser.parse(data, false);
+ assertThat(parser.getState(), is(State.DELIMITER));
+ assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>", "powerLevel: 9001", "<<COMPLETE>>"));
+ assertThat(handler.content, Matchers.contains("Hello", "<<LAST>>", "secondary\r\ncontent", "<<LAST>>"));
+
+ /* Test Progression to END State */
+ parser.parse(data, true);
+ assertThat(parser.getState(), is(State.END));
+ assertThat(data.remaining(), is(0));
+ }
+
+ @Test
+ public void testCrAsLineTermination()
+ {
+ TestHandler handler = new TestHandler()
+ {
+ @Override
+ public boolean messageComplete()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean content(ByteBuffer buffer, boolean last)
+ {
+ super.content(buffer, last);
+ return false;
+ }
+ };
+ MultiPartParser parser = new MultiPartParser(handler, "AaB03x");
+
+ ByteBuffer data = BufferUtil.toBuffer(
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"field1\"\r\n" +
+ "\r" +
+ "Joe Blow\r\n" +
+ "--AaB03x--\r\n");
+
+ BadMessageException x = assertThrows(BadMessageException.class,
+ () -> parser.parse(data, true),
+ "Invalid EOL");
+ assertThat(x.getMessage(), containsString("Bad EOL"));
+ }
+
+ @Test // TODO: Parameterize
+ public void testBadHeaderNames() throws Exception
+ {
+ String[] bad = new String[]
+ {
+ "Foo\\Bar: value\r\n",
+ "Foo@Bar: value\r\n",
+ "Foo,Bar: value\r\n",
+ "Foo}Bar: value\r\n",
+ "Foo{Bar: value\r\n",
+ "Foo=Bar: value\r\n",
+ "Foo>Bar: value\r\n",
+ "Foo<Bar: value\r\n",
+ "Foo)Bar: value\r\n",
+ "Foo(Bar: value\r\n",
+ "Foo?Bar: value\r\n",
+ "Foo\"Bar: value\r\n",
+ "Foo/Bar: value\r\n",
+ "Foo]Bar: value\r\n",
+ "Foo[Bar: value\r\n",
+ "\u0192\u00f8\u00f8\u00df\u00e5\u00ae: value\r\n"
+ };
+
+ for (int i = 0; i < bad.length; i++)
+ {
+ ByteBuffer buffer = BufferUtil.toBuffer(
+ "--AaB03x\r\n" + bad[i] + "\r\n--AaB03x--\r\n");
+
+ MultiPartParser.Handler handler = new TestHandler();
+ MultiPartParser parser = new MultiPartParser(handler, "AaB03x");
+
+ try
+ {
+ parser.parse(buffer, true);
+ }
+ catch (BadMessageException e)
+ {
+ assertTrue(e.getMessage().contains("Illegal character"));
+ }
+ }
+ }
+
+ @Test
+ public void splitTest()
+ {
+ TestHandler handler = new TestHandler()
+ {
+ @Override
+ public boolean messageComplete()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean content(ByteBuffer buffer, boolean last)
+ {
+ super.content(buffer, last);
+ return false;
+ }
+ };
+
+ MultiPartParser parser = new MultiPartParser(handler, "---------------------------9051914041544843365972754266");
+ ByteBuffer data = BufferUtil.toBuffer(
+ "POST / HTTP/1.1\n" +
+ "Host: localhost:8000\n" +
+ "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:29.0) Gecko/20100101 Firefox/29.0\n" +
+ "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n" +
+ "Accept-Language: en-US,en;q=0.5\n" +
+ "Accept-Encoding: gzip, deflate\n" +
+ "Cookie: __atuvc=34%7C7; permanent=0; _gitlab_session=226ad8a0be43681acf38c2fab9497240; __profilin=p%3Dt; request_method=GET\n" +
+ "Connection: keep-alive\n" +
+ "Content-Type: multipart/form-data; boundary=---------------------------9051914041544843365972754266\n" +
+ "Content-Length: 554\n" +
+ "\r\n" +
+ "-----------------------------9051914041544843365972754266\n" +
+ "Content-Disposition: form-data; name=\"text\"\n" +
+ "\n" +
+ "text default\r\n" +
+ "-----------------------------9051914041544843365972754266\n" +
+ "Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\n" +
+ "Content-Type: text/plain\n" +
+ "\n" +
+ "Content of a.txt.\n" +
+ "\r\n" +
+ "-----------------------------9051914041544843365972754266\n" +
+ "Content-Disposition: form-data; name=\"file2\"; filename=\"a.html\"\n" +
+ "Content-Type: text/html\n" +
+ "\n" +
+ "<!DOCTYPE html><title>Content of a.html.</title>\n" +
+ "\r\n" +
+ "-----------------------------9051914041544843365972754266\n" +
+ "Field1: value1\n" +
+ "Field2: value2\n" +
+ "Field3: value3\n" +
+ "Field4: value4\n" +
+ "Field5: value5\n" +
+ "Field6: value6\n" +
+ "Field7: value7\n" +
+ "Field8: value8\n" +
+ "Field9: value\n" +
+ " 9\n" +
+ "\r\n" +
+ "-----------------------------9051914041544843365972754266\n" +
+ "Field1: value1\n" +
+ "\r\n" +
+ "But the amount of denudation which the strata have\n" +
+ "in many places suffered, independently of the rate\n" +
+ "of accumulation of the degraded matter, probably\n" +
+ "offers the best evidence of the lapse of time. I remember\n" +
+ "having been much struck with the evidence of\n" +
+ "denudation, when viewing volcanic islands, which\n" +
+ "have been worn by the waves and pared all round\n" +
+ "into perpendicular cliffs of one or two thousand feet\n" +
+ "in height; for the gentle slope of the lava-streams,\n" +
+ "due to their formerly liquid state, showed at a glance\n" +
+ "how far the hard, rocky beds had once extended into\n" +
+ "the open ocean.\n" +
+ "\r\n" +
+ "-----------------------------9051914041544843365972754266--" +
+ "===== ajlkfja;lkdj;lakjd;lkjf ==== epilogue here ==== kajflajdfl;kjafl;kjl;dkfja ====\n\r\n\r\r\r\n\n\n");
+
+ int length = data.remaining();
+ for (int i = 0; i < length - 1; i++)
+ {
+ //partition 0 to i
+ ByteBuffer dataSeg = data.slice();
+ dataSeg.position(0);
+ dataSeg.limit(i);
+ assertThat("First " + i, parser.parse(dataSeg, false), is(false));
+
+ //partition i
+ dataSeg = data.slice();
+ dataSeg.position(i);
+ dataSeg.limit(i + 1);
+ assertThat("Second " + i, parser.parse(dataSeg, false), is(false));
+
+ //partition i to length
+ dataSeg = data.slice();
+ dataSeg.position(i + 1);
+ dataSeg.limit(length);
+ assertThat("Third " + i, parser.parse(dataSeg, true), is(true));
+
+ assertThat(handler.fields, Matchers.contains("Content-Disposition: form-data; name=\"text\"", "<<COMPLETE>>",
+ "Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"",
+ "Content-Type: text/plain", "<<COMPLETE>>",
+ "Content-Disposition: form-data; name=\"file2\"; filename=\"a.html\"",
+ "Content-Type: text/html", "<<COMPLETE>>",
+ "Field1: value1", "Field2: value2", "Field3: value3",
+ "Field4: value4", "Field5: value5", "Field6: value6",
+ "Field7: value7", "Field8: value8", "Field9: value 9", "<<COMPLETE>>",
+ "Field1: value1", "<<COMPLETE>>"));
+
+ assertThat(handler.contentString(), is("text default" + "<<LAST>>" +
+ "Content of a.txt.\n" + "<<LAST>>" +
+ "<!DOCTYPE html><title>Content of a.html.</title>\n" + "<<LAST>>" +
+ "<<LAST>>" +
+ "But the amount of denudation which the strata have\n" +
+ "in many places suffered, independently of the rate\n" +
+ "of accumulation of the degraded matter, probably\n" +
+ "offers the best evidence of the lapse of time. I remember\n" +
+ "having been much struck with the evidence of\n" +
+ "denudation, when viewing volcanic islands, which\n" +
+ "have been worn by the waves and pared all round\n" +
+ "into perpendicular cliffs of one or two thousand feet\n" +
+ "in height; for the gentle slope of the lava-streams,\n" +
+ "due to their formerly liquid state, showed at a glance\n" +
+ "how far the hard, rocky beds had once extended into\n" +
+ "the open ocean.\n" + "<<LAST>>"));
+
+ handler.clear();
+ parser.reset();
+ }
+ }
+
+ @Test
+ public void testGeneratedForm()
+ {
+ TestHandler handler = new TestHandler()
+ {
+ @Override
+ public boolean messageComplete()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean content(ByteBuffer buffer, boolean last)
+ {
+ super.content(buffer, last);
+ return false;
+ }
+
+ @Override
+ public boolean headerComplete()
+ {
+ return false;
+ }
+ };
+
+ MultiPartParser parser = new MultiPartParser(handler, "WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW");
+ ByteBuffer data = BufferUtil.toBuffer("" +
+ "Content-Type: multipart/form-data; boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
+ "\r\n" +
+ "--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
+ "Content-Disposition: form-data; name=\"part1\"\r\n" +
+ "\n" +
+ "wNfミxVamt\r\n" +
+ "--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\n" +
+ "Content-Disposition: form-data; name=\"part2\"\r\n" +
+ "\r\n" +
+ "&ᄈ취ᅢO\r\n" +
+ "--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW--");
+
+ parser.parse(data, true);
+ assertThat(parser.getState(), is(State.END));
+ assertThat(handler.fields.size(), is(2));
+ }
+
+ static class TestHandler implements MultiPartParser.Handler
+ {
+ List<String> fields = new ArrayList<>();
+ List<String> content = new ArrayList<>();
+
+ @Override
+ public void parsedField(String name, String value)
+ {
+ fields.add(name + ": " + value);
+ }
+
+ public String contentString()
+ {
+ StringBuilder sb = new StringBuilder();
+ for (String s : content)
+ {
+ sb.append(s);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public boolean headerComplete()
+ {
+ fields.add("<<COMPLETE>>");
+ return false;
+ }
+
+ @Override
+ public boolean content(ByteBuffer buffer, boolean last)
+ {
+ if (BufferUtil.hasContent(buffer))
+ content.add(BufferUtil.toString(buffer));
+ if (last)
+ content.add("<<LAST>>");
+ return last;
+ }
+
+ public void clear()
+ {
+ fields.clear();
+ content.clear();
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/PathMapTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/PathMapTest.java
new file mode 100644
index 0000000..54bba20
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/PathMapTest.java
@@ -0,0 +1,204 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ *
+ */
+public class PathMapTest
+{
+ @Test
+ public void testPathMap() throws Exception
+ {
+ PathMap<String> p = new PathMap<>();
+
+ p.put("/abs/path", "1");
+ p.put("/abs/path/longer", "2");
+ p.put("/animal/bird/*", "3");
+ p.put("/animal/fish/*", "4");
+ p.put("/animal/*", "5");
+ p.put("*.tar.gz", "6");
+ p.put("*.gz", "7");
+ p.put("/", "8");
+ p.put("/XXX:/YYY", "9");
+ p.put("", "10");
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+ p.put("/\u20ACuro/*", "11");
+
+ String[][] tests = {
+ {"/abs/path", "1"},
+ {"/abs/path/xxx", "8"},
+ {"/abs/pith", "8"},
+ {"/abs/path/longer", "2"},
+ {"/abs/path/", "8"},
+ {"/abs/path/xxx", "8"},
+ {"/animal/bird/eagle/bald", "3"},
+ {"/animal/fish/shark/grey", "4"},
+ {"/animal/insect/bug", "5"},
+ {"/animal", "5"},
+ {"/animal/", "5"},
+ {"/animal/x", "5"},
+ {"/animal/*", "5"},
+ {"/suffix/path.tar.gz", "6"},
+ {"/suffix/path.gz", "7"},
+ {"/animal/path.gz", "5"},
+ {"/Other/path", "8"},
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+ {"/\u20ACuro/path", "11"},
+ {"/", "10"}
+ };
+
+ for (String[] test : tests)
+ {
+ assertEquals(test[1], p.getMatch(test[0]).getValue(), test[0]);
+ }
+
+ assertEquals("1", p.get("/abs/path"), "Get absolute path");
+ assertEquals("/abs/path", p.getMatch("/abs/path").getKey(), "Match absolute path");
+ assertEquals("[/animal/bird/*=3, /animal/*=5, *.tar.gz=6, *.gz=7, /=8]", p.getMatches("/animal/bird/path.tar.gz").toString(), "all matches");
+ assertEquals("[/animal/fish/*=4, /animal/*=5, /=8]", p.getMatches("/animal/fish/").toString(), "Dir matches");
+ assertEquals("[/animal/fish/*=4, /animal/*=5, /=8]", p.getMatches("/animal/fish").toString(), "Dir matches");
+ assertEquals("[=10, /=8]", p.getMatches("/").toString(), "Root matches");
+ assertEquals("[/=8]", p.getMatches("").toString(), "Dir matches");
+
+ assertEquals("/Foo/bar", PathMap.pathMatch("/Foo/bar", "/Foo/bar"), "pathMatch exact");
+ assertEquals("/Foo", PathMap.pathMatch("/Foo/*", "/Foo/bar"), "pathMatch prefix");
+ assertEquals("/Foo", PathMap.pathMatch("/Foo/*", "/Foo/"), "pathMatch prefix");
+ assertEquals("/Foo", PathMap.pathMatch("/Foo/*", "/Foo"), "pathMatch prefix");
+ assertEquals("/Foo/bar.ext", PathMap.pathMatch("*.ext", "/Foo/bar.ext"), "pathMatch suffix");
+ assertEquals("/Foo/bar.ext", PathMap.pathMatch("/", "/Foo/bar.ext"), "pathMatch default");
+
+ assertEquals(null, PathMap.pathInfo("/Foo/bar", "/Foo/bar"), "pathInfo exact");
+ assertEquals("/bar", PathMap.pathInfo("/Foo/*", "/Foo/bar"), "pathInfo prefix");
+ assertEquals("/*", PathMap.pathInfo("/Foo/*", "/Foo/*"), "pathInfo prefix");
+ assertEquals("/", PathMap.pathInfo("/Foo/*", "/Foo/"), "pathInfo prefix");
+ assertEquals(null, PathMap.pathInfo("/Foo/*", "/Foo"), "pathInfo prefix");
+ assertEquals(null, PathMap.pathInfo("*.ext", "/Foo/bar.ext"), "pathInfo suffix");
+ assertEquals(null, PathMap.pathInfo("/", "/Foo/bar.ext"), "pathInfo default");
+ assertEquals("9", p.getMatch("/XXX").getValue(), "multi paths");
+ assertEquals("9", p.getMatch("/YYY").getValue(), "multi paths");
+
+ p.put("/*", "0");
+
+ assertEquals("1", p.get("/abs/path"), "Get absolute path");
+ assertEquals("/abs/path", p.getMatch("/abs/path").getKey(), "Match absolute path");
+ assertEquals("1", p.getMatch("/abs/path").getValue(), "Match absolute path");
+ assertEquals("0", p.getMatch("/abs/path/xxx").getValue(), "Mismatch absolute path");
+ assertEquals("0", p.getMatch("/abs/pith").getValue(), "Mismatch absolute path");
+ assertEquals("2", p.getMatch("/abs/path/longer").getValue(), "Match longer absolute path");
+ assertEquals("0", p.getMatch("/abs/path/").getValue(), "Not exact absolute path");
+ assertEquals("0", p.getMatch("/abs/path/xxx").getValue(), "Not exact absolute path");
+
+ assertEquals("3", p.getMatch("/animal/bird/eagle/bald").getValue(), "Match longest prefix");
+ assertEquals("4", p.getMatch("/animal/fish/shark/grey").getValue(), "Match longest prefix");
+ assertEquals("5", p.getMatch("/animal/insect/bug").getValue(), "Match longest prefix");
+ assertEquals("5", p.getMatch("/animal").getValue(), "mismatch exact prefix");
+ assertEquals("5", p.getMatch("/animal/").getValue(), "mismatch exact prefix");
+
+ assertEquals("0", p.getMatch("/suffix/path.tar.gz").getValue(), "Match longest suffix");
+ assertEquals("0", p.getMatch("/suffix/path.gz").getValue(), "Match longest suffix");
+ assertEquals("5", p.getMatch("/animal/path.gz").getValue(), "prefix rather than suffix");
+
+ assertEquals("0", p.getMatch("/Other/path").getValue(), "default");
+
+ assertEquals("", PathMap.pathMatch("/*", "/xxx/zzz"), "pathMatch /*");
+ assertEquals("/xxx/zzz", PathMap.pathInfo("/*", "/xxx/zzz"), "pathInfo /*");
+
+ assertTrue(PathMap.match("/", "/anything"), "match /");
+ assertTrue(PathMap.match("/*", "/anything"), "match /*");
+ assertTrue(PathMap.match("/foo", "/foo"), "match /foo");
+ assertTrue(!PathMap.match("/foo", "/bar"), "!match /foo");
+ assertTrue(PathMap.match("/foo/*", "/foo"), "match /foo/*");
+ assertTrue(PathMap.match("/foo/*", "/foo/"), "match /foo/*");
+ assertTrue(PathMap.match("/foo/*", "/foo/anything"), "match /foo/*");
+ assertTrue(!PathMap.match("/foo/*", "/bar"), "!match /foo/*");
+ assertTrue(!PathMap.match("/foo/*", "/bar/"), "!match /foo/*");
+ assertTrue(!PathMap.match("/foo/*", "/bar/anything"), "!match /foo/*");
+ assertTrue(PathMap.match("*.foo", "anything.foo"), "match *.foo");
+ assertTrue(!PathMap.match("*.foo", "anything.bar"), "!match *.foo");
+
+ assertEquals("10", p.getMatch("/").getValue(), "match / with ''");
+
+ assertTrue(PathMap.match("", "/"), "match \"\"");
+ }
+
+ /**
+ * See JIRA issue: JETTY-88.
+ *
+ * @throws Exception failed test
+ */
+ @Test
+ public void testPathMappingsOnlyMatchOnDirectoryNames() throws Exception
+ {
+ String spec = "/xyz/*";
+
+ assertMatch(spec, "/xyz");
+ assertMatch(spec, "/xyz/");
+ assertMatch(spec, "/xyz/123");
+ assertMatch(spec, "/xyz/123/");
+ assertMatch(spec, "/xyz/123.txt");
+ assertNotMatch(spec, "/xyz123");
+ assertNotMatch(spec, "/xyz123;jessionid=99");
+ assertNotMatch(spec, "/xyz123/");
+ assertNotMatch(spec, "/xyz123/456");
+ assertNotMatch(spec, "/xyz.123");
+ assertNotMatch(spec, "/xyz;123"); // as if the ; was encoded and part of the path
+ assertNotMatch(spec, "/xyz?123"); // as if the ? was encoded and part of the path
+ }
+
+ @Test
+ public void testPrecidenceVsOrdering() throws Exception
+ {
+ PathMap<String> p = new PathMap<>();
+ p.put("/dump/gzip/*", "prefix");
+ p.put("*.txt", "suffix");
+
+ assertEquals(null, p.getMatch("/foo/bar"));
+ assertEquals("prefix", p.getMatch("/dump/gzip/something").getValue());
+ assertEquals("suffix", p.getMatch("/foo/something.txt").getValue());
+ assertEquals("prefix", p.getMatch("/dump/gzip/something.txt").getValue());
+
+ p = new PathMap<>();
+ p.put("*.txt", "suffix");
+ p.put("/dump/gzip/*", "prefix");
+
+ assertEquals(null, p.getMatch("/foo/bar"));
+ assertEquals("prefix", p.getMatch("/dump/gzip/something").getValue());
+ assertEquals("suffix", p.getMatch("/foo/something.txt").getValue());
+ assertEquals("prefix", p.getMatch("/dump/gzip/something.txt").getValue());
+ }
+
+ private void assertMatch(String spec, String path)
+ {
+ boolean match = PathMap.match(spec, path);
+ assertTrue(match, "PathSpec '" + spec + "' should match path '" + path + "'");
+ }
+
+ private void assertNotMatch(String spec, String path)
+ {
+ boolean match = PathMap.match(spec, path);
+ assertFalse(match, "PathSpec '" + spec + "' should not match path '" + path + "'");
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedCSVTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedCSVTest.java
new file mode 100644
index 0000000..fbf608f
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedCSVTest.java
@@ -0,0 +1,152 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class QuotedCSVTest
+{
+ @Test
+ public void testOWS()
+ {
+ QuotedCSV values = new QuotedCSV();
+ values.addValue(" value 0.5 ; pqy = vwz ; q =0.5 , value 1.0 , other ; param ");
+ assertThat(values, Matchers.contains(
+ "value 0.5;pqy=vwz;q=0.5",
+ "value 1.0",
+ "other;param"));
+ }
+
+ @Test
+ public void testEmpty()
+ {
+ QuotedCSV values = new QuotedCSV();
+ values.addValue(",aaaa, , bbbb ,,cccc,");
+ assertThat(values, Matchers.contains(
+ "aaaa",
+ "bbbb",
+ "cccc"));
+ }
+
+ @Test
+ public void testQuoted()
+ {
+ QuotedCSV values = new QuotedCSV();
+ values.addValue("A;p=\"v\",B,\"C, D\"");
+ assertThat(values, Matchers.contains(
+ "A;p=\"v\"",
+ "B",
+ "\"C, D\""));
+ }
+
+ @Test
+ public void testOpenQuote()
+ {
+ QuotedCSV values = new QuotedCSV();
+ values.addValue("value;p=\"v");
+ assertThat(values, Matchers.contains(
+ "value;p=\"v"));
+ }
+
+ @Test
+ public void testQuotedNoQuotes()
+ {
+ QuotedCSV values = new QuotedCSV(false);
+ values.addValue("A;p=\"v\",B,\"C, D\"");
+ assertThat(values, Matchers.contains(
+ "A;p=v",
+ "B",
+ "C, D"));
+ }
+
+ @Test
+ public void testOpenQuoteNoQuotes()
+ {
+ QuotedCSV values = new QuotedCSV(false);
+ values.addValue("value;p=\"v");
+ assertThat(values, Matchers.contains(
+ "value;p=v"));
+ }
+
+ @Test
+ public void testParamsOnly()
+ {
+ QuotedCSV values = new QuotedCSV(false);
+ values.addValue("for=192.0.2.43, for=\"[2001:db8:cafe::17]\", for=unknown");
+ assertThat(values, Matchers.contains(
+ "for=192.0.2.43",
+ "for=[2001:db8:cafe::17]",
+ "for=unknown"));
+ }
+
+ @Test
+ public void testMutation()
+ {
+ QuotedCSV values = new QuotedCSV(false)
+ {
+
+ @Override
+ protected void parsedValue(StringBuffer buffer)
+ {
+ if (buffer.toString().contains("DELETE"))
+ {
+ String s = StringUtil.strip(buffer.toString(), "DELETE");
+ buffer.setLength(0);
+ buffer.append(s);
+ }
+ if (buffer.toString().contains("APPEND"))
+ {
+ String s = StringUtil.replace(buffer.toString(), "APPEND", "Append") + "!";
+ buffer.setLength(0);
+ buffer.append(s);
+ }
+ }
+
+ @Override
+ protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
+ {
+ String name = paramValue > 0 ? buffer.substring(paramName, paramValue - 1) : buffer.substring(paramName);
+ if ("IGNORE".equals(name))
+ buffer.setLength(paramName - 1);
+ }
+ };
+
+ values.addValue("normal;param=val, testAPPENDandDELETEvalue ; n=v; IGNORE = this; x=y ");
+ assertThat(values, Matchers.contains(
+ "normal;param=val",
+ "testAppendandvalue!;n=v;x=y"));
+ }
+
+ @Test
+ public void testUnQuote()
+ {
+ assertThat(QuotedCSV.unquote(""), is(""));
+ assertThat(QuotedCSV.unquote("\"\""), is(""));
+ assertThat(QuotedCSV.unquote("foo"), is("foo"));
+ assertThat(QuotedCSV.unquote("\"foo\""), is("foo"));
+ assertThat(QuotedCSV.unquote("f\"o\"o"), is("foo"));
+ assertThat(QuotedCSV.unquote("\"\\\"foo\""), is("\"foo"));
+ assertThat(QuotedCSV.unquote("\\foo"), is("\\foo"));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java
new file mode 100644
index 0000000..b941e95
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java
@@ -0,0 +1,346 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+
+public class QuotedQualityCSVTest
+{
+
+ @Test
+ public void test7231Sec532Example1()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ values.addValue(" audio/*; q=0.2, audio/basic");
+ assertThat(values, Matchers.contains("audio/basic", "audio/*"));
+ }
+
+ @Test
+ public void test7231Sec532Example2()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ values.addValue("text/plain; q=0.5, text/html,");
+ values.addValue("text/x-dvi; q=0.8, text/x-c");
+ assertThat(values, Matchers.contains("text/html", "text/x-c", "text/x-dvi", "text/plain"));
+ }
+
+ @Test
+ public void test7231Sec532Example3()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ values.addValue("text/*, text/plain, text/plain;format=flowed, */*");
+
+ // Note this sort is only on quality and not the most specific type as per 5.3.2
+ assertThat(values, Matchers.contains("text/*", "text/plain", "text/plain;format=flowed", "*/*"));
+ }
+
+ @Test
+ public void test7231532Example3MostSpecific()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV(QuotedQualityCSV.MOST_SPECIFIC_MIME_ORDERING);
+ values.addValue("text/*, text/plain, text/plain;format=flowed, */*");
+
+ assertThat(values, Matchers.contains("text/plain;format=flowed", "text/plain", "text/*", "*/*"));
+ }
+
+ @Test
+ public void test7231Sec532Example4()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ values.addValue("text/*;q=0.3, text/html;q=0.7, text/html;level=1,");
+ values.addValue("text/html;level=2;q=0.4, */*;q=0.5");
+ assertThat(values, Matchers.contains(
+ "text/html;level=1",
+ "text/html",
+ "*/*",
+ "text/html;level=2",
+ "text/*"
+ ));
+ }
+
+ @Test
+ public void test7231Sec534Example1()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ values.addValue("compress, gzip");
+ values.addValue("");
+ values.addValue("*");
+ values.addValue("compress;q=0.5, gzip;q=1.0");
+ values.addValue("gzip;q=1.0, identity; q=0.5, *;q=0");
+
+ assertThat(values, Matchers.contains(
+ "compress",
+ "gzip",
+ "*",
+ "gzip",
+ "gzip",
+ "compress",
+ "identity"
+ ));
+ }
+
+ @Test
+ public void testOWS()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ values.addValue(" value 0.5 ; p = v ; q =0.5 , value 1.0 ");
+ assertThat(values, Matchers.contains(
+ "value 1.0",
+ "value 0.5;p=v"));
+ }
+
+ @Test
+ public void testEmpty()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ values.addValue(",aaaa, , bbbb ,,cccc,");
+ assertThat(values, Matchers.contains(
+ "aaaa",
+ "bbbb",
+ "cccc"));
+ }
+
+ @Test
+ public void testQuoted()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ values.addValue(" value 0.5 ; p = \"v ; q = \\\"0.5\\\" , value 1.0 \" ");
+ assertThat(values, Matchers.contains(
+ "value 0.5;p=\"v ; q = \\\"0.5\\\" , value 1.0 \""));
+ }
+
+ @Test
+ public void testOpenQuote()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ values.addValue("value;p=\"v");
+ assertThat(values, Matchers.contains(
+ "value;p=\"v"));
+ }
+
+ @Test
+ public void testQuotedQuality()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ values.addValue(" value 0.5 ; p = v ; q = \"0.5\" , value 1.0 ");
+ assertThat(values, Matchers.contains(
+ "value 1.0",
+ "value 0.5;p=v"));
+ }
+
+ @Test
+ public void testBadQuality()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ values.addValue("value0.5;p=v;q=0.5,value1.0,valueBad;q=X");
+ assertThat(values, Matchers.contains(
+ "value1.0",
+ "value0.5;p=v"));
+ }
+
+ @Test
+ public void testBad()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+
+ // None of these should throw exceptions
+ values.addValue(null);
+ values.addValue("");
+
+ values.addValue(";");
+ values.addValue("=");
+ values.addValue(",");
+
+ values.addValue(";;");
+ values.addValue(";=");
+ values.addValue(";,");
+ values.addValue("=;");
+ values.addValue("==");
+ values.addValue("=,");
+ values.addValue(",;");
+ values.addValue(",=");
+ values.addValue(",,");
+
+ values.addValue(";;;");
+ values.addValue(";;=");
+ values.addValue(";;,");
+ values.addValue(";=;");
+ values.addValue(";==");
+ values.addValue(";=,");
+ values.addValue(";,;");
+ values.addValue(";,=");
+ values.addValue(";,,");
+
+ values.addValue("=;;");
+ values.addValue("=;=");
+ values.addValue("=;,");
+ values.addValue("==;");
+ values.addValue("===");
+ values.addValue("==,");
+ values.addValue("=,;");
+ values.addValue("=,=");
+ values.addValue("=,,");
+
+ values.addValue(",;;");
+ values.addValue(",;=");
+ values.addValue(",;,");
+ values.addValue(",=;");
+ values.addValue(",==");
+ values.addValue(",=,");
+ values.addValue(",,;");
+ values.addValue(",,=");
+ values.addValue(",,,");
+
+ values.addValue("x;=1");
+ values.addValue("=1");
+ values.addValue("q=x");
+ values.addValue("q=0");
+ values.addValue("q=");
+ values.addValue("q=,");
+ values.addValue("q=;");
+ }
+
+ private static final String[] preferBrotli = {"br", "gzip"};
+ private static final String[] preferGzip = {"gzip", "br"};
+ private static final String[] noFormats = {};
+
+ @Test
+ public void testFirefoxContentEncodingWithBrotliPreference()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV(preferBrotli);
+ values.addValue("gzip, deflate, br");
+ assertThat(values, contains("br", "gzip", "deflate"));
+ }
+
+ @Test
+ public void testFirefoxContentEncodingWithGzipPreference()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV(preferGzip);
+ values.addValue("gzip, deflate, br");
+ assertThat(values, contains("gzip", "br", "deflate"));
+ }
+
+ @Test
+ public void testFirefoxContentEncodingWithNoPreference()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV(noFormats);
+ values.addValue("gzip, deflate, br");
+ assertThat(values, contains("gzip", "deflate", "br"));
+ }
+
+ @Test
+ public void testChromeContentEncodingWithBrotliPreference()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV(preferBrotli);
+ values.addValue("gzip, deflate, sdch, br");
+ assertThat(values, contains("br", "gzip", "deflate", "sdch"));
+ }
+
+ @Test
+ public void testComplexEncodingWithGzipPreference()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV(preferGzip);
+ values.addValue("gzip;q=0.9, identity;q=0.1, *;q=0.01, deflate;q=0.9, sdch;q=0.7, br;q=0.9");
+ assertThat(values, contains("gzip", "br", "deflate", "sdch", "identity", "*"));
+ }
+
+ @Test
+ public void testComplexEncodingWithBrotliPreference()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV(preferBrotli);
+ values.addValue("gzip;q=0.9, identity;q=0.1, *;q=0, deflate;q=0.9, sdch;q=0.7, br;q=0.99");
+ assertThat(values, contains("br", "gzip", "deflate", "sdch", "identity"));
+ }
+
+ @Test
+ public void testStarEncodingWithGzipPreference()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV(preferGzip);
+ values.addValue("br, *");
+ assertThat(values, contains("*", "br"));
+ }
+
+ @Test
+ public void testStarEncodingWithBrotliPreference()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV(preferBrotli);
+ values.addValue("gzip, *");
+ assertThat(values, contains("*", "gzip"));
+ }
+
+ @Test
+ public void testSameQuality()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ values.addValue("one;q=0.5,two;q=0.5,three;q=0.5");
+ assertThat(values.getValues(), Matchers.contains("one", "two", "three"));
+ }
+
+ @Test
+ public void testNoQuality()
+ {
+ QuotedQualityCSV values = new QuotedQualityCSV();
+ values.addValue("one,two;,three;x=y");
+ assertThat(values.getValues(), Matchers.contains("one", "two", "three;x=y"));
+ }
+
+ @Test
+ public void testQuality()
+ {
+ List<String> results = new ArrayList<>();
+
+ QuotedQualityCSV values = new QuotedQualityCSV()
+ {
+ @Override
+ protected void parsedValue(StringBuffer buffer)
+ {
+ results.add("parsedValue: " + buffer.toString());
+
+ super.parsedValue(buffer);
+ }
+
+ @Override
+ protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
+ {
+ String param = buffer.substring(paramName, buffer.length());
+ results.add("parsedParam: " + param);
+
+ super.parsedParam(buffer, valueLength, paramName, paramValue);
+ }
+ };
+
+ // The provided string is not legal according to some RFCs ( not a token because of = and not a parameter because not preceded by ; )
+ // The string is legal according to RFC7239 which allows for just parameters (called forwarded-pairs)
+ values.addValue("p=0.5,q=0.5");
+
+ // The QuotedCSV implementation is lenient and adopts the later interpretation and thus sees q=0.5 and p=0.5 both as parameters
+ assertThat(results, contains("parsedValue: ", "parsedParam: p=0.5",
+ "parsedValue: ", "parsedParam: q=0.5"));
+
+ // However the QuotedQualityCSV only handles the q parameter and that is consumed from the parameter string.
+ assertThat(values, contains("p=0.5", ""));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java
new file mode 100644
index 0000000..e0ab1c4
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java
@@ -0,0 +1,134 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class SyntaxTest
+{
+ @Test
+ public void testRequireValidRFC2616TokenGood()
+ {
+ String[] tokens = {
+ "name",
+ "",
+ null,
+ "n.a.m.e",
+ "na-me",
+ "+name",
+ "na*me",
+ "na$me",
+ "#name"
+ };
+
+ for (String token : tokens)
+ {
+ Syntax.requireValidRFC2616Token(token, "Test Based");
+ // No exception should occur here
+ }
+ }
+
+ @Test
+ public void testRequireValidRFC2616TokenBad()
+ {
+ String[] tokens = {
+ "\"name\"",
+ "name\t",
+ "na me",
+ "name\u0082",
+ "na\tme",
+ "na;me",
+ "{name}",
+ "[name]",
+ "\""
+ };
+
+ for (String token : tokens)
+ {
+ try
+ {
+ Syntax.requireValidRFC2616Token(token, "Test Based");
+ fail("RFC2616 Token [" + token + "] Should have thrown " + IllegalArgumentException.class.getName());
+ }
+ catch (IllegalArgumentException e)
+ {
+ assertThat("Testing Bad RFC2616 Token [" + token + "]", e.getMessage(),
+ allOf(containsString("Test Based"),
+ containsString("RFC2616")));
+ }
+ }
+ }
+
+ @Test
+ public void testRequireValidRFC6265CookieValueGood()
+ {
+ String[] values = {
+ "value",
+ "",
+ null,
+ "val=ue",
+ "val-ue",
+ "\"value\"",
+ "val/ue",
+ "v.a.l.u.e"
+ };
+
+ for (String value : values)
+ {
+ Syntax.requireValidRFC6265CookieValue(value);
+ // No exception should occur here
+ }
+ }
+
+ @Test
+ public void testRequireValidRFC6265CookieValueBad()
+ {
+ String[] values = {
+ "va\tlue",
+ "\t",
+ "value\u0000",
+ "val\u0082ue",
+ "va lue",
+ "va;lue",
+ "\"value",
+ "value\"",
+ "val\\ue",
+ "val\"ue",
+ "\""
+ };
+
+ for (String value : values)
+ {
+ try
+ {
+ Syntax.requireValidRFC6265CookieValue(value);
+ fail("RFC6265 Cookie Value [" + value + "] Should have thrown " + IllegalArgumentException.class.getName());
+ }
+ catch (IllegalArgumentException e)
+ {
+ assertThat("Testing Bad RFC6265 Cookie Value [" + value + "]", e.getMessage(), containsString("RFC6265"));
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/matchers/HttpFieldsContainsHeaderKey.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/matchers/HttpFieldsContainsHeaderKey.java
new file mode 100644
index 0000000..352dadb
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/matchers/HttpFieldsContainsHeaderKey.java
@@ -0,0 +1,57 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.matchers;
+
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+public class HttpFieldsContainsHeaderKey extends TypeSafeMatcher<HttpFields>
+{
+ private final String keyName;
+
+ public HttpFieldsContainsHeaderKey(String keyName)
+ {
+ this.keyName = keyName;
+ }
+
+ public HttpFieldsContainsHeaderKey(HttpHeader header)
+ {
+ this.keyName = header.asString();
+ }
+
+ @Override
+ public void describeTo(Description description)
+ {
+ description.appendText("expecting http field name ").appendValue(keyName);
+ }
+
+ @Override
+ protected boolean matchesSafely(HttpFields fields)
+ {
+ return fields.containsKey(this.keyName);
+ }
+
+ public static Matcher<HttpFields> containsKey(String keyName)
+ {
+ return new HttpFieldsContainsHeaderKey(keyName);
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/matchers/HttpFieldsContainsHeaderValue.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/matchers/HttpFieldsContainsHeaderValue.java
new file mode 100644
index 0000000..4f052ce
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/matchers/HttpFieldsContainsHeaderValue.java
@@ -0,0 +1,75 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.matchers;
+
+import java.util.Locale;
+
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+public class HttpFieldsContainsHeaderValue extends TypeSafeMatcher<HttpFields>
+{
+ private final String keyName;
+ private final String value;
+
+ public HttpFieldsContainsHeaderValue(String keyName, String value)
+ {
+ this.keyName = keyName;
+ this.value = value;
+ }
+
+ public HttpFieldsContainsHeaderValue(HttpHeader header, String value)
+ {
+ this(header.asString(), value);
+ }
+
+ @Override
+ public void describeTo(Description description)
+ {
+ description.appendText("expecting http header ").appendValue(keyName).appendText(" with value ").appendValue(value);
+ }
+
+ @Override
+ protected boolean matchesSafely(HttpFields fields)
+ {
+ HttpField field = fields.getField(this.keyName);
+ if (field == null)
+ return false;
+
+ // Use HttpField.contains() logic
+ if (field.contains(this.value))
+ return true;
+
+ // Simple equals
+ if (this.value == field.getValue())
+ return true;
+
+ // Try individual value logic
+ String lcValue = this.value.toLowerCase(Locale.ENGLISH);
+ for (String value : field.getValues())
+ {
+ if (value.toLowerCase(Locale.ENGLISH).contains(lcValue))
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/matchers/HttpFieldsHeaderValue.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/matchers/HttpFieldsHeaderValue.java
new file mode 100644
index 0000000..0479262
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/matchers/HttpFieldsHeaderValue.java
@@ -0,0 +1,60 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.matchers;
+
+import java.util.Objects;
+
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+public class HttpFieldsHeaderValue extends TypeSafeMatcher<HttpFields>
+{
+ private final String keyName;
+ private final String value;
+
+ public HttpFieldsHeaderValue(String keyName, String value)
+ {
+ this.keyName = keyName;
+ this.value = value;
+ }
+
+ public HttpFieldsHeaderValue(HttpHeader header, String value)
+ {
+ this(header.asString(), value);
+ }
+
+ @Override
+ public void describeTo(Description description)
+ {
+ description.appendText("expecting http header ").appendValue(keyName).appendText(" with value ").appendValue(value);
+ }
+
+ @Override
+ protected boolean matchesSafely(HttpFields fields)
+ {
+ HttpField field = fields.getField(this.keyName);
+ if (field == null)
+ return false;
+
+ return Objects.equals(this.value, field.getValue());
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/matchers/HttpFieldsMatchersTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/matchers/HttpFieldsMatchersTest.java
new file mode 100644
index 0000000..eaf6752
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/matchers/HttpFieldsMatchersTest.java
@@ -0,0 +1,122 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.matchers;
+
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.junit.jupiter.api.Test;
+
+import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeader;
+import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeaderValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class HttpFieldsMatchersTest
+{
+ @Test
+ public void testContainsHeader()
+ {
+ HttpFields fields = new HttpFields();
+ fields.put("a", "foo");
+ fields.put("b", "bar");
+ fields.put("c", "fizz");
+
+ assertThat(fields, containsHeader("a"));
+ }
+
+ @Test
+ public void testNotContainsHeader()
+ {
+ HttpFields fields = new HttpFields();
+ fields.put("a", "foo");
+ fields.put("b", "bar");
+ fields.put("c", "fizz");
+
+ AssertionError x = assertThrows(AssertionError.class, () ->
+ {
+ assertThat(fields, not(containsHeader("a")));
+ });
+
+ assertThat(x.getMessage(), containsString("not expecting http field name \"a\""));
+ }
+
+ @Test
+ public void testContainsHeaderMisMatch()
+ {
+ HttpFields fields = new HttpFields();
+ fields.put("a", "foo");
+ fields.put("b", "bar");
+ fields.put("c", "fizz");
+
+ AssertionError x = assertThrows(AssertionError.class, () ->
+ {
+ assertThat(fields, containsHeader("z"));
+ });
+
+ assertThat(x.getMessage(), containsString("expecting http field name \"z\""));
+ }
+
+ @Test
+ public void testContainsHeaderValueMisMatchNoSuchHeader()
+ {
+ HttpFields fields = new HttpFields();
+ fields.put("a", "foo");
+ fields.put("b", "bar");
+ fields.put("c", "fizz");
+
+ AssertionError x = assertThrows(AssertionError.class, () ->
+ {
+ assertThat(fields, containsHeaderValue("z", "floom"));
+ });
+
+ assertThat(x.getMessage(), containsString("expecting http header \"z\" with value \"floom\""));
+ }
+
+ @Test
+ public void testContainsHeaderValueMisMatchNoSuchValue()
+ {
+ HttpFields fields = new HttpFields();
+ fields.put("a", "foo");
+ fields.put("b", "bar");
+ fields.put("c", "fizz");
+
+ AssertionError x = assertThrows(AssertionError.class, () ->
+ {
+ assertThat(fields, containsHeaderValue("a", "floom"));
+ });
+
+ assertThat(x.getMessage(), containsString("expecting http header \"a\" with value \"floom\""));
+ }
+
+ @Test
+ public void testContainsHeaderValue()
+ {
+ HttpFields fields = new HttpFields();
+ fields.put("a", "foo");
+ fields.put("b", "bar");
+ fields.put("c", "fizz");
+ fields.put(HttpHeader.CONTENT_TYPE, "text/plain;charset=utf-8");
+
+ assertThat(fields, containsHeaderValue("b", "bar"));
+ assertThat(fields, containsHeaderValue("content-type", "text/plain"));
+ assertThat(fields, containsHeaderValue("content-type", "charset=UTF-8"));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/PathMappingsTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/PathMappingsTest.java
new file mode 100644
index 0000000..e9d5151
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/PathMappingsTest.java
@@ -0,0 +1,464 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+public class PathMappingsTest
+{
+ private void assertMatch(PathMappings<String> pathmap, String path, String expectedValue)
+ {
+ String msg = String.format(".getMatch(\"%s\")", path);
+ MappedResource<String> match = pathmap.getMatch(path);
+ assertThat(msg, match, notNullValue());
+ String actualMatch = match.getResource();
+ assertEquals(expectedValue, actualMatch, msg);
+ }
+
+ public void dumpMappings(PathMappings<String> p)
+ {
+ for (MappedResource<String> res : p)
+ {
+ System.out.printf(" %s%n", res);
+ }
+ }
+
+ /**
+ * Test the match order rules with a mixed Servlet and regex path specs
+ * <p>
+ * <ul>
+ * <li>Exact match</li>
+ * <li>Longest prefix match</li>
+ * <li>Longest suffix match</li>
+ * </ul>
+ */
+ @Test
+ public void testMixedMatchOrder()
+ {
+ PathMappings<String> p = new PathMappings<>();
+
+ p.put(new ServletPathSpec(""), "root");
+ p.put(new ServletPathSpec("/"), "default");
+ p.put(new ServletPathSpec("/animal/bird/*"), "birds");
+ p.put(new ServletPathSpec("/animal/fish/*"), "fishes");
+ p.put(new ServletPathSpec("/animal/*"), "animals");
+ p.put(new RegexPathSpec("^/animal/.*/chat$"), "animalChat");
+ p.put(new RegexPathSpec("^/animal/.*/cam$"), "animalCam");
+ p.put(new RegexPathSpec("^/entrance/cam$"), "entranceCam");
+
+ // dumpMappings(p);
+
+ assertMatch(p, "/animal/bird/eagle", "birds");
+ assertMatch(p, "/animal/fish/bass/sea", "fishes");
+ assertMatch(p, "/animal/peccary/javalina/evolution", "animals");
+ assertMatch(p, "/", "root");
+ assertMatch(p, "/other", "default");
+ assertMatch(p, "/animal/bird/eagle/chat", "animalChat");
+ assertMatch(p, "/animal/bird/penguin/chat", "animalChat");
+ assertMatch(p, "/animal/fish/trout/cam", "animalCam");
+ assertMatch(p, "/entrance/cam", "entranceCam");
+ }
+
+ /**
+ * Test the match order rules imposed by the Servlet API (default vs any)
+ */
+ @Test
+ public void testServletMatchDefault()
+ {
+ PathMappings<String> p = new PathMappings<>();
+
+ p.put(new ServletPathSpec("/"), "default");
+ p.put(new ServletPathSpec("/*"), "any");
+
+ assertMatch(p, "/abs/path", "any");
+ assertMatch(p, "/abs/path/xxx", "any");
+ assertMatch(p, "/animal/bird/eagle/bald", "any");
+ assertMatch(p, "/", "any");
+ }
+
+ /**
+ * Test the match order rules with a mixed Servlet and URI Template path specs
+ * <p>
+ * <ul>
+ * <li>Exact match</li>
+ * <li>Longest prefix match</li>
+ * <li>Longest suffix match</li>
+ * </ul>
+ */
+ @Test
+ public void testMixedMatchUriOrder()
+ {
+ PathMappings<String> p = new PathMappings<>();
+
+ p.put(new ServletPathSpec("/"), "default");
+ p.put(new ServletPathSpec("/animal/bird/*"), "birds");
+ p.put(new ServletPathSpec("/animal/fish/*"), "fishes");
+ p.put(new ServletPathSpec("/animal/*"), "animals");
+ p.put(new UriTemplatePathSpec("/animal/{type}/{name}/chat"), "animalChat");
+ p.put(new UriTemplatePathSpec("/animal/{type}/{name}/cam"), "animalCam");
+ p.put(new UriTemplatePathSpec("/entrance/cam"), "entranceCam");
+
+ // dumpMappings(p);
+
+ assertMatch(p, "/animal/bird/eagle", "birds");
+ assertMatch(p, "/animal/fish/bass/sea", "fishes");
+ assertMatch(p, "/animal/peccary/javalina/evolution", "animals");
+ assertMatch(p, "/", "default");
+ assertMatch(p, "/animal/bird/eagle/chat", "animalChat");
+ assertMatch(p, "/animal/bird/penguin/chat", "animalChat");
+ assertMatch(p, "/animal/fish/trout/cam", "animalCam");
+ assertMatch(p, "/entrance/cam", "entranceCam");
+ }
+
+ /**
+ * Test the match order rules for URI Template based specs
+ * <p>
+ * <ul>
+ * <li>Exact match</li>
+ * <li>Longest prefix match</li>
+ * <li>Longest suffix match</li>
+ * </ul>
+ */
+ @Test
+ public void testUriTemplateMatchOrder()
+ {
+ PathMappings<String> p = new PathMappings<>();
+
+ p.put(new UriTemplatePathSpec("/a/{var}/c"), "endpointA");
+ p.put(new UriTemplatePathSpec("/a/b/c"), "endpointB");
+ p.put(new UriTemplatePathSpec("/a/{var1}/{var2}"), "endpointC");
+ p.put(new UriTemplatePathSpec("/{var1}/d"), "endpointD");
+ p.put(new UriTemplatePathSpec("/b/{var2}"), "endpointE");
+
+ // dumpMappings(p);
+
+ assertMatch(p, "/a/b/c", "endpointB");
+ assertMatch(p, "/a/d/c", "endpointA");
+ assertMatch(p, "/a/x/y", "endpointC");
+
+ assertMatch(p, "/b/d", "endpointE");
+ }
+
+ @Test
+ public void testPathMap() throws Exception
+ {
+ PathMappings<String> p = new PathMappings<>();
+
+ p.put(new ServletPathSpec("/abs/path"), "1");
+ p.put(new ServletPathSpec("/abs/path/longer"), "2");
+ p.put(new ServletPathSpec("/animal/bird/*"), "3");
+ p.put(new ServletPathSpec("/animal/fish/*"), "4");
+ p.put(new ServletPathSpec("/animal/*"), "5");
+ p.put(new ServletPathSpec("*.tar.gz"), "6");
+ p.put(new ServletPathSpec("*.gz"), "7");
+ p.put(new ServletPathSpec("/"), "8");
+ // p.put(new ServletPathSpec("/XXX:/YYY"), "9"); // special syntax from Jetty 3.1.x
+ p.put(new ServletPathSpec(""), "10");
+ p.put(new ServletPathSpec("/\u20ACuro/*"), "11");
+
+ assertEquals("/Foo/bar", new ServletPathSpec("/Foo/bar").getPathMatch("/Foo/bar"), "pathMatch exact");
+ assertEquals("/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo/bar"), "pathMatch prefix");
+ assertEquals("/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo/"), "pathMatch prefix");
+ assertEquals("/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo"), "pathMatch prefix");
+ assertEquals("/Foo/bar.ext", new ServletPathSpec("*.ext").getPathMatch("/Foo/bar.ext"), "pathMatch suffix");
+ assertEquals("/Foo/bar.ext", new ServletPathSpec("/").getPathMatch("/Foo/bar.ext"), "pathMatch default");
+
+ assertEquals(null, new ServletPathSpec("/Foo/bar").getPathInfo("/Foo/bar"), "pathInfo exact");
+ assertEquals("/bar", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/bar"), "pathInfo prefix");
+ assertEquals("/*", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/*"), "pathInfo prefix");
+ assertEquals("/", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/"), "pathInfo prefix");
+ assertEquals(null, new ServletPathSpec("/Foo/*").getPathInfo("/Foo"), "pathInfo prefix");
+ assertEquals(null, new ServletPathSpec("*.ext").getPathInfo("/Foo/bar.ext"), "pathInfo suffix");
+ assertEquals(null, new ServletPathSpec("/").getPathInfo("/Foo/bar.ext"), "pathInfo default");
+
+ p.put(new ServletPathSpec("/*"), "0");
+
+ // assertEquals("1", p.get("/abs/path"), "Get absolute path");
+ assertEquals("/abs/path", p.getMatch("/abs/path").getPathSpec().getDeclaration(), "Match absolute path");
+ assertEquals("1", p.getMatch("/abs/path").getResource(), "Match absolute path");
+ assertEquals("0", p.getMatch("/abs/path/xxx").getResource(), "Mismatch absolute path");
+ assertEquals("0", p.getMatch("/abs/pith").getResource(), "Mismatch absolute path");
+ assertEquals("2", p.getMatch("/abs/path/longer").getResource(), "Match longer absolute path");
+ assertEquals("0", p.getMatch("/abs/path/").getResource(), "Not exact absolute path");
+ assertEquals("0", p.getMatch("/abs/path/xxx").getResource(), "Not exact absolute path");
+
+ assertEquals("3", p.getMatch("/animal/bird/eagle/bald").getResource(), "Match longest prefix");
+ assertEquals("4", p.getMatch("/animal/fish/shark/grey").getResource(), "Match longest prefix");
+ assertEquals("5", p.getMatch("/animal/insect/bug").getResource(), "Match longest prefix");
+ assertEquals("5", p.getMatch("/animal").getResource(), "mismatch exact prefix");
+ assertEquals("5", p.getMatch("/animal/").getResource(), "mismatch exact prefix");
+
+ assertEquals("0", p.getMatch("/suffix/path.tar.gz").getResource(), "Match longest suffix");
+ assertEquals("0", p.getMatch("/suffix/path.gz").getResource(), "Match longest suffix");
+ assertEquals("5", p.getMatch("/animal/path.gz").getResource(), "prefix rather than suffix");
+
+ assertEquals("0", p.getMatch("/Other/path").getResource(), "default");
+
+ assertEquals("", new ServletPathSpec("/*").getPathMatch("/xxx/zzz"), "pathMatch /*");
+ assertEquals("/xxx/zzz", new ServletPathSpec("/*").getPathInfo("/xxx/zzz"), "pathInfo /*");
+
+ assertTrue(new ServletPathSpec("/").matches("/anything"), "match /");
+ assertTrue(new ServletPathSpec("/*").matches("/anything"), "match /*");
+ assertTrue(new ServletPathSpec("/foo").matches("/foo"), "match /foo");
+ assertTrue(!new ServletPathSpec("/foo").matches("/bar"), "!match /foo");
+ assertTrue(new ServletPathSpec("/foo/*").matches("/foo"), "match /foo/*");
+ assertTrue(new ServletPathSpec("/foo/*").matches("/foo/"), "match /foo/*");
+ assertTrue(new ServletPathSpec("/foo/*").matches("/foo/anything"), "match /foo/*");
+ assertTrue(!new ServletPathSpec("/foo/*").matches("/bar"), "!match /foo/*");
+ assertTrue(!new ServletPathSpec("/foo/*").matches("/bar/"), "!match /foo/*");
+ assertTrue(!new ServletPathSpec("/foo/*").matches("/bar/anything"), "!match /foo/*");
+ assertTrue(new ServletPathSpec("*.foo").matches("anything.foo"), "match *.foo");
+ assertTrue(!new ServletPathSpec("*.foo").matches("anything.bar"), "!match *.foo");
+ assertTrue(new ServletPathSpec("/On*").matches("/On*"), "match /On*");
+ assertTrue(!new ServletPathSpec("/On*").matches("/One"), "!match /One");
+
+ assertEquals("10", p.getMatch("/").getResource(), "match / with ''");
+
+ assertTrue(new ServletPathSpec("").matches("/"), "match \"\"");
+ }
+
+ /**
+ * See JIRA issue: JETTY-88.
+ *
+ * @throws Exception failed test
+ */
+ @Test
+ public void testPathMappingsOnlyMatchOnDirectoryNames() throws Exception
+ {
+ ServletPathSpec spec = new ServletPathSpec("/xyz/*");
+
+ PathSpecAssert.assertMatch(spec, "/xyz");
+ PathSpecAssert.assertMatch(spec, "/xyz/");
+ PathSpecAssert.assertMatch(spec, "/xyz/123");
+ PathSpecAssert.assertMatch(spec, "/xyz/123/");
+ PathSpecAssert.assertMatch(spec, "/xyz/123.txt");
+ PathSpecAssert.assertNotMatch(spec, "/xyz123");
+ PathSpecAssert.assertNotMatch(spec, "/xyz123;jessionid=99");
+ PathSpecAssert.assertNotMatch(spec, "/xyz123/");
+ PathSpecAssert.assertNotMatch(spec, "/xyz123/456");
+ PathSpecAssert.assertNotMatch(spec, "/xyz.123");
+ PathSpecAssert.assertNotMatch(spec, "/xyz;123"); // as if the ; was encoded and part of the path
+ PathSpecAssert.assertNotMatch(spec, "/xyz?123"); // as if the ? was encoded and part of the path
+ }
+
+ @Test
+ public void testPrecidenceVsOrdering() throws Exception
+ {
+ PathMappings<String> p = new PathMappings<>();
+ p.put(new ServletPathSpec("/dump/gzip/*"), "prefix");
+ p.put(new ServletPathSpec("*.txt"), "suffix");
+
+ assertEquals(null, p.getMatch("/foo/bar"));
+ assertEquals("prefix", p.getMatch("/dump/gzip/something").getResource());
+ assertEquals("suffix", p.getMatch("/foo/something.txt").getResource());
+ assertEquals("prefix", p.getMatch("/dump/gzip/something.txt").getResource());
+
+ p = new PathMappings<>();
+ p.put(new ServletPathSpec("*.txt"), "suffix");
+ p.put(new ServletPathSpec("/dump/gzip/*"), "prefix");
+
+ assertEquals(null, p.getMatch("/foo/bar"));
+ assertEquals("prefix", p.getMatch("/dump/gzip/something").getResource());
+ assertEquals("suffix", p.getMatch("/foo/something.txt").getResource());
+ assertEquals("prefix", p.getMatch("/dump/gzip/something.txt").getResource());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "*",
+ "/foo/*/bar",
+ "*/foo",
+ "*.foo/*"
+ })
+ public void testBadPathSpecs(String str)
+ {
+ assertThrows(IllegalArgumentException.class, () ->
+ {
+ new ServletPathSpec(str);
+ });
+ }
+
+ @Test
+ public void testPutRejectsDuplicates()
+ {
+ PathMappings<String> p = new PathMappings<>();
+ assertThat(p.put(new UriTemplatePathSpec("/a/{var1}/c"), "resourceA"), is(true));
+ assertThat(p.put(new UriTemplatePathSpec("/a/{var2}/c"), "resourceAA"), is(false));
+ assertThat(p.put(new UriTemplatePathSpec("/a/b/c"), "resourceB"), is(true));
+ assertThat(p.put(new UriTemplatePathSpec("/a/b/c"), "resourceBB"), is(false));
+ assertThat(p.put(new ServletPathSpec("/a/b/c"), "resourceBB"), is(false));
+ assertThat(p.put(new RegexPathSpec("/a/b/c"), "resourceBB"), is(false));
+
+ assertThat(p.put(new ServletPathSpec("/*"), "resourceC"), is(true));
+ assertThat(p.put(new RegexPathSpec("/(.*)"), "resourceCC"), is(true));
+ }
+
+ @Test
+ public void testGetUriTemplatePathSpec()
+ {
+ PathMappings<String> p = new PathMappings<>();
+ p.put(new UriTemplatePathSpec("/a/{var1}/c"), "resourceA");
+ p.put(new UriTemplatePathSpec("/a/b/c"), "resourceB");
+
+ assertThat(p.get(new UriTemplatePathSpec("/a/{var1}/c")), equalTo("resourceA"));
+ assertThat(p.get(new UriTemplatePathSpec("/a/{foo}/c")), equalTo("resourceA"));
+ assertThat(p.get(new UriTemplatePathSpec("/a/b/c")), equalTo("resourceB"));
+ assertThat(p.get(new UriTemplatePathSpec("/a/d/c")), nullValue());
+ assertThat(p.get(new RegexPathSpec("/a/b/c")), nullValue());
+ }
+
+ @Test
+ public void testGetRegexPathSpec()
+ {
+ PathMappings<String> p = new PathMappings<>();
+ p.put(new RegexPathSpec("/a/b/c"), "resourceA");
+ p.put(new RegexPathSpec("/(.*)/b/c"), "resourceB");
+ p.put(new RegexPathSpec("/a/(.*)/c"), "resourceC");
+ p.put(new RegexPathSpec("/a/b/(.*)"), "resourceD");
+
+ assertThat(p.get(new RegexPathSpec("/a/(.*)/c")), equalTo("resourceC"));
+ assertThat(p.get(new RegexPathSpec("/a/b/c")), equalTo("resourceA"));
+ assertThat(p.get(new RegexPathSpec("/(.*)/b/c")), equalTo("resourceB"));
+ assertThat(p.get(new RegexPathSpec("/a/b/(.*)")), equalTo("resourceD"));
+ assertThat(p.get(new RegexPathSpec("/a/d/c")), nullValue());
+ assertThat(p.get(new ServletPathSpec("/a/b/c")), nullValue());
+ }
+
+ @Test
+ public void testGetServletPathSpec()
+ {
+ PathMappings<String> p = new PathMappings<>();
+ p.put(new ServletPathSpec("/"), "resourceA");
+ p.put(new ServletPathSpec("/*"), "resourceB");
+ p.put(new ServletPathSpec("/a/*"), "resourceC");
+ p.put(new ServletPathSpec("*.do"), "resourceD");
+
+ assertThat(p.get(new ServletPathSpec("/")), equalTo("resourceA"));
+ assertThat(p.get(new ServletPathSpec("/*")), equalTo("resourceB"));
+ assertThat(p.get(new ServletPathSpec("/a/*")), equalTo("resourceC"));
+ assertThat(p.get(new ServletPathSpec("*.do")), equalTo("resourceD"));
+ assertThat(p.get(new ServletPathSpec("*.gz")), nullValue());
+ assertThat(p.get(new ServletPathSpec("/a/b/*")), nullValue());
+ assertThat(p.get(new ServletPathSpec("/a/d/c")), nullValue());
+ assertThat(p.get(new RegexPathSpec("/a/b/c")), nullValue());
+ }
+
+ @Test
+ public void testRemoveUriTemplatePathSpec()
+ {
+ PathMappings<String> p = new PathMappings<>();
+
+ p.put(new UriTemplatePathSpec("/a/{var1}/c"), "resourceA");
+ assertThat(p.remove(new UriTemplatePathSpec("/a/{var1}/c")), is(true));
+
+ p.put(new UriTemplatePathSpec("/a/{var1}/c"), "resourceA");
+ assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), is(false));
+ assertThat(p.remove(new UriTemplatePathSpec("/a/{b}/c")), is(true));
+ assertThat(p.remove(new UriTemplatePathSpec("/a/{b}/c")), is(false));
+
+ p.put(new UriTemplatePathSpec("/{var1}/b/c"), "resourceA");
+ assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), is(false));
+ assertThat(p.remove(new UriTemplatePathSpec("/{a}/b/c")), is(true));
+ assertThat(p.remove(new UriTemplatePathSpec("/{a}/b/c")), is(false));
+
+ p.put(new UriTemplatePathSpec("/a/b/{var1}"), "resourceA");
+ assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), is(false));
+ assertThat(p.remove(new UriTemplatePathSpec("/a/b/{c}")), is(true));
+ assertThat(p.remove(new UriTemplatePathSpec("/a/b/{c}")), is(false));
+
+ p.put(new UriTemplatePathSpec("/{var1}/{var2}/{var3}"), "resourceA");
+ assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), is(false));
+ assertThat(p.remove(new UriTemplatePathSpec("/{a}/{b}/{c}")), is(true));
+ assertThat(p.remove(new UriTemplatePathSpec("/{a}/{b}/{c}")), is(false));
+ }
+
+ @Test
+ public void testRemoveRegexPathSpec()
+ {
+ PathMappings<String> p = new PathMappings<>();
+
+ p.put(new RegexPathSpec("/a/(.*)/c"), "resourceA");
+ assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(false));
+ assertThat(p.remove(new RegexPathSpec("/a/(.*)/c")), is(true));
+ assertThat(p.remove(new RegexPathSpec("/a/(.*)/c")), is(false));
+
+ p.put(new RegexPathSpec("/(.*)/b/c"), "resourceA");
+ assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(false));
+ assertThat(p.remove(new RegexPathSpec("/(.*)/b/c")), is(true));
+ assertThat(p.remove(new RegexPathSpec("/(.*)/b/c")), is(false));
+
+ p.put(new RegexPathSpec("/a/b/(.*)"), "resourceA");
+ assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(false));
+ assertThat(p.remove(new RegexPathSpec("/a/b/(.*)")), is(true));
+ assertThat(p.remove(new RegexPathSpec("/a/b/(.*)")), is(false));
+
+ p.put(new RegexPathSpec("/a/b/c"), "resourceA");
+ assertThat(p.remove(new RegexPathSpec("/a/b/d")), is(false));
+ assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(true));
+ assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(false));
+ }
+
+ @Test
+ public void testRemoveServletPathSpec()
+ {
+ PathMappings<String> p = new PathMappings<>();
+
+ p.put(new ServletPathSpec("/a/*"), "resourceA");
+ assertThat(p.remove(new ServletPathSpec("/a/b")), is(false));
+ assertThat(p.remove(new ServletPathSpec("/a/*")), is(true));
+ assertThat(p.remove(new ServletPathSpec("/a/*")), is(false));
+
+ p.put(new ServletPathSpec("/a/b/*"), "resourceA");
+ assertThat(p.remove(new ServletPathSpec("/a/b/c")), is(false));
+ assertThat(p.remove(new ServletPathSpec("/a/b/*")), is(true));
+ assertThat(p.remove(new ServletPathSpec("/a/b/*")), is(false));
+
+ p.put(new ServletPathSpec("*.do"), "resourceA");
+ assertThat(p.remove(new ServletPathSpec("*.gz")), is(false));
+ assertThat(p.remove(new ServletPathSpec("*.do")), is(true));
+ assertThat(p.remove(new ServletPathSpec("*.do")), is(false));
+
+ p.put(new ServletPathSpec("/"), "resourceA");
+ assertThat(p.remove(new ServletPathSpec("/a")), is(false));
+ assertThat(p.remove(new ServletPathSpec("/")), is(true));
+ assertThat(p.remove(new ServletPathSpec("/")), is(false));
+
+ p.put(new ServletPathSpec(""), "resourceA");
+ assertThat(p.remove(new ServletPathSpec("/")), is(false));
+ assertThat(p.remove(new ServletPathSpec("")), is(true));
+ assertThat(p.remove(new ServletPathSpec("")), is(false));
+
+ p.put(new ServletPathSpec("/a/b/c"), "resourceA");
+ assertThat(p.remove(new ServletPathSpec("/a/b/d")), is(false));
+ assertThat(p.remove(new ServletPathSpec("/a/b/c")), is(true));
+ assertThat(p.remove(new ServletPathSpec("/a/b/c")), is(false));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/PathSpecAssert.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/PathSpecAssert.java
new file mode 100644
index 0000000..a26e16f
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/PathSpecAssert.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class PathSpecAssert
+{
+ public static void assertMatch(PathSpec spec, String path)
+ {
+ boolean match = spec.matches(path);
+ assertThat(spec.getClass().getSimpleName() + " '" + spec + "' should match path '" + path + "'", match, is(true));
+ }
+
+ public static void assertNotMatch(PathSpec spec, String path)
+ {
+ boolean match = spec.matches(path);
+ assertThat(spec.getClass().getSimpleName() + " '" + spec + "' should not match path '" + path + "'", match, is(false));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/RegexPathSpecTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/RegexPathSpecTest.java
new file mode 100644
index 0000000..d03c0b3
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/RegexPathSpecTest.java
@@ -0,0 +1,147 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class RegexPathSpecTest
+{
+ public static void assertMatches(PathSpec spec, String path)
+ {
+ String msg = String.format("Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path);
+ assertThat(msg, spec.matches(path), is(true));
+ }
+
+ public static void assertNotMatches(PathSpec spec, String path)
+ {
+ String msg = String.format("!Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path);
+ assertThat(msg, spec.matches(path), is(false));
+ }
+
+ @Test
+ public void testExactSpec()
+ {
+ RegexPathSpec spec = new RegexPathSpec("^/a$");
+ assertEquals("^/a$", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/a$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(1, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.EXACT, spec.getGroup(), "Spec.group");
+
+ assertMatches(spec, "/a");
+
+ assertNotMatches(spec, "/aa");
+ assertNotMatches(spec, "/a/");
+ }
+
+ @Test
+ public void testMiddleSpec()
+ {
+ RegexPathSpec spec = new RegexPathSpec("^/rest/([^/]*)/list$");
+ assertEquals("^/rest/([^/]*)/list$", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/rest/([^/]*)/list$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(3, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.MIDDLE_GLOB, spec.getGroup(), "Spec.group");
+
+ assertMatches(spec, "/rest/api/list");
+ assertMatches(spec, "/rest/1.0/list");
+ assertMatches(spec, "/rest/2.0/list");
+ assertMatches(spec, "/rest/accounts/list");
+
+ assertNotMatches(spec, "/a");
+ assertNotMatches(spec, "/aa");
+ assertNotMatches(spec, "/aa/bb");
+ assertNotMatches(spec, "/rest/admin/delete");
+ assertNotMatches(spec, "/rest/list");
+ }
+
+ @Test
+ public void testMiddleSpecNoGrouping()
+ {
+ RegexPathSpec spec = new RegexPathSpec("^/rest/[^/]+/list$");
+ assertEquals("^/rest/[^/]+/list$", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/rest/[^/]+/list$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(3, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.MIDDLE_GLOB, spec.getGroup(), "Spec.group");
+
+ assertMatches(spec, "/rest/api/list");
+ assertMatches(spec, "/rest/1.0/list");
+ assertMatches(spec, "/rest/2.0/list");
+ assertMatches(spec, "/rest/accounts/list");
+
+ assertNotMatches(spec, "/a");
+ assertNotMatches(spec, "/aa");
+ assertNotMatches(spec, "/aa/bb");
+ assertNotMatches(spec, "/rest/admin/delete");
+ assertNotMatches(spec, "/rest/list");
+ }
+
+ @Test
+ public void testPrefixSpec()
+ {
+ RegexPathSpec spec = new RegexPathSpec("^/a/(.*)$");
+ assertEquals("^/a/(.*)$", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/a/(.*)$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(2, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.PREFIX_GLOB, spec.getGroup(), "Spec.group");
+
+ assertMatches(spec, "/a/");
+ assertMatches(spec, "/a/b");
+ assertMatches(spec, "/a/b/c/d/e");
+
+ assertNotMatches(spec, "/a");
+ assertNotMatches(spec, "/aa");
+ assertNotMatches(spec, "/aa/bb");
+ }
+
+ @Test
+ public void testSuffixSpec()
+ {
+ RegexPathSpec spec = new RegexPathSpec("^(.*).do$");
+ assertEquals("^(.*).do$", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^(.*).do$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(0, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.SUFFIX_GLOB, spec.getGroup(), "Spec.group");
+
+ assertMatches(spec, "/a.do");
+ assertMatches(spec, "/a/b/c.do");
+ assertMatches(spec, "/abcde.do");
+ assertMatches(spec, "/abc/efg.do");
+
+ assertNotMatches(spec, "/a");
+ assertNotMatches(spec, "/aa");
+ assertNotMatches(spec, "/aa/bb");
+ assertNotMatches(spec, "/aa/bb.do/more");
+ }
+
+ @Test
+ public void testEquals()
+ {
+ assertThat(new RegexPathSpec("^(.*).do$"), equalTo(new RegexPathSpec("^(.*).do$")));
+ assertThat(new RegexPathSpec("/foo"), equalTo(new RegexPathSpec("/foo")));
+ assertThat(new RegexPathSpec("^(.*).do$"), not(equalTo(new RegexPathSpec("^(.*).gz$"))));
+ assertThat(new RegexPathSpec("^(.*).do$"), not(equalTo(new RegexPathSpec("^.*.do$"))));
+ assertThat(new RegexPathSpec("/foo"), not(equalTo(new ServletPathSpec("/foo"))));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/ServletPathSpecMatchListTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/ServletPathSpecMatchListTest.java
new file mode 100644
index 0000000..37bbc42
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/ServletPathSpecMatchListTest.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Tests of {@link PathMappings#getMatches(String)}
+ */
+@SuppressWarnings("Duplicates")
+public class ServletPathSpecMatchListTest
+{
+ public static Stream<Arguments> data()
+ {
+ ArrayList<Arguments> data = new ArrayList<>();
+
+ // From old PathMapTest
+ data.add(Arguments.of("All matches", "/animal/bird/path.tar.gz", "[/animal/bird/*=birds, /animal/*=animals, *.tar.gz=tarball, *.gz=gzipped, /=default]"));
+ data.add(Arguments.of("Dir matches", "/animal/fish/", "[/animal/fish/*=fishes, /animal/*=animals, /=default]"));
+ data.add(Arguments.of("Dir matches", "/animal/fish", "[/animal/fish/*=fishes, /animal/*=animals, /=default]"));
+ data.add(Arguments.of("Root matches", "/", "[=root, /=default]"));
+ data.add(Arguments.of("Dir matches", "", "[/=default]"));
+
+ return data.stream();
+ }
+
+ private static PathMappings<String> mappings;
+
+ static
+ {
+ mappings = new PathMappings<>();
+
+ // From old PathMapTest
+ mappings.put(new ServletPathSpec("/abs/path"), "abspath"); // 1
+ mappings.put(new ServletPathSpec("/abs/path/longer"), "longpath"); // 2
+ mappings.put(new ServletPathSpec("/animal/bird/*"), "birds"); // 3
+ mappings.put(new ServletPathSpec("/animal/fish/*"), "fishes"); // 4
+ mappings.put(new ServletPathSpec("/animal/*"), "animals"); // 5
+ mappings.put(new ServletPathSpec("*.tar.gz"), "tarball"); // 6
+ mappings.put(new ServletPathSpec("*.gz"), "gzipped"); // 7
+ mappings.put(new ServletPathSpec("/"), "default"); // 8
+ // 9 was the old Jetty ":" spec delimited case (no longer valid)
+ mappings.put(new ServletPathSpec(""), "root"); // 10
+ mappings.put(new ServletPathSpec("/\u20ACuro/*"), "money"); // 11
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testGetMatches(String message, String inputPath, String expectedListing)
+ {
+ List<MappedResource<String>> matches = mappings.getMatches(inputPath);
+
+ StringBuilder actual = new StringBuilder();
+ actual.append('[');
+ boolean delim = false;
+ for (MappedResource<String> res : matches)
+ {
+ if (delim)
+ actual.append(", ");
+ actual.append(res.getPathSpec().getDeclaration()).append('=').append(res.getResource());
+ delim = true;
+ }
+ actual.append(']');
+
+ assertThat(message + " on [" + inputPath + "]", actual.toString(), is(expectedListing));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/ServletPathSpecOrderTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/ServletPathSpecOrderTest.java
new file mode 100644
index 0000000..945cc99
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/ServletPathSpecOrderTest.java
@@ -0,0 +1,97 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import java.util.ArrayList;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Tests of {@link PathMappings#getMatch(String)}, with a focus on correct mapping selection order
+ */
+@SuppressWarnings("Duplicates")
+public class ServletPathSpecOrderTest
+{
+ public static Stream<Arguments> data()
+ {
+ ArrayList<Arguments> data = new ArrayList<>();
+
+ // From old PathMapTest
+ data.add(Arguments.of("/abs/path", "abspath"));
+ data.add(Arguments.of("/abs/path/xxx", "default"));
+ data.add(Arguments.of("/abs/pith", "default"));
+ data.add(Arguments.of("/abs/path/longer", "longpath"));
+ data.add(Arguments.of("/abs/path/", "default"));
+ data.add(Arguments.of("/abs/path/foo", "default"));
+ data.add(Arguments.of("/animal/bird/eagle/bald", "birds"));
+ data.add(Arguments.of("/animal/fish/shark/hammerhead", "fishes"));
+ data.add(Arguments.of("/animal/insect/ladybug", "animals"));
+ data.add(Arguments.of("/animal", "animals"));
+ data.add(Arguments.of("/animal/", "animals"));
+ data.add(Arguments.of("/animal/other", "animals"));
+ data.add(Arguments.of("/animal/*", "animals"));
+ data.add(Arguments.of("/downloads/distribution.tar.gz", "tarball"));
+ data.add(Arguments.of("/downloads/script.gz", "gzipped"));
+ data.add(Arguments.of("/animal/arhive.gz", "animals"));
+ data.add(Arguments.of("/Other/path", "default"));
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+ data.add(Arguments.of("/\u20ACuro/path", "money"));
+ data.add(Arguments.of("/", "root"));
+
+ // Extra tests
+ data.add(Arguments.of("/downloads/readme.txt", "default"));
+ data.add(Arguments.of("/downloads/logs.tgz", "default"));
+ data.add(Arguments.of("/main.css", "default"));
+
+ return data.stream();
+ }
+
+ private static PathMappings<String> mappings;
+
+ static
+ {
+ mappings = new PathMappings<>();
+
+ // From old PathMapTest
+ mappings.put(new ServletPathSpec("/abs/path"), "abspath"); // 1
+ mappings.put(new ServletPathSpec("/abs/path/longer"), "longpath"); // 2
+ mappings.put(new ServletPathSpec("/animal/bird/*"), "birds"); // 3
+ mappings.put(new ServletPathSpec("/animal/fish/*"), "fishes"); // 4
+ mappings.put(new ServletPathSpec("/animal/*"), "animals"); // 5
+ mappings.put(new ServletPathSpec("*.tar.gz"), "tarball"); // 6
+ mappings.put(new ServletPathSpec("*.gz"), "gzipped"); // 7
+ mappings.put(new ServletPathSpec("/"), "default"); // 8
+ // 9 was the old Jetty ":" spec delimited case (no longer valid)
+ mappings.put(new ServletPathSpec(""), "root"); // 10
+ mappings.put(new ServletPathSpec("/\u20ACuro/*"), "money"); // 11
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testMatch(String inputPath, String expectedResource)
+ {
+ assertThat("Match on [" + inputPath + "]", mappings.getMatch(inputPath).getResource(), is(expectedResource));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/ServletPathSpecTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/ServletPathSpecTest.java
new file mode 100644
index 0000000..c169308
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/ServletPathSpecTest.java
@@ -0,0 +1,205 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class ServletPathSpecTest
+{
+ private void assertBadServletPathSpec(String pathSpec)
+ {
+ try
+ {
+ new ServletPathSpec(pathSpec);
+ fail("Expected IllegalArgumentException for a bad servlet pathspec on: " + pathSpec);
+ }
+ catch (IllegalArgumentException e)
+ {
+ // expected path
+ System.out.println(e);
+ }
+ }
+
+ private void assertMatches(ServletPathSpec spec, String path)
+ {
+ String msg = String.format("Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path);
+ assertThat(msg, spec.matches(path), is(true));
+ }
+
+ private void assertNotMatches(ServletPathSpec spec, String path)
+ {
+ String msg = String.format("!Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path);
+ assertThat(msg, spec.matches(path), is(false));
+ }
+
+ @Test
+ public void testBadServletPathSpecA()
+ {
+ assertBadServletPathSpec("foo");
+ }
+
+ @Test
+ public void testBadServletPathSpecB()
+ {
+ assertBadServletPathSpec("/foo/*.do");
+ }
+
+ @Test
+ public void testBadServletPathSpecC()
+ {
+ assertBadServletPathSpec("foo/*.do");
+ }
+
+ @Test
+ public void testBadServletPathSpecD()
+ {
+ assertBadServletPathSpec("foo/*.*do");
+ }
+
+ @Test
+ public void testBadServletPathSpecE()
+ {
+ assertBadServletPathSpec("*do");
+ }
+
+ @Test
+ public void testDefaultPathSpec()
+ {
+ ServletPathSpec spec = new ServletPathSpec("/");
+ assertEquals("/", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals(-1, spec.getPathDepth(), "Spec.pathDepth");
+ }
+
+ @Test
+ public void testExactPathSpec()
+ {
+ ServletPathSpec spec = new ServletPathSpec("/abs/path");
+ assertEquals("/abs/path", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals(2, spec.getPathDepth(), "Spec.pathDepth");
+
+ assertMatches(spec, "/abs/path");
+
+ assertNotMatches(spec, "/abs/path/");
+ assertNotMatches(spec, "/abs/path/more");
+ assertNotMatches(spec, "/foo");
+ assertNotMatches(spec, "/foo/abs/path");
+ assertNotMatches(spec, "/foo/abs/path/");
+ }
+
+ @Test
+ public void testGetPathInfo()
+ {
+ assertEquals(null, new ServletPathSpec("/Foo/bar").getPathInfo("/Foo/bar"), "pathInfo exact");
+ assertEquals("/bar", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/bar"), "pathInfo prefix");
+ assertEquals("/*", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/*"), "pathInfo prefix");
+ assertEquals("/", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/"), "pathInfo prefix");
+ assertEquals(null, new ServletPathSpec("/Foo/*").getPathInfo("/Foo"), "pathInfo prefix");
+ assertEquals(null, new ServletPathSpec("*.ext").getPathInfo("/Foo/bar.ext"), "pathInfo suffix");
+ assertEquals(null, new ServletPathSpec("/").getPathInfo("/Foo/bar.ext"), "pathInfo default");
+ assertEquals("/", new ServletPathSpec("").getPathInfo("/"), "pathInfo root");
+ assertEquals("", new ServletPathSpec("").getPathInfo(""), "pathInfo root");
+ assertEquals("/xxx/zzz", new ServletPathSpec("/*").getPathInfo("/xxx/zzz"), "pathInfo default");
+ }
+
+ @Test
+ public void testNullPathSpec()
+ {
+ ServletPathSpec spec = new ServletPathSpec(null);
+ assertEquals("", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals(-1, spec.getPathDepth(), "Spec.pathDepth");
+ }
+
+ @Test
+ public void testRootPathSpec()
+ {
+ ServletPathSpec spec = new ServletPathSpec("");
+ assertEquals("", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals(-1, spec.getPathDepth(), "Spec.pathDepth");
+ }
+
+ @Test
+ public void testPathMatch()
+ {
+ assertEquals("/Foo/bar", new ServletPathSpec("/Foo/bar").getPathMatch("/Foo/bar"), "pathMatch exact");
+ assertEquals("/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo/bar"), "pathMatch prefix");
+ assertEquals("/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo/"), "pathMatch prefix");
+ assertEquals("/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo"), "pathMatch prefix");
+ assertEquals("/Foo/bar.ext", new ServletPathSpec("*.ext").getPathMatch("/Foo/bar.ext"), "pathMatch suffix");
+ assertEquals("/Foo/bar.ext", new ServletPathSpec("/").getPathMatch("/Foo/bar.ext"), "pathMatch default");
+ assertEquals("", new ServletPathSpec("").getPathMatch("/"), "pathInfo root");
+ assertEquals("", new ServletPathSpec("").getPathMatch(""), "pathInfo root");
+ assertEquals("", new ServletPathSpec("/*").getPathMatch("/xxx/zzz"), "pathMatch default");
+ }
+
+ @Test
+ public void testPrefixPathSpec()
+ {
+ ServletPathSpec spec = new ServletPathSpec("/downloads/*");
+ assertEquals("/downloads/*", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals(2, spec.getPathDepth(), "Spec.pathDepth");
+
+ assertMatches(spec, "/downloads/logo.jpg");
+ assertMatches(spec, "/downloads/distribution.tar.gz");
+ assertMatches(spec, "/downloads/distribution.tgz");
+ assertMatches(spec, "/downloads/distribution.zip");
+
+ assertMatches(spec, "/downloads");
+
+ assertEquals("/", spec.getPathInfo("/downloads/"), "Spec.pathInfo");
+ assertEquals("/distribution.zip", spec.getPathInfo("/downloads/distribution.zip"), "Spec.pathInfo");
+ assertEquals("/dist/9.0/distribution.tar.gz", spec.getPathInfo("/downloads/dist/9.0/distribution.tar.gz"), "Spec.pathInfo");
+ }
+
+ @Test
+ public void testSuffixPathSpec()
+ {
+ ServletPathSpec spec = new ServletPathSpec("*.gz");
+ assertEquals("*.gz", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals(0, spec.getPathDepth(), "Spec.pathDepth");
+
+ assertMatches(spec, "/downloads/distribution.tar.gz");
+ assertMatches(spec, "/downloads/jetty.log.gz");
+
+ assertNotMatches(spec, "/downloads/distribution.zip");
+ assertNotMatches(spec, "/downloads/distribution.tgz");
+ assertNotMatches(spec, "/abs/path");
+
+ assertEquals(null, spec.getPathInfo("/downloads/distribution.tar.gz"), "Spec.pathInfo");
+ }
+
+ @Test
+ public void testEquals()
+ {
+ assertThat(new ServletPathSpec("*.gz"), equalTo(new ServletPathSpec("*.gz")));
+ assertThat(new ServletPathSpec("/foo"), equalTo(new ServletPathSpec("/foo")));
+ assertThat(new ServletPathSpec("/foo/bar"), equalTo(new ServletPathSpec("/foo/bar")));
+ assertThat(new ServletPathSpec("*.gz"), not(equalTo(new ServletPathSpec("*.do"))));
+ assertThat(new ServletPathSpec("/foo"), not(equalTo(new ServletPathSpec("/bar"))));
+ assertThat(new ServletPathSpec("/bar/foo"), not(equalTo(new ServletPathSpec("/foo/bar"))));
+ assertThat(new ServletPathSpec("/foo"), not(equalTo(new RegexPathSpec("/foo"))));
+ }
+
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/UriTemplatePathSpecBadSpecsTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/UriTemplatePathSpecBadSpecsTest.java
new file mode 100644
index 0000000..2730eae
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/UriTemplatePathSpecBadSpecsTest.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests for bad path specs on ServerEndpoint Path Param / URI Template
+ */
+public class UriTemplatePathSpecBadSpecsTest
+{
+ public static Stream<Arguments> data()
+ {
+ String[] badSpecs = new String[]{
+ "/a/b{var}", // bad syntax - variable does not encompass whole path segment
+ "a/{var}", // bad syntax - no start slash
+ "/a/{var/b}", // path segment separator in variable name
+ "/{var}/*", // bad syntax - no globs allowed
+ "/{var}.do", // bad syntax - variable does not encompass whole path segment
+ "/a/{var*}", // use of glob character not allowed in variable name
+ "/a/{}", // bad syntax - no variable name
+ // MIGHT BE ALLOWED "/a/{---}", // no alpha in variable name
+ "{var}", // bad syntax - no start slash
+ "/a/{my special variable}", // bad syntax - space in variable name
+ "/a/{var}/{var}", // variable name duplicate
+ // MIGHT BE ALLOWED "/a/{var}/{Var}/{vAR}", // variable name duplicated (diff case)
+ "/a/../../../{var}", // path navigation not allowed
+ "/a/./{var}", // path navigation not allowed
+ "/a//{var}" // bad syntax - double path slash (no path segment)
+ };
+
+ return Stream.of(badSpecs).map(Arguments::of);
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}")
+ @MethodSource("data")
+ public void testBadPathSpec(String pathSpec)
+ {
+ assertThrows(IllegalArgumentException.class, () -> new UriTemplatePathSpec(pathSpec));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/UriTemplatePathSpecTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/UriTemplatePathSpecTest.java
new file mode 100644
index 0000000..f58d4f2
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/UriTemplatePathSpecTest.java
@@ -0,0 +1,297 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Tests for URI Template Path Specs
+ */
+public class UriTemplatePathSpecTest
+{
+ private void assertDetectedVars(UriTemplatePathSpec spec, String... expectedVars)
+ {
+ String prefix = String.format("Spec(\"%s\")", spec.getDeclaration());
+ assertEquals(expectedVars.length, spec.getVariableCount(), prefix + ".variableCount");
+ assertEquals(expectedVars.length, spec.getVariables().length, prefix + ".variable.length");
+ for (int i = 0; i < expectedVars.length; i++)
+ {
+ assertThat(String.format("%s.variable[%d]", prefix, i), spec.getVariables()[i], is(expectedVars[i]));
+ }
+ }
+
+ private void assertMatches(PathSpec spec, String path)
+ {
+ String msg = String.format("Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path);
+ assertThat(msg, spec.matches(path), is(true));
+ }
+
+ private void assertNotMatches(PathSpec spec, String path)
+ {
+ String msg = String.format("!Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path);
+ assertThat(msg, spec.matches(path), is(false));
+ }
+
+ @Test
+ public void testDefaultPathSpec()
+ {
+ UriTemplatePathSpec spec = new UriTemplatePathSpec("/");
+ assertEquals("/", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(1, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.EXACT, spec.getGroup(), "Spec.group");
+
+ assertEquals(0, spec.getVariableCount(), "Spec.variableCount");
+ assertEquals(0, spec.getVariables().length, "Spec.variable.length");
+ }
+
+ @Test
+ public void testExactOnePathSpec()
+ {
+ UriTemplatePathSpec spec = new UriTemplatePathSpec("/a");
+ assertEquals("/a", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/a$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(1, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.EXACT, spec.getGroup(), "Spec.group");
+
+ assertMatches(spec, "/a");
+ assertMatches(spec, "/a?type=other");
+ assertNotMatches(spec, "/a/b");
+ assertNotMatches(spec, "/a/");
+
+ assertEquals(0, spec.getVariableCount(), "Spec.variableCount");
+ assertEquals(0, spec.getVariables().length, "Spec.variable.length");
+ }
+
+ @Test
+ public void testExactPathSpecTestWebapp()
+ {
+ UriTemplatePathSpec spec = new UriTemplatePathSpec("/deep.thought/");
+ assertEquals("/deep.thought/", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/deep\\.thought/$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(1, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.EXACT, spec.getGroup(), "Spec.group");
+
+ assertMatches(spec, "/deep.thought/");
+ assertNotMatches(spec, "/deep.thought");
+
+ assertEquals(0, spec.getVariableCount(), "Spec.variableCount");
+ assertEquals(0, spec.getVariables().length, "Spec.variable.length");
+ }
+
+ @Test
+ public void testExactTwoPathSpec()
+ {
+ UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/b");
+ assertEquals("/a/b", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/a/b$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(2, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.EXACT, spec.getGroup(), "Spec.group");
+
+ assertEquals(0, spec.getVariableCount(), "Spec.variableCount");
+ assertEquals(0, spec.getVariables().length, "Spec.variable.length");
+
+ assertMatches(spec, "/a/b");
+
+ assertNotMatches(spec, "/a/b/");
+ assertNotMatches(spec, "/a/");
+ assertNotMatches(spec, "/a/bb");
+ }
+
+ @Test
+ public void testMiddleVarPathSpec()
+ {
+ UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/{var}/c");
+ assertEquals("/a/{var}/c", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/a/([^/]+)/c$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(3, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.MIDDLE_GLOB, spec.getGroup(), "Spec.group");
+
+ assertDetectedVars(spec, "var");
+
+ assertMatches(spec, "/a/b/c");
+ assertMatches(spec, "/a/zz/c");
+ assertMatches(spec, "/a/hello+world/c");
+ assertNotMatches(spec, "/a/bc");
+ assertNotMatches(spec, "/a/b/");
+ assertNotMatches(spec, "/a/b");
+
+ Map<String, String> mapped = spec.getPathParams("/a/b/c");
+ assertThat("Spec.pathParams", mapped, notNullValue());
+ assertThat("Spec.pathParams.size", mapped.size(), is(1));
+ assertEquals("b", mapped.get("var"), "Spec.pathParams[var]");
+ }
+
+ @Test
+ public void testOneVarPathSpec()
+ {
+ UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/{foo}");
+ assertEquals("/a/{foo}", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/a/([^/]+)$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(2, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.PREFIX_GLOB, spec.getGroup(), "Spec.group");
+
+ assertDetectedVars(spec, "foo");
+
+ assertMatches(spec, "/a/b");
+ assertNotMatches(spec, "/a/");
+ assertNotMatches(spec, "/a");
+
+ Map<String, String> mapped = spec.getPathParams("/a/b");
+ assertThat("Spec.pathParams", mapped, notNullValue());
+ assertThat("Spec.pathParams.size", mapped.size(), is(1));
+ assertEquals("b", mapped.get("foo"), "Spec.pathParams[foo]");
+ }
+
+ @Test
+ public void testOneVarSuffixPathSpec()
+ {
+ UriTemplatePathSpec spec = new UriTemplatePathSpec("/{var}/b/c");
+ assertEquals("/{var}/b/c", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/([^/]+)/b/c$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(3, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.SUFFIX_GLOB, spec.getGroup(), "Spec.group");
+
+ assertDetectedVars(spec, "var");
+
+ assertMatches(spec, "/a/b/c");
+ assertMatches(spec, "/az/b/c");
+ assertMatches(spec, "/hello+world/b/c");
+ assertNotMatches(spec, "/a/bc");
+ assertNotMatches(spec, "/a/b/");
+ assertNotMatches(spec, "/a/b");
+
+ Map<String, String> mapped = spec.getPathParams("/a/b/c");
+ assertThat("Spec.pathParams", mapped, notNullValue());
+ assertThat("Spec.pathParams.size", mapped.size(), is(1));
+ assertEquals("a", mapped.get("var"), "Spec.pathParams[var]");
+ }
+
+ @Test
+ public void testTwoVarComplexInnerPathSpec()
+ {
+ UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/{var1}/c/{var2}/e");
+ assertEquals("/a/{var1}/c/{var2}/e", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/a/([^/]+)/c/([^/]+)/e$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(5, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.MIDDLE_GLOB, spec.getGroup(), "Spec.group");
+
+ assertDetectedVars(spec, "var1", "var2");
+
+ assertMatches(spec, "/a/b/c/d/e");
+ assertNotMatches(spec, "/a/bc/d/e");
+ assertNotMatches(spec, "/a/b/d/e");
+ assertNotMatches(spec, "/a/b//d/e");
+
+ Map<String, String> mapped = spec.getPathParams("/a/b/c/d/e");
+ assertThat("Spec.pathParams", mapped, notNullValue());
+ assertThat("Spec.pathParams.size", mapped.size(), is(2));
+ assertEquals("b", mapped.get("var1"), "Spec.pathParams[var1]");
+ assertEquals("d", mapped.get("var2"), "Spec.pathParams[var2]");
+ }
+
+ @Test
+ public void testTwoVarComplexOuterPathSpec()
+ {
+ UriTemplatePathSpec spec = new UriTemplatePathSpec("/{var1}/b/{var2}/{var3}");
+ assertEquals("/{var1}/b/{var2}/{var3}", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/([^/]+)/b/([^/]+)/([^/]+)$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(4, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.MIDDLE_GLOB, spec.getGroup(), "Spec.group");
+
+ assertDetectedVars(spec, "var1", "var2", "var3");
+
+ assertMatches(spec, "/a/b/c/d");
+ assertNotMatches(spec, "/a/bc/d/e");
+ assertNotMatches(spec, "/a/c/d/e");
+ assertNotMatches(spec, "/a//d/e");
+
+ Map<String, String> mapped = spec.getPathParams("/a/b/c/d");
+ assertThat("Spec.pathParams", mapped, notNullValue());
+ assertThat("Spec.pathParams.size", mapped.size(), is(3));
+ assertEquals("a", mapped.get("var1"), "Spec.pathParams[var1]");
+ assertEquals("c", mapped.get("var2"), "Spec.pathParams[var2]");
+ assertEquals("d", mapped.get("var3"), "Spec.pathParams[var3]");
+ }
+
+ @Test
+ public void testTwoVarPrefixPathSpec()
+ {
+ UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/{var1}/{var2}");
+ assertEquals("/a/{var1}/{var2}", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/a/([^/]+)/([^/]+)$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(3, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.PREFIX_GLOB, spec.getGroup(), "Spec.group");
+
+ assertDetectedVars(spec, "var1", "var2");
+
+ assertMatches(spec, "/a/b/c");
+ assertNotMatches(spec, "/a/bc");
+ assertNotMatches(spec, "/a/b/");
+ assertNotMatches(spec, "/a/b");
+
+ Map<String, String> mapped = spec.getPathParams("/a/b/c");
+ assertThat("Spec.pathParams", mapped, notNullValue());
+ assertThat("Spec.pathParams.size", mapped.size(), is(2));
+ assertEquals("b", mapped.get("var1"), "Spec.pathParams[var1]");
+ assertEquals("c", mapped.get("var2"), "Spec.pathParams[var2]");
+ }
+
+ @Test
+ public void testVarOnlyPathSpec()
+ {
+ UriTemplatePathSpec spec = new UriTemplatePathSpec("/{var1}");
+ assertEquals("/{var1}", spec.getDeclaration(), "Spec.pathSpec");
+ assertEquals("^/([^/]+)$", spec.getPattern().pattern(), "Spec.pattern");
+ assertEquals(1, spec.getPathDepth(), "Spec.pathDepth");
+ assertEquals(PathSpecGroup.PREFIX_GLOB, spec.getGroup(), "Spec.group");
+
+ assertDetectedVars(spec, "var1");
+
+ assertMatches(spec, "/a");
+ assertNotMatches(spec, "/");
+ assertNotMatches(spec, "/a/b");
+ assertNotMatches(spec, "/a/b/c");
+
+ Map<String, String> mapped = spec.getPathParams("/a");
+ assertThat("Spec.pathParams", mapped, notNullValue());
+ assertThat("Spec.pathParams.size", mapped.size(), is(1));
+ assertEquals("a", mapped.get("var1"), "Spec.pathParams[var1]");
+ }
+
+ @Test
+ public void testEquals()
+ {
+ assertThat(new UriTemplatePathSpec("/{var1}"), equalTo(new UriTemplatePathSpec("/{var1}")));
+ assertThat(new UriTemplatePathSpec("/{var1}"), equalTo(new UriTemplatePathSpec("/{var2}")));
+ assertThat(new UriTemplatePathSpec("/{var1}/{var2}"), equalTo(new UriTemplatePathSpec("/{var2}/{var1}")));
+ assertThat(new UriTemplatePathSpec("/{var1}"), not(equalTo(new UriTemplatePathSpec("/{var1}/{var2}"))));
+ assertThat(new UriTemplatePathSpec("/a/b/c"), not(equalTo(new UriTemplatePathSpec("/a/{var}/c"))));
+ assertThat(new UriTemplatePathSpec("/foo"), not(equalTo(new ServletPathSpec("/foo"))));
+ }
+}
diff --git a/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/WebSocketUriMappingTest.java b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/WebSocketUriMappingTest.java
new file mode 100644
index 0000000..0889a05
--- /dev/null
+++ b/third_party/jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/WebSocketUriMappingTest.java
@@ -0,0 +1,182 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.http.pathmap;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class WebSocketUriMappingTest
+{
+ private PathMappings<String> mapping = new PathMappings<>();
+
+ private String getBestMatch(String uriPath)
+ {
+ List<MappedResource<String>> resources = mapping.getMatches(uriPath);
+ assertThat("Matches on " + uriPath, resources, is(not(nullValue())));
+ if (resources.isEmpty())
+ return null;
+ return resources.get(0).getResource();
+ }
+
+ @Test
+ public void testJsrExampleI()
+ {
+ mapping.put("/a/b", "endpointA");
+
+ assertThat(getBestMatch("/a/b"), is("endpointA"));
+ assertNull(getBestMatch("/a/c"));
+ }
+
+ @Test
+ public void testJsrExampleII()
+ {
+ mapping.put(new UriTemplatePathSpec("/a/{var}"), "endpointA");
+
+ assertThat(getBestMatch("/a/b"), is("endpointA"));
+ assertThat(getBestMatch("/a/apple"), is("endpointA"));
+ assertNull(getBestMatch("/a"));
+ assertNull(getBestMatch("/a/b/c"));
+ }
+
+ @Test
+ public void testJsrExampleIII()
+ {
+ mapping.put(new UriTemplatePathSpec("/a/{var}/c"), "endpointA");
+ mapping.put(new UriTemplatePathSpec("/a/b/c"), "endpointB");
+ mapping.put(new UriTemplatePathSpec("/a/{var1}/{var2}"), "endpointC");
+
+ assertThat(getBestMatch("/a/b/c"), is("endpointB"));
+ assertThat(getBestMatch("/a/d/c"), is("endpointA"));
+ assertThat(getBestMatch("/a/x/y"), is("endpointC"));
+ }
+
+ @Test
+ public void testJsrExampleIV()
+ {
+ mapping.put(new UriTemplatePathSpec("/{var1}/d"), "endpointA");
+ mapping.put(new UriTemplatePathSpec("/b/{var2}"), "endpointB");
+
+ assertThat(getBestMatch("/b/d"), is("endpointB"));
+ }
+
+ @Test
+ public void testPrefixVsSuffix()
+ {
+ mapping.put(new UriTemplatePathSpec("/{a}/b"), "suffix");
+ mapping.put(new UriTemplatePathSpec("/{a}/{b}"), "prefix");
+
+ List<MappedResource<String>> matches = mapping.getMatches("/a/b");
+
+ assertThat(getBestMatch("/a/b"), is("suffix"));
+ }
+
+ @Test
+ public void testMiddleVsSuffix()
+ {
+ mapping.put(new UriTemplatePathSpec("/a/{b}/c"), "middle");
+ mapping.put(new UriTemplatePathSpec("/a/b/{c}"), "suffix");
+
+ assertThat(getBestMatch("/a/b/c"), is("suffix"));
+ }
+
+ @Test
+ public void testMiddleVsSuffix2()
+ {
+ mapping.put(new UriTemplatePathSpec("/{a}/b/{c}"), "middle");
+ mapping.put(new UriTemplatePathSpec("/{a}/b/c"), "suffix");
+
+ assertThat(getBestMatch("/a/b/c"), is("suffix"));
+ }
+
+ @Test
+ public void testMiddleVsPrefix()
+ {
+ mapping.put(new UriTemplatePathSpec("/a/{b}/{c}/d"), "middle");
+ mapping.put(new UriTemplatePathSpec("/a/b/c/{d}"), "prefix");
+
+ assertThat(getBestMatch("/a/b/c/d"), is("prefix"));
+ }
+
+ @Test
+ public void testMiddleVsMiddle()
+ {
+ // This works but only because its an alphabetical check and '{' > 'c'.
+ mapping.put(new UriTemplatePathSpec("/a/{b}/{c}/d"), "middle1");
+ mapping.put(new UriTemplatePathSpec("/a/{b}/c/d"), "middle2");
+
+ assertThat(getBestMatch("/a/b/c/d"), is("middle2"));
+ }
+
+ @Test
+ public void testMiddleVsMiddle2()
+ {
+ mapping.put(new UriTemplatePathSpec("/{a}/{bz}/c/{d}"), "middle1");
+ mapping.put(new UriTemplatePathSpec("/{a}/{ba}/{c}/d"), "middle2");
+
+ assertThat(getBestMatch("/a/b/c/d"), is("middle1"));
+ }
+
+ @Test
+ public void testMiddleVsMiddle3()
+ {
+ mapping.put(new UriTemplatePathSpec("/{a}/{ba}/c/{d}"), "middle1");
+ mapping.put(new UriTemplatePathSpec("/{a}/{bz}/{c}/d"), "middle2");
+
+ assertThat(getBestMatch("/a/b/c/d"), is("middle1"));
+ }
+
+ @Test
+ public void testPrefixVsPrefix()
+ {
+ // This works but only because its an alphabetical check and '{' > 'b'.
+ mapping.put(new UriTemplatePathSpec("/a/{b}/{c}"), "prefix1");
+ mapping.put(new UriTemplatePathSpec("/a/b/{c}"), "prefix2");
+
+ assertThat(getBestMatch("/a/b/c"), is("prefix2"));
+ }
+
+ @Test
+ public void testSuffixVsSuffix()
+ {
+ // This works but only because its an alphabetical check and '{' > 'b'.
+ mapping.put(new UriTemplatePathSpec("/{a}/{b}/c"), "suffix1");
+ mapping.put(new UriTemplatePathSpec("/{a}/b/c"), "suffix2");
+
+ assertThat(getBestMatch("/a/b/c"), is("suffix2"));
+ }
+
+ @Test
+ public void testDifferentLengths()
+ {
+ mapping.put(new UriTemplatePathSpec("/a/{var}/c"), "endpointA");
+ mapping.put(new UriTemplatePathSpec("/a/{var}/c/d"), "endpointB");
+ mapping.put(new UriTemplatePathSpec("/a/{var1}/{var2}/d/e"), "endpointC");
+
+ assertThat(getBestMatch("/a/b/c"), is("endpointA"));
+ assertThat(getBestMatch("/a/d/c/d"), is("endpointB"));
+ assertThat(getBestMatch("/a/x/y/d/e"), is("endpointC"));
+ }
+}
diff --git a/third_party/jetty-http/src/test/resources/jetty-logging.properties b/third_party/jetty-http/src/test/resources/jetty-logging.properties
new file mode 100644
index 0000000..799aa62
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/jetty-logging.properties
@@ -0,0 +1,4 @@
+org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
+#org.eclipse.jetty.LEVEL=DEBUG
+#org.eclipse.jetty.server.LEVEL=DEBUG
+#org.eclipse.jetty.http.LEVEL=DEBUG
diff --git a/third_party/jetty-http/src/test/resources/keystore b/third_party/jetty-http/src/test/resources/keystore
new file mode 100644
index 0000000..b727bd0
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/keystore
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-company-urlencoded-apache-httpcomp.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-company-urlencoded-apache-httpcomp.expected.txt
new file mode 100644
index 0000000..1eed1a4
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-company-urlencoded-apache-httpcomp.expected.txt
@@ -0,0 +1,9 @@
+Request-Header|Accept-Encoding|gzip,deflate
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|248
+Request-Header|Content-Type|multipart/form-data; boundary=DHbU6ChASebwm4iE8z9Lakv4ybMmkp
+Request-Header|Host|localhost:9090
+Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
+Request-Header|X-BrowserId|apache-httpcomp
+Parts-Count|1
+Part-ContainsContents|company|bob+%26+frank%27s+shoe+repair
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-company-urlencoded-apache-httpcomp.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-company-urlencoded-apache-httpcomp.raw
new file mode 100644
index 0000000..7059e47
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-company-urlencoded-apache-httpcomp.raw
@@ -0,0 +1,7 @@
+--DHbU6ChASebwm4iE8z9Lakv4ybMmkp
+Content-Disposition: form-data; name="company"
+Content-Type: application/x-www-form-urlencoded; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+bob+%26+frank%27s+shoe+repair
+--DHbU6ChASebwm4iE8z9Lakv4ybMmkp--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-complex-apache-httpcomp.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-complex-apache-httpcomp.expected.txt
new file mode 100644
index 0000000..fde7344
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-complex-apache-httpcomp.expected.txt
@@ -0,0 +1,15 @@
+Request-Header|Accept-Encoding|gzip,deflate
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22940
+Request-Header|Content-Type|multipart/form-data; boundary=owr6UQGvVNunA_sx2AsizBtyq_uK-OjsQXrF
+Request-Header|Host|localhost:9090
+Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
+Request-Header|X-BrowserId|apache-httpcomp
+Parts-Count|6
+Part-ContainsContents|pi|3.14159265358979323846264338327950288419716939937510
+Part-ContainsContents|company|bob & frank's shoe repair
+Part-ContainsContents|power|ꬵо𝗋ⲥ𝖾
+Part-ContainsContents|japanese|オープンソース
+Part-ContainsContents|hello|日食桟橋
+Part-Filename|upload_file|filename
+Part-Sha1sum|upload_file|e75b73644afe9b234d70da9ff225229de68cdff8
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-complex-apache-httpcomp.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-complex-apache-httpcomp.raw
new file mode 100644
index 0000000..87f46ff
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-complex-apache-httpcomp.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-complex-jetty-client.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-complex-jetty-client.expected.txt
new file mode 100644
index 0000000..df6340c
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-complex-jetty-client.expected.txt
@@ -0,0 +1,15 @@
+Request-Header|Accept-Encoding|gzip
+Request-Header|Connection|close
+Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary1275gffetpxz8o0q
+Request-Header|Host|localhost:9090
+Request-Header|Transfer-Encoding|chunked
+Request-Header|User-Agent|Jetty/9.4.9.v20180320
+Request-Header|X-BrowserId|jetty-client
+Parts-Count|6
+Part-ContainsContents|pi|3.14159265358979323846264338327950288419716939937510
+Part-ContainsContents|company|bob & frank's shoe repair
+Part-ContainsContents|power|ꬵо𝗋ⲥ𝖾
+Part-ContainsContents|japanese|オープンソース
+Part-ContainsContents|hello|日食桟橋
+Part-Filename|upload_file|filename
+Part-Sha1sum|upload_file|e75b73644afe9b234d70da9ff225229de68cdff8
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-complex-jetty-client.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-complex-jetty-client.raw
new file mode 100644
index 0000000..04514a1
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-complex-jetty-client.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-duplicate-names-apache-httpcomp.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-duplicate-names-apache-httpcomp.expected.txt
new file mode 100644
index 0000000..796af89
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-duplicate-names-apache-httpcomp.expected.txt
@@ -0,0 +1,8 @@
+Request-Header|Accept-Encoding|gzip,deflate
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|1815
+Request-Header|Content-Type|multipart/form-data; boundary=QW3F8Fg64P2J2dpfEKGKlX0Q9QF2a8SK_7YH
+Request-Header|Host|localhost:9090
+Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
+Request-Header|X-BrowserId|apache-httpcomp
+Parts-Count|10
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-duplicate-names-apache-httpcomp.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-duplicate-names-apache-httpcomp.raw
new file mode 100644
index 0000000..e48b5a6
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-duplicate-names-apache-httpcomp.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-duplicate-names-jetty-client.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-duplicate-names-jetty-client.expected.txt
new file mode 100644
index 0000000..bc73cca
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-duplicate-names-jetty-client.expected.txt
@@ -0,0 +1,8 @@
+Request-Header|Accept-Encoding|gzip
+Request-Header|Connection|close
+Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary14beb4to333d91v8
+Request-Header|Host|localhost:9090
+Request-Header|Transfer-Encoding|chunked
+Request-Header|User-Agent|Jetty/9.4.9.v20180320
+Request-Header|X-BrowserId|jetty-client
+Parts-Count|10
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-duplicate-names-jetty-client.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-duplicate-names-jetty-client.raw
new file mode 100644
index 0000000..44646fd
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-duplicate-names-jetty-client.raw
@@ -0,0 +1,51 @@
+--JettyHttpClientBoundary14beb4to333d91v8
+Content-Disposition: form-data; name="pi"
+Content-Type: text/plain;charset=UTF-8
+
+3.14159265358979323846264338327950288419716939937510
+--JettyHttpClientBoundary14beb4to333d91v8
+Content-Disposition: form-data; name="pi"
+Content-Type: text/plain;charset=UTF-8
+
+3.14159
+--JettyHttpClientBoundary14beb4to333d91v8
+Content-Disposition: form-data; name="pi"
+Content-Type: text/plain;charset=UTF-8
+
+3
+--JettyHttpClientBoundary14beb4to333d91v8
+Content-Disposition: form-data; name="pi"
+Content-Type: text/plain;charset=UTF-8
+
+π
+--JettyHttpClientBoundary14beb4to333d91v8
+Content-Disposition: form-data; name="pi"
+Content-Type: text/plain;charset=UTF-8
+
+π
+--JettyHttpClientBoundary14beb4to333d91v8
+Content-Disposition: form-data; name="pi"
+Content-Type: text/plain;charset=UTF-8
+
+%CF%80
+--JettyHttpClientBoundary14beb4to333d91v8
+Content-Disposition: form-data; name="pi"
+Content-Type: text/plain;charset=UTF-8
+
+π = C/d
+--JettyHttpClientBoundary14beb4to333d91v8
+Content-Disposition: form-data; name="π"
+Content-Type: text/plain;charset=UTF-8
+
+3.14
+--JettyHttpClientBoundary14beb4to333d91v8
+Content-Disposition: form-data; name="%CF%80"
+Content-Type: text/plain;charset=UTF-8
+
+Approximately 3.14
+--JettyHttpClientBoundary14beb4to333d91v8
+Content-Disposition: form-data; name="%FE%FF%03%C0"
+Content-Type: text/plain;charset=UTF-8
+
+Approximately 3.14
+--JettyHttpClientBoundary14beb4to333d91v8--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-encoding-mess-apache-httpcomp.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-encoding-mess-apache-httpcomp.expected.txt
new file mode 100644
index 0000000..5769e30
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-encoding-mess-apache-httpcomp.expected.txt
@@ -0,0 +1,11 @@
+Request-Header|Accept-Encoding|gzip,deflate
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|31148
+Request-Header|Content-Type|multipart/form-data; boundary=qqr2YBBR31U4xVib4vaVuIsrwNY1iw
+Request-Header|Host|localhost:9090
+Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
+Request-Header|X-BrowserId|apache-httpcomp
+Parts-Count|169
+Part-ContainsContents|count|168
+Part-ContainsContents|persian-UTF-8|برج بابل
+Part-ContainsContents|persian-CESU-8|برج بابل
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-encoding-mess-apache-httpcomp.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-encoding-mess-apache-httpcomp.raw
new file mode 100644
index 0000000..17948f0
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-encoding-mess-apache-httpcomp.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-encoding-mess-jetty-client.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-encoding-mess-jetty-client.expected.txt
new file mode 100644
index 0000000..473743d
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-encoding-mess-jetty-client.expected.txt
@@ -0,0 +1,11 @@
+Request-Header|Accept-Encoding|gzip
+Request-Header|Connection|close
+Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary1jcfdl0zps9nf362
+Request-Header|Host|localhost:9090
+Request-Header|Transfer-Encoding|chunked
+Request-Header|User-Agent|Jetty/9.4.9.v20180320
+Request-Header|X-BrowserId|jetty-client
+Parts-Count|169
+Part-ContainsContents|count|168
+Part-ContainsContents|persian-UTF-8|برج بابل
+Part-ContainsContents|persian-CESU-8|برج بابل
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-encoding-mess-jetty-client.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-encoding-mess-jetty-client.raw
new file mode 100644
index 0000000..7f37467
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-encoding-mess-jetty-client.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-chrome.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-chrome.expected.txt
new file mode 100644
index 0000000..7b68976
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-chrome.expected.txt
@@ -0,0 +1,21 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate, br
+Request-Header|Accept-Language|en-US,en;q=0.9
+Request-Header|Cache-Control|max-age=0
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22759
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryafpkbdzB5Ciqre2z
+Request-Header|Cookie|visited=yes
+Request-Header|DNT|1
+Request-Header|Host|localhost:9090
+Request-Header|Origin|http://localhost:9090
+Request-Header|Referer|http://localhost:9090/form-fileupload-multi.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36
+Parts-Count|4
+Part-ContainsContents|description|the larger icon
+Part-ContainsContents|alternate|text.raw
+Part-Filename|file|jetty-avatar-256.png
+Part-Sha1sum|file|e75b73644afe9b234d70da9ff225229de68cdff8
+Part-Filename|file-alt|text.raw
+Part-Sha1sum|file-alt|5fb031816a27d80cc88c390819addab0ec3c189b
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-chrome.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-chrome.raw
new file mode 100644
index 0000000..cb7809c
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-chrome.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-edge.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-edge.expected.txt
new file mode 100644
index 0000000..9c3e254
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-edge.expected.txt
@@ -0,0 +1,17 @@
+Request-Header|Accept|text/html, application/xhtml+xml, image/jxr, */*
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US
+Request-Header|Cache-Control|no-cache
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22824
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------7e21c038151054
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/form-fileupload-multi.html
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299
+Parts-Count|4
+Part-ContainsContents|description|the larger icon
+Part-ContainsContents|alternate|text.raw
+Part-Filename|file|C:\Users\joakim\Pictures\jetty-avatar-256.png
+Part-Sha1sum|file|e75b73644afe9b234d70da9ff225229de68cdff8
+Part-Filename|file-alt|C:\Users\joakim\Pictures\text.raw
+Part-Sha1sum|file-alt|5fb031816a27d80cc88c390819addab0ec3c189b
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-edge.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-edge.raw
new file mode 100644
index 0000000..13fa957
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-edge.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-firefox.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-firefox.expected.txt
new file mode 100644
index 0000000..f918d12
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-firefox.expected.txt
@@ -0,0 +1,17 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US,en;q=0.5
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22774
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------23281168279961
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/form-fileupload-multi.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0
+Parts-Count|4
+Part-ContainsContents|description|the larger icon
+Part-ContainsContents|alternate|text.raw
+Part-Filename|file|jetty-avatar-256.png
+Part-Sha1sum|file|e75b73644afe9b234d70da9ff225229de68cdff8
+Part-Filename|file-alt|text.raw
+Part-Sha1sum|file-alt|5fb031816a27d80cc88c390819addab0ec3c189b
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-firefox.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-firefox.raw
new file mode 100644
index 0000000..ca094b5
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-firefox.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-msie.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-msie.expected.txt
new file mode 100644
index 0000000..b153481
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-msie.expected.txt
@@ -0,0 +1,17 @@
+Request-Header|Accept|text/html, application/xhtml+xml, image/jxr, */*
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US
+Request-Header|Cache-Control|no-cache
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22814
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------7e226692109c
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/form-fileupload-multi.html
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko
+Parts-Count|4
+Part-ContainsContents|description|the larger icon
+Part-ContainsContents|alternate|text.raw
+Part-Filename|file|C:\Users\joakim\Pictures\jetty-avatar-256.png
+Part-Sha1sum|file|e75b73644afe9b234d70da9ff225229de68cdff8
+Part-Filename|file-alt|C:\Users\joakim\Pictures\text.raw
+Part-Sha1sum|file-alt|5fb031816a27d80cc88c390819addab0ec3c189b
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-msie.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-msie.raw
new file mode 100644
index 0000000..786215f
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-msie.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-safari.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-safari.expected.txt
new file mode 100644
index 0000000..12c657e
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-safari.expected.txt
@@ -0,0 +1,18 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-us
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22774
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryEQhxWUv9r38x3LyB
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Origin|http://192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/form-fileupload-multi.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6
+Parts-Count|4
+Part-ContainsContents|description|the larger icon
+Part-ContainsContents|alternate|text.raw
+Part-Filename|file|jetty-avatar-256.png
+Part-Sha1sum|file|e75b73644afe9b234d70da9ff225229de68cdff8
+Part-Filename|file-alt|text.raw
+Part-Sha1sum|file-alt|5fb031816a27d80cc88c390819addab0ec3c189b
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-safari.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-safari.raw
new file mode 100644
index 0000000..1253219
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-alt-safari.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-android-chrome.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-android-chrome.expected.txt
new file mode 100644
index 0000000..ef15f1e
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-android-chrome.expected.txt
@@ -0,0 +1,17 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US,en;q=0.9
+Request-Header|Cache-Control|max-age=0
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22054
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundary2oBNepLIldUG8YwL
+Request-Header|DNT|1
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Origin|http://192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/form-fileupload.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Linux; Android 8.1.0; Pixel 2 XL Build/OPM1.171019.021) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.109 Mobile Safari/537.36
+Parts-Count|2
+Part-ContainsContents|description|the larger icon
+Part-Filename|file|jetty-avatar-256.png
+Part-Sha1sum|file|e75b73644afe9b234d70da9ff225229de68cdff8
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-android-chrome.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-android-chrome.raw
new file mode 100644
index 0000000..2263dfd
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-android-chrome.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-android-firefox.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-android-firefox.expected.txt
new file mode 100644
index 0000000..fceb88d
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-android-firefox.expected.txt
@@ -0,0 +1,14 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US,en;q=0.5
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22105
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------2117751712556306154183865432
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/form-fileupload.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Android 8.1.0; Mobile; rv:59.0) Gecko/59.0 Firefox/59.0
+Parts-Count|2
+Part-ContainsContents|description|the larger icon
+Part-Filename|file|jetty-avatar-256.png
+Part-Sha1sum|file|e75b73644afe9b234d70da9ff225229de68cdff8
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-android-firefox.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-android-firefox.raw
new file mode 100644
index 0000000..3492fdd
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-android-firefox.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-chrome.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-chrome.expected.txt
new file mode 100644
index 0000000..491fe43
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-chrome.expected.txt
@@ -0,0 +1,18 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate, br
+Request-Header|Accept-Language|en-US,en;q=0.9
+Request-Header|Cache-Control|max-age=0
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22054
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundarylxcKjAyTlRs3jNP2
+Request-Header|Cookie|visited=yes
+Request-Header|DNT|1
+Request-Header|Host|localhost:9090
+Request-Header|Origin|http://localhost:9090
+Request-Header|Referer|http://localhost:9090/form-fileupload.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36
+Parts-Count|2
+Part-ContainsContents|description|the larger icon
+Part-Filename|file|jetty-avatar-256.png
+Part-Sha1sum|file|e75b73644afe9b234d70da9ff225229de68cdff8
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-chrome.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-chrome.raw
new file mode 100644
index 0000000..b31c885
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-chrome.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-edge.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-edge.expected.txt
new file mode 100644
index 0000000..3086e32
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-edge.expected.txt
@@ -0,0 +1,14 @@
+Request-Header|Accept|text/html, application/xhtml+xml, image/jxr, */*
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US
+Request-Header|Cache-Control|no-cache
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22085
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------7e225f6151054
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/form-fileupload.html
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299
+Parts-Count|2
+Part-ContainsContents|description|the larger icon
+Part-Filename|file|C:\Users\joakim\Pictures\jetty-avatar-256.png
+Part-Sha1sum|file|e75b73644afe9b234d70da9ff225229de68cdff8
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-edge.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-edge.raw
new file mode 100644
index 0000000..6f60b77
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-edge.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-firefox.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-firefox.expected.txt
new file mode 100644
index 0000000..6b138ba
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-firefox.expected.txt
@@ -0,0 +1,14 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US,en;q=0.5
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22063
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------24464570528145
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/form-fileupload.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0
+Parts-Count|2
+Part-ContainsContents|description|the larger icon
+Part-Filename|file|jetty-avatar-256.png
+Part-Sha1sum|file|e75b73644afe9b234d70da9ff225229de68cdff8
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-firefox.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-firefox.raw
new file mode 100644
index 0000000..cb14119
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-firefox.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-ios-safari.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-ios-safari.expected.txt
new file mode 100644
index 0000000..3620ce2
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-ios-safari.expected.txt
@@ -0,0 +1,15 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-us
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22074
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundary5trdx3OwYr8uMtbA
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Origin|http://192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/form-fileupload.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (iPad; CPU OS 11_2_6 like Mac OS X) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0 Mobile/15D100 Safari/604.1
+Parts-Count|2
+Part-ContainsContents|description|the larger icon
+Part-Filename|file|66A4F66B-9B37-4F69-86A7-456547EBF079.png
+Part-Sha1sum|file|e75b73644afe9b234d70da9ff225229de68cdff8
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-ios-safari.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-ios-safari.raw
new file mode 100644
index 0000000..24fbac7
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-ios-safari.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-msie.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-msie.expected.txt
new file mode 100644
index 0000000..e2f6482
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-msie.expected.txt
@@ -0,0 +1,14 @@
+Request-Header|Accept|text/html, application/xhtml+xml, image/jxr, */*
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US
+Request-Header|Cache-Control|no-cache
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22082
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------7e223ef2109c
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/form-fileupload.html
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko
+Parts-Count|2
+Part-ContainsContents|description|the larger icon
+Part-Filename|file|C:\Users\joakim\Pictures\jetty-avatar-256.png
+Part-Sha1sum|file|e75b73644afe9b234d70da9ff225229de68cdff8
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-msie.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-msie.raw
new file mode 100644
index 0000000..9b27e76
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-msie.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-safari.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-safari.expected.txt
new file mode 100644
index 0000000..9dd8781
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-safari.expected.txt
@@ -0,0 +1,15 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-us
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|22054
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryWl9yEX5Fas0SI2xc
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Origin|http://192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/form-fileupload.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6
+Parts-Count|2
+Part-ContainsContents|description|the larger icon
+Part-Filename|file|jetty-avatar-256.png
+Part-Sha1sum|file|e75b73644afe9b234d70da9ff225229de68cdff8
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-safari.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-safari.raw
new file mode 100644
index 0000000..3b69225
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form-fileupload-safari.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-android-chrome.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-android-chrome.expected.txt
new file mode 100644
index 0000000..271e31b
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-android-chrome.expected.txt
@@ -0,0 +1,16 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US,en;q=0.9
+Request-Header|Cache-Control|max-age=0
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|245
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryD4GyXQgjBRmK3aBz
+Request-Header|DNT|1
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Origin|http://192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/form.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Linux; Android 8.1.0; Pixel 2 XL Build/OPM1.171019.021) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.109 Mobile Safari/537.36
+Parts-Count|2
+Part-ContainsContents|user|Androiduser
+Part-ContainsContents|comment|Dyac!
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-android-chrome.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-android-chrome.raw
new file mode 100644
index 0000000..f5ce1ca
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-android-chrome.raw
@@ -0,0 +1,9 @@
+------WebKitFormBoundaryD4GyXQgjBRmK3aBz
+Content-Disposition: form-data; name="user"
+
+Androiduser
+------WebKitFormBoundaryD4GyXQgjBRmK3aBz
+Content-Disposition: form-data; name="comment"
+
+Dyac!
+------WebKitFormBoundaryD4GyXQgjBRmK3aBz--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-android-firefox.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-android-firefox.expected.txt
new file mode 100644
index 0000000..9f7d230
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-android-firefox.expected.txt
@@ -0,0 +1,13 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US,en;q=0.5
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|306
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------6390283156237600831344307695
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/form.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Android 8.1.0; Mobile; rv:59.0) Gecko/59.0 Firefox/59.0
+Parts-Count|2
+Part-ContainsContents|user|androidfireuser
+Part-ContainsContents|comment|More to say
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-android-firefox.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-android-firefox.raw
new file mode 100644
index 0000000..75dbbde
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-android-firefox.raw
@@ -0,0 +1,9 @@
+-----------------------------6390283156237600831344307695
+Content-Disposition: form-data; name="user"
+
+androidfireuser
+-----------------------------6390283156237600831344307695
+Content-Disposition: form-data; name="comment"
+
+More to say
+-----------------------------6390283156237600831344307695--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-chrome.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-chrome.expected.txt
new file mode 100644
index 0000000..d36342f
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-chrome.expected.txt
@@ -0,0 +1,17 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate, br
+Request-Header|Accept-Language|en-US,en;q=0.9
+Request-Header|Cache-Control|max-age=0
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|256
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundary46EP6zTN86hbbaJC
+Request-Header|Cookie|visited=yes
+Request-Header|DNT|1
+Request-Header|Host|localhost:9090
+Request-Header|Origin|http://localhost:9090
+Request-Header|Referer|http://localhost:9090/form.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36
+Parts-Count|2
+Part-ContainsContents|user|joe
+Part-ContainsContents|comment|this is a simple comment
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-chrome.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-chrome.raw
new file mode 100644
index 0000000..7f8bfc2
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-chrome.raw
@@ -0,0 +1,9 @@
+------WebKitFormBoundary46EP6zTN86hbbaJC
+Content-Disposition: form-data; name="user"
+
+joe
+------WebKitFormBoundary46EP6zTN86hbbaJC
+Content-Disposition: form-data; name="comment"
+
+this is a simple comment
+------WebKitFormBoundary46EP6zTN86hbbaJC--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-edge.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-edge.expected.txt
new file mode 100644
index 0000000..0b7f887
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-edge.expected.txt
@@ -0,0 +1,13 @@
+Request-Header|Accept|text/html, application/xhtml+xml, image/jxr, */*
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US
+Request-Header|Cache-Control|no-cache
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|267
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------7e25e1e151054
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/form.html
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299
+Parts-Count|2
+Part-ContainsContents|user|anotheruser
+Part-ContainsContents|comment|with something to say
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-edge.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-edge.raw
new file mode 100644
index 0000000..48aa4e7
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-edge.raw
@@ -0,0 +1,9 @@
+-----------------------------7e25e1e151054
+Content-Disposition: form-data; name="user"
+
+anotheruser
+-----------------------------7e25e1e151054
+Content-Disposition: form-data; name="comment"
+
+with something to say
+-----------------------------7e25e1e151054--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-firefox.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-firefox.expected.txt
new file mode 100644
index 0000000..9f0e4ee
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-firefox.expected.txt
@@ -0,0 +1,13 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US,en;q=0.5
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|258
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------41184676334
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/form.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0
+Parts-Count|2
+Part-ContainsContents|user|fireuser
+Part-ContainsContents|comment|with detailed message
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-firefox.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-firefox.raw
new file mode 100644
index 0000000..a7c6531
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-firefox.raw
@@ -0,0 +1,9 @@
+-----------------------------41184676334
+Content-Disposition: form-data; name="user"
+
+fireuser
+-----------------------------41184676334
+Content-Disposition: form-data; name="comment"
+
+with detailed message
+-----------------------------41184676334--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-ios-safari.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-ios-safari.expected.txt
new file mode 100644
index 0000000..4d0533d
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-ios-safari.expected.txt
@@ -0,0 +1,14 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-us
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|268
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundary56m5uMm4gNcn4rL1
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Origin|http://192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/form.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (iPad; CPU OS 11_2_6 like Mac OS X) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0 Mobile/15D100 Safari/604.1
+Parts-Count|2
+Part-ContainsContents|user|UseriPad
+Part-ContainsContents|comment|This form isn’t pretty
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-ios-safari.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-ios-safari.raw
new file mode 100644
index 0000000..9664c90
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-ios-safari.raw
@@ -0,0 +1,9 @@
+------WebKitFormBoundary56m5uMm4gNcn4rL1
+Content-Disposition: form-data; name="user"
+
+UseriPad
+------WebKitFormBoundary56m5uMm4gNcn4rL1
+Content-Disposition: form-data; name="comment"
+
+This form isn’t pretty enough
+------WebKitFormBoundary56m5uMm4gNcn4rL1--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-msie.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-msie.expected.txt
new file mode 100644
index 0000000..60cbe9e
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-msie.expected.txt
@@ -0,0 +1,13 @@
+Request-Header|Accept|text/html, application/xhtml+xml, image/jxr, */*
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US
+Request-Header|Cache-Control|no-cache
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|285
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------7e21b6f2109c
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/form.html
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko
+Parts-Count|2
+Part-ContainsContents|user|msieuser
+Part-ContainsContents|comment|with information that they think is important
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-msie.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-msie.raw
new file mode 100644
index 0000000..e562e72
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-msie.raw
@@ -0,0 +1,9 @@
+-----------------------------7e21b6f2109c
+Content-Disposition: form-data; name="user"
+
+msieuser
+-----------------------------7e21b6f2109c
+Content-Disposition: form-data; name="comment"
+
+with information that they think is important
+-----------------------------7e21b6f2109c--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-osx-safari.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-osx-safari.expected.txt
new file mode 100644
index 0000000..236c06f
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-osx-safari.expected.txt
@@ -0,0 +1,14 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-us
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|284
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryjwqONTsAFgubfMZc
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Origin|http://192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/form.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6
+Parts-Count|2
+Part-ContainsContents|user|safariuser
+Part-ContainsContents|comment|with rambling thoughts about bellybutton lint
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-osx-safari.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-osx-safari.raw
new file mode 100644
index 0000000..0e6b82f
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-form1-osx-safari.raw
@@ -0,0 +1,9 @@
+------WebKitFormBoundaryjwqONTsAFgubfMZc
+Content-Disposition: form-data; name="user"
+
+safariuser
+------WebKitFormBoundaryjwqONTsAFgubfMZc
+Content-Disposition: form-data; name="comment"
+
+with rambling thoughts about bellybutton lint
+------WebKitFormBoundaryjwqONTsAFgubfMZc--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-apache-httpcomp.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-apache-httpcomp.expected.txt
new file mode 100644
index 0000000..fc44839
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-apache-httpcomp.expected.txt
@@ -0,0 +1,12 @@
+Request-Header|Accept-Encoding|gzip,deflate
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|1203
+Request-Header|Content-Type|multipart/form-data; boundary=Cku4UvJrPFCXkXjge2a2Y2sgq1bbOa
+Request-Header|Host|localhost:9090
+Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
+Request-Header|X-BrowserId|apache-httpcomp
+Parts-Count|4
+Part-ContainsContents|reporter|<user@company.com>
+Part-ContainsContents|timestamp|2018-03-21T18:52:18+00:00
+Part-ContainsContents|comments|this couldn't be parsed
+Part-ContainsContents|attachment|banana
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-apache-httpcomp.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-apache-httpcomp.raw
new file mode 100644
index 0000000..12ce176
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-apache-httpcomp.raw
@@ -0,0 +1,42 @@
+--Cku4UvJrPFCXkXjge2a2Y2sgq1bbOa
+Content-Disposition: form-data; name="reporter"
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+<user@company.com>
+--Cku4UvJrPFCXkXjge2a2Y2sgq1bbOa
+Content-Disposition: form-data; name="timestamp"
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+2018-03-21T18:52:18+00:00
+--Cku4UvJrPFCXkXjge2a2Y2sgq1bbOa
+Content-Disposition: form-data; name="comments"
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+this couldn't be parsed
+--Cku4UvJrPFCXkXjge2a2Y2sgq1bbOa
+Content-Disposition: form-data; name="attachment"
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+--nM8_n8ugj9L3fIomqyU6h9Wpb6Wt-3w
+Content-Disposition: form-data; name="fruit"
+
+banana
+--nM8_n8ugj9L3fIomqyU6h9Wpb6Wt-3w
+Content-Disposition: form-data; name="color"
+
+yellow
+--nM8_n8ugj9L3fIomqyU6h9Wpb6Wt-3w
+Content-Disposition: form-data; name="cost"
+
+$0.12 USG
+--nM8_n8ugj9L3fIomqyU6h9Wpb6Wt-3w
+Content-Disposition: form-data; name="comments"
+
+--divc688gD49-GaZcLkprfUb8-PWOjF3Z
+--nM8_n8ugj9L3fIomqyU6h9Wpb6Wt-3w--
+
+--Cku4UvJrPFCXkXjge2a2Y2sgq1bbOa--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-binary-apache-httpcomp.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-binary-apache-httpcomp.expected.txt
new file mode 100644
index 0000000..12e9da8
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-binary-apache-httpcomp.expected.txt
@@ -0,0 +1,12 @@
+Request-Header|Accept-Encoding|gzip,deflate
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|1577
+Request-Header|Content-Type|multipart/form-data; boundary=xDeLGHDDsXrlJSXfqDmg5IRop7auqTTBXuI
+Request-Header|Host|localhost:9090
+Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
+Request-Header|X-BrowserId|apache-httpcomp
+Parts-Count|4
+Part-ContainsContents|reporter|<user@company.com>
+Part-ContainsContents|timestamp|2018-03-21T19:00:18+00:00
+Part-ContainsContents|comments|this also couldn't be parsed
+Part-ContainsContents|attachment|cherry
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-binary-apache-httpcomp.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-binary-apache-httpcomp.raw
new file mode 100644
index 0000000..bf8c06a
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-binary-apache-httpcomp.raw
@@ -0,0 +1,50 @@
+--xDeLGHDDsXrlJSXfqDmg5IRop7auqTTBXuI
+Content-Disposition: form-data; name="reporter"
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+<user@company.com>
+--xDeLGHDDsXrlJSXfqDmg5IRop7auqTTBXuI
+Content-Disposition: form-data; name="timestamp"
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+2018-03-21T19:00:18+00:00
+--xDeLGHDDsXrlJSXfqDmg5IRop7auqTTBXuI
+Content-Disposition: form-data; name="comments"
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+this also couldn't be parsed
+--xDeLGHDDsXrlJSXfqDmg5IRop7auqTTBXuI
+Content-Disposition: form-data; name="attachment"
+Content-Type: application/octet-stream
+Content-Transfer-Encoding: binary
+
+--GiQ7DQPSJdaP5c43_Zd1P6xVJTQVLzZ8T9ovx
+Content-Disposition: form-data; name="fruit"
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+cherry
+--GiQ7DQPSJdaP5c43_Zd1P6xVJTQVLzZ8T9ovx
+Content-Disposition: form-data; name="color"
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+red
+--GiQ7DQPSJdaP5c43_Zd1P6xVJTQVLzZ8T9ovx
+Content-Disposition: form-data; name="cost"
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+$1.20 USG
+--GiQ7DQPSJdaP5c43_Zd1P6xVJTQVLzZ8T9ovx
+Content-Disposition: form-data; name="comments"
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+--gq4lBOlNh8FRiH6MLw4GaWE40UC-GeDRTy8bF
+--GiQ7DQPSJdaP5c43_Zd1P6xVJTQVLzZ8T9ovx--
+
+--xDeLGHDDsXrlJSXfqDmg5IRop7auqTTBXuI--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-jetty-client.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-jetty-client.expected.txt
new file mode 100644
index 0000000..294f1ee
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-jetty-client.expected.txt
@@ -0,0 +1,12 @@
+Request-Header|Accept-Encoding|gzip
+Request-Header|Connection|close
+Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary1uz60vid2bq7x1t9
+Request-Header|Host|localhost:9090
+Request-Header|Transfer-Encoding|chunked
+Request-Header|User-Agent|Jetty/9.4.9.v20180320
+Request-Header|X-BrowserId|jetty-client
+Parts-Count|4
+Part-ContainsContents|reporter|<user@company.com>
+Part-ContainsContents|timestamp|2018-03-21T18:52:18+00:00
+Part-ContainsContents|comments|this couldn't be parsed
+Part-ContainsContents|attachment|banana
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-jetty-client.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-jetty-client.raw
new file mode 100644
index 0000000..38f849c
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-nested-jetty-client.raw
@@ -0,0 +1,42 @@
+--JettyHttpClientBoundary1uz60vid2bq7x1t9
+Content-Disposition: form-data; name="reporter"
+Content-Type: text/plain;charset=UTF-8
+
+<user@company.com>
+--JettyHttpClientBoundary1uz60vid2bq7x1t9
+Content-Disposition: form-data; name="timestamp"
+Content-Type: text/plain;charset=UTF-8
+
+2018-03-21T18:52:18+00:00
+--JettyHttpClientBoundary1uz60vid2bq7x1t9
+Content-Disposition: form-data; name="comments"
+Content-Type: text/plain;charset=UTF-8
+
+this couldn't be parsed
+--JettyHttpClientBoundary1uz60vid2bq7x1t9
+Content-Disposition: form-data; name="attachment"; filename="sample"
+Content-Type: multipart/form-data; boundary=JettyHttpClientBoundary10bb1gdlzug0xmmi
+
+--JettyHttpClientBoundary10bb1gdlzug0xmmi
+Content-Disposition: form-data; name="fruit"
+Content-Type: text/plain;charset=UTF-8
+
+banana
+--JettyHttpClientBoundary10bb1gdlzug0xmmi
+Content-Disposition: form-data; name="color"
+Content-Type: text/plain;charset=UTF-8
+
+yellow
+--JettyHttpClientBoundary10bb1gdlzug0xmmi
+Content-Disposition: form-data; name="cost"
+Content-Type: text/plain;charset=UTF-8
+
+$0.12 USD
+--JettyHttpClientBoundary10bb1gdlzug0xmmi
+Content-Disposition: form-data; name="comments"
+Content-Type: text/plain;charset=UTF-8
+
+--gx1KGV2f8WMHHwtWog9AFqjD3IGHzEvk
+--JettyHttpClientBoundary10bb1gdlzug0xmmi--
+
+--JettyHttpClientBoundary1uz60vid2bq7x1t9--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only-apache-httpcomp.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only-apache-httpcomp.expected.txt
new file mode 100644
index 0000000..e090188
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only-apache-httpcomp.expected.txt
@@ -0,0 +1,9 @@
+Request-Header|Accept-Encoding|gzip,deflate
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|173
+Request-Header|Content-Type|multipart/form-data; boundary=xE8WoYDcbqAfj08bxPk669iK22hMMlZL
+Request-Header|Host|localhost:9090
+Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
+Request-Header|X-BrowserId|apache-httpcomp
+Parts-Count|1
+Part-ContainsContents|pi|3.14159265358979323846264338327950288419716939937510
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only-apache-httpcomp.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only-apache-httpcomp.raw
new file mode 100644
index 0000000..0af35a6
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only-apache-httpcomp.raw
@@ -0,0 +1,5 @@
+--xE8WoYDcbqAfj08bxPk669iK22hMMlZL
+Content-Disposition: form-data; name="pi"
+
+3.14159265358979323846264338327950288419716939937510
+--xE8WoYDcbqAfj08bxPk669iK22hMMlZL--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only-jetty-client.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only-jetty-client.expected.txt
new file mode 100644
index 0000000..a9f21f2
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only-jetty-client.expected.txt
@@ -0,0 +1,12 @@
+Request-Header|Accept-Encoding|gzip
+Request-Header|Connection|close
+Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary1shlqpw2yahae6jf
+Request-Header|Host|localhost:9090
+Request-Header|Transfer-Encoding|chunked
+Request-Header|User-Agent|Jetty/9.4.9.v20180320
+Request-Header|X-BrowserId|jetty-client
+Parts-Count|1
+# Start of sequence
+Part-ContainsContents|pi|3.14159 26535 89793 23846 26433 83279 50288
+# End of sequence
+Part-ContainsContents|pi|81592 05600 10165 52563 7567
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only-jetty-client.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only-jetty-client.raw
new file mode 100644
index 0000000..ab78f17
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only-jetty-client.raw
@@ -0,0 +1,6 @@
+--JettyHttpClientBoundary1shlqpw2yahae6jf
+Content-Disposition: form-data; name="pi"
+Content-Type: application/octet-stream
+
+3.14159 26535 89793 23846 26433 83279 50288 41971 69399 37510 58209 74944 59230 78164 06286 20899 86280 34825 34211 70679 82148 08651 32823 06647 09384 46095 50582 23172 53594 08128 48111 74502 84102 70193 85211 05559 64462 29489 54930 38196 44288 10975 66593 34461 28475 64823 37867 83165 27120 19091 45648 56692 34603 48610 45432 66482 13393 60726 02491 41273 72458 70066 06315 58817 48815 20920 96282 92540 91715 36436 78925 90360 01133 05305 48820 46652 13841 46951 94151 16094 33057 27036 57595 91953 09218 61173 81932 61179 31051 18548 07446 23799 62749 56735 18857 52724 89122 79381 83011 94912 98336 73362 44065 66430 86021 39494 63952 24737 19070 21798 60943 70277 05392 17176 29317 67523 84674 81846 76694 05132 00056 81271 45263 56082 77857 71342 75778 96091 73637 17872 14684 40901 22495 34301 46549 58537 10507 92279 68925 89235 42019 95611 21290 21960 86403 44181 59813 62977 47713 09960 51870 72113 49999 99837 29780 49951 05973 17328 16096 31859 50244 59455 34690 83026 42522 30825 33446 85035 26193 11881 71010 00313 78387 52886 58753 32083 81420 61717 76691 47303 59825 34904 28755 46873 11595 62863 88235 37875 93751 95778 18577 80532 17122 68066 13001 92787 66111 95909 21642 01989 38095 25720 10654 85863 27886 59361 53381 82796 82303 01952 03530 18529 68995 77362 25994 13891 24972 17752 83479 13151 55748 57242 45415 06959 50829 53311 68617 27855 88907 50983 81754 63746 49393 19255 06040 09277 01671 13900 98488 24012 85836 16035 63707 66010 47101 81942 95559 61989 46767 83744 94482 55379 77472 68471 04047 53464 62080 46684 25906 94912 93313 67702 89891 52104 75216 20569 66024 05803 81501 93511 25338 24300 35587 64024 74964 73263 91419 92726 04269 92279 67823 54781 63600 93417 21641 21992 45863 15030 28618 29745 55706 74983 85054 94588 58692 69956 90927 21079 75093 02955 32116 53449 87202 75596 02364 80665 49911 98818 34797 75356 63698 07426 54252 78625 51818 41757 46728 90977 77279 38000 81647 06001 61452 49192 17321 72147 72350 14144 19735 68548 16136 11573 52552 13347 57418 49468 43852 33239 07394 14333 45477 62416 86251 89835 69485 56209 92192 22184 27255 02542 56887 67179 04946 01653 46680 49886 27232 79178 60857 84383 82796 79766 81454 10095 38837 86360 95068 00642 25125 20511 73929 84896 08412 84886 26945 60424 19652 85022 21066 11863 06744 27862 20391 94945 04712 37137 86960 95636 43719 17287 46776 46575 73962 41389 08658 32645 99581 33904 78027 59009 94657 64078 95126 94683 98352 59570 98258 22620 52248 94077 26719 47826 84826 01476 99090 26401 36394 43745 53050 68203 49625 24517 49399 65143 14298 09190 65925 09372 21696 46151 57098 58387 41059 78859 59772 97549 89301 61753 92846 81382 68683 86894 27741 55991 85592 52459 53959 43104 99725 24680 84598 72736 44695 84865 38367 36222 62609 91246 08051 24388 43904 51244 13654 97627 80797 71569 14359 97700 12961 60894 41694 86855 58484 06353 42207 22258 28488 64815 84560 28506 01684 27394 52267 46767 88952 52138 52254 99546 66727 82398 64565 96116 35488 62305 77456 49803 55936 34568 17432 41125 15076 06947 94510 96596 09402 52288 79710 89314 56691 36867 22874 89405 60101 50330 86179 28680 92087 47609 17824 93858 90097 14909 67598 52613 65549 78189 31297 84821 68299 89487 22658 80485 75640 14270 47755 51323 79641 45152 37462 34364 54285 84447 95265 86782 10511 41354 73573 95231 13427 16610 21359 69536 23144 29524 84937 18711 01457 65403 59027 99344 03742 00731 05785 39062 19838 74478 08478 48968 33214 45713 86875 19435 06430 21845 31910 48481 00537 06146 80674 91927 81911 97939 95206 14196 63428 75444 06437 45123 71819 21799 98391 01591 95618 14675 14269 12397 48940 90718 64942 31961 56794 52080 95146 55022 52316 03881 93014 20937 62137 85595 66389 37787 08303 90697 92077 34672 21825 62599 66150 14215 03068 03844 77345 49202 60541 46659 25201 49744 28507 32518 66600 21324 34088 19071 04863 31734 64965 14539 05796 26856 10055 08106 65879 69981 63574 73638 40525 71459 10289 70641 40110 97120 62804 39039 75951 56771 57700 42033 78699 36007 23055 87631 76359 42187 31251 47120 53292 81918 26186 12586 73215 79198 41484 88291 64470 60957 52706 95722 09175 67116 72291 09816 90915 28017 35067 12748 58322 28718 35209 35396 57251 21083 57915 13698 82091 44421 00675 10334 67110 31412 67111 36990 86585 16398 31501 97016 51511 68517 14376 57618 35155 65088 49099 89859 98238 73455 28331 63550 76479 18535 89322 61854 89632 13293 30898 57064 20467 52590 70915 48141 65498 59461 63718 02709 81994 30992 44889 57571 28289 05923 23326 09729 97120 84433 57326 54893 82391 19325 97463 66730 58360 41428 13883 03203 82490 37589 85243 74417 02913 27656 18093 77344 40307 07469 21120 19130 20330 38019 76211 01100 44929 32151 60842 44485 96376 69838 95228 68478 31235 52658 21314 49576 85726 24334 41893 03968 64262 43410 77322 69780 28073 18915 44110 10446 82325 27162 01052 65227 21116 60396 66557 30925 47110 55785 37634 66820 65310 98965 26918 62056 47693 12570 58635 66201 85581 00729 36065 98764 86117 91045 33488 50346 11365 76867 53249 44166 80396 26579 78771 85560 84552 96541 26654 08530 61434 44318 58676 97514 56614 06800 70023 78776 59134 40171 27494 70420 56223 05389 94561 31407 11270 00407 85473 32699 39081 45466 46458 80797 27082 66830 63432 85878 56983 05235 80893 30657 57406 79545 71637 75254 20211 49557 61581 40025 01262 28594 13021 64715 50979 25923 09907 96547 37612 55176 56751 35751 78296 66454 77917 45011 29961 48903 04639 94713 29621 07340 43751 89573 59614 58901 93897 13111 79042 97828 56475 03203 19869 15140 28708 08599 04801 09412 14722 13179 47647 77262 24142 54854 54033 21571 85306 14228 81375 85043 06332 17518 29798 66223 71721 59160 77166 92547 48738 98665 49494 50114 65406 28433 66393 79003 97692 65672 14638 53067 36096 57120 91807 63832 71664 16274 88880 07869 25602 90228 47210 40317 21186 08204 19000 42296 61711 96377 92133 75751 14959 50156 60496 31862 94726 54736 42523 08177 03675 15906 73502 35072 83540 56704 03867 43513 62222 47715 89150 49530 98444 89333 09634 08780 76932 59939 78054 19341 44737 74418 42631 29860 80998 88687 41326 04721 56951 62396 58645 73021 63159 81931 95167 35381 29741 67729 47867 24229 24654 36680 09806 76928 23828 06899 64004 82435 40370 14163 14965 89794 09243 23789 69070 69779 42236 25082 21688 95738 37986 23001 59377 64716 51228 93578 60158 81617 55782 97352 33446 04281 51262 72037 34314 65319 77774 16031 99066 55418 76397 92933 44195 21541 34189 94854 44734 56738 31624 99341 91318 14809 27777 10386 38773 43177 20754 56545 32207 77092 12019 05166 09628 04909 26360 19759 88281 61332 31666 36528 61932 66863 36062 73567 63035 44776 28035 04507 77235 54710 58595 48702 79081 43562 40145 17180 62464 36267 94561 27531 81340 78330 33625 42327 83944 97538 24372 05835 31147 71199 26063 81334 67768 79695 97030 98339 13077 10987 04085 91337 46414 42822 77263 46594 70474 58784 77872 01927 71528 07317 67907 70715 72134 44730 60570 07334 92436 93113 83504 93163 12840 42512 19256 51798 06941 13528 01314 70130 47816 43788 51852 90928 54520 11658 39341 96562 13491 43415 95625 86586 55705 52690 49652 09858 03385 07224 26482 93972 85847 83163 05777 75606 88876 44624 82468 57926 03953 52773 48030 48029 00587 60758 25104 74709 16439 61362 67604 49256 27420 42083 20856 61190 62545 43372 13153 59584 50687 72460 29016 18766 79524 06163 42522 57719 54291 62991 93064 55377 99140 37340 43287 52628 88963 99587 94757 29174 64263 57455 25407 90914 51357 11136 94109 11939 32519 10760 20825 20261 87985 31887 70584 29725 91677 81314 96990 09019 21169 71737 27847 68472 68608 49003 37702 42429 16513 00500 51683 23364 35038 95170 29893 92233 45172 20138 12806 96501 17844 08745 19601 21228 59937 16231 30171 14448 46409 03890 64495 44400 61986 90754 85160 26327 50529 83491 87407 86680 88183 38510 22833 45085 04860 82503 93021 33219 71551 84306 35455 00766 82829 49304 13776 55279 39751 75461 39539 84683 39363 83047 46119 96653 85815 38420 56853 38621 86725 23340 28308 71123 28278 92125 07712 62946 32295 63989 89893 58211 67456 27010 21835 64622 01349 67151 88190 97303 81198 00497 34072 39610 36854 06643 19395 09790 19069 96395 52453 00545 05806 85501 95673 02292 19139 33918 56803 44903 98205 95510 02263 53536 19204 19947 45538 59381 02343 95544 95977 83779 02374 21617 27111 72364 34354 39478 22181 85286 24085 14006 66044 33258 88569 86705 43154 70696 57474 58550 33232 33421 07301 54594 05165 53790 68662 73337 99585 11562 57843 22988 27372 31989 87571 41595 78111 96358 33005 94087 30681 21602 87649 62867 44604 77464 91599 50549 73742 56269 01049 03778 19868 35938 14657 41268 04925 64879 85561 45372 34786 73303 90468 83834 36346 55379 49864 19270 56387 29317 48723 32083 76011 23029 91136 79386 27089 43879 93620 16295 15413 37142 48928 30722 01269 01475 46684 76535 76164 77379 46752 00490 75715 55278 19653 62132 39264 06160 13635 81559 07422 02020 31872 77605 27721 90055 61484 25551 87925 30343 51398 44253 22341 57623 36106 42506 39049 75008 65627 10953 59194 65897 51413 10348 22769 30624 74353 63256 91607 81547 81811 52843 66795 70611 08615 33150 44521 27473 92454 49454 23682 88606 13408 41486 37767 00961 20715 12491 40430 27253 86076 48236 34143 34623 51897 57664 52164 13767 96903 14950 19108 57598 44239 19862 91642 19399 49072 36234 64684 41173 94032 65918 40443 78051 33389 45257 42399 50829 65912 28508 55582 15725 03107 12570 12668 30240 29295 25220 11872 67675 62204 15420 51618 41634 84756 51699 98116 14101 00299 60783 86909 29160 30288 40026 91041 40792 88621 50784 24516 70908 70006 99282 12066 04183 71806 53556 72525 32567 53286 12910 42487 76182 58297 65157 95984 70356 22262 93486 00341 58722 98053 49896 50226 29174 87882 02734 20922 22453 39856 26476 69149 05562 84250 39127 57710 28402 79980 66365 82548 89264 88025 45661 01729 67026 64076 55904 29099 45681 50652 65305 37182 94127 03369 31378 51786 09040 70866 71149 65583 43434 76933 85781 71138 64558 73678 12301 45876 87126 60348 91390 95620 09939 36103 10291 61615 28813 84379 09904 23174 73363 94804 57593 14931 40529 76347 57481 19356 70911 01377 51721 00803 15590 24853 09066 92037 67192 20332 29094 33467 68514 22144 77379 39375 17034 43661 99104 03375 11173 54719 18550 46449 02636 55128 16228 82446 25759 16333 03910 72253 83742 18214 08835 08657 39177 15096 82887 47826 56995 99574 49066 17583 44137 52239 70968 34080 05355 98491 75417 38188 39994 46974 86762 65516 58276 58483 58845 31427 75687 90029 09517 02835 29716 34456 21296 40435 23117 60066 51012 41200 65975 58512 76178 58382 92041 97484 42360 80071 93045 76189 32349 22927 96501 98751 87212 72675 07981 25547 09589 04556 35792 12210 33346 69749 92356 30254 94780 24901 14195 21238 28153 09114 07907 38602 51522 74299 58180 72471 62591 66854 51333 12394 80494 70791 19153 26734 30282 44186 04142 63639 54800 04480 02670 49624 82017 92896 47669 75831 83271 31425 17029 69234 88962 76684 40323 26092 75249 60357 99646 92565 04936 81836 09003 23809 29345 95889 70695 36534 94060 34021 66544 37558 90045 63288 22505 45255 64056 44824 65151 87547 11962 18443 96582 53375 43885 69094 11303 15095 26179 37800 29741 20766 51479 39425 90298 96959 46995 56576 12186 56196 73378 62362 56125 21632 08628 69222 10327 48892 18654 36480 22967 80705 76561 51446 32046 92790 68212 07388 37781 42335 62823 60896 32080 68222 46801 22482 61177 18589 63814 09183 90367 36722 20888 32151 37556 00372 79839 40041 52970 02878 30766 70944 47456 01345 56417 25437 09069 79396 12257 14298 94671 54357 84687 88614 44581 23145 93571 98492 25284 71605 04922 12424 70141 21478 05734 55105 00801 90869 96033 02763 47870 81081 75450 11930 71412 23390 86639 38339 52942 57869 05076 43100 63835 19834 38934 15961 31854 34754 64955 69781 03829 30971 64651 43840 70070 73604 11237 35998 43452 25161 05070 27056 23526 60127 64848 30840 76118 30130 52793 20542 74628 65403 60367 45328 65105 70658 74882 25698 15793 67897 66974 22057 50596 83440 86973 50201 41020 67235 85020 07245 22563 26513 41055 92401 90274 21624 84391 40359 98953 53945 90944 07046 91209 14093 87001 26456 00162 37428 80210 92764 57931 06579 22955 24988 72758 46101 26483 69998 92256 95968 81592 05600 10165 52563 7567
+--JettyHttpClientBoundary1shlqpw2yahae6jf--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only2-apache-httpcomp.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only2-apache-httpcomp.expected.txt
new file mode 100644
index 0000000..aa49e32
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only2-apache-httpcomp.expected.txt
@@ -0,0 +1,9 @@
+Request-Header|Accept-Encoding|gzip,deflate
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|240
+Request-Header|Content-Type|multipart/form-data; boundary=L8vdau8TpP0o-AYJDjCuYFQYnjB5gcHIFyap
+Request-Header|Host|localhost:9090
+Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
+Request-Header|X-BrowserId|apache-httpcomp
+Parts-Count|1
+Part-ContainsContents|pi|3.14159265358979323846264338327950288419716939937510
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only2-apache-httpcomp.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only2-apache-httpcomp.raw
new file mode 100644
index 0000000..641c1a1
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-number-only2-apache-httpcomp.raw
@@ -0,0 +1,7 @@
+--L8vdau8TpP0o-AYJDjCuYFQYnjB5gcHIFyap
+Content-Disposition: form-data; name="pi"
+Content-Type: text/plain
+Content-Transfer-Encoding: 8bit
+
+3.14159265358979323846264338327950288419716939937510
+--L8vdau8TpP0o-AYJDjCuYFQYnjB5gcHIFyap--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-apache-httpcomp.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-apache-httpcomp.expected.txt
new file mode 100644
index 0000000..77ad18f
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-apache-httpcomp.expected.txt
@@ -0,0 +1,10 @@
+Request-Header|Accept-Encoding|gzip,deflate
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|406
+Request-Header|Content-Type|multipart/form-data; boundary=u7tfLQaHJEHHUJjnVDbFdc_Oqz4jmkA25mgWd
+Request-Header|Host|localhost:9090
+Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
+Request-Header|X-BrowserId|apache-httpcomp
+Parts-Count|2
+Part-ContainsContents|japanese|オープンソース
+Part-ContainsContents|hello|日食桟橋
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-apache-httpcomp.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-apache-httpcomp.raw
new file mode 100644
index 0000000..dfe4e57
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-apache-httpcomp.raw
@@ -0,0 +1,13 @@
+--u7tfLQaHJEHHUJjnVDbFdc_Oqz4jmkA25mgWd
+Content-Disposition: form-data; name="japanese"
+Content-Type: text/plain; charset=Shift_JIS
+Content-Transfer-Encoding: 8bit
+
+I[v\[X
+--u7tfLQaHJEHHUJjnVDbFdc_Oqz4jmkA25mgWd
+Content-Disposition: form-data; name="hello"
+Content-Type: text/plain; charset=Shift_JIS
+Content-Transfer-Encoding: 8bit
+
+úHV´
+--u7tfLQaHJEHHUJjnVDbFdc_Oqz4jmkA25mgWd--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-chrome.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-chrome.expected.txt
new file mode 100644
index 0000000..1ce9ca1
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-chrome.expected.txt
@@ -0,0 +1,17 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US,en;q=0.9
+Request-Header|Cache-Control|max-age=0
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|354
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryN7pYBoDaXhEcUl13
+Request-Header|DNT|1
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Origin|http://192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/sjis-form-charset.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Linux; Android 8.1.0; Pixel 2 XL Build/OPM1.171019.021) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.109 Mobile Safari/537.36
+Parts-Count|3
+Part-ContainsContents|_charset_|Shift_JIS
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-chrome.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-chrome.raw
new file mode 100644
index 0000000..5c77075
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-chrome.raw
@@ -0,0 +1,13 @@
+------WebKitFormBoundaryN7pYBoDaXhEcUl13
+Content-Disposition: form-data; name="_charset_"
+
+Shift_JIS
+------WebKitFormBoundaryN7pYBoDaXhEcUl13
+Content-Disposition: form-data; name="japanese"
+
+¡
+------WebKitFormBoundaryN7pYBoDaXhEcUl13
+Content-Disposition: form-data; name="hello"
+
+戆^
+------WebKitFormBoundaryN7pYBoDaXhEcUl13--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-firefox.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-firefox.expected.txt
new file mode 100644
index 0000000..0a05ede
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-firefox.expected.txt
@@ -0,0 +1,14 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US,en;q=0.5
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|430
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------117031256520586657911714164254
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/sjis-form-charset.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Android 8.1.0; Mobile; rv:59.0) Gecko/59.0 Firefox/59.0
+Parts-Count|3
+Part-ContainsContents|_charset_|Shift_JIS
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-firefox.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-firefox.raw
new file mode 100644
index 0000000..b3c4ae8
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-firefox.raw
@@ -0,0 +1,13 @@
+-----------------------------117031256520586657911714164254
+Content-Disposition: form-data; name="_charset_"
+
+Shift_JIS
+-----------------------------117031256520586657911714164254
+Content-Disposition: form-data; name="japanese"
+
+¡
+-----------------------------117031256520586657911714164254
+Content-Disposition: form-data; name="hello"
+
+戆^
+-----------------------------117031256520586657911714164254--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-chrome.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-chrome.expected.txt
new file mode 100644
index 0000000..0d91a3d
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-chrome.expected.txt
@@ -0,0 +1,18 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate, br
+Request-Header|Accept-Language|en-US,en;q=0.9
+Request-Header|Cache-Control|max-age=0
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|354
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryDHtjXxgNUcgLjcKs
+Request-Header|Cookie|visited=yes
+Request-Header|DNT|1
+Request-Header|Host|localhost:9090
+Request-Header|Origin|http://localhost:9090
+Request-Header|Referer|http://localhost:9090/sjis-form-charset.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36
+Parts-Count|3
+Part-ContainsContents|_charset_|Shift_JIS
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-chrome.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-chrome.raw
new file mode 100644
index 0000000..6431461
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-chrome.raw
@@ -0,0 +1,13 @@
+------WebKitFormBoundaryDHtjXxgNUcgLjcKs
+Content-Disposition: form-data; name="_charset_"
+
+Shift_JIS
+------WebKitFormBoundaryDHtjXxgNUcgLjcKs
+Content-Disposition: form-data; name="japanese"
+
+¡
+------WebKitFormBoundaryDHtjXxgNUcgLjcKs
+Content-Disposition: form-data; name="hello"
+
+戆^
+------WebKitFormBoundaryDHtjXxgNUcgLjcKs--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-edge.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-edge.expected.txt
new file mode 100644
index 0000000..4b4cc72
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-edge.expected.txt
@@ -0,0 +1,14 @@
+Request-Header|Accept|text/html, application/xhtml+xml, image/jxr, */*
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US
+Request-Header|Cache-Control|no-cache
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|362
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------7e227e17151054
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/sjis-form-charset.html
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299
+Parts-Count|3
+Part-ContainsContents|_charset_|utf-8
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-edge.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-edge.raw
new file mode 100644
index 0000000..71dac77
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-edge.raw
@@ -0,0 +1,13 @@
+-----------------------------7e227e17151054
+Content-Disposition: form-data; name="_charset_"
+
+utf-8
+-----------------------------7e227e17151054
+Content-Disposition: form-data; name="japanese"
+
+健治
+-----------------------------7e227e17151054
+Content-Disposition: form-data; name="hello"
+
+ャユ戆タ
+-----------------------------7e227e17151054--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-firefox.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-firefox.expected.txt
new file mode 100644
index 0000000..f085e29
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-firefox.expected.txt
@@ -0,0 +1,14 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US,en;q=0.5
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|370
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------114782935826962
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/sjis-form-charset.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0
+Parts-Count|3
+Part-ContainsContents|_charset_|Shift_JIS
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-firefox.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-firefox.raw
new file mode 100644
index 0000000..921df60
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-firefox.raw
@@ -0,0 +1,13 @@
+-----------------------------114782935826962
+Content-Disposition: form-data; name="_charset_"
+
+Shift_JIS
+-----------------------------114782935826962
+Content-Disposition: form-data; name="japanese"
+
+¡
+-----------------------------114782935826962
+Content-Disposition: form-data; name="hello"
+
+戆^
+-----------------------------114782935826962--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-ios-safari.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-ios-safari.expected.txt
new file mode 100644
index 0000000..2d6fbab
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-ios-safari.expected.txt
@@ -0,0 +1,15 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-us
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|354
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryvshQXGBfIsRjfMBN
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Origin|http://192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/sjis-form-charset.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (iPad; CPU OS 11_2_6 like Mac OS X) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0 Mobile/15D100 Safari/604.1
+Parts-Count|3
+Part-ContainsContents|_charset_|Shift_JIS
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-ios-safari.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-ios-safari.raw
new file mode 100644
index 0000000..9892c9c
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-ios-safari.raw
@@ -0,0 +1,13 @@
+------WebKitFormBoundaryvshQXGBfIsRjfMBN
+Content-Disposition: form-data; name="_charset_"
+
+Shift_JIS
+------WebKitFormBoundaryvshQXGBfIsRjfMBN
+Content-Disposition: form-data; name="japanese"
+
+¡
+------WebKitFormBoundaryvshQXGBfIsRjfMBN
+Content-Disposition: form-data; name="hello"
+
+戆^
+------WebKitFormBoundaryvshQXGBfIsRjfMBN--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-msie.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-msie.expected.txt
new file mode 100644
index 0000000..5d84aa6
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-msie.expected.txt
@@ -0,0 +1,14 @@
+Request-Header|Accept|text/html, application/xhtml+xml, image/jxr, */*
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US
+Request-Header|Cache-Control|no-cache
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|358
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------7e226e1b2109c
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/sjis-form-charset.html
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko
+Parts-Count|3
+Part-ContainsContents|_charset_|utf-8
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-msie.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-msie.raw
new file mode 100644
index 0000000..9a043e6
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-msie.raw
@@ -0,0 +1,13 @@
+-----------------------------7e226e1b2109c
+Content-Disposition: form-data; name="_charset_"
+
+utf-8
+-----------------------------7e226e1b2109c
+Content-Disposition: form-data; name="japanese"
+
+健治
+-----------------------------7e226e1b2109c
+Content-Disposition: form-data; name="hello"
+
+ャユ戆タ
+-----------------------------7e226e1b2109c--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-safari.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-safari.expected.txt
new file mode 100644
index 0000000..18452b2
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-safari.expected.txt
@@ -0,0 +1,15 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-us
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|354
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryHFCTTESrC7sXQ2Gf
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Origin|http://192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/sjis-form-charset.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6
+Parts-Count|3
+Part-ContainsContents|_charset_|Shift_JIS
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-safari.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-safari.raw
new file mode 100644
index 0000000..ce14357
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-safari.raw
@@ -0,0 +1,13 @@
+------WebKitFormBoundaryHFCTTESrC7sXQ2Gf
+Content-Disposition: form-data; name="_charset_"
+
+Shift_JIS
+------WebKitFormBoundaryHFCTTESrC7sXQ2Gf
+Content-Disposition: form-data; name="japanese"
+
+¡
+------WebKitFormBoundaryHFCTTESrC7sXQ2Gf
+Content-Disposition: form-data; name="hello"
+
+戆^
+------WebKitFormBoundaryHFCTTESrC7sXQ2Gf--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-chrome.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-chrome.expected.txt
new file mode 100644
index 0000000..f5e2236
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-chrome.expected.txt
@@ -0,0 +1,16 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US,en;q=0.9
+Request-Header|Cache-Control|max-age=0
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|249
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryjJR29nbr1TDUu2yh
+Request-Header|DNT|1
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Origin|http://192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/sjis-form.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Linux; Android 8.1.0; Pixel 2 XL Build/OPM1.171019.021) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.109 Mobile Safari/537.36
+Parts-Count|2
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-chrome.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-chrome.raw
new file mode 100644
index 0000000..618c30e
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-chrome.raw
@@ -0,0 +1,9 @@
+------WebKitFormBoundaryjJR29nbr1TDUu2yh
+Content-Disposition: form-data; name="japanese"
+
+¡
+------WebKitFormBoundaryjJR29nbr1TDUu2yh
+Content-Disposition: form-data; name="hello"
+
+戆^
+------WebKitFormBoundaryjJR29nbr1TDUu2yh--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-firefox.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-firefox.expected.txt
new file mode 100644
index 0000000..b3baf19
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-firefox.expected.txt
@@ -0,0 +1,13 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US,en;q=0.5
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|303
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------18591390852002031541755421242
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/sjis-form.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Android 8.1.0; Mobile; rv:59.0) Gecko/59.0 Firefox/59.0
+Parts-Count|2
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-firefox.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-firefox.raw
new file mode 100644
index 0000000..5c8d5b4
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-firefox.raw
@@ -0,0 +1,9 @@
+-----------------------------18591390852002031541755421242
+Content-Disposition: form-data; name="japanese"
+
+¡
+-----------------------------18591390852002031541755421242
+Content-Disposition: form-data; name="hello"
+
+戆^
+-----------------------------18591390852002031541755421242--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-chrome.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-chrome.expected.txt
new file mode 100644
index 0000000..6cba2d9
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-chrome.expected.txt
@@ -0,0 +1,17 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate, br
+Request-Header|Accept-Language|en-US,en;q=0.9
+Request-Header|Cache-Control|max-age=0
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|249
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundarysKD6As9BBil2g6Fc
+Request-Header|Cookie|visited=yes
+Request-Header|DNT|1
+Request-Header|Host|localhost:9090
+Request-Header|Origin|http://localhost:9090
+Request-Header|Referer|http://localhost:9090/sjis-form.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36
+Parts-Count|2
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-chrome.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-chrome.raw
new file mode 100644
index 0000000..02e44b0
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-chrome.raw
@@ -0,0 +1,9 @@
+------WebKitFormBoundarysKD6As9BBil2g6Fc
+Content-Disposition: form-data; name="japanese"
+
+¡
+------WebKitFormBoundarysKD6As9BBil2g6Fc
+Content-Disposition: form-data; name="hello"
+
+戆^
+------WebKitFormBoundarysKD6As9BBil2g6Fc--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-edge.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-edge.expected.txt
new file mode 100644
index 0000000..f51c4cc
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-edge.expected.txt
@@ -0,0 +1,13 @@
+Request-Header|Accept|text/html, application/xhtml+xml, image/jxr, */*
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US
+Request-Header|Cache-Control|no-cache
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|255
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------7e28636151054
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/sjis-form.html
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299
+Parts-Count|2
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-edge.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-edge.raw
new file mode 100644
index 0000000..b6a9a54
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-edge.raw
@@ -0,0 +1,9 @@
+-----------------------------7e28636151054
+Content-Disposition: form-data; name="japanese"
+
+健治
+-----------------------------7e28636151054
+Content-Disposition: form-data; name="hello"
+
+ャユ戆タ
+-----------------------------7e28636151054--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-firefox.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-firefox.expected.txt
new file mode 100644
index 0000000..ad25c45
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-firefox.expected.txt
@@ -0,0 +1,13 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US,en;q=0.5
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|261
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------265001916915724
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/sjis-form.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0
+Parts-Count|2
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-firefox.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-firefox.raw
new file mode 100644
index 0000000..5c8def3
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-firefox.raw
@@ -0,0 +1,9 @@
+-----------------------------265001916915724
+Content-Disposition: form-data; name="japanese"
+
+¡
+-----------------------------265001916915724
+Content-Disposition: form-data; name="hello"
+
+戆^
+-----------------------------265001916915724--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-ios-safari.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-ios-safari.expected.txt
new file mode 100644
index 0000000..e4b4d81
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-ios-safari.expected.txt
@@ -0,0 +1,14 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-us
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|249
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryj1Xj6oPRT7sp3VPE
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Origin|http://192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/sjis-form.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (iPad; CPU OS 11_2_6 like Mac OS X) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0 Mobile/15D100 Safari/604.1
+Parts-Count|2
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-ios-safari.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-ios-safari.raw
new file mode 100644
index 0000000..f0d3975
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-ios-safari.raw
@@ -0,0 +1,9 @@
+------WebKitFormBoundaryj1Xj6oPRT7sp3VPE
+Content-Disposition: form-data; name="japanese"
+
+¡
+------WebKitFormBoundaryj1Xj6oPRT7sp3VPE
+Content-Disposition: form-data; name="hello"
+
+戆^
+------WebKitFormBoundaryj1Xj6oPRT7sp3VPE--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-msie.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-msie.expected.txt
new file mode 100644
index 0000000..d8ddc61
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-msie.expected.txt
@@ -0,0 +1,13 @@
+Request-Header|Accept|text/html, application/xhtml+xml, image/jxr, */*
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-US
+Request-Header|Cache-Control|no-cache
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|255
+Request-Header|Content-Type|multipart/form-data; boundary=---------------------------7e21df392109c
+Request-Header|Host|localhost:9090
+Request-Header|Referer|http://localhost:9090/sjis-form.html
+Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko
+Parts-Count|2
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-msie.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-msie.raw
new file mode 100644
index 0000000..b60882c
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-msie.raw
@@ -0,0 +1,9 @@
+-----------------------------7e21df392109c
+Content-Disposition: form-data; name="japanese"
+
+健治
+-----------------------------7e21df392109c
+Content-Disposition: form-data; name="hello"
+
+ャユ戆タ
+-----------------------------7e21df392109c--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-safari.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-safari.expected.txt
new file mode 100644
index 0000000..2acbd52
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-safari.expected.txt
@@ -0,0 +1,14 @@
+Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Request-Header|Accept-Encoding|gzip, deflate
+Request-Header|Accept-Language|en-us
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|249
+Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundarytsFILMzOBBWaETUj
+Request-Header|Host|192.168.0.119:9090
+Request-Header|Origin|http://192.168.0.119:9090
+Request-Header|Referer|http://192.168.0.119:9090/sjis-form.html
+Request-Header|Upgrade-Insecure-Requests|1
+Request-Header|User-Agent|Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6
+Parts-Count|2
+Part-ContainsContents|japanese|健治
+Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-safari.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-safari.raw
new file mode 100644
index 0000000..82475fa
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-safari.raw
@@ -0,0 +1,9 @@
+------WebKitFormBoundarytsFILMzOBBWaETUj
+Content-Disposition: form-data; name="japanese"
+
+¡
+------WebKitFormBoundarytsFILMzOBBWaETUj
+Content-Disposition: form-data; name="hello"
+
+戆^
+------WebKitFormBoundarytsFILMzOBBWaETUj--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-jetty-client.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-jetty-client.expected.txt
new file mode 100644
index 0000000..a0ec6ed
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-jetty-client.expected.txt
@@ -0,0 +1,10 @@
+Request-Header|Accept-Encoding|gzip
+Request-Header|Connection|close
+Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundaryny8fndkswj5ot6hx
+Request-Header|Host|localhost:9090
+Request-Header|Transfer-Encoding|chunked
+Request-Header|User-Agent|Jetty/9.4.9.v20180320
+Request-Header|X-BrowserId|jetty-client
+Parts-Count|2
+Part-ContainsContents|japanese|オープンソース
+Part-ContainsContents|hello|日食桟橋
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-jetty-client.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-jetty-client.raw
new file mode 100644
index 0000000..eb7b56f
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-sjis-jetty-client.raw
@@ -0,0 +1,11 @@
+--JettyHttpClientBoundaryny8fndkswj5ot6hx
+Content-Disposition: form-data; name="japanese"
+Content-Type: text/plain; charset=Shift-JIS
+
+I[v\[X
+--JettyHttpClientBoundaryny8fndkswj5ot6hx
+Content-Disposition: form-data; name="hello"
+Content-Type: text/plain; charset=Shift-JIS
+
+úHV´
+--JettyHttpClientBoundaryny8fndkswj5ot6hx--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-strange-quoting-apache-httpcomp.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-strange-quoting-apache-httpcomp.expected.txt
new file mode 100644
index 0000000..42010b4
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-strange-quoting-apache-httpcomp.expected.txt
@@ -0,0 +1,11 @@
+Request-Header|Accept-Encoding|gzip,deflate
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|798
+Request-Header|Content-Type|multipart/form-data; boundary=z5xWs05oeiE0TAdFlrrlAX5RSgHrHzVcgskrru
+Request-Header|Host|localhost:9090
+Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
+Request-Header|X-BrowserId|apache-httpcomp
+Parts-Count|4
+Part-ContainsContents|and "I" quote|Value 1
+Part-ContainsContents|and+%22I%22+quote|Value 2
+Part-ContainsContents|value"; what="whoa"|Value 3
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-strange-quoting-apache-httpcomp.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-strange-quoting-apache-httpcomp.raw
new file mode 100644
index 0000000..487ea39
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-strange-quoting-apache-httpcomp.raw
@@ -0,0 +1,25 @@
+--z5xWs05oeiE0TAdFlrrlAX5RSgHrHzVcgskrru
+Content-Disposition: form-data; name="and \"I\" quote"
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Value 1
+--z5xWs05oeiE0TAdFlrrlAX5RSgHrHzVcgskrru
+Content-Disposition: form-data; name="and+%22I%22+quote"
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Value 2
+--z5xWs05oeiE0TAdFlrrlAX5RSgHrHzVcgskrru
+Content-Disposition: form-data; name="value\"; what=\"whoa\""
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Value 3
+--z5xWs05oeiE0TAdFlrrlAX5RSgHrHzVcgskrru
+Content-Disposition: form-data; name="other\"; what=\"Something\""
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Value 4
+--z5xWs05oeiE0TAdFlrrlAX5RSgHrHzVcgskrru--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-text-files-apache-httpcomp.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-text-files-apache-httpcomp.expected.txt
new file mode 100644
index 0000000..c246ce6
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-text-files-apache-httpcomp.expected.txt
@@ -0,0 +1,15 @@
+Request-Header|Accept-Encoding|gzip,deflate
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|737
+Request-Header|Content-Type|multipart/form-data; boundary=B8x_673_DRSeYGTpUMgof-qN1nircWQA
+Request-Header|Host|localhost:9090
+Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
+Request-Header|X-BrowserId|apache-httpcomp
+Parts-Count|3
+Part-ContainsContents|text|text default
+Part-ContainsContents|file1|Content of a.txt
+Part-ContainsContents|file2|<!DOCTYPE html><title>Content of a.html
+Part-Filename|file1|a.txt
+Part-Filename|file2|a.html
+Part-Sha1sum|file1|588A0F273CB5AFE9C8D76DD081812E672F2061E2
+Part-Sha1sum|file2|9A9005159AB90A6D2D9BACB5414EFE932F0CED85
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-text-files-apache-httpcomp.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-text-files-apache-httpcomp.raw
new file mode 100644
index 0000000..b8fee37
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-text-files-apache-httpcomp.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-text-files-jetty-client.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-text-files-jetty-client.expected.txt
new file mode 100644
index 0000000..7e0b528
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-text-files-jetty-client.expected.txt
@@ -0,0 +1,15 @@
+Request-Header|Accept-Encoding|gzip
+Request-Header|Connection|close
+Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary1e87p8a551psw1al
+Request-Header|Host|localhost:9090
+Request-Header|Transfer-Encoding|chunked
+Request-Header|User-Agent|Jetty/9.4.9.v20180320
+Request-Header|X-BrowserId|jetty-client
+Parts-Count|3
+Part-ContainsContents|text|text default
+Part-ContainsContents|file1|Content of a.txt
+Part-ContainsContents|file2|<!DOCTYPE html><title>Content of a.html
+Part-Filename|file1|a.txt
+Part-Filename|file2|a.html
+Part-Sha1sum|file1|588A0F273CB5AFE9C8D76DD081812E672F2061E2
+Part-Sha1sum|file2|9A9005159AB90A6D2D9BACB5414EFE932F0CED85
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-text-files-jetty-client.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-text-files-jetty-client.raw
new file mode 100644
index 0000000..5340542
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-text-files-jetty-client.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-unicode-names-apache-httpcomp.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-unicode-names-apache-httpcomp.expected.txt
new file mode 100644
index 0000000..446b69d
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-unicode-names-apache-httpcomp.expected.txt
@@ -0,0 +1,11 @@
+Request-Header|Accept-Encoding|gzip,deflate
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|475
+Request-Header|Content-Type|multipart/form-data; boundary=yRxfRSltW63lJPc9oHOOVyCn-SmDG6i4Ts9M4E6
+Request-Header|Host|localhost:9090
+Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
+Request-Header|X-BrowserId|apache-httpcomp
+Parts-Count|2
+Part-ContainsContents|こんにちは世界|Greetings 1
+Part-ContainsContents|%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF%E4%B8%96%E7%95%8C|Greetings 2
+
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-unicode-names-apache-httpcomp.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-unicode-names-apache-httpcomp.raw
new file mode 100644
index 0000000..938bdbd
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-unicode-names-apache-httpcomp.raw
@@ -0,0 +1,13 @@
+--yRxfRSltW63lJPc9oHOOVyCn-SmDG6i4Ts9M4E6
+Content-Disposition: form-data; name="こんにちは世界"
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Greetings 1
+--yRxfRSltW63lJPc9oHOOVyCn-SmDG6i4Ts9M4E6
+Content-Disposition: form-data; name="%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF%E4%B8%96%E7%95%8C"
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Greetings 2
+--yRxfRSltW63lJPc9oHOOVyCn-SmDG6i4Ts9M4E6--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-unicode-names-jetty-client.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-unicode-names-jetty-client.expected.txt
new file mode 100644
index 0000000..bc9e8e6
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-unicode-names-jetty-client.expected.txt
@@ -0,0 +1,11 @@
+Request-Header|Accept-Encoding|gzip
+Request-Header|Connection|close
+Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary9iv9jofnq5dkzmgl
+Request-Header|Host|localhost:9090
+Request-Header|Transfer-Encoding|chunked
+Request-Header|User-Agent|Jetty/9.4.9.v20180320
+Request-Header|X-BrowserId|jetty-client
+Parts-Count|2
+Part-ContainsContents|こんにちは世界|Greetings 1
+Part-ContainsContents|%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF%E4%B8%96%E7%95%8C|Greetings 2
+
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-unicode-names-jetty-client.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-unicode-names-jetty-client.raw
new file mode 100644
index 0000000..4188d3e
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-unicode-names-jetty-client.raw
@@ -0,0 +1,11 @@
+--JettyHttpClientBoundary9iv9jofnq5dkzmgl
+Content-Disposition: form-data; name="こんにちは世界"
+Content-Type: text/plain
+
+Greetings 1
+--JettyHttpClientBoundary9iv9jofnq5dkzmgl
+Content-Disposition: form-data; name="%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF%E4%B8%96%E7%95%8C"
+Content-Type: text/plain
+
+Greetings 2
+--JettyHttpClientBoundary9iv9jofnq5dkzmgl--
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-whitespace-only-jetty-client.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-whitespace-only-jetty-client.expected.txt
new file mode 100644
index 0000000..8b7f321
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-whitespace-only-jetty-client.expected.txt
@@ -0,0 +1,10 @@
+Request-Header|Accept-Encoding|gzip
+Request-Header|Connection|close
+Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary1evz7ehqg8tvo10h
+Request-Header|Host|localhost:9090
+Request-Header|Transfer-Encoding|chunked
+Request-Header|User-Agent|Jetty/9.4.9.v20180320
+Request-Header|X-BrowserId|jetty-client
+Parts-Count|1
+Part-Filename|whitespace|whitespace.txt
+Part-Sha1sum|whitespace|353E2CCDDE1069706B950414B230B6C047F98491
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-whitespace-only-jetty-client.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-whitespace-only-jetty-client.raw
new file mode 100644
index 0000000..64f39f6
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-whitespace-only-jetty-client.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-zalgo-text-plain-apache-httpcomp.expected.txt b/third_party/jetty-http/src/test/resources/multipart/browser-capture-zalgo-text-plain-apache-httpcomp.expected.txt
new file mode 100644
index 0000000..9fecae5
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-zalgo-text-plain-apache-httpcomp.expected.txt
@@ -0,0 +1,12 @@
+Request-Header|Accept-Encoding|gzip,deflate
+Request-Header|Connection|keep-alive
+Request-Header|Content-Length|1870
+Request-Header|Content-Type|multipart/form-data; boundary=V9oY7Ug2J-n4sFXLWdb7yd2LtU0hdK36ejhKYh
+Request-Header|Host|localhost:9090
+Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
+Request-Header|X-BrowserId|apache-httpcomp
+Parts-Count|4
+Part-ContainsContents|zalgo-8|y͔͕͍o̪̞͎̥͇̤̕u'̛̰̫̳̰v̧̘̪̠̟̟e̥͈̱ ̥̠͇͎͕̜s̤e̺e̙ͅņ̜ ̲̟͝za̴͖̱̲͈̘l͖̖͓̙̮͔g͕̞͖͘o͕̤͈̗ ̯̲̹̲͓b͙͟e̞͎̜̗͈͉̭͞f̸or̰̩e̡̝̺,̸͕̙̥̼͇̜ ̪͇̹r̘̪ͅị͔̥͈ͅg̠̟̯͖̦͇ht͖̪͍͚̖͡?͙̝͖̞
+Part-ContainsContents|zalgo-16|y͔͕͍o̪̞͎̥͇̤̕u'̛̰̫̳̰v̧̘̪̠̟̟e̥͈̱ ̥̠͇͎͕̜s̤e̺e̙ͅņ̜ ̲̟͝za̴͖̱̲͈̘l͖̖͓̙̮͔g͕̞͖͘o͕̤͈̗ ̯̲̹̲͓b͙͟e̞͎̜̗͈͉̭͞f̸or̰̩e̡̝̺,̸͕̙̥̼͇̜ ̪͇̹r̘̪ͅị͔̥͈ͅg̠̟̯͖̦͇ht͖̪͍͚̖͡?͙̝͖̞
+Part-ContainsContents|zalgo-16-be|y͔͕͍o̪̞͎̥͇̤̕u'̛̰̫̳̰v̧̘̪̠̟̟e̥͈̱ ̥̠͇͎͕̜s̤e̺e̙ͅņ̜ ̲̟͝za̴͖̱̲͈̘l͖̖͓̙̮͔g͕̞͖͘o͕̤͈̗ ̯̲̹̲͓b͙͟e̞͎̜̗͈͉̭͞f̸or̰̩e̡̝̺,̸͕̙̥̼͇̜ ̪͇̹r̘̪ͅị͔̥͈ͅg̠̟̯͖̦͇ht͖̪͍͚̖͡?͙̝͖̞
+Part-ContainsContents|zalgo-16-le|y͔͕͍o̪̞͎̥͇̤̕u'̛̰̫̳̰v̧̘̪̠̟̟e̥͈̱ ̥̠͇͎͕̜s̤e̺e̙ͅņ̜ ̲̟͝za̴͖̱̲͈̘l͖̖͓̙̮͔g͕̞͖͘o͕̤͈̗ ̯̲̹̲͓b͙͟e̞͎̜̗͈͉̭͞f̸or̰̩e̡̝̺,̸͕̙̥̼͇̜ ̪͇̹r̘̪ͅị͔̥͈ͅg̠̟̯͖̦͇ht͖̪͍͚̖͡?͙̝͖̞
diff --git a/third_party/jetty-http/src/test/resources/multipart/browser-capture-zalgo-text-plain-apache-httpcomp.raw b/third_party/jetty-http/src/test/resources/multipart/browser-capture-zalgo-text-plain-apache-httpcomp.raw
new file mode 100644
index 0000000..2cd8c76
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/browser-capture-zalgo-text-plain-apache-httpcomp.raw
Binary files differ
diff --git a/third_party/jetty-http/src/test/resources/multipart/multipart-base64-long.expected.txt b/third_party/jetty-http/src/test/resources/multipart/multipart-base64-long.expected.txt
new file mode 100644
index 0000000..2b1d5de
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/multipart-base64-long.expected.txt
@@ -0,0 +1,4 @@
+Content-Type|multipart/form-data; boundary="JuH4rALGPJfmAquncS_U1du8s59GjKKiG9a8"
+Parts-Count|1
+Part-Filename|png|jetty-avatar-256.png
+Part-Sha1sum|png|e75b73644afe9b234d70da9ff225229de68cdff8
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/multipart-base64-long.raw b/third_party/jetty-http/src/test/resources/multipart/multipart-base64-long.raw
new file mode 100644
index 0000000..2e2b499
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/multipart-base64-long.raw
@@ -0,0 +1,8 @@
+--JuH4rALGPJfmAquncS_U1du8s59GjKKiG9a8
+Content-ID: <junk@company.com>
+Content-Disposition: form-data; name="png"; filename="jetty-avatar-256.png"
+Content-Type: image/png; name=jetty-avatar-256.png
+Content-Transfer-Encoding: base64
+
+iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAI3AAACNwBn+hfPAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7X15fBRF2v+3eyYnOSCQcEMQQUBEhV05xQvvdxHB1d/iq6is1y6or7rgCgqLiJyut+uxrsfeC57LKaIoCCr3KZBwyJFASICQkJDM8fsjdNJTU/VUdc/0HGG+n09/+pyequ56vs9R9VRrw4cPRwIJJHB2Qo92ARJIIIHoIUEACSRwFiNBAAkkcBbDHe0CRBrz5s3Tol2GSGPEiBH+aJfBQDSff7ifgxN1ifS70hprEPBsFHS7CLXRJZ519BDqu2sUBBADDdDO/8eMVk6g8UGVGOKWABwQ+miTiBWcbeTBezdn2zMICSJCiCsCCJPQOyXoqvd1suHGg1DEEtFafV7RLntY3q+ZDOKCAEIQfDu/i/ZLpmCnAUSLFGL5OTY22H7HMd0LYFPwVX5j9b6RbsyiFyoqB9UA2N84RQix/kyB8Nc9knVw5B3HJAHYEHzZ9aGeD/V6GdgXJru/7HrVxhKKQFh5Bk5dy4MVQXEadv+PVwcr5K/8jmOOACwKP3Wt6Jyd34R6rQjGy1G5l/lFygRetQGoNConrLBwEK5VK4mHWI0BUP9j9V2T52OKACwIv1Xh5h23QxBWrlGBBrVG6Bf8p4hARAKs2uDDaRGFi3DD8VurlpYTZbCLcLxrjT0XEwTgkOCzx1RJwKkGK7unVW3EIwVeIwmVDESw+pxCIdxQYcXSsgOn60ApAKttwHzOH3UCUBR+1cYT7n3ZcZVrrWpdEXPL7sM2BitkIPpfXvlUz4WDcGXnY6XbMxLtg4XsffPedRARRJUAQhB+SnBVz9klA9k5q9cK2ZlzXLWxWCUD9pwK7Fhd4SZcK7GOaPV+WD1vRwHw3rcSEUSNABSEPxTBD9c2dYw6LoOKJha9QD9nW/ZfVslABKfIWHRvK1Dxf8NJAk7HNsJhGVBE4AeiRABhEH4r2+xadp2oDOEiAZE/xzvHe4FWSED0e/O++ZgVhJNkQyUDK1YU77hVOKUkKG1tlcBkRKABUSAAG8KvokUoIZetZcdE+6JjKhC9SD+zLXqBvMYQDheBgh1SViVflfvzIIuMi+pohwhUy2fHMqDKK3rvvPctCgoKiSDqQUAGVk18WaNTJQErDZW3bxe8F0htmxsBu5b9j1QbKCIUobf6nKmyqUbGVYTLClTKqEoCVDl4ykBEAiptgfvbiBKARPvbFX5VwbdDDNQ2dYwC7wWpCL5oHQ5SoMqmqvlDXfP+S9UCoI7xnmeo1o/VfdExURlkhM2uqfvy2kb9uYgRQBiE36rgW91WWfPKKjrGg6rwWxV8VQ0hK5fqOxI9F1Uypta8/5PBqiVl3lf9H6sKivotD6I6UNYfBMdU/ityFoDDws9uh7LPuydVLtG+CHZMfpHwi66xGzSSQfW9WCFbu1aAFc0vIwBVqLSFUEjMgJ12wCME3n2DrIFoxwDsCr+KUMuOWyECXjlEdZBBhenZtUz4eRaBKhGoakHZu7FKvLxt3n+pQNWForYpWBV+u3VRbQt237X5fyJjARDa36rwyxqbVeGnruVti8oo2mehov3tCD27GGWx2zhEYOuu8j5UjoGzZrcpqAq/HUvAShuVHZNBRfOrtAPz/7LHzGQfExaAATvaRVXAdYVrVLWVqKyiugByc1VV+K0uRlnCRQRW3ovqe+Ldx7xWgVWhd8ICcILIwtEepP/pKAEoan+7wq+yiIRfRgqi/6bKztsHaAJQMfWsLj6IG4FZ+I1tFUJg6xzKOwkHyYIps4r2VPkNC7uCLyIymXIIlxLwCepj3K++HNGwAGSkYFf4WaHWFc852UjNoBqfFTOPFXD2mCa4JhQrIBRCtkO25rUMqmTKO8du8yB633ZJgAeV9qDSFgzB1wXHzf+hAQ4SgMVEn1CEnyfoqsdUyYBXJvOaVycDFMuzax4BsC9ZZ4750CDQxnEZCahaASKtFwoZqzxn9r9FsCr84SAAWVul1iLI2oJI8Nn3DuYaAzrnmB9wiAAsmv52hZ8ScvOaOheOBqqqqQB+w5NpfR7Lm0nA3Ag0WCMB8++peoRCxqqEAM62DFYEXkYWPLDtVaWtsufZ+7DgaX+eMPPeGSv44PyeJYIAayCSLgBP+GXX2BF8q9uqZADONq8u7D5lAagwPqvtdWZtFmBW8MH8lgX7Wx6sCr8KMfOeNTjbbBmM+rDbsmfKOye6F/t/5n1eWWVkwLsfC1k7MGtw3rtm17x2wi1LtHsBZA9WVfjZbd3CcVUiYMtmLj+7zYLS/OZtSvDZtfHCzURgrNkGQVkE5sYkglXhD8czBsRl4mlN0bYVEuDVm13LCECkIHh1ERGYqC3wyJ591+y9WRIIIISwE4DA/OcJikiIVIWfEm7Zwv5ORVupNlKRtjLv2xV+dttMBMYiahjU/2mmc7y6qAq/HeuLesZsOdgyyrS9KgmY7yd6n5TAy9qGinJQsQLN79v8nlnhZ+vGkkB9eaJtARgwPzT2GK/BqQi/S+GcChGoNFLqBRtQZXtK4/OEn7fwGgb1v8ZxlcZPCb+qJcYjdJEQiaBKpnasAJHCUiEBOwTAKz+rBFiLj33P7H8YQm/eDypPJAhA9DCNNXue94BVzH1W4F0K27JGShEBrz4UVBos76XLhN4rKB+7sNYATzBYUA3dqhVGka152/x/FFSFXnQdew9evdmyqDxnXh1EdZHVQcXiEylOMNfqzDEAYR4IFMKc/ioPlecCiIRbtFYhA1USAGdt3rZi/qua/F7whZ9HBCwoEuCVl60fj4xVhJ99xjwikBEA75laIQCKFHh1FymqSBIARf68d83+B1snlgx8QGSHAouERUYEIsGnhF9lW9UakBEAr6GawTbaUIRfRyAR6KZtlgjY50m5Amw52fpYEX7KElN9xux/m8ETXpkGlbkC5vtZ1f4yC5FSijJLkBJ+83um7m3eDyqb0wQgKiB7XPagVbQMK+gu4rgqCVAEoPqSAXmjFfn85sUs/Kzgs42CXbNEwFoB5rKZQTV4FaG3Y22Fqjllwq9CfOb/lml7UduwQmR2hd8ruDdbLz8aXADAZA2EjQAUc/5l2t58nejBqmh8K4sVLUWVma2fARWNpeLr66a1mQSMMogaBY+szJFkkTCoan8rLhhFAjIBYstnRfhlcQ/KBQiFAFSITKUtsFYe+569CARbRx2BFkD94qQFIKo0dVz2oGWmpwt1gU1WyIOOHTt2LGXOnDk9169ffw4Al3YGuq7Xb2qapgGAZoKxb14HVOLMMb/fH6RZjGO8tQg+n893Zm3e9/l8Pr/P5/N16NDhwJw5c1bm5uaeRjAhsM/b3A3EiwPI3olM+FXcLRcAfefOnU1eeOGFi/bs2dMagKbretAz5T1f83Nlnl/9oTM7fgD+Zs2aHe/WrVvR6NGjC9q3b1/NqbOs7uZtVtgD9gsKCpq8+OKLvXbv3t2SLbt5n6qD+dWb2oCx+Px+vy87O/tY165d9z/88MNbOnbsyNZJ5OpwSUAbPny4oP7WwLEAeNrevE2xKWXqU5rezdmuP1ZYWJgxceLEQT/88MPPf/rpp161tbWpYal8lJGUlHSyS5cuX956662LJk2atA11wm8svOAh5Rsb4Fljdlyw+mNLly5t8cILL1y6YcOG3kVFRd18Pp8r3M9ChIyMjJIHH3zwjZkzZ64HbQGwWlsk8DoA7Ysvvmjx8ssv91u3bt2FBw8e7BrJOqWnpx8aOXLkrLfeeus7BL5zLwAPGt69sC2EhQAkg3+oByozLynTUib0AQRQUVGR0q1bt6cOHjzYK+QKxzA6der0+e7du6chuEHw3AorFoAKIQvJecWKFc2vv/76ZysqKlqEucqW0L17928++OCD9/r06VN+5pAo9mGshQTwzTff5Fx//fVPVVZWNnO63BQ6duy46O23335xyJAhpQgkAJ7wB7QFnX/LkGHVzAfkD51nfqoQghuAe8CAAQ82duEHgD179lz9i1/84kbYi4Wwz5ZaWLJ1A0jiLfv3788YPnz4E9EWfgDYvn37pYMGDXp+3bp1OWgoM1sHYV1gqtOIESP+L9rCDwD79u277oYbbvjH0qVLc0DHslgSg1MEYEAWBDHOqbgEMt+TbZz1yzXXXHPr5s2brwxnxWIZCxcuHPvKK690Ap8UVZ6hjAR4ws9bkqqrq1OuuOKKh0tKSjo5XnFFVFdXZ40fP/46KAo7u5w+fTr1qquueqCkpKR9NMrPQ21tbdNx48bdArHgcxenCcAMXoxAxd9SCfwJG+Xrr79+7ueff36bM1WKTXi93pQJEyY8XV5engJ17a9qKYjcLJ4l4L799tuvKywsvNj5WlvDihUrhhQXF6cj0AJgCYFLDqNHj75q165dPaNScAJbt24dsWfPnlTw5YZrdTtBAKranr1eZglQfqexH2T+//Of/xwgKVOjRHl5ef7XX3/dHPxeEatEYBZuoZsFRohOnjyZumTJkusjUF3LqK6uznjssccuA1/ghe5AZWVl6meffRaT1mRNTU2z0aNH3wh+96qxDdOx0AlAYfIPq/EAlaizzF+tb6g7d+48337t4hv79+/PRni0vqr5HyBEjz/++MCKioqcCFTVFpYvX94XFv3/CRMm/Ly8vLxpdEosx6ZNm66G3PyHsY7UUGD2j1V6BmRWgLSBlpSUpB05ciSfKtj9LZPR1B2fBsLycg9Wn2THgDSgpKQkC3XPwgD7vHl9xuw1ZjIWEUeQW+Dz+ZLmzZt3HVX+pm4N97dMVqmqLRTV+PB+Sa3wfLNmzU6iTrDNMNc/QIP6fD7XP/7xjyuo/4x2ndLT08sQLDfm8R8B8hfpdGArpj/Ph7FiAbg/+OCDzj6fT1jHDJeGlzqlwhWf8o+bfxQLPwCUlpZmooHkNQQODjIahqgbkEe8MuGvf/aTJk26uLS0tDVVvt+0TMaUDilkHULB+H3V5PkOHTocRbASFCqfGTNmdD9y5Egedc9o1yk3N/cAFIJ/cMgC4Gl22XUUEbA+jKwf2uynur7++uvzqML2zXDFrfADwCpC+wNAUlKShgYLwCz85rRS0SAg9j2oan83ANe77757LVW2NB0Y09o5TXnc48ebh8WaEgC6du1aikAZoKwf7a233hpM3S8W6tSuXbuD4Jv7bOwNCNUCsDDzr9AHIa6hBJ9qkPWNcfv27V2ogg3MdFGnYxo7q3046hGN3wEA+O+88849CHQBgODnKxoNp+p6BfUGvPbaa10PHDiQTxXurrxk5CU5x76vH67BSS/5fNC7d28eAXDb4Lvvvttxz5497aj7xUKdevXqdQC0vDlqAahCxEw8rS8jAe7i8/ncP/30U2eqEAOy4pcAvi2ntX+zZs2KevXqVYXgBm7OF9DBjwGoEgA3IPjKK68Mocrm0oBH2zinKat9wMtFNeQ1OTk5x2677bYjkBOADkB74YUXBlD3i4U6paenFz3xxBO7oKZ0AYSXAILMC8l1Klpf5AaYu/+4DfTTTz9tW11dnS4qrEurcwHiFd+e9JDnzznnnALwhd/YpmIALAErm/8fffRRu+3bt3ejyvbL5knolOLcEJR3S2pwpJbWlCNHjlydnJxs1I10QxctWpS3adMmUpnEQp0uu+yyeU2aNDEuUjJFnLAAWNPevC1iIooMeIQgdQEWLlx4LlXIC9JdyIzjAMC3Ev+/T58+hvnPE25zirG5J4AnCJbM/+nTp18JSeP7nYOa0usHnj9Ea8qMjIxTU6dO3QJF//+5557ry0nuDEC065SSknLi7bff/hxi358re5FwAdjgA2X+q/r/UhLYsGEDydjx7P+XevzYUeUjrxk2bNhuNPj/5mdsdgF4yUCidyDV/qtWrWqxZs0aMt/i2qZuXNjEuWc/t7QWu6vpZzNixIi12dnZAP/5BBDAunXrslesWNGVul8s1Kl///6ftWnTxhwhdNYCsDj/nwEV859lYBkZuDhrV0FBQSeqIAPimABWnfQK0/cAID09veL6668/huD3a55DkMoG5MVfpAQwadKkwT4fnWA2rq1zmhIAZsk1Zc3UqVM3ocE64lmp9XWfPHlyn1ivk9vtrnrxxRcX2Ll3uCwAFf/fjvlvFnRVEnBt2bIlu6ysjMw8i+sAoMT879y5814Emv/mZ2qQgDkGwIIlAF53a4ALsGvXrqyvvvqqN1Wun2e4cFmWc0bn58c92FBJP5sbbrhhS7t27TwIJoCgtrd37960xYsXd6fuFwt16t2799JevXpVmg5RyjngfTs1DsDYZs183jnetarugAscDTV37lzS/O+QoqN9snMBG6chCwBedNFFP0H8bnkxAN41VgjAPWHChAG1tbXsqLoAjG/r3AAZAJgh15TeZ555ZiOCydFYB7S3yZMnX1hTU0PKSLTrpOu6Z9q0af9lDtMBCxOiMRKQIgme+a8aB6hfVq9e3WjN/xo/sKaC1ghDhgzZj+AhwMbCmxAEgmuVugBLSkrS5s+f/3OqTOel6Ria41xzW1PhxVcnaGK8/PLLd51//vnVqKuPGUHtr6ysLGnu3Lk9qPvFQp169uy54qqrrirjnOLNQRkEW6UP0f83tmXa32z6y6yBgMa5ffv2fKog8UwA6yu8oOJBSUlJnltuuYXq3w4nAbgBuJ5++uk+p06dIqdXe7xNSpDUhRMzDtKaUtM0TJ48eRMahJ80/5999tnulZWVpHqPdp0A+J988snPINb47PEgUnC6G5A9JhJ2EOdlFkDAcuzYseQDBw60oQoYzz0AKyX+f35+flF6ejrAJ3crBCByAQJSgauqqpL/9a9/9aXK1CZZw+25pHcQEnZW+/BJGT1Etm/fvnsHDhxYjmDtDzDtrbq62vXee++RWaSxUKcuXbqsu+222w6d2aXmeBROghoOAuAF93gCbocYrJKA69///nc+NTFjpkvDBQ522TgNmf9/4YUXHoQ4wYUVfvZjIcb74AVduS7AtGnTeh47diyTKtPDrVOQbMdmVMScg6dBd5IBTzzxxGbwYx5B7e3555/vXFpa2oS6XyzUacyYMYbvT1kALBEEHI9kOrCxthIA5LkAoh4BFwB9+fLl+VRB+mW6HDXbnIasB2Dw4MHFUCcAOxZAvfD7fD73O++8cwlVnqZuDfe1dE5THqrx4wMiPRYAevbsWXzTTTeVItjiBJj25fP59DfeeIPU/rFQp/bt2+946KGHCiCeElwUAwggBMsEYNP/50FF66tq//pA4ObNmztSfxrP/n9BtY8cDqppGm6++ebD4AcAdTTMCOtHYBcgr7tQagG88sor5x46dKg5VeYHWyY7OuLypaLTqJHEvP/v//5vK+gej/r6vv322+1++umnbOp+sVCnu+66y+j3pwhAtoRsAfDYlN23o/2p4J+QFHw+n6uwsLADVeB49v9l2r9t27bH2rVr50Xde2WfsejDoLw58UUuQEAc4NVXX/0ZVZ5UHRjrYHrsCa8fb0jSY/Pz84/dc889RWh4DlT3n/7iiy+Skf9YqFNubu6BKVOmbIL1z58FBQWdGAgkCu7xfsN9Ccy26mAgfeHCha2qqqqE0WiXBlwS1wlANAH06tXrMAInADHWhuAbg39UJwIRuQCuf/7zn+127txJTvhxV67D6bHF8vTY3/72t9vAb4vs89E/+uij3G3btpEDyGKhTrfddtsiWNT0onORygUw1lZjAJRJGrQsXrw4nypIr3QXMuI6AYgOAA4YMOAoAs1/INj85/n/vC4xkgDmzJlDjvqLhfTYli1bVj788MP7oab9tVmzZpGj/mKhTtnZ2aWzZs36Hg3vj/26tIpV4EgQ0IpksS/AvC0TfiEZrF27ljT/B8Xx8N9jHj+2n6Ljwtddd50xwQX73MyfFpe5AFQMQAfg+vLLL1vInvUtzZNwTqpz4db3SmpwWJIe++tf//rHpKQkQGwB1C8rVqzIXr16NWnRxEKdhg4d+nlqaipL5ux7FW2D3bZEABYDgHb9f5G/zyOCAALYsWMH2SjjOQC4WpIA1KxZs+o+ffoYE4CYnyMvAYgXAARoAqi3AKZNm3ahLD12XJTTY7Oysk6PHz9+D8SuaEA9p06d2i3W65SWllYxZ86cFRALOLtQ34AM2QWg/HrK/7dq8lPmf/01mzdvziotLSU/0zQgM1oTIIUOBf+/DMHCb352ogFAlghgy5YtmV9++eU5VFmudjg99sOyWhRK0mNvv/32XZmZmebZcM0IqOe2bduaLF26lPzKTyzU6eqrr/7yzFegWa3P++ajkhUQzoFAsmtY7S86LyMCbuP8+OOP86kCdEzR0dbJkRsOY6XE/+/Xr5+R/st7TrzuPzsEoE+ZMqWn1+slH+R4BzUlAMyUDJFNTU31PPXUU4UQt82Aek6ZMqVLrNcpKSnp9KxZs76CdYEnF6dVoshKMNaqmp/XGAPO7du3j/xYQzyb/7V+4AdJAtCVV155AnwCEEX/WQLgvZeAZ3/gwIG0Tz/9lMy0/FmGC5dnO9eslp7wYL0kPXb48OF7WrdubfSlaSDqePDgwVSZ8oiFOg0aNGhl165dK8D/yrMoFiDtGVCulcT/tyLovGMiEmAFXaiZmjZtepoq/4VNXDgh6V6JVayv8IKaACglJcV32WWXVaJhfjsV7W+ZAKZOnXre6dOnSSYd7/DkGLIEGbfb7Zs0adIu5jBreZp9/86xXieXy+V99tlnvwAt9LJjYe0F4Jny7MNlz4M5bzUOINT+APScnBzyKT6xrxpPSD6qEK/w+/3Iz8+/9EwQy28cQ4NwQ3SMgKZpda/P7Xb7+/fvX7Jo0aK21A+6puq4Kce5IbJrK7z4UpIee+211+7v2rVr1Zldsv2Vl5e7//a3v3Wi7hcLderdu/ea/v37l6FOmM09ADJXIOIugKrAy85bsQx0AFqLFi1IC6Axo6amRi8uLibTcUPFvHnzyOQYAHisrbPpsTMlUXJN0/DUU0/tNHYRHN8w1hoAfcaMGfknT54k1Xu06wTAP3HixM8hFnrZQsYFlAggDPn/7LFwaP+A/by8POmTTMA5tEnWcIeD6bG7qn34qJQeIjto0KCivn37njQdEiqbmpoa/e233ybjGbFQpx49emwZOnRoEQKFmmcFUNpf5PqFxQIIt/8vMvVFcQG9pqbGNXny5EvDUJcEbOIhp9NjD9WopPzuFJwKansvv/xyuyNHjqRR94uFOj3yyCOfo07gjUVGBFa6A21ZN5SZD85a9Fu7Zn+QZTB06NBrNm7cSLJ5As4h26XhfgfTY4tq/PighDbwLrzwwqM33HCDMTUWFZvS/H6/9uqrr5LfjYiFOuXn5xfee++9hbCm/S3FA5wYCMTb51kAvPMiF0Do/69fvz57yZIl5Hx0CTiLB1s5nx57WqIqH330UVHkP2j7vffea7Vnzx5yEpNYqNOvf/3rpWgQelXtb2ksgNQCCNP8f+Ztu0E/rv//5z//WTqEMwHnoGvRT4/t1KlT+Z133nlYcDpIyTz//POk9o+FlN+WLVsemjBhwhYECroXtPBbJYKQA5wy/998zI7gU0FBDYC+Zs2ajiHWIYEQ0DJJQ0sH02P/VFyDcsn4jbFjx+4E3b1Z34Y+++yznM2bN+dQ9xuVmxz1Oo0cOfILBAq8VeGXuQJAmLsBzaY975jMPZBpe27//44dO8gx3C91SsWNzZwd8HhXQRW+Ib7W+0jrZEc1ipM47Qd6baiA6EvkWQ6ayad9Sim/p377298eFJwOanMzZswgtb/TKb8qdcrOzi579tln1yDY9JeNAZBpfTBrywRAaXf2OjIQY3MJIIG1a9dmHz9+PIsq8P80c6ODg19t9QHYWEk7c9c2daOjg2VwEqtPeoXCD4AcoRgq3iupQbEkPfbee+8tSE5OZhs3wGlzq1atyvr2229bUvcbkZOEzg6n/MrqdPPNNy9LS0urRaDgq2h+2ci/AOEHQncBeBCRg13fn7etAdA//fRT0vxvl6w7KvwAsKXSS5pzOoC+cZyHIJuGvJ1D/WQ+yNNjs7OzT48bN24fxOZ/QJuaOnVqZ2nKr4PDflXqlJ6eXjFr1qxVoIN/FCGwQ4FJIiClw0YAUOb/866T+f/C7r/Vq1d3oAoTiQQgmYBc0MTlqJnsNGSzEDk1x+KHpbUokKTH/u///u+ezMxM3gsIeuA7duxIX7JkCTmUeUi2Gxc5mfKrUKdrrrnmmxYtWlQj2O9XsQJ45r9jQUCRUPPOh+oCcK2B7du3k/5/JD4AKsvTj+dJSIG6LxFTGODQhzFVUn4nTpxYCEXtP2XKlHM8Hg/JxE4n/cjqlJycfHrmzJnLYT3oxxsLoOQGWCEAKpgnIwLzdSIy4Ab5ILAGDh8+nHrw4EHSn4uE8MkIIJ7TkHdJpiEHgP4O1O+LEx6sk6f87mvVqpW5L80oaJCQFxUVJX/44Yektdgnw4UrHEz5VanToEGDVnfp0uUkxOY+zwowa3qVQUAwr8PtIKsKu4rpTxGEPm/evHY+n0/I6BkuDb3SnRW+gzV+7JOM5ohnApCR23lpOlq4w+/eKKTH+p5++ukCzimuxTl16tRO1dXVdMqvwxN+yOqk67p36tSpy6Au+FaDf9ZcgBD8f9lxKya/qFtQ+/rrr8kAYN8MF5x2vWX+cfsIBCGdxLflklmIHSC3dZVeLJOkx1533XUHzjvvvCoEazQzNAAoLy93/fWvf82n7tclVcew5s4N+1Wp089+9rN1/fv3LwVf+FlBp4YDU1YAEPisbFsAIjPf2KYsAN41MisgiAg2b95M+v8xYf7H8SzEgIp7E36TWeYna5qGiRMn7kKw0PPoXps1a1aH8vJyOuW3TbKzKb8KX/mdMGECL+mHHQOgEgRU6f6zHATkPlwECrXKdXYDgAFrj8ejFxYWkhHdSAjfSmLwDxDf5n+Zx48fJZ384a5fQbUPH0rSYwcOHFjcr18/c8ovjwg0AFpNTY321ltvkUlirZM13JHrnPmvUqcePXpsHTp06CHwyf1McAAAIABJREFUhd+K4Ku4AQZs9QJQRjWl3UGcE3bzgSP8ALT58+e3On36tPDb7S6tzgVwEpU+PzadkgR14pgAVkmmIW/h1nBeWnj1po2UXzJC+corr7Q9fPiwNOXXSS9NpU4PP/ywof1ZEz+UYb9mMgAEFgG36mH+AKixpghClP/PtQCWLFlC+v89012OZnIBwPeSEXKZLg09HQ5COgmZ+R/u6H9xrR/vS9Jje/XqVXrjjTeWQWz+17ebWEj5ValTfn7+7vvuu283goN/IguA91EQ5ZF/LMI5H4CxttsDQPr85vW6detI/z8Smlc2ACgSQUgnIZuGPNz9/y8V1VhN+SW1//vvv99y9+7dZMrvA62SHR2kpVKn0aNHL4V1wVeNA0CwXb+oEICKwFPXhUIEXAtAlgAUCd+7MQcAa/zAGsk05OEMspZ7/XijmNaUnTp1Kh81alQx6IE/xlqa8pvicMqvSp3y8vKKJk6cuBlqkX+egFvt+guCnYFAon3zMVFwUFXzC4V//fr12ceOHSO/3+608PkAfCchgHj2/9dXeEGNWE3WgD5hHDL7p+Ia6ZTtY8aMEWn/oHY2f/78nE2bNklTfls5nPIrq9PIkSNZ7c8L/qnMAswjAgi2bfUCyGBH2GXBQK75/8knn5Ajuton62if7Gzf+9ZTXvLlxvtnyFdIyK1PhitsgbPTvjpTmUJeXt6pMWPGHIBc+wOANn36dFL763A+5VdWp+zs7GPTpk1jU35FFoCKRaASC2AR7AKE+AEQ3jFeXMA4Jwr8CS2A7777jgwARqT7T/advkb+GfJwmv8fKKTH3nfffbKUX2NbW716debKlSvJIeLDmyfhXAdTflXqNGzYMFHKrx1f32r/f33hQg0C8twCUUzAiuYXTgCydevWdlThIuL/S/r/4/kz5IBCAlCYBgD5UNdNRiErK6vmd7/73T5Ign5noE2dOvVcWcqvk0k/KnVKS0urnDlzJpvyyzP9Q035JbU/ICcAmTBT++bjdoOBASRQUlKScvDgwVZUgSPheycSgMJTv49Ka7FLnvK7Oysri3rg9e3lxx9/TFu8eDE5QOyqbDcudjDlV6VO11xzzTd5eXlVsBb8sxMMBHEcQHhiACIioISfmu9PGAOYO3dueyoBKBJ974dq/NgrTQBqvJ8h75qqIzdMwTPZV3FSU1O9EydO3I3AxmwgSCk988wznaOe8iupU1JSUs3MmTO/gnzYr8j/V9X+gET7g40BhGkCEGOfRwwqml44MnD58uVkADAWEoDi/TPk0gSgMLk3y054sFbS1XjzzTfvbd26NSVR9W2oqKgoSZby27uJC1c6mPKrUqdBgwat6tq160nIh/1SCT+h+P8BCGc3oCjQZ3cJChDKEoASE4CEjkglAMkSZEwpvyraH9OmTZOm/Do53Rcgr5Ou695nnnlmGfhJPlZm+5FF/wGBwDPnSQIQRfdF/j7vGhXfnzf+n5cA5JIlAEVC+GQ9APE8AChSCUDrK71YKv/K78Fu3bpVEZfUt6Hy8nL3+++/34m6X5dUHcMdTPlVqVOfPn3WDRw4sBRiwQ/HhB8i7c9FqDEAnnCbj7PnWMFXngBk/vz5LaOdAHTK58dGyawu8WwByBKAmocpAUgx5dc81z+p/WfPni1N+X00BlJ+n3zyyVCH/bJCbkX782IDYZ8RCAgt2i8KAGqyBKALItD3LksAynZpOL+RJwCF+oQLqn2YJ0mPHTBgQHH//v1PkhfVQaupqdHffPNNMuW3VZKGOx1O+ZXVqUePHtuGDRt2EKGN8ZcF/wAL2h8wWQAhDgCizH3eNSoWQYAFEA8JQP0yXY5qGachTQAKwzN+Xi3lVzbst75tvfrqqwopv8mOpvyq1Omhhx5iJ/xQMf/tDv5R0v6A2AVQEXjqOrvan2cB6AA0aQJQDAQA47n/XykBKMQMwMMK6bEXXHBB6f/8z/+UQnHgz8svv0wO+81yaXiglXPaX6VOHTt23HP//fcXQJzzb2fCD5YQAIvaH1AfCCTaN47xgoOi85T251kAmlICkMPC50PdV3IoxLP/H4kEoJeKasj/AMiU3yBr8v33328p+8pvJFJ+ZXUaPXr05+Br+lAm/FAN+PkF2wBCCwKGIuwi7c81/2UJQB1SdLRzOAFo2ykfmQDk1oBL4pgAZO5N7wwXQhk+X+7140+S9Nj8/HzVlF8Aaim/Dzmc8iurU15eXrHpK79Wp/yy0+8PwT54+zoQsQQgSshJ81+aABQR/5/2jy9q4kK67mwQ0kk4nQD0xuFalZTfAk3jPkO2nWkLFixotnHjRjLl906HU35V6vSrX/1qqa7rHqhl/VH9/lYCf2COs9v1CMeMQFRMwI7fH0QKMZEA1IjNf8DZAUB16bGnyWvy8vKqxo4du990iJKsGEn5peuUnZ197LnnnvsB4mQfs8DzpvqiYgCq/j7lEnAJQKTdWR+ft89eS1kFIhcgwAI4kwBEpndGJAFIkgEYzwRQ4HAC0F+P1qKohr7/6NGjjZRf9sKgNvTdd99lrFixgkwKu7l5Ero4mPKrUqehQ4d+KfnKr8qEHyoDfUBcAxBkavcJiYjAivDzAoBB5v+8efPa+3w+YTmzXBp6OpjdBQBFNX7skSUAOfSNvEhA5v93SdWRZ9OU9gGYc5DWlJmZmTVPPPHEPtMhUvvHRMqvpE5paWmVs2fP/hZ8rW9l6K9qFyAPpPYHQnMBKFfAvG9F+weZ/8uXLye7//pGoO99rWT03zmpuqO+ptNw8gtAH5fWYqdayq8HCtp/586dqYsWLSJdwiuz3ejtoFJQqdPVV18dKym/QqYcMWKEX7cRAGTPWxV8mSUQsL1p0yayByAS/r/s83fx3P8PqExwat+6UUn5nTBhwh7F22lTpkyJi5TfGTNmLIe68DuZ8ivaBwCwb5b3YEVmPXWNqu9P9v97PB7X7t27o54A1FTSj7yh0ovbd1F5K7ELvx+OJQB9ecIjHVx0880372vbtu1pBDfooDZUXFwsTfm9uIkLVzmY8qtSp4EDB67u1q1bOeSTfUYk5ZeHESNG+IFgAjCgYhVQpKCi5aXEsGDBgpbV1dVkAlAkJt/MlpgAW075sOWUZDRInCLHraGbzQQgmaZ0uVy+p556iveVXx60adOmdaqqqiKl2/GUX0mddF33Tpky5QuIR/3ZMfVlg39YyKyDhvLKLlAESwrmbUsmv3l7yZIlJNtfGKHJN2UWQGPGVdluqR/Iw4ZKLz4/TscWrrnmmoPdu3c/BQXf/+TJky5Zyu+5qTpGOJjyq1Kn3r17r7/00kvZlF+rvr+dngBK2APOGdofUJsPgHfMirlvJRgYsL1u3TqSAAZGKPe+ZbKGwXGc5x8KRuXZEygLX/kNOsU7Nnv27A4nTpyI+ZTf3//+99RXflUm/7Bq9gf8Pyxof8DapKAi/9983o7QCy2AWPgCEM4U5oMu6WguiwY2Moxrm4Lrmlr3pwurfZhXJk/5HTBgQDnEDbW+fXg8Hu2NN96QpvyOcjDlV6VO3bt33zZ8+HA25Vd1xh/RqD/R4B9wjvMg1P5AIAFQAT4WPAuAOk+5ANyhwOvXr88uKyuTJABFru+9bbKGd84ls04bFYY3T8KzHYThFxLPH6qBZIQsxo8fz9P+XKh85XdsBFJ+ZXUaO3asecIPWX+/KPKv6vdTUX4l7Q9YjwGIVKBdM18UHJQmAEVj8s0bm7kxoV2KtFsw3vHzDBfeOzfNlu9/uNaP9yTpsT179iz7xS9+UUpcEtA+Xn75ZVL7RyLlV1anDh067HnwwQd3QT7qz27kHwgWfFlXn+0goMz/Nx+zY/oL/X5jPxYSgHj4Q/sU7O2TiRkdU3F+ejxP/xGItskaft0yGR+el44vzk+H3Zm/XraW8ittoB988EHL3bt3Z1HX3N8yCdkOBmpV6nTmK792R/3Z6ftXJYV6sOY/IO4GBIIFnT2n4jKo+v1BRLB169aY8P95aJWk4bE2yXisTTLWVXqxS9KPzmJXtQ+T94uHkiYlJfnfeuutSpdLuY4aAEydOjV1x44dwh8lacAL+Wkwu/WaBpyXquPCMIycO+n140+HpZNjnLzrrruKiUsClMbzzz9Pav+6lF97rooKVOqUm5tbHIav/FIj/SA4BuY8tc+F0RQoYeYJtHnffNxOAJC1DIwEoDyq4LHy+a3eTVyWh53OlvQlX3LJJd5Ro0bxLmIHydRj5cqVbkr4AWBUXjLub+VcN9kbh2txnJo0EXVf+dU0TWnAysKFC3M2bNjQnLrmjtxktHbQFVSp069+9asvTCm/vL7/cET+eTEApedIwUoUTUQEvICglQCgrQSg+J58UzL2fsAANtwsYvf6dzJ9+vRU6p5Op8fW+NVSfh966KEDxCUB7WT69Omk9o+FOmVlZR2bPn3691D/uIfdvn9Arv2F1/HMfyD0gUAii8GK9g8y/2UJQPE++abs45uDBg0yM4S0i2fr1q2u+fPnk5IwrHkSujqZHltSi0OS9Nh77rmnIDk52WjgAKHBvv/++4xvvvmGTPmNhTrddNNNX6WlpXlgX/hVtT9lFZAQCT9AzwfAO2ZH21sJDMZMApBT2FntQwmRe69pGksAMvhnzJiRKk2PdVBT+gDMVkj5HT9+/D7ikgALUynlN8p1SktLOzVz5syVoH1/0badwB8PUu1PwQ2xwPMEnbomVPNfA2InAYiHE14/vin3olLWIUxguWRikTZt2vgWLFgQ4KhTgnDs2DH9H//4BxkFOz/dhYJqHwqqfchwabgiO7zTl31SJk+Pvf322/c0bdrUnPLLi2doALRdu3alLly4kGwDV2S70cfBPBCVOg0ZMuSbVq1amVN+eb6/yBpQ6fqT+f9SUNofCIwBqLQIihRCNf+VEoAiPfnmzmof/lvmwYLjHqws90AycU7IOHjwoH7HHXdkhPOeW08FZitmuTTc0tyNUXnJYSFT2RDZlJQU4yu/KjC+8kva9o6n/ErqlJSUVMt85ZcVfqqrj7cWEQIgFnhbWt8M1SCgiltg3uZp/CBfn3dcmgDUxIUmEZp887/HPBj+4ylY6+SLfZR7/XjnSC3eOVKLrqk6lpyfbntW5a9OePBDmFN+586dS7aBi5q4MMTBlF+VOg0cOHDVmZRf0UAfGQmoWAIAX/uLEHBOpv2B4BhAJPx/Uf+/DpUEoAhp/w2VXty+s6rRCT+LndU+3LGrSjrMVQSFlF//ma/8qkB77rnn4iHl13fmK7+ygT/h6PrjCb1KbEAJqslAsnPhCABqQGwkAB2o8WHoj6dQ6XPY3o8RfFPuxdQDdMCLh42VXiyRpMdeffXVB5iUX6H2V0n57ZyqY0SOc2MZVOrUu3fvdYMGDToKNa1P+f+UFQA4rP2BBgIQCTN7zOo25Qqwwq9v3LgxJhKARu6sknb/NDY8e+A0lkvmBmQh05RnUn6Vtf+cOXM6HD9+XJry6+T0DLI6AfA/8cQTbNIPL9qvEgewq/1527ag4vixRGDeVnULqLH/9YTw8ccfk+Z/foqONg4nABVW+6Rz5DVG+AD866g6Aeyu9mGu5Iu4/fv3Lx44cOAJBDfUoLZyJuX3HOp+LR1O+VWpU7du3baPGDHiAPhTe1kZ968S8QdnW7qvqv0Ba/MBmI+FGgfgmv+rV6+Oev//shPWtGBjgmzIqxkq6bHjxo1jtb8fArfytddea1tcXJxO3W9s6+SQPk8mg2LKr/k7fyqRf8r0p6L+4BwLq/YH6G5A3j5LCLy1SMCp/n8dgL5t2zba/4/A+H/ZHPnnp+voGafDkP1+YG5prTCweVwxEnik1o93Jemx559/ftlNN910FOJgVYBCeOmll8hhv5kuDQ+0dE77q9SpQ4cOe3/zm98YX/mV9f1bMf2N40AwGbCQWQOWwA4EEvn/4Oxb0fIyC0AvKSlJOXDggOQLQM77/zLzf2K7FPzSwXnnnMTuah/+TZi4qhaAxZRfM7ja/69//WteYWGhNOW3qYMTMajU6Z577mGn+xLN8GveZoWcp/kN8DS+6DourJj/gPUpwUTCzztGxQCCzP8PP/ywHZUAlO3S0MPh/PviWj92S1pBJGchCjdk5KYyyOmk14/XFVJ+77777iLTIVL7y77ym6w5n/Irq1Nubu7hp556ypzyKzP3Zf6/LBbAQ1i1P6A2KaiqayDS+rzIf9Dx5cuXk/5/JBKAZF/IicYsROGEjAB6N5E/4bcU0mN/85vf7DrzlV/zhdwHt2jRombr168nU37/NzfJ0eCvSp1uu+02NuXXagDQSsRflRACYFX7A/xuQNF+OBdeAlDU+/9l/n88fwAUkH/iXPYFoBo/8KIkPTY3N7fq4YcfPkhcEtAWVL7y+3hb57S/Sp2ysrKOn0n55XXzqVgEvH5/lei/GbwAYchgJwW14/+z+yJTn0wAKiwsJL/3FgnhkxFAJIKQTuG4x49tko+XyJ7x30pqcVCe8luYkpLiRWBD5arvNWvWZH799ddk3OemHGdTflXqNHTo0K+aNGlSC2sz/lAWQCjjALiwo/0BdRdA1dcX+fykBbBo0aK8aCcAnfL5sUHyEdB4tgBWnfSSLSkvScO5hKD5Acw+pPSV373EJQHt4Jlnnuns9/tJ297JYb8qdUpNTT01a9asFYjjlF8KhiCy4Pn/IgtBJepPzgC0aNGijlQhL2oS3vRVHn6o8IJyA+N/FiKJdSMht0/KPNghmftw5MiRbMovIND+BQUFaQsWLCBTfi/PduPnjqb8yut09dVXRzPlVwl2tT8gHgoMZp8lBKu+vogMNMRIApDM/I/3WYhWyPx/Se/GTMnkGAopv6z27yRL+R3n4IQfgLxOSUlJtdOnT/8Koaf8ysx/gC/0KtZBSODFAKh983GrgT7hAKBYSABaKZmoI55nIar1Q/5FW6J+y8s9+F7y+2HDhu1r166dOeVXiCNHjiT/5z//Ia2+C5u4cI2NrxKpQqVOAwYMWN2jR49wpPxS5j8Q+Mxk2j+sRCBiYBERWIrsq1y3cePGrGgnAPkArG7EPQDrK72gLN1UHbiYMLVlk2O4XC7/U089VUhcEtAGpk2b1lGa8uu49pen/E6ZMmUZ+JF+p1J+WSLgbQcgFPMfoOcEVNX0MlIg/f9PPvmE1ASdUnRHp30GgG2nfDhBDION9CxE4YbMvflZhguiR7yp0ovFkvTYIUOGHDj//PMrVcpSWVnpeu+998ikn3NSddzi4GhLlTpdfPHF6wcPHlwCesSfKPBHjfqzov15LkFYwdPUMK3NoIJ/qlZAEBGsXr06Bsb/042hV3rkZiFyAislA5wGEhaWQnqsOeWX10ADFMrs2bPbx0HKL8aPHy9L+VWNA4hiAkCUtT9g3wIwnxeN8FPy/2VfAIpIAFDi/0fqM+ROwW4PwJ7TPvxHkh7br1+/w4MGDeKl/JqhAYDKV37zkjTc5WDKr0qdzjvvvO2//OUv90Mc7Vcd/BNKH7/j2h8IJgCRBWDHLeARQ8BSWlqaLEsAorRTuBBqF1kso6DahyPUNOQA+gvqF4av/AZYja+//nqboqKimE/5HTNmjDnl1+rAH1m/vwGRFRAx7Q8Em+mAPWEXaXuWCALWsi8ANXU7nwB0qMaPvafpvuB4TgCS+f/d0nTkcLLsSmr9ePcIbSr36NGjbNiwYeaUXx7q29ZLL71EDvvNdGl40MGUX5U6tW/ffu+YMWN2IXwpv6pmvxkR0f4AbQGITH7WzOftiwKAAYs0ASjDxQ1GhBOyz3TFfQKQxP8XWTcvF9WQPQcAN+VX2Kj//ve/5xUUFJApv/dFIOVXVqd77rnH7Ptb6ftnfX2ZGwDiOIlwaX8geByAeW0+bjfiT8YApAlAMTABSDyb/4CCe8NJAKpQTPm95557iiAeqMIG/6Qpvw87mPKrUqfc3NzDTz/99CaoCb9K958KCbBgn6dj2h8I1NwAX9uz+5Sfr2wReDweV0FBQdS/ANSYBwCVevz4UaLyePV760gtjknSYx988MGCMym/gLiRagCwZMmSprKU39udTvlVqNOtt94qS/kNV78/iOMRhcgCUNX4smi/cFm8eDH5BaAkDY6OAweASp8fm0413gFAKglAXZiIW40feEGSINOiRYuqRx55ZD/4DTdIip977rku1P10AI+3cTblV1anzMzMEzNmzJCl/MqEX4UMAHXtH4Rwmv9AcC6Aivkv8/9VXAFd9gWgSCQAfX9SngDUs0n8EoDM/OdF//+ulvJbwKT8CrF27dqM5cuXkz09Q3PcOC/NuWCvSp2GDh36ZQgpv7Lgn4oFwIPjVoF5TkC7XX2s0CuRwNq1a6Pe/7+ikScAyQYAsb0bfthK+TU3Uo3Z1qZMmXKuPOXXOe2vUqfU1NRQv/IrIgFK8HlFheAcgPBrf4BvAaj4/7ygnrL2B6D/+OOP5AQgkfC9G3P/f40fWCOZ32AQU79PyzzSmMH/+3//b0+zZs2MlF/S9y8oKEhdsGAB+Z4vy3LjEgddPZU6DRkyZEWbNm1OIbSUXzskQAl0RGIChvAC6kE/mQUgXbZs2ZJVWlpKJwBJpqcKFT4A3zXiBKC1FV5Qwxt4CUAzJZoyOTnZe+Y7f0pBq2eeeeYcacqv49/5o+vkdrtrZ8yY8SWs+fyqpr6K9vcLtgPghPYHGr4LwJpuxlrW3WfL/P/kk09I//+cVB2tkpz1/7dUelFODAlzaWdXAtDX5V4pId50003mlF8y+Hf48OGk//znP/nU/Xo1ceFaB1N+VepEpPyK9u3EAwBa0GX7joFnAahqf8ua31hWrVoV/fx/ScO4MM4TgGQDnNgh1rLJMc6k/O4E7cPWH582bVp+9FN+6Trpuu77wx/+8AX42X6q3X92uvyA4OcYce0PBFoAoQT9LJHB1q1bY34C0LhPALIwvmHzKS8WSdJjr7rqqgMXXHBBJcQEYLClv7Ky0v3++++TST+dUnRHP7CiUqeLLrpo/eWXX34U9sb8Wxn1Z8XXj5j2Bxp6AYKitxBr/6ARfQqLy1iXlZWl/PTTT3lUoRIJQKFhR5UPR4n+TQ2BXYCyyTEA4Mknn9wBcYMOMJVmzZrV4fjx42Ro3/GUX4U6MSm/ouBfOFJ+AZoUIir0ZrDdgFai/VaFXwegf/zxx22pBKBmbg3dHU4AOlDjw0+NOAFIRm7mBKC9p+nPhQHAJZdcUnzZZZcdA9+f1Uz7msfj0d58801y2G9ekoa78pwz/1Xq1LVr1+233nqrkfJrNdVXReuDs89CKvhOmv9AYDegai+Arcg/zhDBV199Rfr//TIjkQBEC0i8JwBJPwBi0v6KX/n9EXQjN+B/7bXX2hYVFTWh7jemdTIcHPdjJeVXZbYflVF/oqCfivY3I+KWgNkFUA0AWhZ687Jx48ao9/835vH/gIL/f6aLtaTWj79I0mO7detWOmLEiMNQGKgCQHvppZe6UvfLiEDKr6xO7dq12zd27NidkJv+vJiArOsPgmMspM/Tae0PyF0Aq9rfRW17PB7Xrl27WlMFioT/35gzAEtq/dgp/cBpXf1eKZanxz7yyCPboRbM0v72t7/lFRYWNqXud1/LJDRzMOVXpU5333039ZVf1S4+O25ATGl/IHgcgBXtL7IIXBAQwdKlS/OqqqqimgBU4fVjcyNPAKJgJABVeP14rViqKcvvvfdeUdKPGRoA/+zZs7tR94tEyq+sTi1atDg8efLkjbAe+GPNfjb4J3OPzIgJ7Q/IXQBW44cSAHR9/vnnpPl/cROXo74hAHxX4SX9w3hPAJL5/0b0/22F9Nj7779/m67rRgMHAhusWY37Fy1alLNhwwayd2dkbpKjsRWVOv3yl780p/xS2p9n9quO+rPrDkQcqjGAUKP/LgD6Dz/8EPX+//2n6WfdN94TgKTujRu1fuAFycy4zZs3P/X444/vRkOjZ+FHQ7vwTZs2rTt1Pw3Opvyq1CkzM/P4zJkzV8Net5+VXgBALPAqsZSIgR0IZN4WDfqxJfwA9O3bt7ehChOJGYBkU051S9OxT9JFGKuo9QPrFL4A9PejtThQQ9fxjjvu2J6amkol/dQ/yO+//z5zxYoVJLkPzXGjm5Mpvwp1uvHGG7/MyMiogTXtTwm8lWCgCEHnImX+A/aCgFaCf/XLtm3bso4ePUrOCReJvvemEo55uagGLxfJB5HEI4wEoHsLq8jrmjRpUjNx4sQfETyoxYDR968B0KZMmdJDmvLroPb3A5gtGfabmpp6avbs2d+AHvYbzgk/2GemEhuIOEQugB3tzwb/Asjgs88+IzVE51QdLR1OAALkFkBjxs8zXFh8zIPtkjD5Lbfcsr158+bm7/zxCEADgJ07d6YtXrz4HOp+g7Nc6Ouge/dZmbxOV1xxxTdt27Y1p/xa1f5W/H9w9nmIqvYH+ASgc47xjltyA1atWkWb/xGKvLdM0uHSIB0o0hhxU06SUsrvpEmTtkCs/QFTO5g0aVIPj8dDvjwnJ/wA1FJ+p0+fzkv6Ue0C5Gl/HiEAYu3P2446qG5AtqvPzug/fdGiRS1XrlzZ7rvvvsunChKprrfWyRomtU/B0z/RjaaxoUuqjvNSdelHUK+77rodnTp1YpN+2Oi/BkArLi5O+eSTT8iuvwvSXbjOwZTfb8q90jr17dt3Va9evU5ALvyhaH+AFnTZfsS1P6DWDSgSfiEhLFu2LO/NN9/svXz58l7FxcW5KgVxegIQM55om4KvTnix7ATdZdZY0Nyt4dPu6Xh0bzV5na7r/qeffnoD6gRBFvzTJk2a1K2qqopM6Yv2hB+apvkmT55sDPwxd/+xAq6S+qsyEpCyBmJK+wPqQUBl8/+qq666edmyZYOtFKKZW0N3pwcAmKADeO/cNPTZVEF+NqsxIFkD5p2XjtM+YOExyRwBAwcW9OnTx0j6Ic3/iooK97/+9a8LqPvlp+i41cGU3y2nfNI6XXDBBeuGDBlIyrxuAAAUWUlEQVRyGIHCT5n/rPBb8flFiEntD4j9fVkvANc6GDt2bF+rwg/UDU6JdGiudbKGhd3TcUduUlxP/EGhZ7qOD7qkYVCWSzo5BgA8+eSTa6BmJnv/8Ic/dD1x4gT5nT/nU37ldRo3btwiqAm/1aHAVgKCQAxqfwDQ/H5/Hhoi+C7UWQXG2g0gybQ2luQzS/32Bx98cO4999zzmMfjsUz5Mzqm4jGHZ4ehUOH148MyD94vqcUeyTh6FrV+Pw5Jppx2u91+j8cjFAWXBrRN1kMmQQ3A+ek6bmjmxg3N3GifXGdV7T3tQ7f1FeQU6L169dq3cePGj9DQyAF+9B8ej0dv27bt3UeOHBGO+89N0rC7d6ZjIztV6tS5c+etBQUFfwRQSywe09q8qMYJVEgBIMggWtofsJ4NaNb69fuHDh1KGzNmzL12hL+pW8PoPOfMRBVkuDTcmZuEO3Otl+O9klqMLhD3q7dp06b20KFD5I2fbpeCCe2ci5T/8VANKSgA8Oijj36HuoYv677Spk+f3oUSfgAY08rZlF+VOj3wwAML0SC8PKEW+fts1F8m6CCOs4gpS8AcA4BpWxYHCPD7//73v3cqLy8nP/0kwuNtkuO6b1728c2qqiqychkuDb9p5Zz1c9Tjx1+O0JNjdOrUqWjUqFF7Edz1x0b/AUB78803L6HuFwt1at269Z7HH398GwI1uh0S8HO2Zd1+MJ0DcT6q2h+wPicgd5zADz/80MHqHydpwOT2Kfidw33ETkM2ucixY8fI7o1f5zmbHvtqUQ1O+eg2dt99932LOuEQEUC98L/22mvn7N+/vxV1v1io08iRIxeAb86HQgIisz4utT8Q7AIA1shAA6Dt2bOHHORzaZYLF6S70ESvM/l7prvQu4kLreN41h1A7eObFJI04BEHYx+VPj9elaTH5uXllY4bN24bghs7D9qf/vSnn1P3i4U6NWvWrHjGjBk/IFDoWcG3kvCj4uOzUOkdiDpY7WTHCtBramrIN35fy2T8qkV0/XwnsFry8U1d1/0+n0/IciNbJKFdsnOO8tuHa1EmcZRvv/32FWfSY9lGzkIDgL1795ITusRCnYYOHTrf5XLJAnqyUYCU6R8W3z/a5j/ADwIaUO0W1GTBvzhX9ELIUm8p4dcAPO6g+6OSHpuVlVU+ZcqUdaiLgssasXbo0KHUkydPZoruFwt1atKkybEXXnjhG8h9f1nGHzUOAGgE2h8ItABYF8C8zVvqfycbC57SSPvZZR/fpHB5ths5bg2HHRqI9FFZLfZL0mNvuummFRkZGdUI1HSAgAC++OKLZtT9YqFOV1555cKmTZtWg+7SMw/6sTv8F8RxFjGp/YHg7wKIhBym82CuhdfrJQNdjdECqPEDayUf36Tw5QkP2q45GcYSWUNqamrVc889txJi7R/UA7Bu3TqypyfadUpOTj71xz/+8XMEC76ZDKwM+lFxAVjEjfYHgmMALJRiAh6P56wjgHUVXlBjhpKTk301NTUxO7nQlVdeuaJt27YV4Af/2MarAUC3bt0OR66E1tGvX7/FnTt3Pgm1QT2qboDo+cS99gcQMPsVK6YisQ06LiOAxugCyPz/nj17ll188cUlESqOJbjd7tpp06YtQ/CIOPOouKCRcvfff/+u9u3bF0Sn1DRcLlfN9OnT/wt14ZdF/a1qfiDOtD/QQAA8F4DdFwYIvV4vGQNojBaA7OObffr0KXn44Yd/jFBxLOH6669fcOGFF5YhWMilhHDHHXfMj0qhJRg8ePDc/v37l4Jv/quO/Vfp8rOt/UeMGOGPJe0PNHTnqYJ7rdQFiFlD2B6qfcByycc3Bg0aVDxq1KifOnTocDxCxVLCRRdd9O2nn376KYAa0GPjuWPlp0yZsj4nJ+dgNMouQrdu3b5ctmzZvyAe02+seYJvhQQgOAbEofYHAl0A25AHARuXCfBRWS2OUx/f1DT/9ddfXwyg9i9/+cuyTp06HY1c6cTo0KHDtq+//vptWEuMCSAFl8tVM2fOnJdzc3P3Rr4GwWjTps2mVatWvQSx0NsZAET5/SyUtb+tCjoMqwTAMp4fgF/qAjQyC+DPknHoXbt2LcnNza0EUHPllVce2bFjx78feOCBlcnJyVGZgcTlctWce+65az///PMXMjMzq0Fnx0lJ4a677tqzb9++J2+88ca/ud3uqMyg6nK5avLz879bvHjxtKZNm55mywi6/19k/ts1++NS+wPyXgARAioqDQI2IgugsNqH5ZKZhK677roC1JnYPgC+pKQk7+uvv7561KhR2yZMmHDJvn37mh89ejTLRJwBz9PvD2pH1Hk/73hSUlJ1ly5dtlxxxRXrH3vssc2tWrVi+/t5I/9EDTgoJpSWlub573//+9FHH320evLkycOOHDnS9uTJk3k+n49VBnZfflCdk5KSqjt16rRh8ODBP4wfP35Du3btqqA2yk824k+l/19aRoXjMQfN7/e3Q+BcAOaFnQsgYA4AYzstLW1OdXW18IuwxT/PRIs4zvgzY+JPpzGdmIjC7XZ7CwoK3unYsaMxA60o0UQUdaYy0WTXy7ZV+rW5XYCwniTG7gPBwWQzRGUSPTdeQE8U6OMl/YQ6DgCcbXC2Y9b8B+qEm2vWm/YhOF+/+Hy+s2IcgNcPvFdCW7yXXHJJQceOHcthvbHJBNbOmiIVFeE3ICMB3sxSuuB37D1F7Y9XF5YA2DVP8EXXygRfxeyXCn+swxBcXqFZluMxnx+AXxYEbCwuwILjHhRJZv+58847N6DB/FclAJkw2z1HXSsSfnMFreSGqCzsPc1g2xZbZsoSUCEE9npVweeVU1nIY1n7A4ExAB6LiUiAJQAyzNcYgoAnvX48LplVNycn5/i99967C3QDNPbZhm1HgFXIQKb5ZRYAK7hWXAH2evN9zJC1M55LI3quFPHazfoTWQDsM4tpYeeB1dxCLc9ZfAD8J0+epPw6uDX7UaBYwpg91SiUzBd47bXXrtd1vRZiE9S8bYUAwrmtqv3NUBV+SuvzzH/2P8m2hmDhpSwCVY3PPhvRs+KVl0Ssa3+gIQZgWfCN7ePHj9OZgI3A/P9rSS3+VkJ3/em67nv00Ud/QJ35TxGAHQtARahVG7IV7W+AGhkaCQKwSgKyhWdVqGh8ijBjXth54FkAxlr2MnwAfOXl5Y3a/C+o9mHMHtr0B4BBgwb98LOf/ewoQos6qwi+qMGqHuORPUz7LEQugHlbRfCtuADmbRExWiEEFbJln4W5bLJnFHQsHrQ/EGwBKAu+sUgJII4NgMJqH365owoVkg8JpqamVr344ouLEKj9fcw25YOKtJFVrW5X64saOE9rywiAPc7+VgQV5WOFCFSEXvV5sc+Ftx+XMPcC8BqF6GHVP9gTJ06QBBCPmYA+1H0m/KmfTksnnwSARx555F8XXXRRKej+aCsugB2NbmUBZ5uCFSuAd968piCyAHj1p0hARKqy58t7TkDwcxIRJoD40f6AeByAD3XRXKHmN5aKiopGZQHsqPLh14VVWCVJ9zXQr1+/Vc8995x5Akq7FoBVzaRq3lsVfmOffXOqJMA7x96DB5ECUiUBlbUdrW9J+OMNZ70LsLvahzWVXqyt8GJNhQ+rKzw47VP7bfPmzYs//vjjf6JuHLpM+1PR6FAaqhUCAOc8mOMsVN0A3rbo9zyoEoAqGYTyPMHZVkI8aX8g2AWQCjyzeLt37378zLXcl3ugxo/hO07B7+f8gd/0VgTn/fAz+7xrBPfk3AOm6w/X+nFM9nkZAVwul+fFF198q2XLlpXgZ5xRLoBqQMr8HiA4pyL84Gyb1+y2GSICMNYigbfqAojKpkoAKvtWnh9VRuqauILxbUAdgd8HFH0nkM0RcANIat68+R/LysryI176KEHXdc+jjz765qxZs3imfyS0P2wcA7GGYF/FDWDXVjU/7/9FZVclArvPUVQG85otaz3iTfsDchfAiAUEaX40jPrynnvuuRu+//77/EgXPhpwu921v//971+ZMmXKBsizzkSDUvzgk0AojZYSejuanwUl2CpaP1QLwLwdroV3f2rNljXuwRKAD8FRXZ9pzSWCSy+9dP33338/LOKljzBatGixf/r06W+PHj16N9SFX6b9Vc1WWNiH5Bi1zULVCpCdE91PJFyietglBPZ6cLaptai8AOJT+wN1LkBzNLgAIleAcgfcRUVF6V27dn2toqKC/GZcvELXdc+QIUM+mzt37ieZmZnsSD9ZBhq1qGh8Fa1lpUFT2xQoQZcdE92H9/+8sqmSoOwc717UmtquR7wKPxDY1cdqITL4Z1o8rVu3PjV//vzHc3JyCiNeA4fRqlWrwnfeeefpxYsXz1WcTUc0rRY1Yw07jx01s63KLDeUFaKSGafy/kX3okiRPUaRpxWXiiVVkXUlIw7AovDHOzS/35+DOlY2LADKGnAjePKQ+uO7du3KuPzyyyccOnTo4khXJJxwu901PXr0+OHuu+9e+sgjj+yEeoOlBIwlVpnJb0Xrq6ypbVWItDul9a10BMvKKqp3uC0jJbMfiG/tD9QRQDM0BPRYElAlgvr18ePHU2644Ybbd+3adVFZWdk5sslCog1N0/w5OTlFHTp02NO9e/fCwYMHF44cOXLfGVNfpNEojaeiQa2a/uBsq6ypbd4+BZlgWzX9ZeWQCaiMCOyseWVptMIP1BFAUzQQAI8EqLgAjxDql+Li4rQ///nPXQ4fPtzU5XJpxqLrev3a7XbD2NZ1HWcW8zY0TTO2/ZqmaWfW0DTNz9n2A/C5XC6/pml+TdN8prUPgLHvzcnJqe7Tp8/xzMzMWtAmpqrQywjAz1mHS/hlx9ht6pgZKsJsRevLYgC8Y6EKtiME2VgIIBuBBCAiAlUy4C0iYtGZ/+Yll4j6lnlgBYa3iLrhRL6lyM+1Ivzs/6kQADjb1Fp0TGXfClSEXZUAqHLIhFSl/lYE/qwTfiAwF4AnKEBDFyBM+yLw7uFDw1gCQ9C9CCQbDTQBgLNm/5ctg6g8bNl4BGCFDETXsvfl/b+q4IdD26sKGwuRxtaYfdExlXOi/7ciuOF8NmeF8ANiAhAJvEj4jd+5mHu4mDVrZWictV3tb5SDrYuxzwocJaQyrW7V17cb9ANzjD3OHmO3efuy46LrZETAu6eqtpeVy6oGD+XZnDXCD/AJwBBUHhmIYL6HQQK6aR0u4bebTALQBCAjAiuCz7MuKAJiy8qrB2/Nbqvsy45TsEIEsv/RFMtghQyoc2F5Lo1N+IEGAjBeiFn4VQVfJPQiwReZ/aLJJMFZi8rCrkXWjYwERAQgE3iZ4IfL7Ke2qWPU8VBBEYGdMqjUyQ4BWtb6QOMUfiBwKLABVSIQNWAzAYiE3ormVx1PTmlM3qJCBDIBlwm9VeEPh9YXHVM5Fw4YCiXUe6geD/exIDRW4Qf43wUwtg3B1yEmATNY4VIx+UUEAFgTfrbslGDxhJEVVFVCoK6XCb6q8Ktu854DBauNmgrihRt2CMwRImzMwg8EzwgENAivsQ0Ek4BIo5oFnj1GmfyU8KsGIdltkQVgHJORAI8AKC0fivDztqm6ifZVz9mB6v1U/ftQ/89u3ZXL1tiFH+ATAMAnAbMG0E3bxqIhsOGzcQCZ4FvR/ipdSjIrQCT8doWcJ+y8Y6Iy8dbUNnXMyvlQoTKwJ1SEWkdLZTobBN+AiACAYBIwhFlEBOaeAzaoaPxORAAQ7JvXqhARgHlbtoi0uMo2ex/R/4rKCGKbty87LoPsd6qDryIJ1f+0XLazSfgB8afBDLAkYAgwjwjMgq9BLPzmfavCb8UFMG/bIQHKMpAJPrXwyqNSB1F9RQhXQ7Z6n1ADgHb/1+5vAJx9gm/AbAGIfDdKgM2/M877TefN2xqChV+1y0+1UYmE31jbIQEZKVgVepHg29H6snPRQCTLE/J/na2Cb4DtBZAFcFgBNrQ/T+BZMrCq9VW1vwGR0IiIIBxkYEfbh6L5qeMiRKqBh0vzswh7+c92oTeDHQgEyEnArO0BvlbnaX9K8GVa30oXILsfKhFQ56xoe7uCr9JYY6FBx0IZhEgIPR8qLgAQLMyAWPB5gu6k5ueVld2WkYB52y4hiO7B+x+qrKJ92fEETEgIvBp4A4EAdStAVfgh2Tav2W3qmLk81LFwEgF1DW+bWvPKLqtLAiYkBD00sL0AMsE3X8fuqwp7qJqf/W+qnOw2e0yFCOxuU2te2cMm+AmhSEAV5hgAEEgCVqwAlgTM91QVehWfnz1ulQDM21atAqvHeGtemW0LfkLQEwgVol4AVRIAAoWdFXxjTQl8KKY/Ww7ZcbtEYKyt/kZ0TFReqUAnhD6BcIKyAMywQgSiNaCu8cPRpUQJnAoRUOesanrKAhAdq0dC6BNwCprf7ze681SEU8WEt2Pmh6L5eZAJmSoZ8I6pXk9tU8fqkRD8BJwGLwhobAOBml/FCjCDtSaoe4WTAFRcAXY/VHKgtlX2A5AQ/AQiBbMFEHCc2A/XNm9fdtwKnCYC2b1EZUgIfwIxA83v94u64XjHVIVZJujhNvllUBFEVWKwsy86Vo+E4CcQDZgJAFDXyKHuU/8lO6cClYCl7Fi4hDwR2U8gZsESQP1x0fUKx0K9HwUr4wB4oK63QwrUPZXKlhD+BKIJ0Xf7zME62XE2mOdkgw713lYJIJzHA5AQ/ARiAbIPd8qIgD3HGz/AOx5N2HEPQj0XgITwJxArELkA5G8cPh8pqAhhKGQRhITgJxBr0IYPHw4AmDdvnh3BVP1NqEKv8nsn3YOQ/ich+AnEKuoJwIBNIqi/X4R+Ywd2hTAk4U0IfwKxjCACMCNEMqj/jzDcI1IIi7AmhD6BeAFJAGaEiQy4ZXDovjw4JpgJoU8gHqFMADw4SAoxj4TAJ9AYEBIB8NAYSSEh7Ak0VoSdABJIIIH4AS8TMIEEEjhLkCCABBI4i5EggAQSOIvx/wEH6mZY7Q+SDAAAAABJRU5ErkJg
+--JuH4rALGPJfmAquncS_U1du8s59GjKKiG9a8--
diff --git a/third_party/jetty-http/src/test/resources/multipart/multipart-base64.expected.txt b/third_party/jetty-http/src/test/resources/multipart/multipart-base64.expected.txt
new file mode 100644
index 0000000..5d4a189
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/multipart-base64.expected.txt
@@ -0,0 +1,4 @@
+Content-Type|multipart/form-data; boundary="8GbcZNTauFWYMt7GeM9BxFMdlNBJ6aLJhGdXp"
+Parts-Count|1
+Part-Filename|png|jetty-avatar-256.png
+Part-Sha1sum|png|e75b73644afe9b234d70da9ff225229de68cdff8
\ No newline at end of file
diff --git a/third_party/jetty-http/src/test/resources/multipart/multipart-base64.raw b/third_party/jetty-http/src/test/resources/multipart/multipart-base64.raw
new file mode 100644
index 0000000..514a6a1
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/multipart-base64.raw
@@ -0,0 +1,389 @@
+--8GbcZNTauFWYMt7GeM9BxFMdlNBJ6aLJhGdXp
+Content-ID: <junk@company.com>
+Content-Disposition: form-data; name="png"; filename="jetty-avatar-256.png"
+Content-Type: image/png; name=jetty-avatar-256.png
+Content-Transfer-Encoding: base64
+
+iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz
+AAAI3AAACNwBn+hfPAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURB
+VHic7X15fBRF2v+3eyYnOSCQcEMQQUBEhV05xQvvdxHB1d/iq6is1y6or7rgCgqLiJyut+uxrsfe
+C57LKaIoCCr3KZBwyJFASICQkJDM8fsjdNJTU/VUdc/0HGG+n09/+pyequ56vs9R9VRrw4cPRwIJ
+JHB2Qo92ARJIIIHoIUEACSRwFiNBAAkkcBbDHe0CRBrz5s3Tol2GSGPEiBH+aJfBQDSff7ifgxN1
+ifS70hprEPBsFHS7CLXRJZ519BDqu2sUBBADDdDO/8eMVk6g8UGVGOKWABwQ+miTiBWcbeTBezdn
+2zMICSJCiCsCCJPQOyXoqvd1suHGg1DEEtFafV7RLntY3q+ZDOKCAEIQfDu/i/ZLpmCnAUSLFGL5
+OTY22H7HMd0LYFPwVX5j9b6RbsyiFyoqB9UA2N84RQix/kyB8Nc9knVw5B3HJAHYEHzZ9aGeD/V6
+GdgXJru/7HrVxhKKQFh5Bk5dy4MVQXEadv+PVwcr5K/8jmOOACwKP3Wt6Jyd34R6rQjGy1G5l/lF
+ygRetQGoNConrLBwEK5VK4mHWI0BUP9j9V2T52OKACwIv1Xh5h23QxBWrlGBBrVG6Bf8p4hARAKs
+2uDDaRGFi3DD8VurlpYTZbCLcLxrjT0XEwTgkOCzx1RJwKkGK7unVW3EIwVeIwmVDESw+pxCIdxQ
+YcXSsgOn60ApAKttwHzOH3UCUBR+1cYT7n3ZcZVrrWpdEXPL7sM2BitkIPpfXvlUz4WDcGXnY6Xb
+MxLtg4XsffPedRARRJUAQhB+SnBVz9klA9k5q9cK2ZlzXLWxWCUD9pwK7Fhd4SZcK7GOaPV+WD1v
+RwHw3rcSEUSNABSEPxTBD9c2dYw6LoOKJha9QD9nW/ZfVslABKfIWHRvK1Dxf8NJAk7HNsJhGVBE
+4AeiRABhEH4r2+xadp2oDOEiAZE/xzvHe4FWSED0e/O++ZgVhJNkQyUDK1YU77hVOKUkKG1tlcBk
+RKABUSAAG8KvokUoIZetZcdE+6JjKhC9SD+zLXqBvMYQDheBgh1SViVflfvzIIuMi+pohwhUy2fH
+MqDKK3rvvPctCgoKiSDqQUAGVk18WaNTJQErDZW3bxe8F0htmxsBu5b9j1QbKCIUobf6nKmyqUbG
+VYTLClTKqEoCVDl4ykBEAiptgfvbiBKARPvbFX5VwbdDDNQ2dYwC7wWpCL5oHQ5SoMqmqvlDXfP+
+S9UCoI7xnmeo1o/VfdExURlkhM2uqfvy2kb9uYgRQBiE36rgW91WWfPKKjrGg6rwWxV8VQ0hK5fq
+OxI9F1Uypta8/5PBqiVl3lf9H6sKivotD6I6UNYfBMdU/ityFoDDws9uh7LPuydVLtG+CHZMfpHw
+i66xGzSSQfW9WCFbu1aAFc0vIwBVqLSFUEjMgJ12wCME3n2DrIFoxwDsCr+KUMuOWyECXjlEdZBB
+henZtUz4eRaBKhGoakHZu7FKvLxt3n+pQNWForYpWBV+u3VRbQt237X5fyJjARDa36rwyxqbVeGn
+ruVti8oo2mehov3tCD27GGWx2zhEYOuu8j5UjoGzZrcpqAq/HUvAShuVHZNBRfOrtAPz/7LHzGQf
+ExaAATvaRVXAdYVrVLWVqKyiugByc1VV+K0uRlnCRQRW3ovqe+Ldx7xWgVWhd8ICcILIwtEepP/p
+KAEoan+7wq+yiIRfRgqi/6bKztsHaAJQMfWsLj6IG4FZ+I1tFUJg6xzKOwkHyYIps4r2VPkNC7uC
+LyIymXIIlxLwCepj3K++HNGwAGSkYFf4WaHWFc852UjNoBqfFTOPFXD2mCa4JhQrIBRCtkO25rUM
+qmTKO8du8yB633ZJgAeV9qDSFgzB1wXHzf+hAQ4SgMVEn1CEnyfoqsdUyYBXJvOaVycDFMuzax4B
+sC9ZZ4750CDQxnEZCahaASKtFwoZqzxn9r9FsCr84SAAWVul1iLI2oJI8Nn3DuYaAzrnmB9wiAAs
+mv52hZ8ScvOaOheOBqqqqQB+w5NpfR7Lm0nA3Ag0WCMB8++peoRCxqqEAM62DFYEXkYWPLDtVaWt
+sufZ+7DgaX+eMPPeGSv44PyeJYIAayCSLgBP+GXX2BF8q9uqZADONq8u7D5lAagwPqvtdWZtFmBW
+8MH8lgX7Wx6sCr8KMfOeNTjbbBmM+rDbsmfKOye6F/t/5n1eWWVkwLsfC1k7MGtw3rtm17x2wi1L
+tHsBZA9WVfjZbd3CcVUiYMtmLj+7zYLS/OZtSvDZtfHCzURgrNkGQVkE5sYkglXhD8czBsRl4mlN
+0bYVEuDVm13LCECkIHh1ERGYqC3wyJ591+y9WRIIIISwE4DA/OcJikiIVIWfEm7Zwv5ORVupNlKR
+tjLv2xV+dttMBMYiahjU/2mmc7y6qAq/HeuLesZsOdgyyrS9KgmY7yd6n5TAy9qGinJQsQLN79v8
+nlnhZ+vGkkB9eaJtARgwPzT2GK/BqQi/S+GcChGoNFLqBRtQZXtK4/OEn7fwGgb1v8ZxlcZPCb+q
+JcYjdJEQiaBKpnasAJHCUiEBOwTAKz+rBFiLj33P7H8YQm/eDypPJAhA9DCNNXue94BVzH1W4F0K
+27JGShEBrz4UVBos76XLhN4rKB+7sNYATzBYUA3dqhVGka152/x/FFSFXnQdew9evdmyqDxnXh1E
+dZHVQcXiEylOMNfqzDEAYR4IFMKc/ioPlecCiIRbtFYhA1USAGdt3rZi/qua/F7whZ9HBCwoEuCV
+l60fj4xVhJ99xjwikBEA75laIQCKFHh1FymqSBIARf68d83+B1snlgx8QGSHAouERUYEIsGnhF9l
+W9UakBEAr6GawTbaUIRfRyAR6KZtlgjY50m5Amw52fpYEX7KElN9xux/m8ETXpkGlbkC5vtZ1f4y
+C5FSijJLkBJ+83um7m3eDyqb0wQgKiB7XPagVbQMK+gu4rgqCVAEoPqSAXmjFfn85sUs/Kzgs42C
+XbNEwFoB5rKZQTV4FaG3Y22Fqjllwq9CfOb/lml7UduwQmR2hd8ruDdbLz8aXADAZA2EjQAUc/5l
+2t58nejBqmh8K4sVLUWVma2fARWNpeLr66a1mQSMMogaBY+szJFkkTCoan8rLhhFAjIBYstnRfhl
+cQ/KBQiFAFSITKUtsFYe+569CARbRx2BFkD94qQFIKo0dVz2oGWmpwt1gU1WyIOOHTt2LGXOnDk9
+169ffw4Al3YGuq7Xb2qapgGAZoKxb14HVOLMMb/fH6RZjGO8tQg+n893Zm3e9/l8Pr/P5/N16NDh
+wJw5c1bm5uaeRjAhsM/b3A3EiwPI3olM+FXcLRcAfefOnU1eeOGFi/bs2dMagKbretAz5T1f83Nl
+nl/9oTM7fgD+Zs2aHe/WrVvR6NGjC9q3b1/NqbOs7uZtVtgD9gsKCpq8+OKLvXbv3t2SLbt5n6qD
++dWb2oCx+Px+vy87O/tY165d9z/88MNbOnbsyNZJ5OpwSUAbPny4oP7WwLEAeNrevE2xKWXqU5re
+zdmuP1ZYWJgxceLEQT/88MPPf/rpp161tbWpYal8lJGUlHSyS5cuX956662LJk2atA11wm8svOAh
+5Rsb4Fljdlyw+mNLly5t8cILL1y6YcOG3kVFRd18Pp8r3M9ChIyMjJIHH3zwjZkzZ64HbQGwWlsk
+8DoA7Ysvvmjx8ssv91u3bt2FBw8e7BrJOqWnpx8aOXLkrLfeeus7BL5zLwAPGt69sC2EhQAkg3+o
+ByozLynTUib0AQRQUVGR0q1bt6cOHjzYK+QKxzA6der0+e7du6chuEHw3AorFoAKIQvJecWKFc2v
+v/76ZysqKlqEucqW0L17928++OCD9/r06VN+5pAo9mGshQTwzTff5Fx//fVPVVZWNnO63BQ6duy4
+6O23335xyJAhpQgkAJ7wB7QFnX/LkGHVzAfkD51nfqoQghuAe8CAAQ82duEHgD179lz9i1/84kbY
+i4Wwz5ZaWLJ1A0jiLfv3788YPnz4E9EWfgDYvn37pYMGDXp+3bp1OWgoM1sHYV1gqtOIESP+L9rC
+DwD79u277oYbbvjH0qVLc0DHslgSg1MEYEAWBDHOqbgEMt+TbZz1yzXXXHPr5s2brwxnxWIZCxcu
+HPvKK690Ap8UVZ6hjAR4ws9bkqqrq1OuuOKKh0tKSjo5XnFFVFdXZ40fP/46KAo7u5w+fTr1qquu
+eqCkpKR9NMrPQ21tbdNx48bdArHgcxenCcAMXoxAxd9SCfwJG+Xrr79+7ueff36bM1WKTXi93pQJ
+EyY8XV5engJ17a9qKYjcLJ4l4L799tuvKywsvNj5WlvDihUrhhQXF6cj0AJgCYFLDqNHj75q165d
+PaNScAJbt24dsWfPnlTw5YZrdTtBAKranr1eZglQfqexH2T+//Of/xwgKVOjRHl5ef7XX3/dHPxe
+EatEYBZuoZsFRohOnjyZumTJkusjUF3LqK6uznjssccuA1/ghe5AZWVl6meffRaT1mRNTU2z0aNH
+3wh+96qxDdOx0AlAYfIPq/EAlaizzF+tb6g7d+48337t4hv79+/PRni0vqr5HyBEjz/++MCKioqc
+CFTVFpYvX94XFv3/CRMm/Ly8vLxpdEosx6ZNm66G3PyHsY7UUGD2j1V6BmRWgLSBlpSUpB05ciSf
+Ktj9LZPR1B2fBsLycg9Wn2THgDSgpKQkC3XPwgD7vHl9xuw1ZjIWEUeQW+Dz+ZLmzZt3HVX+pm4N
+97dMVqmqLRTV+PB+Sa3wfLNmzU6iTrDNMNc/QIP6fD7XP/7xjyuo/4x2ndLT08sQLDfm8R8B8hfp
+dGArpj/Ph7FiAbg/+OCDzj6fT1jHDJeGlzqlwhWf8o+bfxQLPwCUlpZmooHkNQQODjIahqgbkEe8
+MuGvf/aTJk26uLS0tDVVvt+0TMaUDilkHULB+H3V5PkOHTocRbASFCqfGTNmdD9y5Egedc9o1yk3
+N/cAFIJ/cMgC4Gl22XUUEbA+jKwf2uynur7++uvzqML2zXDFrfADwCpC+wNAUlKShgYLwCz85rRS
+0SAg9j2oan83ANe77757LVW2NB0Y09o5TXnc48ebh8WaEgC6du1aikAZoKwf7a233hpM3S8W6tSu
+XbuD4Jv7bOwNCNUCsDDzr9AHIa6hBJ9qkPWNcfv27V2ogg3MdFGnYxo7q3046hGN3wEA+O+88849
+CHQBgODnKxoNp+p6BfUGvPbaa10PHDiQTxXurrxk5CU5x76vH67BSS/5fNC7d28eAXDb4Lvvvttx
+z5497aj7xUKdevXqdQC0vDlqAahCxEw8rS8jAe7i8/ncP/30U2eqEAOy4pcAvi2ntX+zZs2KevXq
+VYXgBm7OF9DBjwGoEgA3IPjKK68Mocrm0oBH2zinKat9wMtFNeQ1OTk5x2677bYjkBOADkB74YUX
+BlD3i4U6paenFz3xxBO7oKZ0AYSXAILMC8l1Klpf5AaYu/+4DfTTTz9tW11dnS4qrEurcwHiFd+e
+9JDnzznnnALwhd/YpmIALAErm/8fffRRu+3bt3ejyvbL5knolOLcEJR3S2pwpJbWlCNHjlydnJxs
+1I10QxctWpS3adMmUpnEQp0uu+yyeU2aNDEuUjJFnLAAWNPevC1iIooMeIQgdQEWLlx4LlXIC9Jd
+yIzjAMC3Ev+/T58+hvnPE25zirG5J4AnCJbM/+nTp18JSeP7nYOa0usHnj9Ea8qMjIxTU6dO3QJF
+//+5557ry0nuDEC065SSknLi7bff/hxi358re5FwAdjgA2X+q/r/UhLYsGEDydjx7P+XevzYUeUj
+rxk2bNhuNPj/5mdsdgF4yUCidyDV/qtWrWqxZs0aMt/i2qZuXNjEuWc/t7QWu6vpZzNixIi12dnZ
+AP/5BBDAunXrslesWNGVul8s1Kl///6ftWnTxhwhdNYCsDj/nwEV859lYBkZuDhrV0FBQSeqIAPi
+mABWnfQK0/cAID09veL6668/huD3a55DkMoG5MVfpAQwadKkwT4fnWA2rq1zmhIAZsk1Zc3UqVM3
+ocE64lmp9XWfPHlyn1ivk9vtrnrxxRcX2Ll3uCwAFf/fjvlvFnRVEnBt2bIlu6ysjMw8i+sAoMT8
+79y5814Emv/mZ2qQgDkGwIIlAF53a4ALsGvXrqyvvvqqN1Wun2e4cFmWc0bn58c92FBJP5sbbrhh
+S7t27TwIJoCgtrd37960xYsXd6fuFwt16t2799JevXpVmg5RyjngfTs1DsDYZs183jnetarugAsc
+DTV37lzS/O+QoqN9snMBG6chCwBedNFFP0H8bnkxAN41VgjAPWHChAG1tbXsqLoAjG/r3AAZAJgh
+15TeZ555ZiOCydFYB7S3yZMnX1hTU0PKSLTrpOu6Z9q0af9lDtMBCxOiMRKQIgme+a8aB6hfVq9e
+3WjN/xo/sKaC1ghDhgzZj+AhwMbCmxAEgmuVugBLSkrS5s+f/3OqTOel6Ria41xzW1PhxVcnaGK8
+/PLLd51//vnVqKuPGUHtr6ysLGnu3Lk9qPvFQp169uy54qqrrirjnOLNQRkEW6UP0f83tmXa32z6
+y6yBgMa5ffv2fKog8UwA6yu8oOJBSUlJnltuuYXq3w4nAbgBuJ5++uk+p06dIqdXe7xNSpDUhRMz
+DtKaUtM0TJ48eRMahJ80/5999tnulZWVpHqPdp0A+J988snPINb47PEgUnC6G5A9JhJ2EOdlFkDA
+cuzYseQDBw60oQoYzz0AKyX+f35+flF6ejrAJ3crBCByAQJSgauqqpL/9a9/9aXK1CZZw+25pHcQ
+EnZW+/BJGT1Etm/fvnsHDhxYjmDtDzDtrbq62vXee++RWaSxUKcuXbqsu+222w6d2aXmeBROghoO
+AuAF93gCbocYrJKA69///nc+NTFjpkvDBQ522TgNmf9/4YUXHoQ4wYUVfvZjIcb74AVduS7AtGnT
+eh47diyTKtPDrVOQbMdmVMScg6dBd5IBTzzxxGbwYx5B7e3555/vXFpa2oS6XyzUacyYMYbvT1kA
+LBEEHI9kOrCxthIA5LkAoh4BFwB9+fLl+VRB+mW6HDXbnIasB2Dw4MHFUCcAOxZAvfD7fD73O++8
+cwlVnqZuDfe1dE5THqrx4wMiPRYAevbsWXzTTTeVItjiBJj25fP59DfeeIPU/rFQp/bt2+946KGH
+CiCeElwUAwggBMsEYNP/50FF66tq//pA4ObNmztSfxrP/n9BtY8cDqppGm6++ebD4AcAdTTMCOtH
+YBcgr7tQagG88sor5x46dKg5VeYHWyY7OuLypaLTqJHEvP/v//5vK+gej/r6vv322+1++umnbOp+
+sVCnu+66y+j3pwhAtoRsAfDYlN23o/2p4J+QFHw+n6uwsLADVeB49v9l2r9t27bH2rVr50Xde2Wf
+sejDoLw58UUuQEAc4NVXX/0ZVZ5UHRjrYHrsCa8fb0jSY/Pz84/dc889RWh4DlT3n/7iiy+Skf9Y
+qFNubu6BKVOmbIL1z58FBQWdGAgkCu7xfsN9Ccy26mAgfeHCha2qqqqE0WiXBlwS1wlANAH06tXr
+MAInADHWhuAbg39UJwIRuQCuf/7zn+127txJTvhxV67D6bHF8vTY3/72t9vAb4vs89E/+uij3G3b
+tpEDyGKhTrfddtsiWNT0onORygUw1lZjAJRJGrQsXrw4nypIr3QXMuI6AYgOAA4YMOAoAs1/INj8
+5/n/vC4xkgDmzJlDjvqLhfTYli1bVj788MP7oab9tVmzZpGj/mKhTtnZ2aWzZs36Hg3vj/26tIpV
+4EgQ0IpksS/AvC0TfiEZrF27ljT/B8Xx8N9jHj+2n6Ljwtddd50xwQX73MyfFpe5AFQMQAfg+vLL
+L1vInvUtzZNwTqpz4db3SmpwWJIe++tf//rHpKQkQGwB1C8rVqzIXr16NWnRxEKdhg4d+nlqaipL
+5ux7FW2D3bZEABYDgHb9f5G/zyOCAALYsWMH2SjjOQC4WpIA1KxZs+o+ffoYE4CYnyMvAYgXAARo
+Aqi3AKZNm3ahLD12XJTTY7Oysk6PHz9+D8SuaEA9p06d2i3W65SWllYxZ86cFRALOLtQ34AM2QWg
+/HrK/7dq8lPmf/01mzdvziotLSU/0zQgM1oTIIUOBf+/DMHCb352ogFAlghgy5YtmV9++eU5VFmu
+djg99sOyWhRK0mNvv/32XZmZmebZcM0IqOe2bduaLF26lPzKTyzU6eqrr/7yzFegWa3P++ajkhUQ
+zoFAsmtY7S86LyMCbuP8+OOP86kCdEzR0dbJkRsOY6XE/+/Xr5+R/st7TrzuPzsEoE+ZMqWn1+sl
+H+R4BzUlAMyUDJFNTU31PPXUU4UQt82Aek6ZMqVLrNcpKSnp9KxZs76CdYEnF6dVoshKMNaqmp/X
+GAPO7du3j/xYQzyb/7V+4AdJAtCVV155AnwCEEX/WQLgvZeAZ3/gwIG0Tz/9lMy0/FmGC5dnO9es
+lp7wYL0kPXb48OF7WrdubfSlaSDqePDgwVSZ8oiFOg0aNGhl165dK8D/yrMoFiDtGVCulcT/tyLo
+vGMiEmAFXaiZmjZtepoq/4VNXDgh6V6JVayv8IKaACglJcV32WWXVaJhfjsV7W+ZAKZOnXre6dOn
+SSYd7/DkGLIEGbfb7Zs0adIu5jBreZp9/86xXieXy+V99tlnvwAt9LJjYe0F4Jny7MNlz4M5bzUO
+INT+APScnBzyKT6xrxpPSD6qEK/w+/3Iz8+/9EwQy28cQ4NwQ3SMgKZpda/P7Xb7+/fvX7Jo0aK2
+1A+6puq4Kce5IbJrK7z4UpIee+211+7v2rVr1Zldsv2Vl5e7//a3v3Wi7hcLderdu/ea/v37l6FO
+mM09ADJXIOIugKrAy85bsQx0AFqLFi1IC6Axo6amRi8uLibTcUPFvHnzyOQYAHisrbPpsTMlUXJN
+0/DUU0/tNHYRHN8w1hoAfcaMGfknT54k1Xu06wTAP3HixM8hFnrZQsYFlAggDPn/7LFwaP+A/by8
+POmTTMA5tEnWcIeD6bG7qn34qJQeIjto0KCivn37njQdEiqbmpoa/e233ybjGbFQpx49emwZOnRo
+EQKFmmcFUNpf5PqFxQIIt/8vMvVFcQG9pqbGNXny5EvDUJcEbOIhp9NjD9WopPzuFJwKansvv/xy
+uyNHjqRR94uFOj3yyCOfo07gjUVGBFa6A21ZN5SZD85a9Fu7Zn+QZTB06NBrNm7cSLJ5As4h26Xh
+fgfTY4tq/PighDbwLrzwwqM33HCDMTUWFZvS/H6/9uqrr5LfjYiFOuXn5xfee++9hbCm/S3FA5wY
+CMTb51kAvPMiF0Do/69fvz57yZIl5Hx0CTiLB1s5nx57WqIqH330UVHkP2j7vffea7Vnzx5yEpNY
+qNOvf/3rpWgQelXtb2ksgNQCCNP8f+Ztu0E/rv//5z//WTqEMwHnoGvRT4/t1KlT+Z133nlYcDpI
+yTz//POk9o+FlN+WLVsemjBhwhYECroXtPBbJYKQA5wy/998zI7gU0FBDYC+Zs2ajiHWIYEQ0DJJ
+Q0sH02P/VFyDcsn4jbFjx+4E3b1Z34Y+++yznM2bN+dQ9xuVmxz1Oo0cOfILBAq8VeGXuQJAmLsB
+zaY975jMPZBpe27//44dO8gx3C91SsWNzZwd8HhXQRW+Ib7W+0jrZEc1ipM47Qd6baiA6EvkWQ6a
+yad9Sim/p377298eFJwOanMzZswgtb/TKb8qdcrOzi579tln1yDY9JeNAZBpfTBrywRAaXf2OjIQ
+Y3MJIIG1a9dmHz9+PIsq8P80c6ODg19t9QHYWEk7c9c2daOjg2VwEqtPeoXCD4AcoRgq3iupQbEk
+Pfbee+8tSE5OZhs3wGlzq1atyvr2229bUvcbkZOEzg6n/MrqdPPNNy9LS0urRaDgq2h+2ci/AOEH
+QncBeBCRg13fn7etAdA//fRT0vxvl6w7KvwAsKXSS5pzOoC+cZyHIJuGvJ1D/WQ+yNNjs7OzT48b
+N24fxOZ/QJuaOnVqZ2nKr4PDflXqlJ6eXjFr1qxVoIN/FCGwQ4FJIiClw0YAUOb/866T+f/C7r/V
+q1d3oAoTiQQgmYBc0MTlqJnsNGSzEDk1x+KHpbUokKTH/u///u+ezMxM3gsIeuA7duxIX7JkCTmU
+eUi2Gxc5mfKrUKdrrrnmmxYtWlQj2O9XsQJ45r9jQUCRUPPOh+oCcK2B7du3k/5/JD4AKsvTj+dJ
+SIG6LxFTGODQhzFVUn4nTpxYCEXtP2XKlHM8Hg/JxE4n/cjqlJycfHrmzJnLYT3oxxsLoOQGWCEA
+KpgnIwLzdSIy4Ab5ILAGDh8+nHrw4EHSn4uE8MkIIJ7TkHdJpiEHgP4O1O+LEx6sk6f87mvVqpW5
+L80oaJCQFxUVJX/44Yektdgnw4UrHEz5VanToEGDVnfp0uUkxOY+zwowa3qVQUAwr8PtIKsKu4rp
+TxGEPm/evHY+n0/I6BkuDb3SnRW+gzV+7JOM5ohnApCR23lpOlq4w+/eKKTH+p5++ukCzimuxTl1
+6tRO1dXVdMqvwxN+yOqk67p36tSpy6Au+FaDf9ZcgBD8f9lxKya/qFtQ+/rrr8kAYN8MF5x2vWX+
+cfsIBCGdxLflklmIHSC3dZVeLJOkx1533XUHzjvvvCoEazQzNAAoLy93/fWvf82n7tclVcew5s4N
++1Wp089+9rN1/fv3LwVf+FlBp4YDU1YAEPisbFsAIjPf2KYsAN41MisgiAg2b95M+v8xYf7H8SzE
+gIp7E36TWeYna5qGiRMn7kKw0PPoXps1a1aH8vJyOuW3TbKzKb8KX/mdMGECL+mHHQOgEgRU6f6z
+HATkPlwECrXKdXYDgAFrj8ejFxYWkhHdSAjfSmLwDxDf5n+Zx48fJZ384a5fQbUPH0rSYwcOHFjc
+r18/c8ovjwg0AFpNTY321ltvkUlirZM13JHrnPmvUqcePXpsHTp06CHwyf1McAAAIABJREFUhd+K
+4Ku4AQZs9QJQRjWl3UGcE3bzgSP8ALT58+e3On36tPDb7S6tzgVwEpU+PzadkgR14pgAVkmmIW/h
+1nBeWnj1po2UXzJC+corr7Q9fPiwNOXXSS9NpU4PP/ywof1ZEz+UYb9mMgAEFgG36mH+AKixpghC
+lP/PtQCWLFlC+v89012OZnIBwPeSEXKZLg09HQ5COgmZ+R/u6H9xrR/vS9Jje/XqVXrjjTeWQWz+
+17ebWEj5ValTfn7+7vvuu283goN/IguA91EQ5ZF/LMI5H4CxttsDQPr85vW6detI/z8Smlc2ACgS
+QUgnIZuGPNz9/y8V1VhN+SW1//vvv99y9+7dZMrvA62SHR2kpVKn0aNHL4V1wVeNA0CwXb+oEICK
+wFPXhUIEXAtAlgAUCd+7MQcAa/zAGsk05OEMspZ7/XijmNaUnTp1Kh81alQx6IE/xlqa8pvicMqv
+Sp3y8vKKJk6cuBlqkX+egFvt+guCnYFAon3zMVFwUFXzC4V//fr12ceOHSO/3+608PkAfCchgHj2
+/9dXeEGNWE3WgD5hHDL7p+Ia6ZTtY8aMEWn/oHY2f/78nE2bNklTfls5nPIrq9PIkSNZ7c8L/qnM
+AswjAgi2bfUCyGBH2GXBQK75/8knn5Ajuton62if7Gzf+9ZTXvLlxvtnyFdIyK1PhitsgbPTvjpT
+mUJeXt6pMWPGHIBc+wOANn36dFL763A+5VdWp+zs7GPTpk1jU35FFoCKRaASC2AR7AKE+AEQ3jFe
+XMA4Jwr8CS2A7777jgwARqT7T/advkb+GfJwmv8fKKTH3nfffbKUX2NbW716debKlSvJIeLDmyfh
+XAdTflXqNGzYMFHKrx1f32r/f33hQg0C8twCUUzAiuYXTgCydevWdlThIuL/S/r/4/kz5IBCAlCY
+BgD5UNdNRiErK6vmd7/73T5Ign5noE2dOvVcWcqvk0k/KnVKS0urnDlzJpvyyzP9Q035JbU/ICcA
+mTBT++bjdoOBASRQUlKScvDgwVZUgSPheycSgMJTv49Ka7FLnvK7Oysri3rg9e3lxx9/TFu8eDE5
+QOyqbDcudjDlV6VO11xzzTd5eXlVsBb8sxMMBHEcQHhiACIioISfmu9PGAOYO3dueyoBKBJ974dq
+/NgrTQBqvJ8h75qqIzdMwTPZV3FSU1O9EydO3I3AxmwgSCk988wznaOe8iupU1JSUs3MmTO/gnzY
+r8j/V9X+gET7g40BhGkCEGOfRwwqml44MnD58uVkADAWEoDi/TPk0gSgMLk3y054sFbS1XjzzTfv
+bd26NSVR9W2oqKgoSZby27uJC1c6mPKrUqdBgwat6tq160nIh/1SCT+h+P8BCGc3oCjQZ3cJChDK
+EoASE4CEjkglAMkSZEwpvyraH9OmTZOm/Do53Rcgr5Ou695nnnlmGfhJPlZm+5FF/wGBwDPnSQIQ
+RfdF/j7vGhXfnzf+n5cA5JIlAEVC+GQ9APE8AChSCUDrK71YKv/K78Fu3bpVEZfUt6Hy8nL3+++/
+34m6X5dUHcMdTPlVqVOfPn3WDRw4sBRiwQ/HhB8i7c9FqDEAnnCbj7PnWMFXngBk/vz5LaOdAHTK
+58dGyawu8WwByBKAmocpAUgx5dc81z+p/WfPni1N+X00BlJ+n3zyyVCH/bJCbkX782IDYZ8RCAgt
+2i8KAGqyBKALItD3LksAynZpOL+RJwCF+oQLqn2YJ0mPHTBgQHH//v1PkhfVQaupqdHffPNNMuW3
+VZKGOx1O+ZXVqUePHtuGDRt2EKGN8ZcF/wAL2h8wWQAhDgCizH3eNSoWQYAFEA8JQP0yXY5qGach
+TQAKwzN+Xi3lVzbst75tvfrqqwopv8mOpvyq1Omhhx5iJ/xQMf/tDv5R0v6A2AVQEXjqOrvan2cB
+6AA0aQJQDAQA47n/XykBKMQMwMMK6bEXXHBB6f/8z/+UQnHgz8svv0wO+81yaXiglXPaX6VOHTt2
+3HP//fcXQJzzb2fCD5YQAIvaH1AfCCTaN47xgoOi85T251kAmlICkMPC50PdV3IoxLP/H4kEoJeK
+asj/AMiU3yBr8v33328p+8pvJFJ+ZXUaPXr05+Br+lAm/FAN+PkF2wBCCwKGIuwi7c81/2UJQB1S
+dLRzOAFo2ykfmQDk1oBL4pgAZO5N7wwXQhk+X+7140+S9Nj8/HzVlF8Aaim/Dzmc8iurU15eXrHp
+K79Wp/yy0+8PwT54+zoQsQQgSshJ81+aABQR/5/2jy9q4kK67mwQ0kk4nQD0xuFalZTfAk3jPkO2
+nWkLFixotnHjRjLl906HU35V6vSrX/1qqa7rHqhl/VH9/lYCf2COs9v1CMeMQFRMwI7fH0QKMZEA
+1IjNf8DZAUB16bGnyWvy8vKqxo4du990iJKsGEn5peuUnZ197LnnnvsB4mQfs8DzpvqiYgCq/j7l
+EnAJQKTdWR+ft89eS1kFIhcgwAI4kwBEpndGJAFIkgEYzwRQ4HAC0F+P1qKohr7/6NGjjZRf9sKg
+NvTdd99lrFixgkwKu7l5Ero4mPKrUqehQ4d+KfnKr8qEHyoDfUBcAxBkavcJiYjAivDzAoBB5v+8
+efPa+3w+YTmzXBp6OpjdBQBFNX7skSUAOfSNvEhA5v93SdWRZ9OU9gGYc5DWlJmZmTVPPPHEPtMh
+UvvHRMqvpE5paWmVs2fP/hZ8rW9l6K9qFyAPpPYHQnMBKFfAvG9F+weZ/8uXLye7//pGoO99rWT0
+3zmpuqO+ptNw8gtAH5fWYqdayq8HCtp/586dqYsWLSJdwiuz3ejtoFJQqdPVV18dKym/QqYcMWKE
+X7cRAGTPWxV8mSUQsL1p0yayByAS/r/s83fx3P8PqExwat+6UUn5nTBhwh7F22lTpkyJi5TfGTNm
+LIe68DuZ8ivaBwCwb5b3YEVmPXWNqu9P9v97PB7X7t27o54A1FTSj7yh0ovbd1F5K7ELvx+OJQB9
+ecIjHVx0880372vbtu1pBDfooDZUXFwsTfm9uIkLVzmY8qtSp4EDB67u1q1bOeSTfUYk5ZeHESNG
++IFgAjCgYhVQpKCi5aXEsGDBgpbV1dVkAlAkJt/MlpgAW075sOWUZDRInCLHraGbzQQgmaZ0uVy+
+p556iveVXx60adOmdaqqqiKl2/GUX0mddF33Tpky5QuIR/3ZMfVlg39YyKyDhvLKLlAESwrmbUsm
+v3l7yZIlJNtfGKHJN2UWQGPGVdluqR/Iw4ZKLz4/TscWrrnmmoPdu3c/BQXf/+TJky5Zyu+5qTpG
+OJjyq1Kn3r17r7/00kvZlF+rvr+dngBK2APOGdofUJsPgHfMirlvJRgYsL1u3TqSAAZGKPe+ZbKG
+wXGc5x8KRuXZEygLX/kNOsU7Nnv27A4nTpyI+ZTf3//+99RXflUm/7Bq9gf8Pyxof8DapKAi/998
+3o7QCy2AWPgCEM4U5oMu6WguiwY2Moxrm4Lrmlr3pwurfZhXJk/5HTBgQDnEDbW+fXg8Hu2NN96Q
+pvyOcjDlV6VO3bt33zZ8+HA25Vd1xh/RqD/R4B9wjvMg1P5AIAFQAT4WPAuAOk+5ANyhwOvXr88u
+KyuTJABFru+9bbKGd84ls04bFYY3T8KzHYThFxLPH6qBZIQsxo8fz9P+XKh85XdsBFJ+ZXUaO3as
+ecIPWX+/KPKv6vdTUX4l7Q9YjwGIVKBdM18UHJQmAEVj8s0bm7kxoV2KtFsw3vHzDBfeOzfNlu9/
+uNaP9yTpsT179iz7xS9+UUpcEtA+Xn75ZVL7RyLlV1anDh067HnwwQd3QT7qz27kHwgWfFlXn+0g
+oMz/Nx+zY/oL/X5jPxYSgHj4Q/sU7O2TiRkdU3F+ejxP/xGItskaft0yGR+el44vzk+H3Zm/XraW
+8ittoB988EHL3bt3Z1HX3N8yCdkOBmpV6nTmK792R/3Z6ftXJYV6sOY/IO4GBIIFnT2n4jKo+v1B
+RLB169aY8P95aJWk4bE2yXisTTLWVXqxS9KPzmJXtQ+T94uHkiYlJfnfeuutSpdLuY4aAEydOjV1
+x44dwh8lacAL+Wkwu/WaBpyXquPCMIycO+n140+HpZNjnLzrrruKiUsClMbzzz9Pav+6lF97rooK
+VOqUm5tbHIav/FIj/SA4BuY8tc+F0RQoYeYJtHnffNxOAJC1DIwEoDyq4LHy+a3eTVyWh53OlvQl
+X3LJJd5Ro0bxLmIHydRj5cqVbkr4AWBUXjLub+VcN9kbh2txnJo0EXVf+dU0TWnAysKFC3M2bNjQ
+nLrmjtxktHbQFVSp069+9asvTCm/vL7/cET+eTEApedIwUoUTUQEvICglQCgrQSg+J58UzL2fsAA
+NtwsYvf6dzJ9+vRU6p5Op8fW+NVSfh966KEDxCUB7WT69Omk9o+FOmVlZR2bPn3691D/uIfdvn9A
+rv2F1/HMfyD0gUAii8GK9g8y/2UJQPE++abs45uDBg0yM4S0i2fr1q2u+fPnk5IwrHkSujqZHltS
+i0OS9Nh77rmnIDk52WjgAKHBvv/++4xvvvmGTPmNhTrddNNNX6WlpXlgX/hVtT9lFZAQCT9AzwfA
+O2ZH21sJDMZMApBT2FntQwmRe69pGksAMvhnzJiRKk2PdVBT+gDMVkj5HT9+/D7ikgALUynlN8p1
+SktLOzVz5syVoH1/0badwB8PUu1PwQ2xwPMEnbomVPNfA2InAYiHE14/vin3olLWIUxguWRikTZt
+2vgWLFgQ4KhTgnDs2DH9H//4BxkFOz/dhYJqHwqqfchwabgiO7zTl31SJk+Pvf322/c0bdrUnPLL
+i2doALRdu3alLly4kGwDV2S70cfBPBCVOg0ZMuSbVq1amVN+eb6/yBpQ6fqT+f9SUNofCIwBqLQI
+ihRCNf+VEoAiPfnmzmof/lvmwYLjHqws90AycU7IOHjwoH7HHXdkhPOeW08FZitmuTTc0tyNUXnJ
+YSFT2RDZlJQU4yu/KjC+8kva9o6n/ErqlJSUVMt85ZcVfqqrj7cWEQIgFnhbWt8M1SCgiltg3uZp
+/CBfn3dcmgDUxIUmEZp887/HPBj+4ylY6+SLfZR7/XjnSC3eOVKLrqk6lpyfbntW5a9OePBDmFN+
+586dS7aBi5q4MMTBlF+VOg0cOHDVmZRf0UAfGQmoWAIAX/uLEHBOpv2B4BhAJPx/Uf+/DpUEoAhp
+/w2VXty+s6rRCT+LndU+3LGrSjrMVQSFlF//ma/8qkB77rnn4iHl13fmK7+ygT/h6PrjCb1KbEAJ
+qslAsnPhCABqQGwkAB2o8WHoj6dQ6XPY3o8RfFPuxdQDdMCLh42VXiyRpMdeffXVB5iUX6H2V0n5
+7ZyqY0SOc2MZVOrUu3fvdYMGDToKNa1P+f+UFQA4rP2BBgIQCTN7zOo25Qqwwq9v3LgxJhKARu6s
+knb/NDY8e+A0lkvmBmQh05RnUn6Vtf+cOXM6HD9+XJry6+T0DLI6AfA/8cQTbNIPL9qvEgewq/15
+27ag4vixRGDeVnULqLH/9YTw8ccfk+Z/foqONg4nABVW+6Rz5DVG+AD866g6Aeyu9mGu5Iu4/fv3
+Lx44cOAJBDfUoLZyJuX3HOp+LR1O+VWpU7du3baPGDHiAPhTe1kZ968S8QdnW7qvqv0Ba/MBmI+F
+Ggfgmv+rV6+Oev//shPWtGBjgmzIqxkq6bHjxo1jtb8fArfytddea1tcXJxO3W9s6+SQPk8mg2LK
+r/k7fyqRf8r0p6L+4BwLq/YH6G5A3j5LCLy1SMCp/n8dgL5t2zba/4/A+H/ZHPnnp+voGafDkP1+
+YG5prTCweVwxEnik1o93Jemx559/ftlNN910FOJgVYBCeOmll8hhv5kuDQ+0dE77q9SpQ4cOe3/z
+m98YX/mV9f1bMf2N40AwGbCQWQOWwA4EEvn/4Oxb0fIyC0AvKSlJOXDggOQLQM77/zLzf2K7FPzS
+wXnnnMTuah/+TZi4qhaAxZRfM7ja/69//WteYWGhNOW3qYMTMajU6Z577mGn+xLN8GveZoWcp/kN
+8DS+6DourJj/gPUpwUTCzztGxQCCzP8PP/ywHZUAlO3S0MPh/PviWj92S1pBJGchCjdk5KYyyOmk
+14/XFVJ+77777iLTIVL7y77ym6w5n/Irq1Nubu7hp556ypzyKzP3Zf6/LBbAQ1i1P6A2KaiqayDS
++rzIf9Dx5cuXk/5/JBKAZF/IicYsROGEjAB6N5E/4bcU0mN/85vf7DrzlV/zhdwHt2jRombr168n
+U37/NzfJ0eCvSp1uu+02NuXXagDQSsRflRACYFX7A/xuQNF+OBdeAlDU+/9l/n88fwAUkH/iXPYF
+oBo/8KIkPTY3N7fq4YcfPkhcEtAWVL7y+3hb57S/Sp2ysrKOn0n55XXzqVgEvH5/lei/GbwAYchg
+JwW14/+z+yJTn0wAKiwsJL/3FgnhkxFAJIKQTuG4x49tko+XyJ7x30pqcVCe8luYkpLiRWBD5arv
+NWvWZH799ddk3OemHGdTflXqNHTo0K+aNGlSC2sz/lAWQCjjALiwo/0BdRdA1dcX+fykBbBo0aK8
+aCcAnfL5sUHyEdB4tgBWnfSSLSkvScO5hKD5Acw+pPSV373EJQHt4Jlnnuns9/tJ297JYb8qdUpN
+TT01a9asFYjjlF8KhiCy4Pn/IgtBJepPzgC0aNGijlQhL2oS3vRVHn6o8IJyA+N/FiKJdSMht0/K
+PNghmftw5MiRbMovIND+BQUFaQsWLCBTfi/PduPnjqb8yut09dVXRzPlVwl2tT8gHgoMZp8lBKu+
+vogMNMRIApDM/I/3WYhWyPx/Se/GTMnkGAopv6z27yRL+R3n4IQfgLxOSUlJtdOnT/8Koaf8ysx/
+gC/0KtZBSODFAKh983GrgT7hAKBYSABaKZmoI55nIar1Q/5FW6J+y8s9+F7y+2HDhu1r166dOeVX
+iCNHjiT/5z//Ia2+C5u4cI2NrxKpQqVOAwYMWN2jR49wpPxS5j8Q+Mxk2j+sRCBiYBERWIrsq1y3
+cePGrGgnAPkArG7EPQDrK72gLN1UHbiYMLVlk2O4XC7/U089VUhcEtAGpk2b1lGa8uu49pen/E6Z
+MmUZ+JF+p1J+WSLgbQcgFPMfoOcEVNX0MlIg/f9PPvmE1ASdUnRHp30GgG2nfDhBDION9CxE4YbM
+vflZhguiR7yp0ovFkvTYIUOGHDj//PMrVcpSWVnpeu+998ikn3NSddzi4GhLlTpdfPHF6wcPHlwC
+esSfKPBHjfqzov15LkFYwdPUMK3NoIJ/qlZAEBGsXr06Bsb/042hV3rkZiFyAislA5wGEhaWQnqs
+OeWX10ADFMrs2bPbx0HKL8aPHy9L+VWNA4hiAkCUtT9g3wIwnxeN8FPy/2VfAIpIAFDi/0fqM+RO
+wW4PwJ7TPvxHkh7br1+/w4MGDeKl/JqhAYDKV37zkjTc5WDKr0qdzjvvvO2//OUv90Mc7Vcd/BNK
+H7/j2h8IJgCRBWDHLeARQ8BSWlqaLEsAorRTuBBqF1kso6DahyPUNOQA+gvqF4av/AZYja+//nqb
+oqKimE/5HTNmjDnl1+rAH1m/vwGRFRAx7Q8Em+mAPWEXaXuWCALWsi8ANXU7nwB0qMaPvafpvuB4
+TgCS+f/d0nTkcLLsSmr9ePcIbSr36NGjbNiwYeaUXx7q29ZLL71EDvvNdGl40MGUX5U6tW/ffu+Y
+MWN2IXwpv6pmvxkR0f4AbQGITH7WzOftiwKAAYs0ASjDxQ1GhBOyz3TFfQKQxP8XWTcvF9WQPQcA
+N+VX2Kj//ve/5xUUFJApv/dFIOVXVqd77rnH7Ptb6ftnfX2ZGwDiOIlwaX8geByAeW0+bjfiT8YA
+pAlAMTABSDyb/4CCe8NJAKpQTPm95557iiAeqMIG/6Qpvw87mPKrUqfc3NzDTz/99CaoCb9K958K
+CbBgn6dj2h8I1NwAX9uz+5Sfr2wReDweV0FBQdS/ANSYBwCVevz4UaLyePV760gtjknSYx988MGC
+Mym/gLiRagCwZMmSprKU39udTvlVqNOtt94qS/kNV78/iOMRhcgCUNX4smi/cFm8eDH5BaAkDY6O
+AweASp8fm0413gFAKglAXZiIW40feEGSINOiRYuqRx55ZD/4DTdIip977rku1P10AI+3cTblV1an
+zMzMEzNmzJCl/MqEX4UMAHXtH4Rwmv9AcC6Aivkv8/9VXAFd9gWgSCQAfX9SngDUs0n8EoDM/OdF
+//+ulvJbwKT8CrF27dqM5cuXkz09Q3PcOC/NuWCvSp2GDh36ZQgpv7Lgn4oFwIPjVoF5TkC7XX2s
+0CuRwNq1a6Pe/7+ikScAyQYAsb0bfthK+TU3Uo3Z1qZMmXKuPOXXOe2vUqfU1NRQv/IrIgFK8HlF
+heAcgPBrf4BvAaj4/7ygnrL2B6D/+OOP5AQgkfC9G3P/f40fWCOZ32AQU79PyzzSmMH/+3//b0+z
+Zs2MlF/S9y8oKEhdsGAB+Z4vy3LjEgddPZU6DRkyZEWbNm1OIbSUXzskQAl0RGIChvAC6kE/mQUg
+XbZs2ZJVWlpKJwBJpqcKFT4A3zXiBKC1FV5Qwxt4CUAzJZoyOTnZe+Y7f0pBq2eeeeYcacqv49/5
+o+vkdrtrZ8yY8SWs+fyqpr6K9vcLtgPghPYHGr4LwJpuxlrW3WfL/P/kk09I//+cVB2tkpz1/7dU
+elFODAlzaWdXAtDX5V4pId50003mlF8y+Hf48OGk//znP/nU/Xo1ceFaB1N+VepEpPyK9u3EAwBa
+0GX7joFnAahqf8ua31hWrVoV/fx/ScO4MM4TgGQDnNgh1rLJMc6k/O4E7cPWH582bVp+9FN+6Trp
+uu77wx/+8AX42X6q3X92uvyA4OcYce0PBFoAoQT9LJHB1q1bY34C0LhPALIwvmHzKS8WSdJjr7rq
+qgMXXHBBJcQEYLClv7Ky0v3++++TST+dUnRHP7CiUqeLLrpo/eWXX34U9sb8Wxn1Z8XXj5j2Bxp6
+AYKitxBr/6ARfQqLy1iXlZWl/PTTT3lUoRIJQKFhR5UPR4n+TQ2BXYCyyTEA4Mknn9wBcYMOMJVm
+zZrV4fjx42Ro3/GUX4U6MSm/ouBfOFJ+AZoUIir0ZrDdgFai/VaFXwegf/zxx22pBKBmbg3dHU4A
+OlDjw0+NOAFIRm7mBKC9p+nPhQHAJZdcUnzZZZcdA9+f1Uz7msfj0d58801y2G9ekoa78pwz/1Xq
+1LVr1+233nqrkfJrNdVXReuDs89CKvhOmv9AYDegai+Arcg/zhDBV199Rfr//TIjkQBEC0i8JwBJ
+PwBi0v6KX/n9EXQjN+B/7bXX2hYVFTWh7jemdTIcHPdjJeVXZbYflVF/oqCfivY3I+KWgNkFUA0A
+WhZ687Jx48ao9/835vH/gIL/f6aLtaTWj79I0mO7detWOmLEiMNQGKgCQHvppZe6UvfLiEDKr6xO
+7dq12zd27NidkJv+vJiArOsPgmMspM/Tae0PyF0Aq9rfRW17PB7Xrl27WlMFioT/35gzAEtq/dgp
+/cBpXf1eKZanxz7yyCPboRbM0v72t7/lFRYWNqXud1/LJDRzMOVXpU5333039ZVf1S4+O25ATGl/
+IHgcgBXtL7IIXBAQwdKlS/OqqqqimgBU4fVjcyNPAKJgJABVeP14rViqKcvvvfdeUdKPGRoA/+zZ
+s7tR94tEyq+sTi1atDg8efLkjbAe+GPNfjb4J3OPzIgJ7Q/IXQBW44cSAHR9/vnnpPl/cROXo74h
+AHxX4SX9w3hPAJL5/0b0/22F9Nj7779/m67rRgMHAhusWY37Fy1alLNhwwayd2dkbpKjsRWVOv3y
+l780p/xS2p9n9quO+rPrDkQcqjGAUKP/LgD6Dz/8EPX+//2n6WfdN94TgKTujRu1fuAFycy4zZs3
+P/X444/vRkOjZ+FHQ7vwTZs2rTt1Pw3Opvyq1CkzM/P4zJkzV8Net5+VXgBALPAqsZSIgR0IZN4W
+DfqxJfwA9O3bt7ehChOJGYBkU051S9OxT9JFGKuo9QPrFL4A9PejtThQQ9fxjjvu2J6amkol/dQ/
+yO+//z5zxYoVJLkPzXGjm5Mpvwp1uvHGG7/MyMiogTXtTwm8lWCgCEHnImX+A/aCgFaCf/XLtm3b
+so4ePUrOCReJvvemEo55uagGLxfJB5HEI4wEoHsLq8jrmjRpUjNx4sQfETyoxYDR968B0KZMmdJD
+mvLroPb3A5gtGfabmpp6avbs2d+AHvYbzgk/2GemEhuIOEQugB3tzwb/Asjgs88+IzVE51QdLR1O
+AALkFkBjxs8zXFh8zIPtkjD5Lbfcsr158+bm7/zxCEADgJ07d6YtXrz4HOp+g7Nc6Ouge/dZmbxO
+V1xxxTdt27Y1p/xa1f5W/H9w9nmIqvYH+ASgc47xjltyA1atWkWb/xGKvLdM0uHSIB0o0hhxU06S
+UsrvpEmTtkCs/QFTO5g0aVIPj8dDvjwnJ/wA1FJ+p0+fzkv6Ue0C5Gl/HiEAYu3P2446qG5AtqvP
+zug/fdGiRS1XrlzZ7rvvvsunChKprrfWyRomtU/B0z/RjaaxoUuqjvNSdelHUK+77rodnTp1YpN+
+2Oi/BkArLi5O+eSTT8iuvwvSXbjOwZTfb8q90jr17dt3Va9evU5ALvyhaH+AFnTZfsS1P6DWDSgS
+fiEhLFu2LO/NN9/svXz58l7FxcW5KgVxegIQM55om4KvTnix7ATdZdZY0Nyt4dPu6Xh0bzV5na7r
+/qeffnoD6gRBFvzTJk2a1K2qqopM6Yv2hB+apvkmT55sDPwxd/+xAq6S+qsyEpCyBmJK+wPqQUBl
+8/+qq666edmyZYOtFKKZW0N3pwcAmKADeO/cNPTZVEF+NqsxIFkD5p2XjtM+YOExyRwBAwcW9OnT
+x0j6Ic3/iooK97/+9a8LqPvlp+i41cGU3y2nfNI6XXDBBeuGDBlIyrxuAAAUWUlEQVRyGIHCT5n/
+rPBb8flFiEntD4j9fVkvANc6GDt2bF+rwg/UDU6JdGiudbKGhd3TcUduUlxP/EGhZ7qOD7qkYVCW
+Szo5BgA8+eSTa6BmJnv/8Ic/dD1x4gT5nT/nU37ldRo3btwiqAm/1aHAVgKCQAxqfwDQ/H5/Hhoi
++C7UWQXG2g0gybQ2luQzS/32Bx98cO4999zzmMfjsUz5Mzqm4jGHZ4ehUOH148MyD94vqcUeyTh6
+FrV+Pw5Jppx2u91+j8cjFAWXBrRN1kMmQQ3A+ek6bmjmxg3N3GifXGdV7T3tQ7f1FeQU6L169dq3
+cePGj9DQyAF+9B8ej0dv27bt3UeOHBGO+89N0rC7d6ZjIztV6tS5c+etBQUFfwRQSywe09q8qMYJ
+VEgBIMggWtofsJ4NaNb69fuHDh1KGzNmzL12hL+pW8PoPOfMRBVkuDTcmZuEO3Otl+O9klqMLhD3
+q7dp06b20KFD5I2fbpeCCe2ci5T/8VANKSgA8Oijj36HuoYv677Spk+f3oUSfgAY08rZlF+VOj3w
+wAML0SC8PKEW+fts1F8m6CCOs4gpS8AcA4BpWxYHCPD7//73v3cqLy8nP/0kwuNtkuO6b1728c2q
+qiqychkuDb9p5Zz1c9Tjx1+O0JNjdOrUqWjUqFF7Edz1x0b/AUB78803L6HuFwt1at269Z7HH398
+GwI1uh0S8HO2Zd1+MJ0DcT6q2h+wPicgd5zADz/80MHqHydpwOT2Kfidw33ETkM2ucixY8fI7o1f
+5zmbHvtqUQ1O+eg2dt99932LOuEQEUC98L/22mvn7N+/vxV1v1io08iRIxeAb86HQgIisz4utT8Q
+7AIA1shAA6Dt2bOHHORzaZYLF6S70ESvM/l7prvQu4kLreN41h1A7eObFJI04BEHYx+VPj9elaTH
+5uXllY4bN24bghs7D9qf/vSnn1P3i4U6NWvWrHjGjBk/IFDoWcG3kvCj4uOzUOkdiDpY7WTHCtBr
+amrIN35fy2T8qkV0/XwnsFry8U1d1/0+n0/IciNbJKFdsnOO8tuHa1EmcZRvv/32FWfSY9lGzkID
+gL1795ITusRCnYYOHTrf5XLJAnqyUYCU6R8W3z/a5j/ADwIaUO0W1GTBvzhX9ELIUm8p4dcAPO6g
++6OSHpuVlVU+ZcqUdaiLgssasXbo0KHUkydPZoruFwt1atKkybEXXnjhG8h9f1nGHzUOAGgE2h8I
+tABYF8C8zVvqfycbC57SSPvZZR/fpHB5ths5bg2HHRqI9FFZLfZL0mNvuummFRkZGdUI1HSAgAC+
++OKLZtT9YqFOV1555cKmTZtWg+7SMw/6sTv8F8RxFjGp/YHg7wKIhBym82CuhdfrJQNdjdECqPED
+ayUf36Tw5QkP2q45GcYSWUNqamrVc889txJi7R/UA7Bu3TqypyfadUpOTj71xz/+8XMEC76ZDKwM
++lFxAVjEjfYHgmMALJRiAh6P56wjgHUVXlBjhpKTk301NTUxO7nQlVdeuaJt27YV4Af/2MarAUC3
+bt0OR66E1tGvX7/FnTt3Pgm1QT2qboDo+cS99gcQMPsVK6YisQ06LiOAxugCyPz/nj17ll188cUl
+ESqOJbjd7tpp06YtQ/CIOPOouKCRcvfff/+u9u3bF0Sn1DRcLlfN9OnT/wt14ZdF/a1qfiDOtD/Q
+QAA8F4DdFwYIvV4vGQNojBaA7OObffr0KXn44Yd/jFBxLOH6669fcOGFF5YhWMilhHDHHXfMj0qh
+JRg8ePDc/v37l4Jv/quO/Vfp8rOt/UeMGOGPJe0PNHTnqYJ7rdQFiFlD2B6qfcByycc3Bg0aVDxq
+1KifOnTocDxCxVLCRRdd9O2nn376KYAa0GPjuWPlp0yZsj4nJ+dgNMouQrdu3b5ctmzZvyAe02+s
+eYJvhQQgOAbEofYHAl0A25AHARuXCfBRWS2OUx/f1DT/9ddfXwyg9i9/+cuyTp06HY1c6cTo0KHD
+tq+//vptWEuMCSAFl8tVM2fOnJdzc3P3Rr4GwWjTps2mVatWvQSx0NsZAET5/SyUtb+tCjoMqwTA
+Mp4fgF/qAjQyC+DPknHoXbt2LcnNza0EUHPllVce2bFjx78feOCBlcnJyVGZgcTlctWce+65az//
+/PMXMjMzq0Fnx0lJ4a677tqzb9++J2+88ca/ud3uqMyg6nK5avLz879bvHjxtKZNm55mywi6/19k
+/ts1++NS+wPyXgARAioqDQI2IgugsNqH5ZKZhK677roC1JnYPgC+pKQk7+uvv7561KhR2yZMmHDJ
+vn37mh89ejTLRJwBz9PvD2pH1Hk/73hSUlJ1ly5dtlxxxRXrH3vssc2tWrVi+/t5I/9EDTgoJpSW
+lub573//+9FHH320evLkycOOHDnS9uTJk3k+n49VBnZfflCdk5KSqjt16rRh8ODBP4wfP35Du3bt
+qqA2yk824k+l/19aRoXjMQfN7/e3Q+BcAOaFnQsgYA4AYzstLW1OdXW18IuwxT/PRIs4zvgzY+JP
+pzGdmIjC7XZ7CwoK3unYsaMxA60o0UQUdaYy0WTXy7ZV+rW5XYCwniTG7gPBwWQzRGUSPTdeQE8U
+6OMl/YQ6DgCcbXC2Y9b8B+qEm2vWm/YhOF+/+Hy+s2IcgNcPvFdCW7yXXHJJQceOHcthvbHJBNbO
+miIVFeE3ICMB3sxSuuB37D1F7Y9XF5YA2DVP8EXXygRfxeyXCn+swxBcXqFZluMxnx+AXxYEbCwu
+wILjHhRJZv+58847N6DB/FclAJkw2z1HXSsSfnMFreSGqCzsPc1g2xZbZsoSUCEE9npVweeVU1nI
+Y1n7A4ExAB6LiUiAJQAyzNcYgoAnvX48LplVNycn5/i99967C3QDNPbZhm1HgFXIQKb5ZRYAK7hW
+XAH2evN9zJC1M55LI3quFPHazfoTWQDsM4tpYeeB1dxCLc9ZfAD8J0+epPw6uDX7UaBYwpg91SiU
+zBd47bXXrtd1vRZiE9S8bYUAwrmtqv3NUBV+SuvzzH/2P8m2hmDhpSwCVY3PPhvRs+KVl0Ssa3+g
+IQZgWfCN7ePHj9OZgI3A/P9rSS3+VkJ3/em67nv00Ud/QJ35TxGAHQtARahVG7IV7W+AGhkaCQKw
+SgKyhWdVqGh8ijBjXth54FkAxlr2MnwAfOXl5Y3a/C+o9mHMHtr0B4BBgwb98LOf/ewoQos6qwi+
+qMGqHuORPUz7LEQugHlbRfCtuADmbRExWiEEFbJln4W5bLJnFHQsHrQ/EGwBKAu+sUgJII4NgMJq
+H365owoVkg8JpqamVr344ouLEKj9fcw25YOKtJFVrW5X64saOE9rywiAPc7+VgQV5WOFCFSEXvV5
+sc+Ftx+XMPcC8BqF6GHVP9gTJ06QBBCPmYA+1H0m/KmfTksnnwSARx555F8XXXRRKej+aCsugB2N
+bmUBZ5uCFSuAd968piCyAHj1p0hARKqy58t7TkDwcxIRJoD40f6AeByAD3XRXKHmN5aKiopGZQHs
+qPLh14VVWCVJ9zXQr1+/Vc8995x5Akq7FoBVzaRq3lsVfmOffXOqJMA7x96DB5ECUiUBlbUdrW9J
++OMNZ70LsLvahzWVXqyt8GJNhQ+rKzw47VP7bfPmzYs//vjjf6JuHLpM+1PR6FAaqhUCAOc8mOMs
+VN0A3rbo9zyoEoAqGYTyPMHZVkI8aX8g2AWQCjyzeLt37378zLXcl3ugxo/hO07B7+f8gd/0VgTn
+/fAz+7xrBPfk3AOm6w/X+nFM9nkZAVwul+fFF198q2XLlpXgZ5xRLoBqQMr8HiA4pyL84Gyb1+y2
+GSICMNYigbfqAojKpkoAKvtWnh9VRuqauILxbUAdgd8HFH0nkM0RcANIat68+R/LysryI176KEHX
+dc+jjz765qxZs3imfyS0P2wcA7GGYF/FDWDXVjU/7/9FZVclArvPUVQG85otaz3iTfsDchfAiAUE
+aX40jPrynnvuuRu+//77/EgXPhpwu921v//971+ZMmXKBsizzkSDUvzgk0AojZYSejuanwUl2Cpa
+P1QLwLwdroV3f2rNljXuwRKAD8FRXZ9pzSWCSy+9dP33338/LOKljzBatGixf/r06W+PHj16N9SF
+X6b9Vc1WWNiH5Bi1zULVCpCdE91PJFyietglBPZ6cLaptai8AOJT+wN1LkBzNLgAIleAcgfcRUVF
+6V27dn2toqKC/GZcvELXdc+QIUM+mzt37ieZmZnsSD9ZBhq1qGh8Fa1lpUFT2xQoQZcdE92H9/+8
+sqmSoOwc717UmtquR7wKPxDY1cdqITL4Z1o8rVu3PjV//vzHc3JyCiNeA4fRqlWrwnfeeefpxYsX
+z1WcTUc0rRY1Yw07jx01s63KLDeUFaKSGafy/kX3okiRPUaRpxWXiiVVkXUlIw7AovDHOzS/35+D
+OlY2LADKGnAjePKQ+uO7du3KuPzyyyccOnTo4khXJJxwu901PXr0+OHuu+9e+sgjj+yEeoOlBIwl
+VpnJb0Xrq6ypbVWItDul9a10BMvKKqp3uC0jJbMfiG/tD9QRQDM0BPRYElAlgvr18ePHU2644Ybb
+d+3adVFZWdk5sslCog1N0/w5OTlFHTp02NO9e/fCwYMHF44cOXLfGVNfpNEojaeiQa2a/uBsq6yp
+bd4+BZlgWzX9ZeWQCaiMCOyseWVptMIP1BFAUzQQAI8EqLgAjxDql+Li4rQ///nPXQ4fPtzU5XJp
+xqLrev3a7XbD2NZ1HWcW8zY0TTO2/ZqmaWfW0DTNz9n2A/C5XC6/pml+TdN8prUPgLHvzcnJqe7T
+p8/xzMzMWtAmpqrQywjAz1mHS/hlx9ht6pgZKsJsRevLYgC8Y6EKtiME2VgIIBuBBCAiAlUy4C0i
+YtGZ/+Yll4j6lnlgBYa3iLrhRL6lyM+1Ivzs/6kQADjb1Fp0TGXfClSEXZUAqHLIhFSl/lYE/qwT
+fiAwF4AnKEBDFyBM+yLw7uFDw1gCQ9C9CCQbDTQBgLNm/5ctg6g8bNl4BGCFDETXsvfl/b+q4IdD
+26sKGwuRxtaYfdExlXOi/7ciuOF8NmeF8ANiAhAJvEj4jd+5mHu4mDVrZWictV3tb5SDrYuxzwoc
+JaQyrW7V17cb9ANzjD3OHmO3efuy46LrZETAu6eqtpeVy6oGD+XZnDXCD/AJwBBUHhmIYL6HQQK6
+aR0u4bebTALQBCAjAiuCz7MuKAJiy8qrB2/Nbqvsy45TsEIEsv/RFMtghQyoc2F5Lo1N+IEGAjBe
+iFn4VQVfJPQiwReZ/aLJJMFZi8rCrkXWjYwERAQgE3iZ4IfL7Ke2qWPU8VBBEYGdMqjUyQ4BWtb6
+QOMUfiBwKLABVSIQNWAzAYiE3ormVx1PTmlM3qJCBDIBlwm9VeEPh9YXHVM5Fw4YCiXUe6geD/ex
+IDRW4Qf43wUwtg3B1yEmATNY4VIx+UUEAFgTfrbslGDxhJEVVFVCoK6XCb6q8Ktu854DBauNmgri
+hRt2CMwRImzMwg8EzwgENAivsQ0Ek4BIo5oFnj1GmfyU8KsGIdltkQVgHJORAI8AKC0fivDztqm6
+ifZVz9mB6v1U/ftQ/89u3ZXL1tiFH+ATAMAnAbMG0E3bxqIhsOGzcQCZ4FvR/ipdSjIrQCT8doWc
+J+y8Y6Iy8dbUNnXMyvlQoTKwJ1SEWkdLZTobBN+AiACAYBIwhFlEBOaeAzaoaPxORAAQ7JvXqhAR
+gHlbtoi0uMo2ex/R/4rKCGKbty87LoPsd6qDryIJ1f+0XLazSfgB8afBDLAkYAgwjwjMgq9BLPzm
+favCb8UFMG/bIQHKMpAJPrXwyqNSB1F9RQhXQ7Z6n1ADgHb/1+5vAJx9gm/AbAGIfDdKgM2/M877
+TefN2xqChV+1y0+1UYmE31jbIQEZKVgVepHg29H6snPRQCTLE/J/na2Cb4DtBZAFcFgBNrQ/T+BZ
+MrCq9VW1vwGR0IiIIBxkYEfbh6L5qeMiRKqBh0vzswh7+c92oTeDHQgEyEnArO0BvlbnaX9K8GVa
+30oXILsfKhFQ56xoe7uCr9JYY6FBx0IZhEgIPR8qLgAQLMyAWPB5gu6k5ueVld2WkYB52y4hiO7B
++x+qrKJ92fEETEgIvBp4A4EAdStAVfgh2Tav2W3qmLk81LFwEgF1DW+bWvPKLqtLAiYkBD00sL0A
+MsE3X8fuqwp7qJqf/W+qnOw2e0yFCOxuU2te2cMm+AmhSEAV5hgAEEgCVqwAlgTM91QVehWfnz1u
+lQDM21atAqvHeGtemW0LfkLQEwgVol4AVRIAAoWdFXxjTQl8KKY/Ww7ZcbtEYKyt/kZ0TFReqUAn
+hD6BcIKyAMywQgSiNaCu8cPRpUQJnAoRUOesanrKAhAdq0dC6BNwCprf7ze681SEU8WEt2Pmh6L5
+eZAJmSoZ8I6pXk9tU8fqkRD8BJwGLwhobAOBml/FCjCDtSaoe4WTAFRcAXY/VHKgtlX2A5AQ/AQi
+BbMFEHCc2A/XNm9fdtwKnCYC2b1EZUgIfwIxA83v94u64XjHVIVZJujhNvllUBFEVWKwsy86Vo+E
+4CcQDZgJAFDXyKHuU/8lO6cClYCl7Fi4hDwR2U8gZsESQP1x0fUKx0K9HwUr4wB4oK63QwrUPZXK
+lhD+BKIJ0Xf7zME62XE2mOdkgw713lYJIJzHA5AQ/ARiAbIPd8qIgD3HGz/AOx5N2HEPQj0XgITw
+JxArELkA5G8cPh8pqAhhKGQRhITgJxBr0IYPHw4AmDdvnh3BVP1NqEKv8nsn3YOQ/ich+AnEKuoJ
+wIBNIqi/X4R+Ywd2hTAk4U0IfwKxjCACMCNEMqj/jzDcI1IIi7AmhD6BeAFJAGaEiQy4ZXDovjw4
+JpgJoU8gHqFMADw4SAoxj4TAJ9AYEBIB8NAYSSEh7Ak0VoSdABJIIIH4AS8TMIEEEjhLkCCABBI4
+i5EggAQSOIvx/wEH6mZY7Q+SDAAAAABJRU5ErkJg
+--8GbcZNTauFWYMt7GeM9BxFMdlNBJ6aLJhGdXp--
diff --git a/third_party/jetty-http/src/test/resources/multipart/multipart-uppercase.expected.txt b/third_party/jetty-http/src/test/resources/multipart/multipart-uppercase.expected.txt
new file mode 100644
index 0000000..ef8470f
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/multipart-uppercase.expected.txt
@@ -0,0 +1,5 @@
+Content-Type|multipart/form-data; boundary="8Q4MHJ3LWIQEQQ_OXYU5U9ZLYEH60_CFZQYANCZ"
+Parts-Count|2
+Part-ContainsContents|STATE|TEXAS
+Part-ContainsContents|CITY|AUSTIN
+
diff --git a/third_party/jetty-http/src/test/resources/multipart/multipart-uppercase.raw b/third_party/jetty-http/src/test/resources/multipart/multipart-uppercase.raw
new file mode 100644
index 0000000..3aecb11
--- /dev/null
+++ b/third_party/jetty-http/src/test/resources/multipart/multipart-uppercase.raw
@@ -0,0 +1,13 @@
+--8Q4MHJ3LWIQEQQ_OXYU5U9ZLYEH60_CFZQYANCZ
+CONTENT-DISPOSITION: FORM-DATA; NAME="STATE"
+CONTENT-TYPE: TEXT/PLAIN; CHARSET=WINDOWS-1252
+CONTENT-TRANSFER-ENCODING: 8BIT
+
+TEXAS
+--8Q4MHJ3LWIQEQQ_OXYU5U9ZLYEH60_CFZQYANCZ
+CONTENT-DISPOSITION: FORM-DATA; NAME="CITY"
+CONTENT-TYPE: TEXT/PLAIN; CHARSET=WINDOWS-1252
+CONTENT-TRANSFER-ENCODING: 8BIT
+
+AUSTIN
+--8Q4MHJ3LWIQEQQ_OXYU5U9ZLYEH60_CFZQYANCZ--
diff --git a/third_party/jetty-io/pom.xml b/third_party/jetty-io/pom.xml
new file mode 100644
index 0000000..04ea199
--- /dev/null
+++ b/third_party/jetty-io/pom.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>jetty-project</artifactId>
+ <groupId>org.eclipse.jetty</groupId>
+ <version>9.4.44.v20210927</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>jetty-io</artifactId>
+ <name>Jetty :: IO Utility</name>
+ <properties>
+ <bundle-symbolic-name>${project.groupId}.io</bundle-symbolic-name>
+ <spotbugs.onlyAnalyze>org.eclipse.jetty.io.*</spotbugs.onlyAnalyze>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-util</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-jmx</artifactId>
+ <version>${project.version}</version>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.toolchain</groupId>
+ <artifactId>jetty-test-helper</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractByteBufferPool.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractByteBufferPool.java
new file mode 100644
index 0000000..c46d134
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractByteBufferPool.java
@@ -0,0 +1,110 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+
+@ManagedObject
+abstract class AbstractByteBufferPool implements ByteBufferPool
+{
+ private final int _factor;
+ private final int _maxQueueLength;
+ private final long _maxHeapMemory;
+ private final AtomicLong _heapMemory = new AtomicLong();
+ private final long _maxDirectMemory;
+ private final AtomicLong _directMemory = new AtomicLong();
+
+ protected AbstractByteBufferPool(int factor, int maxQueueLength, long maxHeapMemory, long maxDirectMemory)
+ {
+ _factor = factor <= 0 ? 1024 : factor;
+ _maxQueueLength = maxQueueLength;
+ _maxHeapMemory = maxHeapMemory;
+ _maxDirectMemory = maxDirectMemory;
+ }
+
+ protected int getCapacityFactor()
+ {
+ return _factor;
+ }
+
+ protected int getMaxQueueLength()
+ {
+ return _maxQueueLength;
+ }
+
+ protected void decrementMemory(ByteBuffer buffer)
+ {
+ updateMemory(buffer, false);
+ }
+
+ protected void incrementMemory(ByteBuffer buffer)
+ {
+ updateMemory(buffer, true);
+ }
+
+ private void updateMemory(ByteBuffer buffer, boolean addOrSub)
+ {
+ AtomicLong memory = buffer.isDirect() ? _directMemory : _heapMemory;
+ int capacity = buffer.capacity();
+ memory.addAndGet(addOrSub ? capacity : -capacity);
+ }
+
+ protected void releaseExcessMemory(boolean direct, Consumer<Boolean> clearFn)
+ {
+ long maxMemory = direct ? _maxDirectMemory : _maxHeapMemory;
+ if (maxMemory > 0)
+ {
+ while (getMemory(direct) > maxMemory)
+ {
+ clearFn.accept(direct);
+ }
+ }
+ }
+
+ @ManagedAttribute("The bytes retained by direct ByteBuffers")
+ public long getDirectMemory()
+ {
+ return getMemory(true);
+ }
+
+ @ManagedAttribute("The bytes retained by heap ByteBuffers")
+ public long getHeapMemory()
+ {
+ return getMemory(false);
+ }
+
+ public long getMemory(boolean direct)
+ {
+ AtomicLong memory = direct ? _directMemory : _heapMemory;
+ return memory.get();
+ }
+
+ @ManagedOperation(value = "Clears this ByteBufferPool", impact = "ACTION")
+ public void clear()
+ {
+ _heapMemory.set(0);
+ _directMemory.set(0);
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java
new file mode 100644
index 0000000..4adac79
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java
@@ -0,0 +1,326 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Invocable;
+
+/**
+ * <p>A convenience base implementation of {@link Connection}.</p>
+ * <p>This class uses the capabilities of the {@link EndPoint} API to provide a
+ * more traditional style of async reading. A call to {@link #fillInterested()}
+ * will schedule a callback to {@link #onFillable()} or {@link #onFillInterestedFailed(Throwable)}
+ * as appropriate.</p>
+ */
+public abstract class AbstractConnection implements Connection
+{
+ private static final Logger LOG = Log.getLogger(AbstractConnection.class);
+
+ private final List<Listener> _listeners = new CopyOnWriteArrayList<>();
+ private final long _created = System.currentTimeMillis();
+ private final EndPoint _endPoint;
+ private final Executor _executor;
+ private final Callback _readCallback;
+ private int _inputBufferSize = 2048;
+
+ protected AbstractConnection(EndPoint endp, Executor executor)
+ {
+ if (executor == null)
+ throw new IllegalArgumentException("Executor must not be null!");
+ _endPoint = endp;
+ _executor = executor;
+ _readCallback = new ReadCallback();
+ }
+
+ @Override
+ public void addListener(Listener listener)
+ {
+ _listeners.add(listener);
+ }
+
+ @Override
+ public void removeListener(Listener listener)
+ {
+ _listeners.remove(listener);
+ }
+
+ public int getInputBufferSize()
+ {
+ return _inputBufferSize;
+ }
+
+ public void setInputBufferSize(int inputBufferSize)
+ {
+ _inputBufferSize = inputBufferSize;
+ }
+
+ protected Executor getExecutor()
+ {
+ return _executor;
+ }
+
+ protected void failedCallback(final Callback callback, final Throwable x)
+ {
+ Runnable failCallback = () ->
+ {
+ try
+ {
+ callback.failed(x);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ };
+
+ switch (Invocable.getInvocationType(callback))
+ {
+ case BLOCKING:
+ try
+ {
+ getExecutor().execute(failCallback);
+ }
+ catch (RejectedExecutionException e)
+ {
+ LOG.debug(e);
+ callback.failed(x);
+ }
+ break;
+
+ case NON_BLOCKING:
+ failCallback.run();
+ break;
+
+ case EITHER:
+ Invocable.invokeNonBlocking(failCallback);
+ }
+ }
+
+ /**
+ * <p>Utility method to be called to register read interest.</p>
+ * <p>After a call to this method, {@link #onFillable()} or {@link #onFillInterestedFailed(Throwable)}
+ * will be called back as appropriate.</p>
+ *
+ * @see #onFillable()
+ */
+ public void fillInterested()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("fillInterested {}", this);
+ getEndPoint().fillInterested(_readCallback);
+ }
+
+ public void tryFillInterested()
+ {
+ tryFillInterested(_readCallback);
+ }
+
+ public void tryFillInterested(Callback callback)
+ {
+ getEndPoint().tryFillInterested(callback);
+ }
+
+ public boolean isFillInterested()
+ {
+ return getEndPoint().isFillInterested();
+ }
+
+ /**
+ * <p>Callback method invoked when the endpoint is ready to be read.</p>
+ *
+ * @see #fillInterested()
+ */
+ public abstract void onFillable();
+
+ /**
+ * <p>Callback method invoked when the endpoint failed to be ready to be read.</p>
+ *
+ * @param cause the exception that caused the failure
+ */
+ protected void onFillInterestedFailed(Throwable cause)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} onFillInterestedFailed {}", this, cause);
+ if (_endPoint.isOpen())
+ {
+ boolean close = true;
+ if (cause instanceof TimeoutException)
+ close = onReadTimeout(cause);
+ if (close)
+ {
+ if (_endPoint.isOutputShutdown())
+ _endPoint.close();
+ else
+ {
+ _endPoint.shutdownOutput();
+ fillInterested();
+ }
+ }
+ }
+ }
+
+ /**
+ * <p>Callback method invoked when the endpoint failed to be ready to be read after a timeout</p>
+ *
+ * @param timeout the cause of the read timeout
+ * @return true to signal that the endpoint must be closed, false to keep the endpoint open
+ */
+ protected boolean onReadTimeout(Throwable timeout)
+ {
+ return true;
+ }
+
+ @Override
+ public void onOpen()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onOpen {}", this);
+
+ for (Listener listener : _listeners)
+ {
+ onOpened(listener);
+ }
+ }
+
+ private void onOpened(Listener listener)
+ {
+ try
+ {
+ listener.onOpened(this);
+ }
+ catch (Throwable x)
+ {
+ LOG.info("Failure while notifying listener " + listener, x);
+ }
+ }
+
+ @Override
+ public void onClose()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onClose {}", this);
+
+ for (Listener listener : _listeners)
+ {
+ onClosed(listener);
+ }
+ }
+
+ private void onClosed(Listener listener)
+ {
+ try
+ {
+ listener.onClosed(this);
+ }
+ catch (Throwable x)
+ {
+ LOG.info("Failure while notifying listener " + listener, x);
+ }
+ }
+
+ @Override
+ public EndPoint getEndPoint()
+ {
+ return _endPoint;
+ }
+
+ @Override
+ public void close()
+ {
+ getEndPoint().close();
+ }
+
+ @Override
+ public boolean onIdleExpired()
+ {
+ return true;
+ }
+
+ @Override
+ public long getMessagesIn()
+ {
+ return -1;
+ }
+
+ @Override
+ public long getMessagesOut()
+ {
+ return -1;
+ }
+
+ @Override
+ public long getBytesIn()
+ {
+ return -1;
+ }
+
+ @Override
+ public long getBytesOut()
+ {
+ return -1;
+ }
+
+ @Override
+ public long getCreatedTimeStamp()
+ {
+ return _created;
+ }
+
+ @Override
+ public final String toString()
+ {
+ return String.format("%s@%h::%s", getClass().getSimpleName(), this, getEndPoint());
+ }
+
+ public String toConnectionString()
+ {
+ return String.format("%s@%h",
+ getClass().getSimpleName(),
+ this);
+ }
+
+ private class ReadCallback implements Callback
+ {
+ @Override
+ public void succeeded()
+ {
+ onFillable();
+ }
+
+ @Override
+ public void failed(final Throwable x)
+ {
+ onFillInterestedFailed(x);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("AC.ReadCB@%h{%s}", AbstractConnection.this, AbstractConnection.this);
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java
new file mode 100644
index 0000000..99237f8
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java
@@ -0,0 +1,489 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+public abstract class AbstractEndPoint extends IdleTimeout implements EndPoint
+{
+ private static final Logger LOG = Log.getLogger(AbstractEndPoint.class);
+
+ private final AtomicReference<State> _state = new AtomicReference<>(State.OPEN);
+ private final long _created = System.currentTimeMillis();
+ private volatile Connection _connection;
+
+ private final FillInterest _fillInterest = new FillInterest()
+ {
+ @Override
+ protected void needsFillInterest() throws IOException
+ {
+ AbstractEndPoint.this.needsFillInterest();
+ }
+ };
+
+ private final WriteFlusher _writeFlusher = new WriteFlusher(this)
+ {
+ @Override
+ protected void onIncompleteFlush()
+ {
+ AbstractEndPoint.this.onIncompleteFlush();
+ }
+ };
+
+ protected AbstractEndPoint(Scheduler scheduler)
+ {
+ super(scheduler);
+ }
+
+ protected final void shutdownInput()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("shutdownInput {}", this);
+ while (true)
+ {
+ State s = _state.get();
+ switch (s)
+ {
+ case OPEN:
+ if (!_state.compareAndSet(s, State.ISHUTTING))
+ continue;
+ try
+ {
+ doShutdownInput();
+ }
+ finally
+ {
+ if (!_state.compareAndSet(State.ISHUTTING, State.ISHUT))
+ {
+ // If somebody else switched to CLOSED while we were ishutting,
+ // then we do the close for them
+ if (_state.get() == State.CLOSED)
+ doOnClose(null);
+ else
+ throw new IllegalStateException();
+ }
+ }
+ return;
+
+ case ISHUTTING: // Somebody else ishutting
+ case ISHUT: // Already ishut
+ return;
+
+ case OSHUTTING:
+ if (!_state.compareAndSet(s, State.CLOSED))
+ continue;
+ // The thread doing the OSHUT will close
+ return;
+
+ case OSHUT:
+ if (!_state.compareAndSet(s, State.CLOSED))
+ continue;
+ // Already OSHUT so we close
+ doOnClose(null);
+ return;
+
+ case CLOSED: // already closed
+ return;
+ }
+ }
+ }
+
+ @Override
+ public final void shutdownOutput()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("shutdownOutput {}", this);
+ while (true)
+ {
+ State s = _state.get();
+ switch (s)
+ {
+ case OPEN:
+ if (!_state.compareAndSet(s, State.OSHUTTING))
+ continue;
+ try
+ {
+ doShutdownOutput();
+ }
+ finally
+ {
+ if (!_state.compareAndSet(State.OSHUTTING, State.OSHUT))
+ {
+ // If somebody else switched to CLOSED while we were oshutting,
+ // then we do the close for them
+ if (_state.get() == State.CLOSED)
+ doOnClose(null);
+ else
+ throw new IllegalStateException();
+ }
+ }
+ return;
+
+ case ISHUTTING:
+ if (!_state.compareAndSet(s, State.CLOSED))
+ continue;
+ // The thread doing the ISHUT will close
+ return;
+
+ case ISHUT:
+ if (!_state.compareAndSet(s, State.CLOSED))
+ continue;
+ // Already ISHUT so we close
+ doOnClose(null);
+ return;
+
+ case OSHUTTING: // Somebody else oshutting
+ case OSHUT: // Already oshut
+ return;
+
+ case CLOSED: // already closed
+ return;
+ }
+ }
+ }
+
+ @Override
+ public final void close()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("close {}", this);
+ close(null);
+ }
+
+ protected final void close(Throwable failure)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("close({}) {}", failure, this);
+ while (true)
+ {
+ State s = _state.get();
+ switch (s)
+ {
+ case OPEN:
+ case ISHUT: // Already ishut
+ case OSHUT: // Already oshut
+ if (!_state.compareAndSet(s, State.CLOSED))
+ continue;
+ doOnClose(failure);
+ return;
+
+ case ISHUTTING: // Somebody else ishutting
+ case OSHUTTING: // Somebody else oshutting
+ if (!_state.compareAndSet(s, State.CLOSED))
+ continue;
+ // The thread doing the IO SHUT will call doOnClose
+ return;
+
+ case CLOSED: // already closed
+ return;
+ }
+ }
+ }
+
+ protected void doShutdownInput()
+ {
+ }
+
+ protected void doShutdownOutput()
+ {
+ }
+
+ private void doOnClose(Throwable failure)
+ {
+ try
+ {
+ doClose();
+ }
+ finally
+ {
+ if (failure == null)
+ onClose();
+ else
+ onClose(failure);
+ }
+ }
+
+ protected void doClose()
+ {
+ }
+
+ protected void onClose(Throwable failure)
+ {
+ super.onClose();
+ _writeFlusher.onFail(failure);
+ _fillInterest.onFail(failure);
+ }
+
+ @Override
+ public boolean isOutputShutdown()
+ {
+ switch (_state.get())
+ {
+ case CLOSED:
+ case OSHUT:
+ case OSHUTTING:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean isInputShutdown()
+ {
+ switch (_state.get())
+ {
+ case CLOSED:
+ case ISHUT:
+ case ISHUTTING:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ switch (_state.get())
+ {
+ case CLOSED:
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ public void checkFlush() throws IOException
+ {
+ State s = _state.get();
+ switch (s)
+ {
+ case OSHUT:
+ case OSHUTTING:
+ case CLOSED:
+ throw new IOException(s.toString());
+ default:
+ break;
+ }
+ }
+
+ public void checkFill() throws IOException
+ {
+ State s = _state.get();
+ switch (s)
+ {
+ case ISHUT:
+ case ISHUTTING:
+ case CLOSED:
+ throw new IOException(s.toString());
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public long getCreatedTimeStamp()
+ {
+ return _created;
+ }
+
+ @Override
+ public Connection getConnection()
+ {
+ return _connection;
+ }
+
+ @Override
+ public void setConnection(Connection connection)
+ {
+ _connection = connection;
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return false;
+ }
+
+ protected void reset()
+ {
+ _state.set(State.OPEN);
+ _writeFlusher.onClose();
+ _fillInterest.onClose();
+ }
+
+ @Override
+ public void onOpen()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onOpen {}", this);
+ if (_state.get() != State.OPEN)
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public void onClose()
+ {
+ super.onClose();
+ _writeFlusher.onClose();
+ _fillInterest.onClose();
+ }
+
+ @Override
+ public void fillInterested(Callback callback)
+ {
+ notIdle();
+ _fillInterest.register(callback);
+ }
+
+ @Override
+ public boolean tryFillInterested(Callback callback)
+ {
+ notIdle();
+ return _fillInterest.tryRegister(callback);
+ }
+
+ @Override
+ public boolean isFillInterested()
+ {
+ return _fillInterest.isInterested();
+ }
+
+ @Override
+ public void write(Callback callback, ByteBuffer... buffers) throws IllegalStateException
+ {
+ _writeFlusher.write(callback, buffers);
+ }
+
+ protected abstract void onIncompleteFlush();
+
+ protected abstract void needsFillInterest() throws IOException;
+
+ public FillInterest getFillInterest()
+ {
+ return _fillInterest;
+ }
+
+ public WriteFlusher getWriteFlusher()
+ {
+ return _writeFlusher;
+ }
+
+ @Override
+ protected void onIdleExpired(TimeoutException timeout)
+ {
+ Connection connection = _connection;
+ if (connection != null && !connection.onIdleExpired())
+ return;
+
+ boolean outputShutdown = isOutputShutdown();
+ boolean inputShutdown = isInputShutdown();
+ boolean fillFailed = _fillInterest.onFail(timeout);
+ boolean writeFailed = _writeFlusher.onFail(timeout);
+
+ // If the endpoint is half closed and there was no fill/write handling, then close here.
+ // This handles the situation where the connection has completed its close handling
+ // and the endpoint is half closed, but the other party does not complete the close.
+ // This perhaps should not check for half closed, however the servlet spec case allows
+ // for a dispatched servlet or suspended request to extend beyond the connections idle
+ // time. So if this test would always close an idle endpoint that is not handled, then
+ // we would need a mode to ignore timeouts for some HTTP states
+ if (isOpen() && (outputShutdown || inputShutdown) && !(fillFailed || writeFailed))
+ close();
+ else
+ LOG.debug("Ignored idle endpoint {}", this);
+ }
+
+ @Override
+ public void upgrade(Connection newConnection)
+ {
+ Connection oldConnection = getConnection();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} upgrading from {} to {}", this, oldConnection, newConnection);
+
+ ByteBuffer buffer = (oldConnection instanceof Connection.UpgradeFrom)
+ ? ((Connection.UpgradeFrom)oldConnection).onUpgradeFrom()
+ : null;
+ oldConnection.onClose();
+ oldConnection.getEndPoint().setConnection(newConnection);
+
+ if (BufferUtil.hasContent(buffer))
+ {
+ if (newConnection instanceof Connection.UpgradeTo)
+ ((Connection.UpgradeTo)newConnection).onUpgradeTo(buffer);
+ else
+ throw new IllegalStateException("Cannot upgrade: " + newConnection + " does not implement " + Connection.UpgradeTo.class.getName());
+ }
+ newConnection.onOpen();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s->%s", toEndPointString(), toConnectionString());
+ }
+
+ public String toEndPointString()
+ {
+ Class<?> c = getClass();
+ String name = c.getSimpleName();
+ while (name.length() == 0 && c.getSuperclass() != null)
+ {
+ c = c.getSuperclass();
+ name = c.getSimpleName();
+ }
+
+ return String.format("%s@%h{l=%s,r=%s,%s,fill=%s,flush=%s,to=%d/%d}",
+ name,
+ this,
+ getLocalAddress(),
+ getRemoteAddress(),
+ _state.get(),
+ _fillInterest.toStateString(),
+ _writeFlusher.toStateString(),
+ getIdleFor(),
+ getIdleTimeout());
+ }
+
+ public String toConnectionString()
+ {
+ Connection connection = getConnection();
+ if (connection == null) // can happen during upgrade
+ return "<null>";
+ if (connection instanceof AbstractConnection)
+ return ((AbstractConnection)connection).toConnectionString();
+ return String.format("%s@%x", connection.getClass().getSimpleName(), connection.hashCode());
+ }
+
+ private enum State
+ {
+ OPEN, ISHUTTING, ISHUT, OSHUTTING, OSHUT, CLOSED
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java
new file mode 100644
index 0000000..66d036e
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java
@@ -0,0 +1,241 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.function.IntFunction;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * <p>A ByteBuffer pool where ByteBuffers are held in queues that are held in array elements.</p>
+ * <p>Given a capacity {@code factor} of 1024, the first array element holds a queue of ByteBuffers
+ * each of capacity 1024, the second array element holds a queue of ByteBuffers each of capacity
+ * 2048, and so on.</p>
+ */
+@ManagedObject
+public class ArrayByteBufferPool extends AbstractByteBufferPool
+{
+ private static final Logger LOG = Log.getLogger(MappedByteBufferPool.class);
+
+ private final int _minCapacity;
+ private final ByteBufferPool.Bucket[] _direct;
+ private final ByteBufferPool.Bucket[] _indirect;
+
+ /**
+ * Creates a new ArrayByteBufferPool with a default configuration.
+ */
+ public ArrayByteBufferPool()
+ {
+ this(-1, -1, -1);
+ }
+
+ /**
+ * Creates a new ArrayByteBufferPool with the given configuration.
+ *
+ * @param minCapacity the minimum ByteBuffer capacity
+ * @param factor the capacity factor
+ * @param maxCapacity the maximum ByteBuffer capacity
+ */
+ public ArrayByteBufferPool(int minCapacity, int factor, int maxCapacity)
+ {
+ this(minCapacity, factor, maxCapacity, -1, -1, -1);
+ }
+
+ /**
+ * Creates a new ArrayByteBufferPool with the given configuration.
+ *
+ * @param minCapacity the minimum ByteBuffer capacity
+ * @param factor the capacity factor
+ * @param maxCapacity the maximum ByteBuffer capacity
+ * @param maxQueueLength the maximum ByteBuffer queue length
+ */
+ public ArrayByteBufferPool(int minCapacity, int factor, int maxCapacity, int maxQueueLength)
+ {
+ this(minCapacity, factor, maxCapacity, maxQueueLength, -1, -1);
+ }
+
+ /**
+ * Creates a new ArrayByteBufferPool with the given configuration.
+ *
+ * @param minCapacity the minimum ByteBuffer capacity
+ * @param factor the capacity factor
+ * @param maxCapacity the maximum ByteBuffer capacity
+ * @param maxQueueLength the maximum ByteBuffer queue length
+ * @param maxHeapMemory the max heap memory in bytes
+ * @param maxDirectMemory the max direct memory in bytes
+ */
+ public ArrayByteBufferPool(int minCapacity, int factor, int maxCapacity, int maxQueueLength, long maxHeapMemory, long maxDirectMemory)
+ {
+ super(factor, maxQueueLength, maxHeapMemory, maxDirectMemory);
+
+ factor = getCapacityFactor();
+ if (minCapacity <= 0)
+ minCapacity = 0;
+ if (maxCapacity <= 0)
+ maxCapacity = 64 * 1024;
+ if ((maxCapacity % factor) != 0 || factor >= maxCapacity)
+ throw new IllegalArgumentException("The capacity factor must be a divisor of maxCapacity");
+ _minCapacity = minCapacity;
+
+ int length = maxCapacity / factor;
+ _direct = new ByteBufferPool.Bucket[length];
+ _indirect = new ByteBufferPool.Bucket[length];
+ }
+
+ @Override
+ public ByteBuffer acquire(int size, boolean direct)
+ {
+ int capacity = size < _minCapacity ? size : (bucketFor(size) + 1) * getCapacityFactor();
+ ByteBufferPool.Bucket bucket = bucketFor(size, direct, null);
+ if (bucket == null)
+ return newByteBuffer(capacity, direct);
+ ByteBuffer buffer = bucket.acquire();
+ if (buffer == null)
+ return newByteBuffer(capacity, direct);
+ decrementMemory(buffer);
+ return buffer;
+ }
+
+ @Override
+ public void release(ByteBuffer buffer)
+ {
+ if (buffer == null)
+ return;
+
+ int capacity = buffer.capacity();
+ // Validate that this buffer is from this pool.
+ if ((capacity % getCapacityFactor()) != 0)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("ByteBuffer {} does not belong to this pool, discarding it", BufferUtil.toDetailString(buffer));
+ return;
+ }
+
+ boolean direct = buffer.isDirect();
+ ByteBufferPool.Bucket bucket = bucketFor(capacity, direct, this::newBucket);
+ if (bucket != null)
+ {
+ bucket.release(buffer);
+ incrementMemory(buffer);
+ releaseExcessMemory(direct, this::clearOldestBucket);
+ }
+ }
+
+ private Bucket newBucket(int key)
+ {
+ return new Bucket(this, key * getCapacityFactor(), getMaxQueueLength());
+ }
+
+ @Override
+ public void clear()
+ {
+ super.clear();
+ for (int i = 0; i < _direct.length; ++i)
+ {
+ Bucket bucket = _direct[i];
+ if (bucket != null)
+ bucket.clear();
+ _direct[i] = null;
+ bucket = _indirect[i];
+ if (bucket != null)
+ bucket.clear();
+ _indirect[i] = null;
+ }
+ }
+
+ private void clearOldestBucket(boolean direct)
+ {
+ long oldest = Long.MAX_VALUE;
+ int index = -1;
+ Bucket[] buckets = bucketsFor(direct);
+ for (int i = 0; i < buckets.length; ++i)
+ {
+ Bucket bucket = buckets[i];
+ if (bucket == null)
+ continue;
+ long lastUpdate = bucket.getLastUpdate();
+ if (lastUpdate < oldest)
+ {
+ oldest = lastUpdate;
+ index = i;
+ }
+ }
+ if (index >= 0)
+ {
+ Bucket bucket = buckets[index];
+ buckets[index] = null;
+ // The same bucket may be concurrently
+ // removed, so we need this null guard.
+ if (bucket != null)
+ bucket.clear(this::decrementMemory);
+ }
+ }
+
+ private int bucketFor(int capacity)
+ {
+ return (capacity - 1) / getCapacityFactor();
+ }
+
+ private ByteBufferPool.Bucket bucketFor(int capacity, boolean direct, IntFunction<Bucket> newBucket)
+ {
+ if (capacity < _minCapacity)
+ return null;
+ int b = bucketFor(capacity);
+ if (b >= _direct.length)
+ return null;
+ Bucket[] buckets = bucketsFor(direct);
+ Bucket bucket = buckets[b];
+ if (bucket == null && newBucket != null)
+ buckets[b] = bucket = newBucket.apply(b + 1);
+ return bucket;
+ }
+
+ @ManagedAttribute("The number of pooled direct ByteBuffers")
+ public long getDirectByteBufferCount()
+ {
+ return getByteBufferCount(true);
+ }
+
+ @ManagedAttribute("The number of pooled heap ByteBuffers")
+ public long getHeapByteBufferCount()
+ {
+ return getByteBufferCount(false);
+ }
+
+ private long getByteBufferCount(boolean direct)
+ {
+ return Arrays.stream(bucketsFor(direct))
+ .filter(Objects::nonNull)
+ .mapToLong(Bucket::size)
+ .sum();
+ }
+
+ // Package local for testing
+ ByteBufferPool.Bucket[] bucketsFor(boolean direct)
+ {
+ return direct ? _direct : _indirect;
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java
new file mode 100644
index 0000000..0e7fb3c
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java
@@ -0,0 +1,539 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayDeque;
+import java.util.Queue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Locker;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * ByteArrayEndPoint.
+ */
+public class ByteArrayEndPoint extends AbstractEndPoint
+{
+ static final Logger LOG = Log.getLogger(ByteArrayEndPoint.class);
+ static final InetAddress NOIP;
+ static final InetSocketAddress NOIPPORT;
+ private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 1024;
+
+ static
+ {
+ InetAddress noip = null;
+ try
+ {
+ noip = Inet4Address.getByName("0.0.0.0");
+ }
+ catch (UnknownHostException e)
+ {
+ LOG.warn(e);
+ }
+ finally
+ {
+ NOIP = noip;
+ NOIPPORT = new InetSocketAddress(NOIP, 0);
+ }
+ }
+
+ private static final ByteBuffer EOF = BufferUtil.allocate(0);
+
+ private final Runnable _runFillable = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ getFillInterest().fillable();
+ }
+ };
+
+ private final Locker _locker = new Locker();
+ private final Condition _hasOutput = _locker.newCondition();
+ private final Queue<ByteBuffer> _inQ = new ArrayDeque<>();
+ private final int _outputSize;
+ private ByteBuffer _out;
+ private boolean _growOutput;
+
+ /**
+ *
+ */
+ public ByteArrayEndPoint()
+ {
+ this(null, 0, null, null);
+ }
+
+ /**
+ * @param input the input bytes
+ * @param outputSize the output size
+ */
+ public ByteArrayEndPoint(byte[] input, int outputSize)
+ {
+ this(null, 0, input != null ? BufferUtil.toBuffer(input) : null, BufferUtil.allocate(outputSize));
+ }
+
+ /**
+ * @param input the input string (converted to bytes using default encoding charset)
+ * @param outputSize the output size
+ */
+ public ByteArrayEndPoint(String input, int outputSize)
+ {
+ this(null, 0, input != null ? BufferUtil.toBuffer(input) : null, BufferUtil.allocate(outputSize));
+ }
+
+ public ByteArrayEndPoint(Scheduler scheduler, long idleTimeoutMs)
+ {
+ this(scheduler, idleTimeoutMs, null, null);
+ }
+
+ public ByteArrayEndPoint(Scheduler timer, long idleTimeoutMs, byte[] input, int outputSize)
+ {
+ this(timer, idleTimeoutMs, input != null ? BufferUtil.toBuffer(input) : null, BufferUtil.allocate(outputSize));
+ }
+
+ public ByteArrayEndPoint(Scheduler timer, long idleTimeoutMs, String input, int outputSize)
+ {
+ this(timer, idleTimeoutMs, input != null ? BufferUtil.toBuffer(input) : null, BufferUtil.allocate(outputSize));
+ }
+
+ public ByteArrayEndPoint(Scheduler timer, long idleTimeoutMs, ByteBuffer input, ByteBuffer output)
+ {
+ super(timer);
+ if (BufferUtil.hasContent(input))
+ addInput(input);
+ _outputSize = (output == null) ? 1024 : output.capacity();
+ _out = output == null ? BufferUtil.allocate(_outputSize) : output;
+ setIdleTimeout(idleTimeoutMs);
+ onOpen();
+ }
+
+ @Override
+ public void doShutdownOutput()
+ {
+ super.doShutdownOutput();
+ try (Locker.Lock lock = _locker.lock())
+ {
+ _hasOutput.signalAll();
+ }
+ }
+
+ @Override
+ public void doClose()
+ {
+ super.doClose();
+ try (Locker.Lock lock = _locker.lock())
+ {
+ _hasOutput.signalAll();
+ }
+ }
+
+ @Override
+ public InetSocketAddress getLocalAddress()
+ {
+ return NOIPPORT;
+ }
+
+ @Override
+ public InetSocketAddress getRemoteAddress()
+ {
+ return NOIPPORT;
+ }
+
+ @Override
+ protected void onIncompleteFlush()
+ {
+ // Don't need to do anything here as takeOutput does the signalling.
+ }
+
+ protected void execute(Runnable task)
+ {
+ new Thread(task, "BAEPoint-" + Integer.toHexString(hashCode())).start();
+ }
+
+ @Override
+ protected void needsFillInterest() throws IOException
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ if (!isOpen())
+ throw new ClosedChannelException();
+
+ ByteBuffer in = _inQ.peek();
+ if (BufferUtil.hasContent(in) || isEOF(in))
+ execute(_runFillable);
+ }
+ }
+
+ /**
+ *
+ */
+ public void addInputEOF()
+ {
+ addInput((ByteBuffer)null);
+ }
+
+ /**
+ * @param in The in to set.
+ */
+ public void addInput(ByteBuffer in)
+ {
+ boolean fillable = false;
+ try (Locker.Lock lock = _locker.lock())
+ {
+ if (isEOF(_inQ.peek()))
+ throw new RuntimeIOException(new EOFException());
+ boolean wasEmpty = _inQ.isEmpty();
+ if (in == null)
+ {
+ _inQ.add(EOF);
+ fillable = true;
+ }
+ if (BufferUtil.hasContent(in))
+ {
+ _inQ.add(in);
+ fillable = wasEmpty;
+ }
+ }
+ if (fillable)
+ _runFillable.run();
+ }
+
+ public void addInputAndExecute(ByteBuffer in)
+ {
+ boolean fillable = false;
+ try (Locker.Lock lock = _locker.lock())
+ {
+ if (isEOF(_inQ.peek()))
+ throw new RuntimeIOException(new EOFException());
+ boolean wasEmpty = _inQ.isEmpty();
+ if (in == null)
+ {
+ _inQ.add(EOF);
+ fillable = true;
+ }
+ if (BufferUtil.hasContent(in))
+ {
+ _inQ.add(in);
+ fillable = wasEmpty;
+ }
+ }
+ if (fillable)
+ execute(_runFillable);
+ }
+
+ public void addInput(String s)
+ {
+ addInput(BufferUtil.toBuffer(s, StandardCharsets.UTF_8));
+ }
+
+ public void addInput(String s, Charset charset)
+ {
+ addInput(BufferUtil.toBuffer(s, charset));
+ }
+
+ /**
+ * @return Returns the out.
+ */
+ public ByteBuffer getOutput()
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ return _out;
+ }
+ }
+
+ /**
+ * @return Returns the out.
+ */
+ public String getOutputString()
+ {
+ return getOutputString(StandardCharsets.UTF_8);
+ }
+
+ /**
+ * @param charset the charset to encode the output as
+ * @return Returns the out.
+ */
+ public String getOutputString(Charset charset)
+ {
+ return BufferUtil.toString(_out, charset);
+ }
+
+ /**
+ * @return Returns the out.
+ */
+ public ByteBuffer takeOutput()
+ {
+ ByteBuffer b;
+
+ try (Locker.Lock lock = _locker.lock())
+ {
+ b = _out;
+ _out = BufferUtil.allocate(_outputSize);
+ }
+ getWriteFlusher().completeWrite();
+ return b;
+ }
+
+ /**
+ * Wait for some output
+ *
+ * @param time Time to wait
+ * @param unit Units for time to wait
+ * @return The buffer of output
+ * @throws InterruptedException if interrupted
+ */
+ public ByteBuffer waitForOutput(long time, TimeUnit unit) throws InterruptedException
+ {
+ ByteBuffer b;
+
+ try (Locker.Lock lock = _locker.lock())
+ {
+ while (BufferUtil.isEmpty(_out) && !isOutputShutdown())
+ {
+ if (!_hasOutput.await(time, unit))
+ return null;
+ }
+ b = _out;
+ _out = BufferUtil.allocate(_outputSize);
+ }
+ getWriteFlusher().completeWrite();
+ return b;
+ }
+
+ /**
+ * @return Returns the out.
+ */
+ public String takeOutputString()
+ {
+ return takeOutputString(StandardCharsets.UTF_8);
+ }
+
+ /**
+ * @param charset the charset to encode the output as
+ * @return Returns the out.
+ */
+ public String takeOutputString(Charset charset)
+ {
+ ByteBuffer buffer = takeOutput();
+ return BufferUtil.toString(buffer, charset);
+ }
+
+ /**
+ * @param out The out to set.
+ */
+ public void setOutput(ByteBuffer out)
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ _out = out;
+ }
+ getWriteFlusher().completeWrite();
+ }
+
+ /**
+ * @return <code>true</code> if there are bytes remaining to be read from the encoded input
+ */
+ public boolean hasMore()
+ {
+ return getOutput().position() > 0;
+ }
+
+ /*
+ * @see org.eclipse.io.EndPoint#fill(org.eclipse.io.Buffer)
+ */
+ @Override
+ public int fill(ByteBuffer buffer) throws IOException
+ {
+ int filled = 0;
+ try (Locker.Lock lock = _locker.lock())
+ {
+ while (true)
+ {
+ if (!isOpen())
+ throw new EofException("CLOSED");
+
+ if (isInputShutdown())
+ return -1;
+
+ if (_inQ.isEmpty())
+ break;
+
+ ByteBuffer in = _inQ.peek();
+ if (isEOF(in))
+ {
+ filled = -1;
+ break;
+ }
+
+ if (BufferUtil.hasContent(in))
+ {
+ filled = BufferUtil.append(buffer, in);
+ if (BufferUtil.isEmpty(in))
+ _inQ.poll();
+ break;
+ }
+ _inQ.poll();
+ }
+ }
+
+ if (filled > 0)
+ notIdle();
+ else if (filled < 0)
+ shutdownInput();
+ return filled;
+ }
+
+ /*
+ * @see org.eclipse.io.EndPoint#flush(org.eclipse.io.Buffer, org.eclipse.io.Buffer, org.eclipse.io.Buffer)
+ */
+ @Override
+ public boolean flush(ByteBuffer... buffers) throws IOException
+ {
+ boolean flushed = true;
+ try (Locker.Lock lock = _locker.lock())
+ {
+ if (!isOpen())
+ throw new IOException("CLOSED");
+ if (isOutputShutdown())
+ throw new IOException("OSHUT");
+
+ boolean idle = true;
+
+ for (ByteBuffer b : buffers)
+ {
+ if (BufferUtil.hasContent(b))
+ {
+ if (_growOutput && b.remaining() > BufferUtil.space(_out))
+ {
+ BufferUtil.compact(_out);
+ if (b.remaining() > BufferUtil.space(_out))
+ {
+ // Don't grow larger than MAX_BUFFER_SIZE to avoid memory issues.
+ if (_out.capacity() < MAX_BUFFER_SIZE)
+ {
+ long newBufferCapacity = Math.min((long)(_out.capacity() + b.remaining() * 1.5), MAX_BUFFER_SIZE);
+ ByteBuffer n = BufferUtil.allocate(Math.toIntExact(newBufferCapacity));
+ BufferUtil.append(n, _out);
+ _out = n;
+ }
+ }
+ }
+
+ if (BufferUtil.append(_out, b) > 0)
+ idle = false;
+
+ if (BufferUtil.hasContent(b))
+ {
+ flushed = false;
+ break;
+ }
+ }
+ }
+ if (!idle)
+ {
+ notIdle();
+ _hasOutput.signalAll();
+ }
+ }
+ return flushed;
+ }
+
+ /**
+ *
+ */
+ @Override
+ public void reset()
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ _inQ.clear();
+ _hasOutput.signalAll();
+ BufferUtil.clear(_out);
+ }
+ super.reset();
+ }
+
+ /*
+ * @see org.eclipse.io.EndPoint#getConnection()
+ */
+ @Override
+ public Object getTransport()
+ {
+ return null;
+ }
+
+ /**
+ * @return the growOutput
+ */
+ public boolean isGrowOutput()
+ {
+ return _growOutput;
+ }
+
+ /**
+ * @param growOutput the growOutput to set
+ */
+ public void setGrowOutput(boolean growOutput)
+ {
+ _growOutput = growOutput;
+ }
+
+ @Override
+ public String toString()
+ {
+ int q;
+ ByteBuffer b;
+ String o;
+ try (Locker.Lock lock = _locker.lock())
+ {
+ q = _inQ.size();
+ b = _inQ.peek();
+ o = BufferUtil.toDetailString(_out);
+ }
+ return String.format("%s[q=%d,q[0]=%s,o=%s]", super.toString(), q, b, o);
+ }
+
+ /**
+ * Compares a ByteBuffer Object to EOF by Reference
+ *
+ * @param buffer the input ByteBuffer to be compared to EOF
+ * @return Whether the reference buffer is equal to that of EOF
+ */
+ private static boolean isEOF(ByteBuffer buffer)
+ {
+ @SuppressWarnings("ReferenceEquality")
+ boolean isEof = (buffer == EOF);
+ return isEof;
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferAccumulator.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferAccumulator.java
new file mode 100644
index 0000000..3a4c512
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferAccumulator.java
@@ -0,0 +1,201 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jetty.util.BufferUtil;
+
+/**
+ * Accumulates data into a list of ByteBuffers which can then be combined into a single buffer or written to an OutputStream.
+ * The buffer list automatically grows as data is written to it, the buffers are taken from the
+ * supplied {@link ByteBufferPool} or freshly allocated if one is not supplied.
+ *
+ * The method {@link #ensureBuffer(int, int)} is used to write directly to the last buffer stored in the buffer list,
+ * if there is less than a certain amount of space available in that buffer then a new one will be allocated and returned instead.
+ * @see #ensureBuffer(int, int)
+ */
+public class ByteBufferAccumulator implements AutoCloseable
+{
+ private final List<ByteBuffer> _buffers = new ArrayList<>();
+ private final ByteBufferPool _bufferPool;
+ private final boolean _direct;
+
+ public ByteBufferAccumulator()
+ {
+ this(null, false);
+ }
+
+ public ByteBufferAccumulator(ByteBufferPool bufferPool, boolean direct)
+ {
+ _bufferPool = (bufferPool == null) ? new NullByteBufferPool() : bufferPool;
+ _direct = direct;
+ }
+
+ /**
+ * Get the amount of bytes which have been accumulated.
+ * This will add up the remaining of each buffer in the accumulator.
+ * @return the total length of the content in the accumulator.
+ */
+ public int getLength()
+ {
+ int length = 0;
+ for (ByteBuffer buffer : _buffers)
+ length = Math.addExact(length, buffer.remaining());
+ return length;
+ }
+
+ public ByteBufferPool getByteBufferPool()
+ {
+ return _bufferPool;
+ }
+
+ /**
+ * Get the last buffer of the accumulator, this can be written to directly to avoid copying into the accumulator.
+ * @param minAllocationSize new buffers will be allocated to have at least this size.
+ * @return a buffer with at least {@code minSize} space to write into.
+ */
+ public ByteBuffer ensureBuffer(int minAllocationSize)
+ {
+ return ensureBuffer(1, minAllocationSize);
+ }
+
+ /**
+ * Get the last buffer of the accumulator, this can be written to directly to avoid copying into the accumulator.
+ * @param minSize the smallest amount of remaining space before a new buffer is allocated.
+ * @param minAllocationSize new buffers will be allocated to have at least this size.
+ * @return a buffer with at least {@code minSize} space to write into.
+ */
+ public ByteBuffer ensureBuffer(int minSize, int minAllocationSize)
+ {
+ ByteBuffer buffer = _buffers.isEmpty() ? BufferUtil.EMPTY_BUFFER : _buffers.get(_buffers.size() - 1);
+ if (BufferUtil.space(buffer) < minSize)
+ {
+ buffer = _bufferPool.acquire(minAllocationSize, _direct);
+ _buffers.add(buffer);
+ }
+
+ return buffer;
+ }
+
+ public void copyBytes(byte[] buf, int offset, int length)
+ {
+ copyBuffer(BufferUtil.toBuffer(buf, offset, length));
+ }
+
+ public void copyBuffer(ByteBuffer buffer)
+ {
+ while (buffer.hasRemaining())
+ {
+ ByteBuffer b = ensureBuffer(buffer.remaining());
+ int pos = BufferUtil.flipToFill(b);
+ BufferUtil.put(buffer, b);
+ BufferUtil.flipToFlush(b, pos);
+ }
+ }
+
+ /**
+ * Take the combined buffer containing all content written to the accumulator.
+ * The caller is responsible for releasing this {@link ByteBuffer} back into the {@link ByteBufferPool}.
+ * @return a buffer containing all content written to the accumulator.
+ * @see #toByteBuffer()
+ */
+ public ByteBuffer takeByteBuffer()
+ {
+ ByteBuffer combinedBuffer;
+ if (_buffers.size() == 1)
+ {
+ combinedBuffer = _buffers.get(0);
+ _buffers.clear();
+ return combinedBuffer;
+ }
+
+ int length = getLength();
+ combinedBuffer = _bufferPool.acquire(length, _direct);
+ BufferUtil.clearToFill(combinedBuffer);
+ for (ByteBuffer buffer : _buffers)
+ {
+ combinedBuffer.put(buffer);
+ _bufferPool.release(buffer);
+ }
+ BufferUtil.flipToFlush(combinedBuffer, 0);
+ _buffers.clear();
+ return combinedBuffer;
+ }
+
+ /**
+ * Take the combined buffer containing all content written to the accumulator.
+ * The returned buffer is still contained within the accumulator and will be released back to the {@link ByteBufferPool}
+ * when the accumulator is closed.
+ * @return a buffer containing all content written to the accumulator.
+ * @see #takeByteBuffer()
+ * @see #close()
+ */
+ public ByteBuffer toByteBuffer()
+ {
+ ByteBuffer combinedBuffer = takeByteBuffer();
+ _buffers.add(combinedBuffer);
+ return combinedBuffer;
+ }
+
+ /**
+ * @return a newly allocated byte array containing all content written into the accumulator.
+ */
+ public byte[] toByteArray()
+ {
+ int length = getLength();
+ if (length == 0)
+ return new byte[0];
+
+ byte[] bytes = new byte[length];
+ ByteBuffer buffer = BufferUtil.toBuffer(bytes);
+ BufferUtil.clear(buffer);
+ writeTo(buffer);
+ return bytes;
+ }
+
+ public void writeTo(ByteBuffer buffer)
+ {
+ int pos = BufferUtil.flipToFill(buffer);
+ for (ByteBuffer bb : _buffers)
+ {
+ buffer.put(bb.slice());
+ }
+ BufferUtil.flipToFlush(buffer, pos);
+ }
+
+ public void writeTo(OutputStream out) throws IOException
+ {
+ for (ByteBuffer bb : _buffers)
+ {
+ BufferUtil.writeTo(bb.slice(), out);
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ _buffers.forEach(_bufferPool::release);
+ _buffers.clear();
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java
new file mode 100644
index 0000000..dfc2daa
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java
@@ -0,0 +1,62 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.BufferUtil;
+
+/**
+ * Simple wrapper of a ByteBuffer as an OutputStream.
+ * The buffer does not grow and this class will throw an
+ * {@link java.nio.BufferOverflowException} if the buffer capacity is exceeded.
+ */
+public class ByteBufferOutputStream extends OutputStream
+{
+ final ByteBuffer _buffer;
+
+ public ByteBufferOutputStream(ByteBuffer buffer)
+ {
+ _buffer = buffer;
+ }
+
+ public void close()
+ {
+ }
+
+ public void flush()
+ {
+ }
+
+ public void write(byte[] b)
+ {
+ write(b, 0, b.length);
+ }
+
+ public void write(byte[] b, int off, int len)
+ {
+ BufferUtil.append(_buffer, b, off, len);
+ }
+
+ public void write(int b)
+ {
+ BufferUtil.append(_buffer, (byte)b);
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream2.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream2.java
new file mode 100644
index 0000000..3b579b8
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream2.java
@@ -0,0 +1,128 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements an output stream in which the data is written into a list of ByteBuffer,
+ * the buffer list automatically grows as data is written to it, the buffers are taken from the
+ * supplied {@link ByteBufferPool} or freshly allocated if one is not supplied.
+ *
+ * Designed to mimic {@link java.io.ByteArrayOutputStream} but with better memory usage, and less copying.
+ */
+public class ByteBufferOutputStream2 extends OutputStream
+{
+ private final ByteBufferAccumulator _accumulator;
+ private int _size = 0;
+
+ public ByteBufferOutputStream2()
+ {
+ this(null, false);
+ }
+
+ public ByteBufferOutputStream2(ByteBufferPool bufferPool, boolean direct)
+ {
+ _accumulator = new ByteBufferAccumulator((bufferPool == null) ? new NullByteBufferPool() : bufferPool, direct);
+ }
+
+ public ByteBufferPool getByteBufferPool()
+ {
+ return _accumulator.getByteBufferPool();
+ }
+
+ /**
+ * Take the combined buffer containing all content written to the OutputStream.
+ * The caller is responsible for releasing this {@link ByteBuffer} back into the {@link ByteBufferPool}.
+ * @return a buffer containing all content written to the OutputStream.
+ */
+ public ByteBuffer takeByteBuffer()
+ {
+ return _accumulator.takeByteBuffer();
+ }
+
+ /**
+ * Take the combined buffer containing all content written to the OutputStream.
+ * The returned buffer is still contained within the OutputStream and will be released back to the {@link ByteBufferPool}
+ * when the OutputStream is closed.
+ * @return a buffer containing all content written to the OutputStream.
+ */
+ public ByteBuffer toByteBuffer()
+ {
+ return _accumulator.toByteBuffer();
+ }
+
+ /**
+ * @return a newly allocated byte array containing all content written into the OutputStream.
+ */
+ public byte[] toByteArray()
+ {
+ return _accumulator.toByteArray();
+ }
+
+ public int size()
+ {
+ return _size;
+ }
+
+ @Override
+ public void write(int b)
+ {
+ write(new byte[]{(byte)b}, 0, 1);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len)
+ {
+ _size += len;
+ _accumulator.copyBytes(b, off, len);
+ }
+
+ public void write(ByteBuffer buffer)
+ {
+ _size += buffer.remaining();
+ _accumulator.copyBuffer(buffer);
+ }
+
+ public void writeTo(ByteBuffer buffer)
+ {
+ _accumulator.writeTo(buffer);
+ }
+
+ public void writeTo(OutputStream out) throws IOException
+ {
+ _accumulator.writeTo(out);
+ }
+
+ @Override
+ public void close()
+ {
+ _accumulator.close();
+ _size = 0;
+ }
+
+ @Override
+ public synchronized String toString()
+ {
+ return String.format("%s@%x{size=%d, byteAccumulator=%s}", getClass().getSimpleName(),
+ hashCode(), _size, _accumulator);
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferPool.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferPool.java
new file mode 100644
index 0000000..6250226
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferPool.java
@@ -0,0 +1,264 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+
+import org.eclipse.jetty.util.BufferUtil;
+
+/**
+ * <p>A {@link ByteBuffer} pool.</p>
+ * <p>Acquired buffers may be {@link #release(ByteBuffer) released} but they do not need to;
+ * if they are released, they may be recycled and reused, otherwise they will be garbage
+ * collected as usual.</p>
+ */
+public interface ByteBufferPool
+{
+ /**
+ * <p>Requests a {@link ByteBuffer} of the given size.</p>
+ * <p>The returned buffer may have a bigger capacity than the size being
+ * requested but it will have the limit set to the given size.</p>
+ *
+ * @param size the size of the buffer
+ * @param direct whether the buffer must be direct or not
+ * @return the requested buffer
+ * @see #release(ByteBuffer)
+ */
+ ByteBuffer acquire(int size, boolean direct);
+
+ /**
+ * <p>Returns a {@link ByteBuffer}, usually obtained with {@link #acquire(int, boolean)}
+ * (but not necessarily), making it available for recycling and reuse.</p>
+ *
+ * @param buffer the buffer to return
+ * @see #acquire(int, boolean)
+ */
+ void release(ByteBuffer buffer);
+
+ /**
+ * <p>Removes a {@link ByteBuffer} that was previously obtained with {@link #acquire(int, boolean)}.</p>
+ * <p>The buffer will not be available for further reuse.</p>
+ *
+ * @param buffer the buffer to remove
+ * @see #acquire(int, boolean)
+ * @see #release(ByteBuffer)
+ */
+ default void remove(ByteBuffer buffer)
+ {
+ }
+
+ /**
+ * <p>Creates a new ByteBuffer of the given capacity and the given directness.</p>
+ *
+ * @param capacity the ByteBuffer capacity
+ * @param direct the ByteBuffer directness
+ * @return a newly allocated ByteBuffer
+ */
+ default ByteBuffer newByteBuffer(int capacity, boolean direct)
+ {
+ return direct ? BufferUtil.allocateDirect(capacity) : BufferUtil.allocate(capacity);
+ }
+
+ class Lease
+ {
+ private final ByteBufferPool byteBufferPool;
+ private final List<ByteBuffer> buffers;
+ private final List<Boolean> recycles;
+
+ public Lease(ByteBufferPool byteBufferPool)
+ {
+ this.byteBufferPool = byteBufferPool;
+ this.buffers = new ArrayList<>();
+ this.recycles = new ArrayList<>();
+ }
+
+ public ByteBuffer acquire(int capacity, boolean direct)
+ {
+ ByteBuffer buffer = byteBufferPool.acquire(capacity, direct);
+ BufferUtil.clearToFill(buffer);
+ return buffer;
+ }
+
+ public void append(ByteBuffer buffer, boolean recycle)
+ {
+ buffers.add(buffer);
+ recycles.add(recycle);
+ }
+
+ public void insert(int index, ByteBuffer buffer, boolean recycle)
+ {
+ buffers.add(index, buffer);
+ recycles.add(index, recycle);
+ }
+
+ public List<ByteBuffer> getByteBuffers()
+ {
+ return buffers;
+ }
+
+ public long getTotalLength()
+ {
+ long length = 0;
+ for (ByteBuffer buffer : buffers)
+ {
+ length += buffer.remaining();
+ }
+ return length;
+ }
+
+ public int getSize()
+ {
+ return buffers.size();
+ }
+
+ public void recycle()
+ {
+ for (int i = 0; i < buffers.size(); ++i)
+ {
+ ByteBuffer buffer = buffers.get(i);
+ if (recycles.get(i))
+ release(buffer);
+ }
+ buffers.clear();
+ recycles.clear();
+ }
+
+ public void release(ByteBuffer buffer)
+ {
+ byteBufferPool.release(buffer);
+ }
+ }
+
+ class Bucket
+ {
+ private final Queue<ByteBuffer> _queue = new ConcurrentLinkedQueue<>();
+ private final ByteBufferPool _pool;
+ private final int _capacity;
+ private final int _maxSize;
+ private final AtomicInteger _size;
+ private final AtomicLong _lastUpdate = new AtomicLong(System.nanoTime());
+
+ public Bucket(ByteBufferPool pool, int capacity, int maxSize)
+ {
+ _pool = pool;
+ _capacity = capacity;
+ _maxSize = maxSize;
+ _size = maxSize > 0 ? new AtomicInteger() : null;
+ }
+
+ public ByteBuffer acquire()
+ {
+ ByteBuffer buffer = queuePoll();
+ if (buffer == null)
+ return null;
+ if (_size != null)
+ _size.decrementAndGet();
+ return buffer;
+ }
+
+ /**
+ * @param direct whether to create a direct buffer when none is available
+ * @return a ByteBuffer
+ * @deprecated use {@link #acquire()} instead
+ */
+ @Deprecated
+ public ByteBuffer acquire(boolean direct)
+ {
+ ByteBuffer buffer = queuePoll();
+ if (buffer == null)
+ return _pool.newByteBuffer(_capacity, direct);
+ if (_size != null)
+ _size.decrementAndGet();
+ return buffer;
+ }
+
+ public void release(ByteBuffer buffer)
+ {
+ _lastUpdate.lazySet(System.nanoTime());
+ BufferUtil.clear(buffer);
+ if (_size == null)
+ queueOffer(buffer);
+ else if (_size.incrementAndGet() <= _maxSize)
+ queueOffer(buffer);
+ else
+ _size.decrementAndGet();
+ }
+
+ public void clear()
+ {
+ clear(null);
+ }
+
+ void clear(Consumer<ByteBuffer> memoryFn)
+ {
+ int size = _size == null ? 0 : _size.get() - 1;
+ while (size >= 0)
+ {
+ ByteBuffer buffer = queuePoll();
+ if (buffer == null)
+ break;
+ if (memoryFn != null)
+ memoryFn.accept(buffer);
+ if (_size != null)
+ {
+ _size.decrementAndGet();
+ --size;
+ }
+ }
+ }
+
+ private void queueOffer(ByteBuffer buffer)
+ {
+ _queue.offer(buffer);
+ }
+
+ private ByteBuffer queuePoll()
+ {
+ return _queue.poll();
+ }
+
+ boolean isEmpty()
+ {
+ return _queue.isEmpty();
+ }
+
+ int size()
+ {
+ return _queue.size();
+ }
+
+ long getLastUpdate()
+ {
+ return _lastUpdate.get();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{%d/%d@%d}", getClass().getSimpleName(), hashCode(), size(), _maxSize, _capacity);
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java
new file mode 100644
index 0000000..13c89e3
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java
@@ -0,0 +1,439 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.CancelledKeyException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Invocable;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * Channel End Point.
+ * <p>Holds the channel and socket for an NIO endpoint.
+ */
+public abstract class ChannelEndPoint extends AbstractEndPoint implements ManagedSelector.Selectable
+{
+ private static final Logger LOG = Log.getLogger(ChannelEndPoint.class);
+
+ private final SocketChannel _channel;
+ private final ManagedSelector _selector;
+ private SelectionKey _key;
+ private boolean _updatePending;
+ // The current value for interestOps.
+ private int _currentInterestOps;
+ // The desired value for interestOps.
+ private int _desiredInterestOps;
+
+ private abstract class RunnableTask implements Runnable, Invocable
+ {
+ final String _operation;
+
+ protected RunnableTask(String op)
+ {
+ _operation = op;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("CEP:%s:%s:%s", ChannelEndPoint.this, _operation, getInvocationType());
+ }
+ }
+
+ private abstract class RunnableCloseable extends RunnableTask implements Closeable
+ {
+ protected RunnableCloseable(String op)
+ {
+ super(op);
+ }
+
+ @Override
+ public void close()
+ {
+ try
+ {
+ ChannelEndPoint.this.close();
+ }
+ catch (Throwable x)
+ {
+ LOG.warn(x);
+ }
+ }
+ }
+
+ private final ManagedSelector.SelectorUpdate _updateKeyAction = this::updateKeyAction;
+
+ private final Runnable _runFillable = new RunnableCloseable("runFillable")
+ {
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return getFillInterest().getCallbackInvocationType();
+ }
+
+ @Override
+ public void run()
+ {
+ getFillInterest().fillable();
+ }
+ };
+
+ private final Runnable _runCompleteWrite = new RunnableCloseable("runCompleteWrite")
+ {
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return getWriteFlusher().getCallbackInvocationType();
+ }
+
+ @Override
+ public void run()
+ {
+ getWriteFlusher().completeWrite();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("CEP:%s:%s:%s->%s", ChannelEndPoint.this, _operation, getInvocationType(), getWriteFlusher());
+ }
+ };
+
+ private final Runnable _runCompleteWriteFillable = new RunnableCloseable("runCompleteWriteFillable")
+ {
+ @Override
+ public InvocationType getInvocationType()
+ {
+ InvocationType fillT = getFillInterest().getCallbackInvocationType();
+ InvocationType flushT = getWriteFlusher().getCallbackInvocationType();
+ if (fillT == flushT)
+ return fillT;
+
+ if (fillT == InvocationType.EITHER && flushT == InvocationType.NON_BLOCKING)
+ return InvocationType.EITHER;
+
+ if (fillT == InvocationType.NON_BLOCKING && flushT == InvocationType.EITHER)
+ return InvocationType.EITHER;
+
+ return InvocationType.BLOCKING;
+ }
+
+ @Override
+ public void run()
+ {
+ getWriteFlusher().completeWrite();
+ getFillInterest().fillable();
+ }
+ };
+
+ public ChannelEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler)
+ {
+ super(scheduler);
+ _channel = channel;
+ _selector = selector;
+ _key = key;
+ }
+
+ @Override
+ public InetSocketAddress getLocalAddress()
+ {
+ return (InetSocketAddress)_channel.socket().getLocalSocketAddress();
+ }
+
+ @Override
+ public InetSocketAddress getRemoteAddress()
+ {
+ return (InetSocketAddress)_channel.socket().getRemoteSocketAddress();
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ return _channel.isOpen();
+ }
+
+ @Override
+ protected void doShutdownOutput()
+ {
+ try
+ {
+ Socket socket = _channel.socket();
+ if (!socket.isOutputShutdown())
+ socket.shutdownOutput();
+ }
+ catch (IOException e)
+ {
+ LOG.debug(e);
+ }
+ }
+
+ @Override
+ public void doClose()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("doClose {}", this);
+ try
+ {
+ _channel.close();
+ }
+ catch (IOException e)
+ {
+ LOG.debug(e);
+ }
+ finally
+ {
+ super.doClose();
+ }
+ }
+
+ @Override
+ public void onClose()
+ {
+ try
+ {
+ super.onClose();
+ }
+ finally
+ {
+ if (_selector != null)
+ _selector.destroyEndPoint(this);
+ }
+ }
+
+ @Override
+ public int fill(ByteBuffer buffer) throws IOException
+ {
+ if (isInputShutdown())
+ return -1;
+
+ int pos = BufferUtil.flipToFill(buffer);
+ int filled;
+ try
+ {
+ filled = _channel.read(buffer);
+ if (filled > 0)
+ notIdle();
+ else if (filled == -1)
+ shutdownInput();
+ }
+ catch (IOException e)
+ {
+ LOG.debug(e);
+ shutdownInput();
+ filled = -1;
+ }
+ finally
+ {
+ BufferUtil.flipToFlush(buffer, pos);
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("filled {} {}", filled, BufferUtil.toDetailString(buffer));
+ return filled;
+ }
+
+ @Override
+ public boolean flush(ByteBuffer... buffers) throws IOException
+ {
+ long flushed;
+ try
+ {
+ flushed = _channel.write(buffers);
+ if (LOG.isDebugEnabled())
+ LOG.debug("flushed {} {}", flushed, this);
+ }
+ catch (IOException e)
+ {
+ throw new EofException(e);
+ }
+
+ if (flushed > 0)
+ notIdle();
+
+ for (ByteBuffer b : buffers)
+ {
+ if (!BufferUtil.isEmpty(b))
+ return false;
+ }
+
+ return true;
+ }
+
+ public SocketChannel getChannel()
+ {
+ return _channel;
+ }
+
+ @Override
+ public Object getTransport()
+ {
+ return _channel;
+ }
+
+ @Override
+ protected void needsFillInterest()
+ {
+ changeInterests(SelectionKey.OP_READ);
+ }
+
+ @Override
+ protected void onIncompleteFlush()
+ {
+ changeInterests(SelectionKey.OP_WRITE);
+ }
+
+ @Override
+ public Runnable onSelected()
+ {
+ // This method runs from the selector thread,
+ // possibly concurrently with changeInterests(int).
+
+ int readyOps = _key.readyOps();
+ int oldInterestOps;
+ int newInterestOps;
+ synchronized (this)
+ {
+ _updatePending = true;
+ // Remove the readyOps, that here can only be OP_READ or OP_WRITE (or both).
+ oldInterestOps = _desiredInterestOps;
+ newInterestOps = oldInterestOps & ~readyOps;
+ _desiredInterestOps = newInterestOps;
+ }
+
+ boolean fillable = (readyOps & SelectionKey.OP_READ) != 0;
+ boolean flushable = (readyOps & SelectionKey.OP_WRITE) != 0;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("onSelected {}->{} r={} w={} for {}", oldInterestOps, newInterestOps, fillable, flushable, this);
+
+ // return task to complete the job
+ Runnable task = fillable
+ ? (flushable
+ ? _runCompleteWriteFillable
+ : _runFillable)
+ : (flushable
+ ? _runCompleteWrite
+ : null);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("task {}", task);
+ return task;
+ }
+
+ private void updateKeyAction(Selector selector)
+ {
+ updateKey();
+ }
+
+ @Override
+ public void updateKey()
+ {
+ // This method runs from the selector thread,
+ // possibly concurrently with changeInterests(int).
+
+ try
+ {
+ int oldInterestOps;
+ int newInterestOps;
+ synchronized (this)
+ {
+ _updatePending = false;
+ oldInterestOps = _currentInterestOps;
+ newInterestOps = _desiredInterestOps;
+ if (oldInterestOps != newInterestOps)
+ {
+ _currentInterestOps = newInterestOps;
+ _key.interestOps(newInterestOps);
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Key interests updated {} -> {} on {}", oldInterestOps, newInterestOps, this);
+ }
+ catch (CancelledKeyException x)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Ignoring key update for cancelled key {}", this, x);
+ close();
+ }
+ catch (Throwable x)
+ {
+ LOG.warn("Ignoring key update for {}", this, x);
+ close();
+ }
+ }
+
+ @Override
+ public void replaceKey(SelectionKey newKey)
+ {
+ _key = newKey;
+ }
+
+ private void changeInterests(int operation)
+ {
+ // This method runs from any thread, possibly
+ // concurrently with updateKey() and onSelected().
+
+ int oldInterestOps;
+ int newInterestOps;
+ boolean pending;
+ synchronized (this)
+ {
+ pending = _updatePending;
+ oldInterestOps = _desiredInterestOps;
+ newInterestOps = oldInterestOps | operation;
+ if (newInterestOps != oldInterestOps)
+ _desiredInterestOps = newInterestOps;
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("changeInterests p={} {}->{} for {}", pending, oldInterestOps, newInterestOps, this);
+
+ if (!pending && _selector != null)
+ _selector.submit(_updateKeyAction);
+ }
+
+ @Override
+ public String toEndPointString()
+ {
+ // We do a best effort to print the right toString() and that's it.
+ return String.format("%s{io=%d/%d,kio=%d,kro=%d}",
+ super.toEndPointString(),
+ _currentInterestOps,
+ _desiredInterestOps,
+ ManagedSelector.safeInterestOps(_key),
+ ManagedSelector.safeReadyOps(_key));
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java
new file mode 100644
index 0000000..87ba42a
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java
@@ -0,0 +1,64 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+
+/**
+ * Factory for client-side {@link Connection} instances.
+ */
+public interface ClientConnectionFactory
+{
+ String CONNECTOR_CONTEXT_KEY = "client.connector";
+
+ /**
+ * @param endPoint the {@link org.eclipse.jetty.io.EndPoint} to link the newly created connection to
+ * @param context the context data to create the connection
+ * @return a new {@link Connection}
+ * @throws IOException if the connection cannot be created
+ */
+ Connection newConnection(EndPoint endPoint, Map<String, Object> context) throws IOException;
+
+ default Connection customize(Connection connection, Map<String, Object> context)
+ {
+ ContainerLifeCycle connector = (ContainerLifeCycle)context.get(CONNECTOR_CONTEXT_KEY);
+ connector.getBeans(Connection.Listener.class).forEach(connection::addListener);
+ return connection;
+ }
+
+ /**
+ * <p>Wraps another ClientConnectionFactory.</p>
+ * <p>This is typically done by protocols that send "preface" bytes with some metadata
+ * before other protocols. The metadata could be, for example, proxying information
+ * or authentication information.</p>
+ */
+ interface Decorator
+ {
+ /**
+ * <p>Wraps the given {@code factory}.</p>
+ *
+ * @param factory the ClientConnectionFactory to wrap
+ * @return the wrapping ClientConnectionFactory
+ */
+ ClientConnectionFactory apply(ClientConnectionFactory factory);
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/Connection.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/Connection.java
new file mode 100644
index 0000000..316bb62
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/Connection.java
@@ -0,0 +1,171 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.Closeable;
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.component.Container;
+
+/**
+ * <p>A {@link Connection} is associated to an {@link EndPoint} so that I/O events
+ * happening on the {@link EndPoint} can be processed by the {@link Connection}.</p>
+ * <p>A typical implementation of {@link Connection} overrides {@link #onOpen()} to
+ * {@link EndPoint#fillInterested(org.eclipse.jetty.util.Callback) set read interest} on the {@link EndPoint},
+ * and when the {@link EndPoint} signals read readyness, this {@link Connection} can
+ * read bytes from the network and interpret them.</p>
+ */
+public interface Connection extends Closeable
+{
+ /**
+ * <p>Adds a listener of connection events.</p>
+ *
+ * @param listener the listener to add
+ */
+ void addListener(Listener listener);
+
+ /**
+ * <p>Removes a listener of connection events.</p>
+ *
+ * @param listener the listener to remove
+ */
+ void removeListener(Listener listener);
+
+ /**
+ * <p>Callback method invoked when this connection is opened.</p>
+ * <p>Creators of the connection implementation are responsible for calling this method.</p>
+ */
+ void onOpen();
+
+ /**
+ * <p>Callback method invoked when this connection is closed.</p>
+ * <p>Creators of the connection implementation are responsible for calling this method.</p>
+ */
+ void onClose();
+
+ /**
+ * @return the {@link EndPoint} associated with this Connection.
+ */
+ EndPoint getEndPoint();
+
+ /**
+ * <p>Performs a logical close of this connection.</p>
+ * <p>For simple connections, this may just mean to delegate the close to the associated
+ * {@link EndPoint} but, for example, SSL connections should write the SSL close message
+ * before closing the associated {@link EndPoint}.</p>
+ */
+ @Override
+ void close();
+
+ /**
+ * <p>Callback method invoked upon an idle timeout event.</p>
+ * <p>Implementations of this method may return true to indicate that the idle timeout
+ * handling should proceed normally, typically failing the EndPoint and causing it to
+ * be closed.</p>
+ * <p>When false is returned, the handling of the idle timeout event is halted
+ * immediately and the EndPoint left in the state it was before the idle timeout event.</p>
+ *
+ * @return true to let the EndPoint handle the idle timeout,
+ * false to tell the EndPoint to halt the handling of the idle timeout.
+ */
+ boolean onIdleExpired();
+
+ long getMessagesIn();
+
+ long getMessagesOut();
+
+ long getBytesIn();
+
+ long getBytesOut();
+
+ long getCreatedTimeStamp();
+
+ /**
+ * <p>{@link Connection} implementations implement this interface when they
+ * can upgrade from the protocol they speak (for example HTTP/1.1)
+ * to a different protocol (e.g. HTTP/2).</p>
+ *
+ * @see EndPoint#upgrade(Connection)
+ * @see UpgradeTo
+ */
+ interface UpgradeFrom
+ {
+ /**
+ * <p>Invoked during an {@link EndPoint#upgrade(Connection) upgrade}
+ * to produce a buffer containing bytes that have not been consumed by
+ * this connection, and that must be consumed by the upgrade-to
+ * connection.</p>
+ *
+ * @return a buffer of unconsumed bytes to pass to the upgrade-to connection.
+ * The buffer does not belong to any pool and should be discarded after
+ * having consumed its bytes.
+ * The returned buffer may be null if there are no unconsumed bytes.
+ */
+ ByteBuffer onUpgradeFrom();
+ }
+
+ /**
+ * <p>{@link Connection} implementations implement this interface when they
+ * can be upgraded to the protocol they speak (e.g. HTTP/2)
+ * from a different protocol (e.g. HTTP/1.1).</p>
+ */
+ interface UpgradeTo
+ {
+ /**
+ * <p>Invoked during an {@link EndPoint#upgrade(Connection) upgrade}
+ * to receive a buffer containing bytes that have not been consumed by
+ * the upgrade-from connection, and that must be consumed by this
+ * connection.</p>
+ *
+ * @param buffer a non-null buffer of unconsumed bytes received from
+ * the upgrade-from connection.
+ * The buffer does not belong to any pool and should be discarded after
+ * having consumed its bytes.
+ */
+ void onUpgradeTo(ByteBuffer buffer);
+ }
+
+ /**
+ * <p>A Listener for connection events.</p>
+ * <p>Listeners can be added to a {@link Connection} to get open and close events.
+ * The AbstractConnectionFactory implements a pattern where objects implement
+ * this interface that have been added via {@link Container#addBean(Object)} to
+ * the Connector or ConnectionFactory are added as listeners to all new connections
+ * </p>
+ */
+ interface Listener
+ {
+ void onOpened(Connection connection);
+
+ void onClosed(Connection connection);
+
+ class Adapter implements Listener
+ {
+ @Override
+ public void onOpened(Connection connection)
+ {
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ConnectionStatistics.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ConnectionStatistics.java
new file mode 100644
index 0000000..ef4d346
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ConnectionStatistics.java
@@ -0,0 +1,401 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.LongAdder;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.statistic.CounterStatistic;
+import org.eclipse.jetty.util.statistic.RateCounter;
+import org.eclipse.jetty.util.statistic.SampleStatistic;
+
+/**
+ * <p>A {@link Connection.Listener} that tracks connection statistics.</p>
+ * <p>Adding an instance of this class as a bean to a ServerConnector
+ * or ConnectionFactory (for the server) or to HttpClient (for the client)
+ * will trigger the tracking of the connection statistics for all
+ * connections managed by the server or by the client.</p>
+ * <p>The statistics for a connection are gathered when the connection
+ * is closed.</p>
+ * <p>ConnectionStatistics instances must be {@link #start() started}
+ * to collect statistics, either as part of starting the whole component
+ * tree, or explicitly if the component tree has already been started.</p>
+ */
+@ManagedObject("Tracks statistics on connections")
+public class ConnectionStatistics extends AbstractLifeCycle implements Connection.Listener, Dumpable
+{
+ private final Stats _stats = new Stats("total");
+ private final Map<String, Stats> _statsMap = new ConcurrentHashMap<>();
+
+ @ManagedOperation(value = "Resets the statistics", impact = "ACTION")
+ public void reset()
+ {
+ _stats.reset();
+ _statsMap.clear();
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ reset();
+ }
+
+ @Override
+ public void onOpened(Connection connection)
+ {
+ if (!isStarted())
+ return;
+ onTotalOpened(connection);
+ onConnectionOpened(connection);
+ }
+
+ protected void onTotalOpened(Connection connection)
+ {
+ _stats.incrementCount();
+ }
+
+ protected void onConnectionOpened(Connection connection)
+ {
+ _statsMap.computeIfAbsent(connection.getClass().getName(), Stats::new).incrementCount();
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ if (!isStarted())
+ return;
+ onTotalClosed(connection);
+ onConnectionClosed(connection);
+ }
+
+ protected void onTotalClosed(Connection connection)
+ {
+ onClosed(_stats, connection);
+ }
+
+ protected void onConnectionClosed(Connection connection)
+ {
+ Stats stats = _statsMap.get(connection.getClass().getName());
+ if (stats != null)
+ onClosed(stats, connection);
+ }
+
+ private void onClosed(Stats stats, Connection connection)
+ {
+ stats.decrementCount();
+ stats.recordDuration(System.currentTimeMillis() - connection.getCreatedTimeStamp());
+ long bytesIn = connection.getBytesIn();
+ if (bytesIn > 0)
+ stats.recordBytesIn(bytesIn);
+ long bytesOut = connection.getBytesOut();
+ if (bytesOut > 0)
+ stats.recordBytesOut(bytesOut);
+ long messagesIn = connection.getMessagesIn();
+ if (messagesIn > 0)
+ stats.recordMessagesIn(messagesIn);
+ long messagesOut = connection.getMessagesOut();
+ if (messagesOut > 0)
+ stats.recordMessagesOut(messagesOut);
+ }
+
+ @ManagedAttribute("Total number of bytes received by tracked connections")
+ public long getReceivedBytes()
+ {
+ return _stats.getReceivedBytes();
+ }
+
+ @ManagedAttribute("Total number of bytes received per second since the last invocation of this method")
+ public long getReceivedBytesRate()
+ {
+ return _stats.getReceivedBytesRate();
+ }
+
+ @ManagedAttribute("Total number of bytes sent by tracked connections")
+ public long getSentBytes()
+ {
+ return _stats.getSentBytes();
+ }
+
+ @ManagedAttribute("Total number of bytes sent per second since the last invocation of this method")
+ public long getSentBytesRate()
+ {
+ return _stats.getSentBytesRate();
+ }
+
+ @ManagedAttribute("The max duration of a connection in ms")
+ public long getConnectionDurationMax()
+ {
+ return _stats.getConnectionDurationMax();
+ }
+
+ @ManagedAttribute("The mean duration of a connection in ms")
+ public double getConnectionDurationMean()
+ {
+ return _stats.getConnectionDurationMean();
+ }
+
+ @ManagedAttribute("The standard deviation of the duration of a connection")
+ public double getConnectionDurationStdDev()
+ {
+ return _stats.getConnectionDurationStdDev();
+ }
+
+ @ManagedAttribute("The total number of connections opened")
+ public long getConnectionsTotal()
+ {
+ return _stats.getConnectionsTotal();
+ }
+
+ @ManagedAttribute("The current number of open connections")
+ public long getConnections()
+ {
+ return _stats.getConnections();
+ }
+
+ @ManagedAttribute("The max number of open connections")
+ public long getConnectionsMax()
+ {
+ return _stats.getConnectionsMax();
+ }
+
+ @ManagedAttribute("The total number of messages received")
+ public long getReceivedMessages()
+ {
+ return _stats.getReceivedMessages();
+ }
+
+ @ManagedAttribute("Total number of messages received per second since the last invocation of this method")
+ public long getReceivedMessagesRate()
+ {
+ return _stats.getReceivedMessagesRate();
+ }
+
+ @ManagedAttribute("The total number of messages sent")
+ public long getSentMessages()
+ {
+ return _stats.getSentMessages();
+ }
+
+ @ManagedAttribute("Total number of messages sent per second since the last invocation of this method")
+ public long getSentMessagesRate()
+ {
+ return _stats.getSentMessagesRate();
+ }
+
+ public Map<String, Stats> getConnectionStatisticsGroups()
+ {
+ return _statsMap;
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ List<Stats> children = new ArrayList<>();
+ children.add(_stats);
+ children.addAll(_statsMap.values());
+ Dumpable.dumpObjects(out, indent, this, children.toArray());
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x", getClass().getSimpleName(), hashCode());
+ }
+
+ public static class Stats implements Dumpable
+ {
+ private final CounterStatistic _connections = new CounterStatistic();
+ private final SampleStatistic _connectionsDuration = new SampleStatistic();
+ private final LongAdder _bytesIn = new LongAdder();
+ private final RateCounter _bytesInRate = new RateCounter();
+ private final LongAdder _bytesOut = new LongAdder();
+ private final RateCounter _bytesOutRate = new RateCounter();
+ private final LongAdder _messagesIn = new LongAdder();
+ private final RateCounter _messagesInRate = new RateCounter();
+ private final LongAdder _messagesOut = new LongAdder();
+ private final RateCounter _messagesOutRate = new RateCounter();
+ private final String _name;
+
+ public Stats(String name)
+ {
+ _name = name;
+ }
+
+ public void reset()
+ {
+ _connections.reset();
+ _connectionsDuration.reset();
+ _bytesIn.reset();
+ _bytesInRate.reset();
+ _bytesOut.reset();
+ _bytesOutRate.reset();
+ _messagesIn.reset();
+ _messagesInRate.reset();
+ _messagesOut.reset();
+ _messagesOutRate.reset();
+ }
+
+ public String getName()
+ {
+ return _name;
+ }
+
+ public long getReceivedBytes()
+ {
+ return _bytesIn.sum();
+ }
+
+ public long getReceivedBytesRate()
+ {
+ long rate = _bytesInRate.getRate();
+ _bytesInRate.reset();
+ return rate;
+ }
+
+ public long getSentBytes()
+ {
+ return _bytesOut.sum();
+ }
+
+ public long getSentBytesRate()
+ {
+ long rate = _bytesOutRate.getRate();
+ _bytesOutRate.reset();
+ return rate;
+ }
+
+ public long getConnectionDurationMax()
+ {
+ return _connectionsDuration.getMax();
+ }
+
+ public double getConnectionDurationMean()
+ {
+ return _connectionsDuration.getMean();
+ }
+
+ public double getConnectionDurationStdDev()
+ {
+ return _connectionsDuration.getStdDev();
+ }
+
+ public long getConnectionsTotal()
+ {
+ return _connections.getTotal();
+ }
+
+ public long getConnections()
+ {
+ return _connections.getCurrent();
+ }
+
+ public long getConnectionsMax()
+ {
+ return _connections.getMax();
+ }
+
+ public long getReceivedMessages()
+ {
+ return _messagesIn.sum();
+ }
+
+ public long getReceivedMessagesRate()
+ {
+ long rate = _messagesInRate.getRate();
+ _messagesInRate.reset();
+ return rate;
+ }
+
+ public long getSentMessages()
+ {
+ return _messagesOut.sum();
+ }
+
+ public long getSentMessagesRate()
+ {
+ long rate = _messagesOutRate.getRate();
+ _messagesOutRate.reset();
+ return rate;
+ }
+
+ public void incrementCount()
+ {
+ _connections.increment();
+ }
+
+ public void decrementCount()
+ {
+ _connections.decrement();
+ }
+
+ public void recordDuration(long duration)
+ {
+ _connectionsDuration.record(duration);
+ }
+
+ public void recordBytesIn(long bytesIn)
+ {
+ _bytesIn.add(bytesIn);
+ _bytesInRate.add(bytesIn);
+ }
+
+ public void recordBytesOut(long bytesOut)
+ {
+ _bytesOut.add(bytesOut);
+ _bytesOutRate.add(bytesOut);
+ }
+
+ public void recordMessagesIn(long messagesIn)
+ {
+ _messagesIn.add(messagesIn);
+ _messagesInRate.add(messagesIn);
+ }
+
+ public void recordMessagesOut(long messagesOut)
+ {
+ _messagesOut.add(messagesOut);
+ _messagesOutRate.add(messagesOut);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ Dumpable.dumpObjects(out, indent, this,
+ String.format("connections=%s", _connections),
+ String.format("durations=%s", _connectionsDuration),
+ String.format("bytes in/out=%s/%s", getReceivedBytes(), getSentBytes()),
+ String.format("messages in/out=%s/%s", getReceivedMessages(), getSentMessages()));
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[%s]", getClass().getSimpleName(), getName());
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/CyclicTimeout.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/CyclicTimeout.java
new file mode 100644
index 0000000..3508662
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/CyclicTimeout.java
@@ -0,0 +1,311 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.util.component.Destroyable;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+import static java.lang.Long.MAX_VALUE;
+
+/**
+ * <p>An abstract implementation of a timeout.</p>
+ * <p>Subclasses should implement {@link #onTimeoutExpired()}.</p>
+ * <p>This implementation is optimised assuming that the timeout
+ * will mostly be cancelled and then reused with a similar value.</p>
+ * <p>The typical scenario to use this class is when you have events
+ * that postpone (by re-scheduling), or cancel then re-schedule, a
+ * timeout for a single entity.
+ * For example: connection idleness, where for each connection there
+ * is a CyclicTimeout and a read/write postpones the timeout; when
+ * the timeout expires, the implementation checks against a timestamp
+ * if the connection is really idle.
+ * Another example: HTTP session expiration, where for each HTTP
+ * session there is a CyclicTimeout and at the beginning of the
+ * request processing the timeout is canceled (via cancel()), but at
+ * the end of the request processing the timeout is re-scheduled.</p>
+ * <p>This implementation has a {@link Timeout} holding the time
+ * at which the scheduled task should fire, and a linked list of
+ * {@link Wakeup}, each holding the actual scheduled task.</p>
+ * <p>Calling {@link #schedule(long, TimeUnit)} the first time will
+ * create a Timeout with an associated Wakeup and submit a task to
+ * the scheduler.
+ * Calling {@link #schedule(long, TimeUnit)} again with the same or
+ * a larger delay will cancel the previous Timeout, but keep the
+ * previous Wakeup without submitting a new task to the scheduler,
+ * therefore reducing the pressure on the scheduler and avoid it
+ * becomes a bottleneck.
+ * When the Wakeup task fires, it will see that the Timeout is now
+ * in the future and will attach a new Wakeup with the future time
+ * to the Timeout, and submit a scheduler task for the new Wakeup.</p>
+ *
+ * @see CyclicTimeouts
+ */
+public abstract class CyclicTimeout implements Destroyable
+{
+ private static final Logger LOG = Log.getLogger(CyclicTimeout.class);
+ private static final Timeout NOT_SET = new Timeout(MAX_VALUE, null);
+ private static final Scheduler.Task DESTROYED = () -> false;
+
+ /* The underlying scheduler to use */
+ private final Scheduler _scheduler;
+ /* Reference to the current Timeout and chain of Wakeup */
+ private final AtomicReference<Timeout> _timeout = new AtomicReference<>(NOT_SET);
+
+ /**
+ * @param scheduler A scheduler used to schedule wakeups
+ */
+ public CyclicTimeout(Scheduler scheduler)
+ {
+ _scheduler = scheduler;
+ }
+
+ public Scheduler getScheduler()
+ {
+ return _scheduler;
+ }
+
+ /**
+ * <p>Schedules a timeout, even if already set, cancelled or expired.</p>
+ * <p>If a timeout is already set, it will be cancelled and replaced
+ * by the new one.</p>
+ *
+ * @param delay The period of time before the timeout expires.
+ * @param units The unit of time of the period.
+ * @return true if the timeout was already set.
+ */
+ public boolean schedule(long delay, TimeUnit units)
+ {
+ long now = System.nanoTime();
+ long newTimeoutAt = now + units.toNanos(delay);
+
+ Wakeup newWakeup = null;
+ boolean result;
+ while (true)
+ {
+ Timeout timeout = _timeout.get();
+ result = timeout._at != MAX_VALUE;
+
+ // Is the current wakeup good to use? ie before our timeout time?
+ Wakeup wakeup = timeout._wakeup;
+ if (wakeup == null || wakeup._at > newTimeoutAt)
+ // No, we need an earlier wakeup.
+ wakeup = newWakeup = new Wakeup(newTimeoutAt, wakeup);
+
+ if (_timeout.compareAndSet(timeout, new Timeout(newTimeoutAt, wakeup)))
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Installed timeout in {} ms, {} wake up in {} ms",
+ units.toMillis(delay),
+ newWakeup != null ? "new" : "existing",
+ TimeUnit.NANOSECONDS.toMillis(wakeup._at - now));
+ }
+ break;
+ }
+ }
+
+ // If we created a new wakeup, we need to actually schedule it.
+ // Any wakeup that is created and discarded by the failed CAS will not be
+ // in the wakeup chain, will not have a scheduler task set and will be GC'd.
+ if (newWakeup != null)
+ newWakeup.schedule(now);
+
+ return result;
+ }
+
+ /**
+ * <p>Cancels this CyclicTimeout so that it won't expire.</p>
+ * <p>After being cancelled, this CyclicTimeout can be scheduled again.</p>
+ *
+ * @return true if this CyclicTimeout was scheduled to expire
+ * @see #destroy()
+ */
+ public boolean cancel()
+ {
+ boolean result;
+ while (true)
+ {
+ Timeout timeout = _timeout.get();
+ result = timeout._at != MAX_VALUE;
+ Wakeup wakeup = timeout._wakeup;
+ Timeout newTimeout = wakeup == null ? NOT_SET : new Timeout(MAX_VALUE, wakeup);
+ if (_timeout.compareAndSet(timeout, newTimeout))
+ break;
+ }
+ return result;
+ }
+
+ /**
+ * <p>Invoked when the timeout expires.</p>
+ */
+ public abstract void onTimeoutExpired();
+
+ /**
+ * <p>Destroys this CyclicTimeout.</p>
+ * <p>After being destroyed, this CyclicTimeout is not used anymore.</p>
+ */
+ @Override
+ public void destroy()
+ {
+ Timeout timeout = _timeout.getAndSet(NOT_SET);
+ Wakeup wakeup = timeout == null ? null : timeout._wakeup;
+ while (wakeup != null)
+ {
+ wakeup.destroy();
+ wakeup = wakeup._next;
+ }
+ }
+
+ /**
+ * A timeout time with a link to a Wakeup chain.
+ */
+ private static class Timeout
+ {
+ private final long _at;
+ private final Wakeup _wakeup;
+
+ private Timeout(long timeoutAt, Wakeup wakeup)
+ {
+ _at = timeoutAt;
+ _wakeup = wakeup;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x:%dms,%s",
+ getClass().getSimpleName(),
+ hashCode(),
+ TimeUnit.NANOSECONDS.toMillis(_at - System.nanoTime()),
+ _wakeup);
+ }
+ }
+
+ /**
+ * A Wakeup chain of real scheduler tasks.
+ */
+ private class Wakeup implements Runnable
+ {
+ private final AtomicReference<Scheduler.Task> _task = new AtomicReference<>();
+ private final long _at;
+ private final Wakeup _next;
+
+ private Wakeup(long wakeupAt, Wakeup next)
+ {
+ _at = wakeupAt;
+ _next = next;
+ }
+
+ private void schedule(long now)
+ {
+ _task.compareAndSet(null, _scheduler.schedule(this, _at - now, TimeUnit.NANOSECONDS));
+ }
+
+ private void destroy()
+ {
+ Scheduler.Task task = _task.getAndSet(DESTROYED);
+ if (task != null)
+ task.cancel();
+ }
+
+ @Override
+ public void run()
+ {
+ long now = System.nanoTime();
+ Wakeup newWakeup = null;
+ boolean hasExpired = false;
+ while (true)
+ {
+ Timeout timeout = _timeout.get();
+
+ // We must look for ourselves in the current wakeup list.
+ // If we find ourselves, then we act and we use our tail for any new
+ // wakeup list, effectively removing any wakeup before us in the list (and making them no-ops).
+ // If we don't find ourselves, then a wakeup that should have expired after us has already run
+ // and removed us from the list, so we become a noop.
+
+ Wakeup wakeup = timeout._wakeup;
+ while (wakeup != null)
+ {
+ if (wakeup == this)
+ break;
+ // Not us, so look at next wakeup in the list.
+ wakeup = wakeup._next;
+ }
+ if (wakeup == null)
+ // Not found, we become a noop.
+ return;
+
+ // We are in the wakeup list! So we have to act and we know our
+ // tail has not expired (else it would have removed us from the list).
+ // Remove ourselves (and any prior Wakeup) from the wakeup list.
+ wakeup = wakeup._next;
+
+ Timeout newTimeout;
+ if (timeout._at <= now)
+ {
+ // We have timed out!
+ hasExpired = true;
+ newTimeout = wakeup == null ? NOT_SET : new Timeout(MAX_VALUE, wakeup);
+ }
+ else if (timeout._at != MAX_VALUE)
+ {
+ // We have not timed out, but we are set to!
+ // Is the current wakeup good to use? ie before our timeout time?
+ if (wakeup == null || wakeup._at >= timeout._at)
+ // No, we need an earlier wakeup.
+ wakeup = newWakeup = new Wakeup(timeout._at, wakeup);
+ newTimeout = new Timeout(timeout._at, wakeup);
+ }
+ else
+ {
+ // We don't timeout, preserve scheduled chain.
+ newTimeout = wakeup == null ? NOT_SET : new Timeout(MAX_VALUE, wakeup);
+ }
+
+ // Loop until we succeed in changing state or we are a noop!
+ if (_timeout.compareAndSet(timeout, newTimeout))
+ break;
+ }
+
+ // If we created a new wakeup, we need to actually schedule it.
+ if (newWakeup != null)
+ newWakeup.schedule(now);
+
+ // If we expired, then do the callback.
+ if (hasExpired)
+ onTimeoutExpired();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x:%dms->%s",
+ getClass().getSimpleName(),
+ hashCode(),
+ _at == MAX_VALUE ? _at : TimeUnit.NANOSECONDS.toMillis(_at - System.nanoTime()),
+ _next);
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/CyclicTimeouts.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/CyclicTimeouts.java
new file mode 100644
index 0000000..a36da72
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/CyclicTimeouts.java
@@ -0,0 +1,199 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.util.Iterator;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.eclipse.jetty.util.component.Destroyable;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * <p>An implementation of a timeout that manages many {@link Expirable expirable} entities whose
+ * timeouts are mostly cancelled or re-scheduled.</p>
+ * <p>A typical scenario is for a parent entity to manage the timeouts of many children entities.</p>
+ * <p>When a new entity is created, call {@link #schedule(Expirable)} with the new entity so that
+ * this instance can be aware and manage the timeout of the new entity.</p>
+ * <p>Eventually, this instance wakes up and iterates over the entities provided by {@link #iterator()}.
+ * During the iteration, each entity:</p>
+ * <ul>
+ * <li>may never expire (see {@link Expirable#getExpireNanoTime()}; the entity is ignored</li>
+ * <li>may be expired; {@link #onExpired(Expirable)} is called with that entity as parameter</li>
+ * <li>may expire at a future time; the iteration records the earliest expiration time among
+ * all non-expired entities</li>
+ * </ul>
+ * <p>When the iteration is complete, this instance is re-scheduled with the earliest expiration time
+ * calculated during the iteration.</p>
+ *
+ * @param <T> the {@link Expirable} entity type
+ * @see CyclicTimeout
+ */
+public abstract class CyclicTimeouts<T extends CyclicTimeouts.Expirable> implements Destroyable
+{
+ private static final Logger LOG = Log.getLogger(CyclicTimeouts.class);
+
+ private final AtomicLong earliestTimeout = new AtomicLong(Long.MAX_VALUE);
+ private final CyclicTimeout cyclicTimeout;
+
+ public CyclicTimeouts(Scheduler scheduler)
+ {
+ cyclicTimeout = new Timeouts(scheduler);
+ }
+
+ /**
+ * @return the entities to iterate over when this instance expires
+ */
+ protected abstract Iterator<T> iterator();
+
+ /**
+ * <p>Invoked during the iteration when the given entity is expired.</p>
+ * <p>This method may be invoked multiple times, and even concurrently,
+ * for the same expirable entity and therefore the expiration of the
+ * entity, if any, should be an idempotent action.</p>
+ *
+ * @param expirable the entity that is expired
+ * @return whether the entity should be removed from the iterator via {@link Iterator#remove()}
+ */
+ protected abstract boolean onExpired(T expirable);
+
+ private void onTimeoutExpired()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Timeouts check for {}", this);
+
+ long now = System.nanoTime();
+ long earliest = Long.MAX_VALUE;
+ // Reset the earliest timeout so we can expire again.
+ // A concurrent call to schedule(long) may lose an
+ // earliest value, but the corresponding entity will
+ // be seen during the iteration below.
+ earliestTimeout.set(earliest);
+
+ Iterator<T> iterator = iterator();
+ if (iterator == null)
+ return;
+
+ // Scan the entities to abort expired entities
+ // and to find the entity that expires the earliest.
+ while (iterator.hasNext())
+ {
+ T expirable = iterator.next();
+ long expiresAt = expirable.getExpireNanoTime();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Entity {} expires in {} ms for {}", expirable, TimeUnit.NANOSECONDS.toMillis(expiresAt - now), this);
+
+ if (expiresAt == -1)
+ continue;
+
+ if (expiresAt <= now)
+ {
+ boolean remove = onExpired(expirable);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Entity {} expired, remove={} for {}", expirable, remove, this);
+ if (remove)
+ iterator.remove();
+ continue;
+ }
+ earliest = Math.min(earliest, expiresAt);
+ }
+
+ if (earliest < Long.MAX_VALUE)
+ schedule(earliest);
+ }
+
+ /**
+ * <p>Manages the timeout of a new entity.</p>
+ *
+ * @param expirable the new entity to manage the timeout for
+ */
+ public void schedule(T expirable)
+ {
+ long expiresAt = expirable.getExpireNanoTime();
+ if (expiresAt < Long.MAX_VALUE)
+ schedule(expiresAt);
+ }
+
+ private void schedule(long expiresAt)
+ {
+ // Schedule a timeout for the earliest entity that may expire.
+ // When the timeout expires, scan the entities for the next
+ // earliest entity that may expire, and reschedule a new timeout.
+ long prevEarliest = earliestTimeout.getAndUpdate(t -> Math.min(t, expiresAt));
+ long expires = expiresAt;
+ while (expires < prevEarliest)
+ {
+ // A new entity expires earlier than previous entities, schedule it.
+ long delay = Math.max(0, expires - System.nanoTime());
+ if (LOG.isDebugEnabled())
+ LOG.debug("Scheduling timeout in {} ms for {}", TimeUnit.NANOSECONDS.toMillis(delay), this);
+ schedule(cyclicTimeout, delay, TimeUnit.NANOSECONDS);
+
+ // If we lost a race and overwrote a schedule() with an earlier time, then that earlier time
+ // is remembered by earliestTimeout, in which case we will loop and set it again ourselves.
+ prevEarliest = expires;
+ expires = earliestTimeout.get();
+ }
+ }
+
+ @Override
+ public void destroy()
+ {
+ cyclicTimeout.destroy();
+ }
+
+ boolean schedule(CyclicTimeout cyclicTimeout, long delay, TimeUnit unit)
+ {
+ return cyclicTimeout.schedule(delay, unit);
+ }
+
+ /**
+ * <p>An entity that may expire.</p>
+ */
+ public interface Expirable
+ {
+ /**
+ * <p>Returns the expiration time in nanoseconds.</p>
+ * <p>The value to return must be calculated taking into account {@link System#nanoTime()},
+ * for example:</p>
+ * {@code expireNanoTime = System.nanoTime() + timeoutNanos}
+ * <p>Returning {@link Long#MAX_VALUE} indicates that this entity does not expire.</p>
+ *
+ * @return the expiration time in nanoseconds, or {@link Long#MAX_VALUE} if this entity does not expire
+ */
+ public long getExpireNanoTime();
+ }
+
+ private class Timeouts extends CyclicTimeout
+ {
+ private Timeouts(Scheduler scheduler)
+ {
+ super(scheduler);
+ }
+
+ @Override
+ public void onTimeoutExpired()
+ {
+ CyclicTimeouts.this.onTimeoutExpired();
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/EndPoint.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/EndPoint.java
new file mode 100644
index 0000000..16776da
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/EndPoint.java
@@ -0,0 +1,286 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadPendingException;
+import java.nio.channels.WritePendingException;
+
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.FutureCallback;
+import org.eclipse.jetty.util.IteratingCallback;
+import org.eclipse.jetty.util.thread.Invocable;
+
+/**
+ * <p>EndPoint is the abstraction for an I/O channel that transports bytes.</p>
+ *
+ * <h3>Asynchronous Methods</h3>
+ * <p>The asynchronous scheduling methods of {@link EndPoint}
+ * has been influenced by NIO.2 Futures and Completion
+ * handlers, but does not use those actual interfaces because they have
+ * some inefficiencies.</p>
+ * <p>This class will frequently be used in conjunction with some of the utility
+ * implementations of {@link Callback}, such as {@link FutureCallback} and
+ * {@link IteratingCallback}.</p>
+ *
+ * <h3>Reads</h3>
+ * <p>A {@link FutureCallback} can be used to block until an endpoint is ready
+ * to fill bytes - the notification will be emitted by the NIO subsystem:</p>
+ * <pre>
+ * FutureCallback callback = new FutureCallback();
+ * endPoint.fillInterested(callback);
+ *
+ * // Blocks until read to fill bytes.
+ * callback.get();
+ *
+ * // Now bytes can be filled in a ByteBuffer.
+ * int filled = endPoint.fill(byteBuffer);
+ * </pre>
+ *
+ * <h3>Asynchronous Reads</h3>
+ * <p>A {@link Callback} can be used to read asynchronously in its own dispatched
+ * thread:</p>
+ * <pre>
+ * endPoint.fillInterested(new Callback()
+ * {
+ * public void onSucceeded()
+ * {
+ * executor.execute(() ->
+ * {
+ * // Fill bytes in a different thread.
+ * int filled = endPoint.fill(byteBuffer);
+ * });
+ * }
+ * public void onFailed(Throwable failure)
+ * {
+ * endPoint.close();
+ * }
+ * });
+ * </pre>
+ *
+ * <h3>Blocking Writes</h3>
+ * <p>The write contract is that the callback is completed when all the bytes
+ * have been written or there is a failure.
+ * Blocking writes look like this:</p>
+ * <pre>
+ * FutureCallback callback = new FutureCallback();
+ * endpoint.write(callback, headerBuffer, contentBuffer);
+ *
+ * // Blocks until the write succeeds or fails.
+ * future.get();
+ * </pre>
+ * <p>Note also that multiple buffers may be passed in {@link #write(Callback, ByteBuffer...)}
+ * so that gather writes can be performed for efficiency.</p>
+ */
+public interface EndPoint extends Closeable
+{
+ /**
+ * @return The local Inet address to which this {@code EndPoint} is bound, or {@code null}
+ * if this {@code EndPoint} does not represent a network connection.
+ */
+ InetSocketAddress getLocalAddress();
+
+ /**
+ * @return The remote Inet address to which this {@code EndPoint} is bound, or {@code null}
+ * if this {@code EndPoint} does not represent a network connection.
+ */
+ InetSocketAddress getRemoteAddress();
+
+ /**
+ * @return whether this EndPoint is open
+ */
+ boolean isOpen();
+
+ /**
+ * @return the epoch time in milliseconds when this EndPoint was created
+ */
+ long getCreatedTimeStamp();
+
+ /**
+ * Shutdown the output.
+ * <p>This call indicates that no more data will be sent on this endpoint that
+ * that the remote end should read an EOF once all previously sent data has been
+ * consumed. Shutdown may be done either at the TCP/IP level, as a protocol exchange (Eg
+ * TLS close handshake) or both.
+ * <p>
+ * If the endpoint has {@link #isInputShutdown()} true, then this call has the same effect
+ * as {@link #close()}.
+ */
+ void shutdownOutput();
+
+ /**
+ * Test if output is shutdown.
+ * The output is shutdown by a call to {@link #shutdownOutput()}
+ * or {@link #close()}.
+ *
+ * @return true if the output is shutdown or the endpoint is closed.
+ */
+ boolean isOutputShutdown();
+
+ /**
+ * Test if the input is shutdown.
+ * The input is shutdown if an EOF has been read while doing
+ * a {@link #fill(ByteBuffer)}. Once the input is shutdown, all calls to
+ * {@link #fill(ByteBuffer)} will return -1, until such time as the
+ * end point is close, when they will return {@link EofException}.
+ *
+ * @return True if the input is shutdown or the endpoint is closed.
+ */
+ boolean isInputShutdown();
+
+ /**
+ * Close any backing stream associated with the endpoint
+ */
+ @Override
+ void close();
+
+ /**
+ * Fill the passed buffer with data from this endpoint. The bytes are appended to any
+ * data already in the buffer by writing from the buffers limit up to it's capacity.
+ * The limit is updated to include the filled bytes.
+ *
+ * @param buffer The buffer to fill. The position and limit are modified during the fill. After the
+ * operation, the position is unchanged and the limit is increased to reflect the new data filled.
+ * @return an {@code int} value indicating the number of bytes
+ * filled or -1 if EOF is read or the input is shutdown.
+ * @throws IOException if the endpoint is closed.
+ */
+ int fill(ByteBuffer buffer) throws IOException;
+
+ /**
+ * Flush data from the passed header/buffer to this endpoint. As many bytes as can be consumed
+ * are taken from the header/buffer position up until the buffer limit. The header/buffers position
+ * is updated to indicate how many bytes have been consumed.
+ *
+ * @param buffer the buffers to flush
+ * @return True IFF all the buffers have been consumed and the endpoint has flushed the data to its
+ * destination (ie is not buffering any data).
+ * @throws IOException If the endpoint is closed or output is shutdown.
+ */
+ boolean flush(ByteBuffer... buffer) throws IOException;
+
+ /**
+ * @return The underlying transport object (socket, channel, etc.)
+ */
+ Object getTransport();
+
+ /**
+ * Get the max idle time in ms.
+ * <p>The max idle time is the time the endpoint can be idle before
+ * extraordinary handling takes place.
+ *
+ * @return the max idle time in ms or if ms <= 0 implies an infinite timeout
+ */
+ long getIdleTimeout();
+
+ /**
+ * Set the idle timeout.
+ *
+ * @param idleTimeout the idle timeout in MS. Timeout <= 0 implies an infinite timeout
+ */
+ void setIdleTimeout(long idleTimeout);
+
+ /**
+ * <p>Requests callback methods to be invoked when a call to {@link #fill(ByteBuffer)} would return data or EOF.</p>
+ *
+ * @param callback the callback to call when an error occurs or we are readable. The callback may implement the {@link Invocable} interface to
+ * self declare its blocking status. Non-blocking callbacks may be called more efficiently without dispatch delays.
+ * @throws ReadPendingException if another read operation is concurrent.
+ */
+ void fillInterested(Callback callback) throws ReadPendingException;
+
+ /**
+ * <p>Requests callback methods to be invoked when a call to {@link #fill(ByteBuffer)} would return data or EOF.</p>
+ *
+ * @param callback the callback to call when an error occurs or we are readable. The callback may implement the {@link Invocable} interface to
+ * self declare its blocking status. Non-blocking callbacks may be called more efficiently without dispatch delays.
+ * @return true if set
+ */
+ boolean tryFillInterested(Callback callback);
+
+ /**
+ * @return whether {@link #fillInterested(Callback)} has been called, but {@link #fill(ByteBuffer)} has not yet
+ * been called
+ */
+ boolean isFillInterested();
+
+ /**
+ * <p>Writes the given buffers via {@link #flush(ByteBuffer...)} and invokes callback methods when either
+ * all the data has been flushed or an error occurs.</p>
+ *
+ * @param callback the callback to call when an error occurs or the write completed. The callback may implement the {@link Invocable} interface to
+ * self declare its blocking status. Non-blocking callbacks may be called more efficiently without dispatch delays.
+ * @param buffers one or more {@link ByteBuffer}s that will be flushed.
+ * @throws WritePendingException if another write operation is concurrent.
+ */
+ void write(Callback callback, ByteBuffer... buffers) throws WritePendingException;
+
+ /**
+ * @return the {@link Connection} associated with this EndPoint
+ * @see #setConnection(Connection)
+ */
+ Connection getConnection();
+
+ /**
+ * @param connection the {@link Connection} associated with this EndPoint
+ * @see #getConnection()
+ * @see #upgrade(Connection)
+ */
+ void setConnection(Connection connection);
+
+ /**
+ * <p>Callback method invoked when this EndPoint is opened.</p>
+ *
+ * @see #onClose()
+ */
+ void onOpen();
+
+ /**
+ * <p>Callback method invoked when this EndPoint is close.</p>
+ *
+ * @see #onOpen()
+ */
+ void onClose();
+
+ /**
+ * Is the endpoint optimized for DirectBuffer usage
+ *
+ * @return True if direct buffers can be used optimally.
+ */
+ boolean isOptimizedForDirectBuffers();
+
+ /**
+ * <p>Upgrades this EndPoint from the current connection to the given new connection.</p>
+ * <p>Closes the current connection, links this EndPoint to the new connection and
+ * then opens the new connection.</p>
+ * <p>If the current connection is an instance of {@link Connection.UpgradeFrom} then
+ * a buffer of unconsumed bytes is requested.
+ * If the buffer of unconsumed bytes is non-null and non-empty, then the new
+ * connection is tested: if it is an instance of {@link Connection.UpgradeTo}, then
+ * the unconsumed buffer is passed to the new connection; otherwise, an exception
+ * is thrown since there are unconsumed bytes that cannot be consumed by the new
+ * connection.</p>
+ *
+ * @param newConnection the connection to upgrade to
+ */
+ void upgrade(Connection newConnection);
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/EofException.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/EofException.java
new file mode 100644
index 0000000..adbc73f
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/EofException.java
@@ -0,0 +1,45 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.EOFException;
+
+/**
+ * A Jetty specialization of EOFException.
+ * <p> This is thrown by Jetty to distinguish between EOF received from
+ * the connection, vs and EOF thrown by some application talking to some other file/socket etc.
+ * The only difference in handling is that Jetty EOFs are logged less verbosely.
+ */
+public class EofException extends EOFException implements QuietException
+{
+ public EofException()
+ {
+ }
+
+ public EofException(String reason)
+ {
+ super(reason);
+ }
+
+ public EofException(Throwable th)
+ {
+ if (th != null)
+ initCause(th);
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/FillInterest.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/FillInterest.java
new file mode 100644
index 0000000..fa7fc5f
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/FillInterest.java
@@ -0,0 +1,174 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.ReadPendingException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Invocable;
+import org.eclipse.jetty.util.thread.Invocable.InvocationType;
+
+/**
+ * A Utility class to help implement {@link EndPoint#fillInterested(Callback)}
+ * by keeping state and calling the context and callback objects.
+ */
+public abstract class FillInterest
+{
+ private static final Logger LOG = Log.getLogger(FillInterest.class);
+ private final AtomicReference<Callback> _interested = new AtomicReference<>(null);
+
+ protected FillInterest()
+ {
+ }
+
+ /**
+ * Call to register interest in a callback when a read is possible.
+ * The callback will be called either immediately if {@link #needsFillInterest()}
+ * returns true or eventually once {@link #fillable()} is called.
+ *
+ * @param callback the callback to register
+ * @throws ReadPendingException if unable to read due to pending read op
+ */
+ public void register(Callback callback) throws ReadPendingException
+ {
+ if (!tryRegister(callback))
+ {
+ LOG.warn("Read pending for {} prevented {}", _interested, callback);
+ throw new ReadPendingException();
+ }
+ }
+
+ /**
+ * Call to register interest in a callback when a read is possible.
+ * The callback will be called either immediately if {@link #needsFillInterest()}
+ * returns true or eventually once {@link #fillable()} is called.
+ *
+ * @param callback the callback to register
+ * @return true if the register succeeded
+ */
+ public boolean tryRegister(Callback callback)
+ {
+ if (callback == null)
+ throw new IllegalArgumentException();
+
+ if (!_interested.compareAndSet(null, callback))
+ return false;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("interested {}", this);
+
+ try
+ {
+ needsFillInterest();
+ }
+ catch (Throwable e)
+ {
+ onFail(e);
+ }
+
+ return true;
+ }
+
+ /**
+ * Call to signal that a read is now possible.
+ *
+ * @return whether the callback was notified that a read is now possible
+ */
+ public boolean fillable()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("fillable {}", this);
+ Callback callback = _interested.get();
+ if (callback != null && _interested.compareAndSet(callback, null))
+ {
+ callback.succeeded();
+ return true;
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} lost race {}", this, callback);
+ return false;
+ }
+
+ /**
+ * @return True if a read callback has been registered
+ */
+ public boolean isInterested()
+ {
+ return _interested.get() != null;
+ }
+
+ public InvocationType getCallbackInvocationType()
+ {
+ Callback callback = _interested.get();
+ return Invocable.getInvocationType(callback);
+ }
+
+ /**
+ * Call to signal a failure to a registered interest
+ *
+ * @param cause the cause of the failure
+ * @return true if the cause was passed to a {@link Callback} instance
+ */
+ public boolean onFail(Throwable cause)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onFail " + this, cause);
+ Callback callback = _interested.get();
+ if (callback != null && _interested.compareAndSet(callback, null))
+ {
+ callback.failed(cause);
+ return true;
+ }
+ return false;
+ }
+
+ public void onClose()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onClose {}", this);
+ Callback callback = _interested.get();
+ if (callback != null && _interested.compareAndSet(callback, null))
+ callback.failed(new ClosedChannelException());
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("FillInterest@%x{%s}", hashCode(), _interested.get());
+ }
+
+ public String toStateString()
+ {
+ return _interested.get() == null ? "-" : "FI";
+ }
+
+ /**
+ * Register the read interest
+ * Abstract method to be implemented by the Specific ReadInterest to
+ * schedule a future call to {@link #fillable()} or {@link #onFail(Throwable)}
+ *
+ * @throws IOException if unable to fulfill interest in fill
+ */
+ protected abstract void needsFillInterest() throws IOException;
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/IdleTimeout.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/IdleTimeout.java
new file mode 100644
index 0000000..ce22c3c
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/IdleTimeout.java
@@ -0,0 +1,199 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * An Abstract implementation of an Idle Timeout.
+ * <p>
+ * This implementation is optimised that timeout operations are not cancelled on
+ * every operation. Rather timeout are allowed to expire and a check is then made
+ * to see when the last operation took place. If the idle timeout has not expired,
+ * the timeout is rescheduled for the earliest possible time a timeout could occur.
+ */
+public abstract class IdleTimeout
+{
+ private static final Logger LOG = Log.getLogger(IdleTimeout.class);
+ private final Scheduler _scheduler;
+ private final AtomicReference<Scheduler.Task> _timeout = new AtomicReference<>();
+ private volatile long _idleTimeout;
+ private volatile long _idleTimestamp = System.nanoTime();
+
+ /**
+ * @param scheduler A scheduler used to schedule checks for the idle timeout.
+ */
+ public IdleTimeout(Scheduler scheduler)
+ {
+ _scheduler = scheduler;
+ }
+
+ public Scheduler getScheduler()
+ {
+ return _scheduler;
+ }
+
+ /**
+ * @return the period of time, in milliseconds, that this object was idle
+ */
+ public long getIdleFor()
+ {
+ return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - _idleTimestamp);
+ }
+
+ /**
+ * @return the idle timeout in milliseconds
+ * @see #setIdleTimeout(long)
+ */
+ public long getIdleTimeout()
+ {
+ return _idleTimeout;
+ }
+
+ /**
+ * <p>Sets the idle timeout in milliseconds.</p>
+ * <p>A value that is less than or zero disables the idle timeout checks.</p>
+ *
+ * @param idleTimeout the idle timeout in milliseconds
+ * @see #getIdleTimeout()
+ */
+ public void setIdleTimeout(long idleTimeout)
+ {
+ long old = _idleTimeout;
+ _idleTimeout = idleTimeout;
+
+ // Do we have an old timeout
+ if (old > 0)
+ {
+ // if the old was less than or equal to the new timeout, then nothing more to do
+ if (old <= idleTimeout)
+ return;
+
+ // old timeout is too long, so cancel it.
+ deactivate();
+ }
+
+ // If we have a new timeout, then check and reschedule
+ if (isOpen())
+ activate();
+ }
+
+ /**
+ * This method should be called when non-idle activity has taken place.
+ */
+ public void notIdle()
+ {
+ _idleTimestamp = System.nanoTime();
+ }
+
+ private void idleCheck()
+ {
+ long idleLeft = checkIdleTimeout();
+ if (idleLeft >= 0)
+ scheduleIdleTimeout(idleLeft > 0 ? idleLeft : getIdleTimeout());
+ }
+
+ private void scheduleIdleTimeout(long delay)
+ {
+ Scheduler.Task newTimeout = null;
+ if (isOpen() && delay > 0 && _scheduler != null)
+ newTimeout = _scheduler.schedule(this::idleCheck, delay, TimeUnit.MILLISECONDS);
+ Scheduler.Task oldTimeout = _timeout.getAndSet(newTimeout);
+ if (oldTimeout != null)
+ oldTimeout.cancel();
+ }
+
+ public void onOpen()
+ {
+ activate();
+ }
+
+ private void activate()
+ {
+ if (_idleTimeout > 0)
+ idleCheck();
+ }
+
+ public void onClose()
+ {
+ deactivate();
+ }
+
+ private void deactivate()
+ {
+ Scheduler.Task oldTimeout = _timeout.getAndSet(null);
+ if (oldTimeout != null)
+ oldTimeout.cancel();
+ }
+
+ protected long checkIdleTimeout()
+ {
+ if (isOpen())
+ {
+ long idleTimestamp = _idleTimestamp;
+ long idleElapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - idleTimestamp);
+ long idleTimeout = getIdleTimeout();
+ long idleLeft = idleTimeout - idleElapsed;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} idle timeout check, elapsed: {} ms, remaining: {} ms", this, idleElapsed, idleLeft);
+
+ if (idleTimeout > 0)
+ {
+ if (idleLeft <= 0)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} idle timeout expired", this);
+ try
+ {
+ onIdleExpired(new TimeoutException("Idle timeout expired: " + idleElapsed + "/" + idleTimeout + " ms"));
+ }
+ finally
+ {
+ notIdle();
+ }
+ }
+ }
+
+ return idleLeft >= 0 ? idleLeft : 0;
+ }
+ return -1;
+ }
+
+ /**
+ * This abstract method is called when the idle timeout has expired.
+ *
+ * @param timeout a TimeoutException
+ */
+ protected abstract void onIdleExpired(TimeoutException timeout);
+
+ /**
+ * This abstract method should be called to check if idle timeouts
+ * should still be checked.
+ *
+ * @return True if the entity monitored should still be checked for idle timeouts
+ */
+ public abstract boolean isOpen();
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/IncludeExcludeConnectionStatistics.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/IncludeExcludeConnectionStatistics.java
new file mode 100644
index 0000000..466168e
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/IncludeExcludeConnectionStatistics.java
@@ -0,0 +1,114 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.util.AbstractSet;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import org.eclipse.jetty.util.IncludeExcludeSet;
+
+public class IncludeExcludeConnectionStatistics extends ConnectionStatistics
+{
+ private final IncludeExcludeSet<Class<? extends Connection>, Connection> _set = new IncludeExcludeSet<>(ConnectionSet.class);
+
+ public void include(String className) throws ClassNotFoundException
+ {
+ _set.include(connectionForName(className));
+ }
+
+ public void include(Class<? extends Connection> clazz)
+ {
+ _set.include(clazz);
+ }
+
+ public void exclude(String className) throws ClassNotFoundException
+ {
+ _set.exclude(connectionForName(className));
+ }
+
+ public void exclude(Class<? extends Connection> clazz)
+ {
+ _set.exclude(clazz);
+ }
+
+ private Class<? extends Connection> connectionForName(String className) throws ClassNotFoundException
+ {
+ Class<?> aClass = Class.forName(className);
+ if (!Connection.class.isAssignableFrom(aClass))
+ throw new IllegalArgumentException("Class is not a Connection");
+
+ @SuppressWarnings("unchecked")
+ Class<? extends Connection> connectionClass = (Class<? extends Connection>)aClass;
+ return connectionClass;
+ }
+
+ @Override
+ public void onOpened(Connection connection)
+ {
+ if (_set.test(connection))
+ super.onOpened(connection);
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ if (_set.test(connection))
+ super.onClosed(connection);
+ }
+
+ public static class ConnectionSet extends AbstractSet<Class<? extends Connection>> implements Predicate<Connection>
+ {
+ private final Set<Class<? extends Connection>> set = new HashSet<>();
+
+ @Override
+ public boolean add(Class<? extends Connection> aClass)
+ {
+ return set.add(aClass);
+ }
+
+ @Override
+ public boolean remove(Object o)
+ {
+ return set.remove(o);
+ }
+
+ @Override
+ public Iterator<Class<? extends Connection>> iterator()
+ {
+ return set.iterator();
+ }
+
+ @Override
+ public int size()
+ {
+ return set.size();
+ }
+
+ @Override
+ public boolean test(Connection connection)
+ {
+ if (connection == null)
+ return false;
+ return set.stream().anyMatch(c -> c.isAssignableFrom(connection.getClass()));
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/LeakTrackingByteBufferPool.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/LeakTrackingByteBufferPool.java
new file mode 100644
index 0000000..9258388
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/LeakTrackingByteBufferPool.java
@@ -0,0 +1,161 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.LeakDetector;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+@ManagedObject
+public class LeakTrackingByteBufferPool extends ContainerLifeCycle implements ByteBufferPool
+{
+ private static final Logger LOG = Log.getLogger(LeakTrackingByteBufferPool.class);
+
+ private final LeakDetector<ByteBuffer> leakDetector = new LeakDetector<ByteBuffer>()
+ {
+ @Override
+ public String id(ByteBuffer resource)
+ {
+ return BufferUtil.toIDString(resource);
+ }
+
+ @Override
+ protected void leaked(LeakInfo leakInfo)
+ {
+ leaked.incrementAndGet();
+ LeakTrackingByteBufferPool.this.leaked(leakInfo);
+ }
+ };
+
+ private final AtomicLong leakedAcquires = new AtomicLong(0);
+ private final AtomicLong leakedReleases = new AtomicLong(0);
+ private final AtomicLong leakedRemoves = new AtomicLong(0);
+ private final AtomicLong leaked = new AtomicLong(0);
+ private final ByteBufferPool delegate;
+
+ public LeakTrackingByteBufferPool(ByteBufferPool delegate)
+ {
+ this.delegate = delegate;
+ addBean(leakDetector);
+ addBean(delegate);
+ }
+
+ @Override
+ public ByteBuffer acquire(int size, boolean direct)
+ {
+ ByteBuffer buffer = delegate.acquire(size, direct);
+ boolean acquired = leakDetector.acquired(buffer);
+ if (!acquired)
+ {
+ leakedAcquires.incrementAndGet();
+ if (LOG.isDebugEnabled())
+ LOG.debug("ByteBuffer leaked acquire for id {}", leakDetector.id(buffer), new Throwable("acquire"));
+ }
+ return buffer;
+ }
+
+ @Override
+ public void release(ByteBuffer buffer)
+ {
+ if (buffer == null)
+ return;
+ boolean released = leakDetector.released(buffer);
+ if (!released)
+ {
+ leakedReleases.incrementAndGet();
+ if (LOG.isDebugEnabled())
+ LOG.debug("ByteBuffer leaked release for id {}", leakDetector.id(buffer), new Throwable("release"));
+ }
+ delegate.release(buffer);
+ }
+
+ @Override
+ public void remove(ByteBuffer buffer)
+ {
+ if (buffer == null)
+ return;
+ boolean released = leakDetector.released(buffer);
+ if (!released)
+ {
+ leakedRemoves.incrementAndGet();
+ if (LOG.isDebugEnabled())
+ LOG.debug("ByteBuffer leaked remove for id {}", leakDetector.id(buffer), new Throwable("remove"));
+ }
+ delegate.remove(buffer);
+ }
+
+ /**
+ * Clears the tracking data returned by {@link #getLeakedAcquires()},
+ * {@link #getLeakedReleases()}, {@link #getLeakedResources()}.
+ */
+ @ManagedAttribute("Clears the tracking data")
+ public void clearTracking()
+ {
+ leakedAcquires.set(0);
+ leakedReleases.set(0);
+ }
+
+ /**
+ * @return count of ByteBufferPool.acquire() calls that detected a leak
+ */
+ @ManagedAttribute("The number of acquires that produced a leak")
+ public long getLeakedAcquires()
+ {
+ return leakedAcquires.get();
+ }
+
+ /**
+ * @return count of ByteBufferPool.release() calls that detected a leak
+ */
+ @ManagedAttribute("The number of releases that produced a leak")
+ public long getLeakedReleases()
+ {
+ return leakedReleases.get();
+ }
+
+ /**
+ * @return count of ByteBufferPool.remove() calls that detected a leak
+ */
+ @ManagedAttribute("The number of removes that produced a leak")
+ public long getLeakedRemoves()
+ {
+ return leakedRemoves.get();
+ }
+
+ /**
+ * @return count of resources that were acquired but not released
+ */
+ @ManagedAttribute("The number of resources that were leaked")
+ public long getLeakedResources()
+ {
+ return leaked.get();
+ }
+
+ protected void leaked(LeakDetector<ByteBuffer>.LeakInfo leakInfo)
+ {
+ LOG.warn("ByteBuffer " + leakInfo.getResourceDescription() + " leaked at:", leakInfo.getStackFrames());
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java
new file mode 100644
index 0000000..4d6fcf7
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java
@@ -0,0 +1,1114 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.SocketTimeoutException;
+import java.nio.channels.CancelledKeyException;
+import java.nio.channels.ClosedSelectorException;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.component.DumpableCollection;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.statistic.SampleStatistic;
+import org.eclipse.jetty.util.thread.ExecutionStrategy;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.eclipse.jetty.util.thread.strategy.EatWhatYouKill;
+
+/**
+ * <p>{@link ManagedSelector} wraps a {@link Selector} simplifying non-blocking operations on channels.</p>
+ * <p>{@link ManagedSelector} runs the select loop, which waits on {@link Selector#select()} until events
+ * happen for registered channels. When events happen, it notifies the {@link EndPoint} associated
+ * with the channel.</p>
+ */
+public class ManagedSelector extends ContainerLifeCycle implements Dumpable
+{
+ private static final Logger LOG = Log.getLogger(ManagedSelector.class);
+ private static final boolean FORCE_SELECT_NOW;
+
+ static
+ {
+ String property = System.getProperty("org.eclipse.jetty.io.forceSelectNow");
+ if (property != null)
+ {
+ FORCE_SELECT_NOW = Boolean.parseBoolean(property);
+ }
+ else
+ {
+ property = System.getProperty("os.name");
+ FORCE_SELECT_NOW = property != null && property.toLowerCase(Locale.ENGLISH).contains("windows");
+ }
+ }
+
+ private final AtomicBoolean _started = new AtomicBoolean(false);
+ private boolean _selecting;
+ private final SelectorManager _selectorManager;
+ private final int _id;
+ private final ExecutionStrategy _strategy;
+ private Selector _selector;
+ private Deque<SelectorUpdate> _updates = new ArrayDeque<>();
+ private Deque<SelectorUpdate> _updateable = new ArrayDeque<>();
+ private final SampleStatistic _keyStats = new SampleStatistic();
+
+ public ManagedSelector(SelectorManager selectorManager, int id)
+ {
+ _selectorManager = selectorManager;
+ _id = id;
+ SelectorProducer producer = new SelectorProducer();
+ Executor executor = selectorManager.getExecutor();
+ _strategy = new EatWhatYouKill(producer, executor);
+ addBean(_strategy, true);
+ setStopTimeout(5000);
+ }
+
+ public Selector getSelector()
+ {
+ return _selector;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+
+ _selector = _selectorManager.newSelector();
+
+ // The producer used by the strategies will never
+ // be idle (either produces a task or blocks).
+
+ // The normal strategy obtains the produced task, schedules
+ // a new thread to produce more, runs the task and then exits.
+ _selectorManager.execute(_strategy::produce);
+
+ // Set started only if we really are started
+ Start start = new Start();
+ submit(start);
+ start._started.await();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ // doStop might be called for a failed managedSelector,
+ // We do not want to wait twice, so we only stop once for each start
+ if (_started.compareAndSet(true, false) && _selector != null)
+ {
+ // Close connections, but only wait a single selector cycle for it to take effect
+ CloseConnections closeConnections = new CloseConnections();
+ submit(closeConnections);
+ closeConnections._complete.await();
+
+ // Wait for any remaining endpoints to be closed and the selector to be stopped
+ StopSelector stopSelector = new StopSelector();
+ submit(stopSelector);
+ stopSelector._stopped.await();
+ }
+
+ super.doStop();
+ }
+
+ @ManagedAttribute(value = "Total number of keys", readonly = true)
+ public int getTotalKeys()
+ {
+ return _selector.keys().size();
+ }
+
+ @ManagedAttribute(value = "Average number of selected keys", readonly = true)
+ public double getAverageSelectedKeys()
+ {
+ return _keyStats.getMean();
+ }
+
+ @ManagedAttribute(value = "Maximum number of selected keys", readonly = true)
+ public double getMaxSelectedKeys()
+ {
+ return _keyStats.getMax();
+ }
+
+ @ManagedAttribute(value = "Total number of select() calls", readonly = true)
+ public long getSelectCount()
+ {
+ return _keyStats.getCount();
+ }
+
+ @ManagedOperation(value = "Resets the statistics", impact = "ACTION")
+ public void resetStats()
+ {
+ _keyStats.reset();
+ }
+
+ protected int nioSelect(Selector selector, boolean now) throws IOException
+ {
+ return now ? selector.selectNow() : selector.select();
+ }
+
+ protected int select(Selector selector) throws IOException
+ {
+ try
+ {
+ int selected = nioSelect(selector, false);
+ if (selected == 0)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Selector {} woken with none selected", selector);
+
+ if (Thread.interrupted() && !isRunning())
+ throw new ClosedSelectorException();
+
+ if (FORCE_SELECT_NOW)
+ selected = nioSelect(selector, true);
+ }
+ return selected;
+ }
+ catch (ClosedSelectorException x)
+ {
+ throw x;
+ }
+ catch (Throwable x)
+ {
+ handleSelectFailure(selector, x);
+ return 0;
+ }
+ }
+
+ protected void handleSelectFailure(Selector selector, Throwable failure) throws IOException
+ {
+ LOG.info("Caught select() failure, trying to recover: {}", failure.toString());
+ if (LOG.isDebugEnabled())
+ LOG.debug(failure);
+
+ Selector newSelector = _selectorManager.newSelector();
+ for (SelectionKey oldKey : selector.keys())
+ {
+ SelectableChannel channel = oldKey.channel();
+ int interestOps = safeInterestOps(oldKey);
+ if (interestOps >= 0)
+ {
+ try
+ {
+ Object attachment = oldKey.attachment();
+ SelectionKey newKey = channel.register(newSelector, interestOps, attachment);
+ if (attachment instanceof Selectable)
+ ((Selectable)attachment).replaceKey(newKey);
+ oldKey.cancel();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Transferred {} iOps={} att={}", channel, interestOps, attachment);
+ }
+ catch (Throwable t)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Could not transfer {}", channel, t);
+ IO.close(channel);
+ }
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Invalid interestOps for {}", channel);
+ IO.close(channel);
+ }
+ }
+
+ IO.close(selector);
+ _selector = newSelector;
+ }
+
+ protected void onSelectFailed(Throwable cause)
+ {
+ // override to change behavior
+ }
+
+ public int size()
+ {
+ Selector s = _selector;
+ if (s == null)
+ return 0;
+ Set<SelectionKey> keys = s.keys();
+ if (keys == null)
+ return 0;
+ return keys.size();
+ }
+
+ /**
+ * Submit an {@link SelectorUpdate} to be acted on between calls to {@link Selector#select()}
+ *
+ * @param update The selector update to apply at next wakeup
+ */
+ public void submit(SelectorUpdate update)
+ {
+ submit(update, false);
+ }
+
+ private void submit(SelectorUpdate update, boolean lazy)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Queued change lazy={} {} on {}", lazy, update, this);
+
+ Selector selector = null;
+ synchronized (ManagedSelector.this)
+ {
+ _updates.offer(update);
+
+ if (_selecting && !lazy)
+ {
+ selector = _selector;
+ // To avoid the extra select wakeup.
+ _selecting = false;
+ }
+ }
+
+ if (selector != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Wakeup on submit {}", this);
+ selector.wakeup();
+ }
+ }
+
+ private void wakeup()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Wakeup {}", this);
+
+ Selector selector = null;
+ synchronized (ManagedSelector.this)
+ {
+ if (_selecting)
+ {
+ selector = _selector;
+ _selecting = false;
+ }
+ }
+
+ if (selector != null)
+ selector.wakeup();
+ }
+
+ private void execute(Runnable task)
+ {
+ try
+ {
+ _selectorManager.execute(task);
+ }
+ catch (RejectedExecutionException x)
+ {
+ if (task instanceof Closeable)
+ IO.close((Closeable)task);
+ }
+ }
+
+ private void processConnect(SelectionKey key, Connect connect)
+ {
+ SelectableChannel channel = key.channel();
+ try
+ {
+ key.attach(connect.attachment);
+ boolean connected = _selectorManager.doFinishConnect(channel);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Connected {} {}", connected, channel);
+ if (connected)
+ {
+ if (connect.timeout.cancel())
+ {
+ key.interestOps(0);
+ execute(new CreateEndPoint(connect, key));
+ }
+ else
+ {
+ throw new SocketTimeoutException("Concurrent Connect Timeout");
+ }
+ }
+ else
+ {
+ throw new ConnectException();
+ }
+ }
+ catch (Throwable x)
+ {
+ connect.failed(x);
+ }
+ }
+
+ protected void endPointOpened(EndPoint endPoint)
+ {
+ _selectorManager.endPointOpened(endPoint);
+ }
+
+ protected void endPointClosed(EndPoint endPoint)
+ {
+ _selectorManager.endPointClosed(endPoint);
+ }
+
+ private void createEndPoint(SelectableChannel channel, SelectionKey selectionKey) throws IOException
+ {
+ EndPoint endPoint = _selectorManager.newEndPoint(channel, this, selectionKey);
+ Connection connection = _selectorManager.newConnection(channel, endPoint, selectionKey.attachment());
+ endPoint.setConnection(connection);
+ submit(selector ->
+ {
+ SelectionKey key = selectionKey;
+ if (key.selector() != selector)
+ {
+ key = channel.keyFor(selector);
+ if (key != null && endPoint instanceof Selectable)
+ ((Selectable)endPoint).replaceKey(key);
+ }
+ if (key != null)
+ key.attach(endPoint);
+ }, true);
+ endPoint.onOpen();
+ endPointOpened(endPoint);
+ _selectorManager.connectionOpened(connection);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Created {}", endPoint);
+ }
+
+ void destroyEndPoint(EndPoint endPoint)
+ {
+ // Waking up the selector is necessary to clean the
+ // cancelled-key set and tell the TCP stack that the
+ // socket is closed (so that senders receive RST).
+ wakeup();
+ execute(new DestroyEndPoint(endPoint));
+ }
+
+ private int getActionSize()
+ {
+ synchronized (ManagedSelector.this)
+ {
+ return _updates.size();
+ }
+ }
+
+ static int safeReadyOps(SelectionKey selectionKey)
+ {
+ try
+ {
+ return selectionKey.readyOps();
+ }
+ catch (Throwable x)
+ {
+ LOG.ignore(x);
+ return -1;
+ }
+ }
+
+ static int safeInterestOps(SelectionKey selectionKey)
+ {
+ try
+ {
+ return selectionKey.interestOps();
+ }
+ catch (Throwable x)
+ {
+ LOG.ignore(x);
+ return -1;
+ }
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ List<String> keys;
+ List<SelectorUpdate> updates;
+ Selector selector = _selector;
+ if (selector != null && selector.isOpen())
+ {
+ DumpKeys dump = new DumpKeys();
+ String updatesAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now());
+ synchronized (ManagedSelector.this)
+ {
+ updates = new ArrayList<>(_updates);
+ _updates.addFirst(dump);
+ _selecting = false;
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("wakeup on dump {}", this);
+ selector.wakeup();
+ keys = dump.get(5, TimeUnit.SECONDS);
+ String keysAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now());
+ if (keys == null)
+ keys = Collections.singletonList("No dump keys retrieved");
+
+ dumpObjects(out, indent,
+ new DumpableCollection("updates @ " + updatesAt, updates),
+ new DumpableCollection("keys @ " + keysAt, keys));
+ }
+ else
+ {
+ dumpObjects(out, indent);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ Selector selector = _selector;
+ return String.format("%s id=%s keys=%d selected=%d updates=%d",
+ super.toString(),
+ _id,
+ selector != null && selector.isOpen() ? selector.keys().size() : -1,
+ selector != null && selector.isOpen() ? selector.selectedKeys().size() : -1,
+ getActionSize());
+ }
+
+ /**
+ * A {@link Selectable} is an {@link EndPoint} that wish to be
+ * notified of non-blocking events by the {@link ManagedSelector}.
+ */
+ public interface Selectable
+ {
+ /**
+ * Callback method invoked when a read or write events has been
+ * detected by the {@link ManagedSelector} for this endpoint.
+ *
+ * @return a job that may block or null
+ */
+ Runnable onSelected();
+
+ /**
+ * Callback method invoked when all the keys selected by the
+ * {@link ManagedSelector} for this endpoint have been processed.
+ */
+ void updateKey();
+
+ /**
+ * Callback method invoked when the SelectionKey is replaced
+ * because the channel has been moved to a new selector.
+ *
+ * @param newKey the new SelectionKey
+ */
+ void replaceKey(SelectionKey newKey);
+ }
+
+ private class SelectorProducer implements ExecutionStrategy.Producer
+ {
+ private Set<SelectionKey> _keys = Collections.emptySet();
+ private Iterator<SelectionKey> _cursor = Collections.emptyIterator();
+
+ @Override
+ public Runnable produce()
+ {
+ while (true)
+ {
+ Runnable task = processSelected();
+ if (task != null)
+ return task;
+
+ processUpdates();
+
+ updateKeys();
+
+ if (!select())
+ return null;
+ }
+ }
+
+ private void processUpdates()
+ {
+ synchronized (ManagedSelector.this)
+ {
+ Deque<SelectorUpdate> updates = _updates;
+ _updates = _updateable;
+ _updateable = updates;
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("updateable {}", _updateable.size());
+
+ for (SelectorUpdate update : _updateable)
+ {
+ if (_selector == null)
+ break;
+ try
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("update {}", update);
+ update.update(_selector);
+ }
+ catch (Throwable ex)
+ {
+ LOG.warn(ex);
+ }
+ }
+ _updateable.clear();
+
+ Selector selector;
+ int updates;
+ synchronized (ManagedSelector.this)
+ {
+ updates = _updates.size();
+ _selecting = updates == 0;
+ selector = _selecting ? null : _selector;
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("updates {}", updates);
+
+ if (selector != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("wakeup on updates {}", this);
+ selector.wakeup();
+ }
+ }
+
+ private boolean select()
+ {
+ try
+ {
+ Selector selector = _selector;
+ if (selector != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Selector {} waiting with {} keys", selector, selector.keys().size());
+ int selected = ManagedSelector.this.select(selector);
+ // The selector may have been recreated.
+ selector = _selector;
+ if (selector != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Selector {} woken up from select, {}/{}/{} selected", selector, selected, selector.selectedKeys().size(), selector.keys().size());
+
+ int updates;
+ synchronized (ManagedSelector.this)
+ {
+ // finished selecting
+ _selecting = false;
+ updates = _updates.size();
+ }
+
+ _keys = selector.selectedKeys();
+ int selectedKeys = _keys.size();
+ if (selectedKeys > 0)
+ _keyStats.record(selectedKeys);
+ _cursor = selectedKeys > 0 ? _keys.iterator() : Collections.emptyIterator();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Selector {} processing {} keys, {} updates", selector, selectedKeys, updates);
+
+ return true;
+ }
+ }
+ }
+ catch (Throwable x)
+ {
+ IO.close(_selector);
+ _selector = null;
+
+ if (isRunning())
+ {
+ LOG.warn("Fatal select() failure", x);
+ onSelectFailed(x);
+ }
+ else
+ {
+ LOG.warn(x.toString());
+ if (LOG.isDebugEnabled())
+ LOG.debug(x);
+ }
+ }
+ return false;
+ }
+
+ private Runnable processSelected()
+ {
+ while (_cursor.hasNext())
+ {
+ SelectionKey key = _cursor.next();
+ Object attachment = key.attachment();
+ SelectableChannel channel = key.channel();
+ if (key.isValid())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("selected {} {} {} ", safeReadyOps(key), key, attachment);
+ try
+ {
+ if (attachment instanceof Selectable)
+ {
+ // Try to produce a task
+ Runnable task = ((Selectable)attachment).onSelected();
+ if (task != null)
+ return task;
+ }
+ else if (key.isConnectable())
+ {
+ processConnect(key, (Connect)attachment);
+ }
+ else
+ {
+ throw new IllegalStateException("key=" + key + ", att=" + attachment + ", iOps=" + safeInterestOps(key) + ", rOps=" + safeReadyOps(key));
+ }
+ }
+ catch (CancelledKeyException x)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Ignoring cancelled key for channel {}", channel);
+ IO.close(attachment instanceof EndPoint ? (EndPoint)attachment : channel);
+ }
+ catch (Throwable x)
+ {
+ LOG.warn("Could not process key for channel {}", channel, x);
+ IO.close(attachment instanceof EndPoint ? (EndPoint)attachment : channel);
+ }
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Selector loop ignoring invalid key for channel {}", channel);
+ IO.close(attachment instanceof EndPoint ? (EndPoint)attachment : channel);
+ }
+ }
+ return null;
+ }
+
+ private void updateKeys()
+ {
+ // Do update keys for only previously selected keys.
+ // This will update only those keys whose selection did not cause an
+ // updateKeys update to be submitted.
+ for (SelectionKey key : _keys)
+ {
+ Object attachment = key.attachment();
+ if (attachment instanceof Selectable)
+ ((Selectable)attachment).updateKey();
+ }
+ _keys.clear();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x", getClass().getSimpleName(), hashCode());
+ }
+ }
+
+ /**
+ * A selector update to be done when the selector has been woken.
+ */
+ public interface SelectorUpdate
+ {
+ void update(Selector selector);
+ }
+
+ private class Start implements SelectorUpdate
+ {
+ private final CountDownLatch _started = new CountDownLatch(1);
+
+ @Override
+ public void update(Selector selector)
+ {
+ ManagedSelector.this._started.set(true);
+ _started.countDown();
+ }
+ }
+
+ private static class DumpKeys implements SelectorUpdate
+ {
+ private final CountDownLatch latch = new CountDownLatch(1);
+ private List<String> keys;
+
+ @Override
+ public void update(Selector selector)
+ {
+ Set<SelectionKey> selectionKeys = selector.keys();
+ List<String> list = new ArrayList<>(selectionKeys.size());
+ for (SelectionKey key : selectionKeys)
+ {
+ if (key != null)
+ list.add(String.format("SelectionKey@%x{i=%d}->%s", key.hashCode(), safeInterestOps(key), key.attachment()));
+ }
+ keys = list;
+ latch.countDown();
+ }
+
+ public List<String> get(long timeout, TimeUnit unit)
+ {
+ try
+ {
+ latch.await(timeout, unit);
+ }
+ catch (InterruptedException x)
+ {
+ LOG.ignore(x);
+ }
+ return keys;
+ }
+ }
+
+ class Acceptor implements SelectorUpdate, Selectable, Closeable
+ {
+ private final SelectableChannel _channel;
+ private SelectionKey _key;
+
+ Acceptor(SelectableChannel channel)
+ {
+ _channel = channel;
+ }
+
+ @Override
+ public void update(Selector selector)
+ {
+ try
+ {
+ _key = _channel.register(selector, SelectionKey.OP_ACCEPT, this);
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} acceptor={}", this, _channel);
+ }
+ catch (Throwable x)
+ {
+ IO.close(_channel);
+ LOG.warn(x);
+ }
+ }
+
+ @Override
+ public Runnable onSelected()
+ {
+ SelectableChannel channel = null;
+ try
+ {
+ while (true)
+ {
+ channel = _selectorManager.doAccept(_channel);
+ if (channel == null)
+ break;
+ _selectorManager.accepted(channel);
+ }
+ }
+ catch (Throwable x)
+ {
+ LOG.warn("Accept failed for channel {}", channel, x);
+ IO.close(channel);
+ }
+ return null;
+ }
+
+ @Override
+ public void updateKey()
+ {
+ }
+
+ @Override
+ public void replaceKey(SelectionKey newKey)
+ {
+ _key = newKey;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ // May be called from any thread.
+ // Implements AbstractConnector.setAccepting(boolean).
+ submit(selector -> _key.cancel());
+ }
+ }
+
+ class Accept implements SelectorUpdate, Runnable, Closeable
+ {
+ private final SelectableChannel channel;
+ private final Object attachment;
+ private SelectionKey key;
+
+ Accept(SelectableChannel channel, Object attachment)
+ {
+ this.channel = channel;
+ this.attachment = attachment;
+ _selectorManager.onAccepting(channel);
+ }
+
+ @Override
+ public void close()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("closed accept of {}", channel);
+ IO.close(channel);
+ }
+
+ @Override
+ public void update(Selector selector)
+ {
+ try
+ {
+ key = channel.register(selector, 0, attachment);
+ execute(this);
+ }
+ catch (Throwable x)
+ {
+ IO.close(channel);
+ _selectorManager.onAcceptFailed(channel, x);
+ if (LOG.isDebugEnabled())
+ LOG.debug(x);
+ }
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ createEndPoint(channel, key);
+ _selectorManager.onAccepted(channel);
+ }
+ catch (Throwable x)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(x);
+ failed(x);
+ }
+ }
+
+ protected void failed(Throwable failure)
+ {
+ IO.close(channel);
+ LOG.warn(String.valueOf(failure));
+ if (LOG.isDebugEnabled())
+ LOG.debug(failure);
+ _selectorManager.onAcceptFailed(channel, failure);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), channel);
+ }
+ }
+
+ class Connect implements SelectorUpdate, Runnable
+ {
+ private final AtomicBoolean failed = new AtomicBoolean();
+ private final SelectableChannel channel;
+ private final Object attachment;
+ private final Scheduler.Task timeout;
+
+ Connect(SelectableChannel channel, Object attachment)
+ {
+ this.channel = channel;
+ this.attachment = attachment;
+ long timeout = ManagedSelector.this._selectorManager.getConnectTimeout();
+ if (timeout > 0)
+ this.timeout = ManagedSelector.this._selectorManager.getScheduler().schedule(this, timeout, TimeUnit.MILLISECONDS);
+ else
+ this.timeout = null;
+ }
+
+ @Override
+ public void update(Selector selector)
+ {
+ try
+ {
+ channel.register(selector, SelectionKey.OP_CONNECT, this);
+ }
+ catch (Throwable x)
+ {
+ failed(x);
+ }
+ }
+
+ @Override
+ public void run()
+ {
+ if (_selectorManager.isConnectionPending(channel))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Channel {} timed out while connecting, closing it", channel);
+ failed(new SocketTimeoutException("Connect Timeout"));
+ }
+ }
+
+ public void failed(Throwable failure)
+ {
+ if (failed.compareAndSet(false, true))
+ {
+ if (timeout != null)
+ timeout.cancel();
+ IO.close(channel);
+ ManagedSelector.this._selectorManager.connectionFailed(channel, failure, attachment);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("Connect@%x{%s,%s}", hashCode(), channel, attachment);
+ }
+ }
+
+ private class CloseConnections implements SelectorUpdate
+ {
+ private final Set<Closeable> _closed;
+ private final CountDownLatch _complete = new CountDownLatch(1);
+
+ private CloseConnections()
+ {
+ this(null);
+ }
+
+ private CloseConnections(Set<Closeable> closed)
+ {
+ _closed = closed;
+ }
+
+ @Override
+ public void update(Selector selector)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Closing {} connections on {}", selector.keys().size(), ManagedSelector.this);
+ for (SelectionKey key : selector.keys())
+ {
+ if (key != null && key.isValid())
+ {
+ Closeable closeable = null;
+ Object attachment = key.attachment();
+ if (attachment instanceof EndPoint)
+ {
+ EndPoint endPoint = (EndPoint)attachment;
+ Connection connection = endPoint.getConnection();
+ if (connection != null)
+ closeable = connection;
+ else
+ closeable = endPoint;
+ }
+
+ if (closeable != null)
+ {
+ if (_closed == null)
+ {
+ IO.close(closeable);
+ }
+ else if (!_closed.contains(closeable))
+ {
+ _closed.add(closeable);
+ IO.close(closeable);
+ }
+ }
+ }
+ }
+ _complete.countDown();
+ }
+ }
+
+ private class StopSelector implements SelectorUpdate
+ {
+ private final CountDownLatch _stopped = new CountDownLatch(1);
+
+ @Override
+ public void update(Selector selector)
+ {
+ for (SelectionKey key : selector.keys())
+ {
+ // Key may be null when using the UnixSocket selector.
+ if (key == null)
+ continue;
+ Object attachment = key.attachment();
+ if (attachment instanceof Closeable)
+ IO.close((Closeable)attachment);
+ }
+ _selector = null;
+ IO.close(selector);
+ _stopped.countDown();
+ }
+ }
+
+ private final class CreateEndPoint implements Runnable
+ {
+ private final Connect _connect;
+ private final SelectionKey _key;
+
+ private CreateEndPoint(Connect connect, SelectionKey key)
+ {
+ _connect = connect;
+ _key = key;
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ createEndPoint(_connect.channel, _key);
+ }
+ catch (Throwable failure)
+ {
+ IO.close(_connect.channel);
+ LOG.warn(String.valueOf(failure));
+ if (LOG.isDebugEnabled())
+ LOG.debug(failure);
+ _connect.failed(failure);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("CreateEndPoint@%x{%s}", hashCode(), _connect);
+ }
+ }
+
+ private class DestroyEndPoint implements Runnable, Closeable
+ {
+ private final EndPoint endPoint;
+
+ public DestroyEndPoint(EndPoint endPoint)
+ {
+ this.endPoint = endPoint;
+ }
+
+ @Override
+ public void run()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Destroyed {}", endPoint);
+ Connection connection = endPoint.getConnection();
+ if (connection != null)
+ _selectorManager.connectionClosed(connection);
+ ManagedSelector.this.endPointClosed(endPoint);
+ }
+
+ @Override
+ public void close()
+ {
+ run();
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/MappedByteBufferPool.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/MappedByteBufferPool.java
new file mode 100644
index 0000000..493b15e
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/MappedByteBufferPool.java
@@ -0,0 +1,234 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * <p>A ByteBuffer pool where ByteBuffers are held in queues that are held in a Map.</p>
+ * <p>Given a capacity {@code factor} of 1024, the Map entry with key {@code 1} holds a
+ * queue of ByteBuffers each of capacity 1024, the Map entry with key {@code 2} holds a
+ * queue of ByteBuffers each of capacity 2048, and so on.</p>
+ */
+@ManagedObject
+public class MappedByteBufferPool extends AbstractByteBufferPool
+{
+ private static final Logger LOG = Log.getLogger(MappedByteBufferPool.class);
+
+ private final ConcurrentMap<Integer, Bucket> _directBuffers = new ConcurrentHashMap<>();
+ private final ConcurrentMap<Integer, Bucket> _heapBuffers = new ConcurrentHashMap<>();
+ private final Function<Integer, Bucket> _newBucket;
+
+ /**
+ * Creates a new MappedByteBufferPool with a default configuration.
+ */
+ public MappedByteBufferPool()
+ {
+ this(-1);
+ }
+
+ /**
+ * Creates a new MappedByteBufferPool with the given capacity factor.
+ *
+ * @param factor the capacity factor
+ */
+ public MappedByteBufferPool(int factor)
+ {
+ this(factor, -1);
+ }
+
+ /**
+ * Creates a new MappedByteBufferPool with the given configuration.
+ *
+ * @param factor the capacity factor
+ * @param maxQueueLength the maximum ByteBuffer queue length
+ */
+ public MappedByteBufferPool(int factor, int maxQueueLength)
+ {
+ this(factor, maxQueueLength, null);
+ }
+
+ /**
+ * Creates a new MappedByteBufferPool with the given configuration.
+ *
+ * @param factor the capacity factor
+ * @param maxQueueLength the maximum ByteBuffer queue length
+ * @param newBucket the function that creates a Bucket
+ */
+ public MappedByteBufferPool(int factor, int maxQueueLength, Function<Integer, Bucket> newBucket)
+ {
+ this(factor, maxQueueLength, newBucket, -1, -1);
+ }
+
+ /**
+ * Creates a new MappedByteBufferPool with the given configuration.
+ *
+ * @param factor the capacity factor
+ * @param maxQueueLength the maximum ByteBuffer queue length
+ * @param newBucket the function that creates a Bucket
+ * @param maxHeapMemory the max heap memory in bytes
+ * @param maxDirectMemory the max direct memory in bytes
+ */
+ public MappedByteBufferPool(int factor, int maxQueueLength, Function<Integer, Bucket> newBucket, long maxHeapMemory, long maxDirectMemory)
+ {
+ super(factor, maxQueueLength, maxHeapMemory, maxDirectMemory);
+ _newBucket = newBucket != null ? newBucket : this::newBucket;
+ }
+
+ private Bucket newBucket(int key)
+ {
+ return new Bucket(this, key * getCapacityFactor(), getMaxQueueLength());
+ }
+
+ @Override
+ public ByteBuffer acquire(int size, boolean direct)
+ {
+ int b = bucketFor(size);
+ int capacity = b * getCapacityFactor();
+ ConcurrentMap<Integer, Bucket> buffers = bucketsFor(direct);
+ Bucket bucket = buffers.get(b);
+ if (bucket == null)
+ return newByteBuffer(capacity, direct);
+ ByteBuffer buffer = bucket.acquire();
+ if (buffer == null)
+ return newByteBuffer(capacity, direct);
+ decrementMemory(buffer);
+ return buffer;
+ }
+
+ @Override
+ public void release(ByteBuffer buffer)
+ {
+ if (buffer == null)
+ return; // nothing to do
+
+ int capacity = buffer.capacity();
+ // Validate that this buffer is from this pool.
+ if ((capacity % getCapacityFactor()) != 0)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("ByteBuffer {} does not belong to this pool, discarding it", BufferUtil.toDetailString(buffer));
+ return;
+ }
+
+ int b = bucketFor(capacity);
+ boolean direct = buffer.isDirect();
+ ConcurrentMap<Integer, Bucket> buckets = bucketsFor(direct);
+ Bucket bucket = buckets.computeIfAbsent(b, _newBucket);
+ bucket.release(buffer);
+ incrementMemory(buffer);
+ releaseExcessMemory(direct, this::clearOldestBucket);
+ }
+
+ @Override
+ public void clear()
+ {
+ super.clear();
+ _directBuffers.values().forEach(Bucket::clear);
+ _directBuffers.clear();
+ _heapBuffers.values().forEach(Bucket::clear);
+ _heapBuffers.clear();
+ }
+
+ private void clearOldestBucket(boolean direct)
+ {
+ long oldest = Long.MAX_VALUE;
+ int index = -1;
+ ConcurrentMap<Integer, Bucket> buckets = bucketsFor(direct);
+ for (Map.Entry<Integer, Bucket> entry : buckets.entrySet())
+ {
+ Bucket bucket = entry.getValue();
+ long lastUpdate = bucket.getLastUpdate();
+ if (lastUpdate < oldest)
+ {
+ oldest = lastUpdate;
+ index = entry.getKey();
+ }
+ }
+ if (index >= 0)
+ {
+ Bucket bucket = buckets.remove(index);
+ // The same bucket may be concurrently
+ // removed, so we need this null guard.
+ if (bucket != null)
+ bucket.clear(this::decrementMemory);
+ }
+ }
+
+ private int bucketFor(int size)
+ {
+ int factor = getCapacityFactor();
+ int bucket = size / factor;
+ if (bucket * factor != size)
+ ++bucket;
+ return bucket;
+ }
+
+ @ManagedAttribute("The number of pooled direct ByteBuffers")
+ public long getDirectByteBufferCount()
+ {
+ return getByteBufferCount(true);
+ }
+
+ @ManagedAttribute("The number of pooled heap ByteBuffers")
+ public long getHeapByteBufferCount()
+ {
+ return getByteBufferCount(false);
+ }
+
+ private long getByteBufferCount(boolean direct)
+ {
+ return bucketsFor(direct).values().stream()
+ .mapToLong(Bucket::size)
+ .sum();
+ }
+
+ // Package local for testing
+ ConcurrentMap<Integer, Bucket> bucketsFor(boolean direct)
+ {
+ return direct ? _directBuffers : _heapBuffers;
+ }
+
+ public static class Tagged extends MappedByteBufferPool
+ {
+ private final AtomicInteger tag = new AtomicInteger();
+
+ @Override
+ public ByteBuffer newByteBuffer(int capacity, boolean direct)
+ {
+ ByteBuffer buffer = super.newByteBuffer(capacity + 4, direct);
+ buffer.limit(buffer.capacity());
+ buffer.putInt(tag.incrementAndGet());
+ ByteBuffer slice = buffer.slice();
+ BufferUtil.clear(slice);
+ return slice;
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NegotiatingClientConnection.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NegotiatingClientConnection.java
new file mode 100644
index 0000000..b83b511
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NegotiatingClientConnection.java
@@ -0,0 +1,130 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import javax.net.ssl.SSLEngine;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public abstract class NegotiatingClientConnection extends AbstractConnection
+{
+ private static final Logger LOG = Log.getLogger(NegotiatingClientConnection.class);
+
+ private final SSLEngine engine;
+ private final ClientConnectionFactory connectionFactory;
+ private final Map<String, Object> context;
+ private volatile boolean completed;
+
+ protected NegotiatingClientConnection(EndPoint endp, Executor executor, SSLEngine sslEngine, ClientConnectionFactory connectionFactory, Map<String, Object> context)
+ {
+ super(endp, executor);
+ this.engine = sslEngine;
+ this.connectionFactory = connectionFactory;
+ this.context = context;
+ }
+
+ public SSLEngine getSSLEngine()
+ {
+ return engine;
+ }
+
+ protected void completed()
+ {
+ completed = true;
+ }
+
+ @Override
+ public void onOpen()
+ {
+ super.onOpen();
+ try
+ {
+ getEndPoint().flush(BufferUtil.EMPTY_BUFFER);
+ if (completed)
+ replaceConnection();
+ else
+ fillInterested();
+ }
+ catch (IOException x)
+ {
+ close();
+ throw new RuntimeIOException(x);
+ }
+ }
+
+ @Override
+ public void onFillable()
+ {
+ while (true)
+ {
+ int filled = fill();
+ if (completed || filled < 0)
+ {
+ replaceConnection();
+ break;
+ }
+ if (filled == 0)
+ {
+ fillInterested();
+ break;
+ }
+ }
+ }
+
+ private int fill()
+ {
+ try
+ {
+ return getEndPoint().fill(BufferUtil.EMPTY_BUFFER);
+ }
+ catch (IOException x)
+ {
+ LOG.debug(x);
+ close();
+ return -1;
+ }
+ }
+
+ private void replaceConnection()
+ {
+ EndPoint endPoint = getEndPoint();
+ try
+ {
+ endPoint.upgrade(connectionFactory.newConnection(endPoint, context));
+ }
+ catch (Throwable x)
+ {
+ LOG.debug(x);
+ close();
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ // Gentler close for SSL.
+ getEndPoint().shutdownOutput();
+ super.close();
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NegotiatingClientConnectionFactory.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NegotiatingClientConnectionFactory.java
new file mode 100644
index 0000000..ff867a2
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NegotiatingClientConnectionFactory.java
@@ -0,0 +1,34 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+public abstract class NegotiatingClientConnectionFactory implements ClientConnectionFactory
+{
+ private final ClientConnectionFactory connectionFactory;
+
+ protected NegotiatingClientConnectionFactory(ClientConnectionFactory connectionFactory)
+ {
+ this.connectionFactory = connectionFactory;
+ }
+
+ public ClientConnectionFactory getClientConnectionFactory()
+ {
+ return connectionFactory;
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficListener.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficListener.java
new file mode 100644
index 0000000..f9f2a11
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficListener.java
@@ -0,0 +1,114 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.net.Socket;
+import java.nio.ByteBuffer;
+
+/**
+ * <p>A listener for raw network traffic within Jetty.</p>
+ * <p>{@link NetworkTrafficListener}s can be installed in a
+ * {@code org.eclipse.jetty.server.NetworkTrafficServerConnector},
+ * and are notified of the following network traffic events:</p>
+ * <ul>
+ * <li>Connection opened, when the server has accepted the connection from a remote client</li>
+ * <li>Incoming bytes, when the server receives bytes sent from a remote client</li>
+ * <li>Outgoing bytes, when the server sends bytes to a remote client</li>
+ * <li>Connection closed, when the server has closed the connection to a remote client</li>
+ * </ul>
+ * <p>{@link NetworkTrafficListener}s can be used to log the network traffic viewed by
+ * a Jetty server (for example logging to filesystem) for activities such as debugging
+ * or request/response cycles or for replaying request/response cycles to other servers.</p>
+ */
+public interface NetworkTrafficListener
+{
+ /**
+ * <p>Callback method invoked when a connection from a remote client has been accepted.</p>
+ * <p>The {@code socket} parameter can be used to extract socket address information of
+ * the remote client.</p>
+ *
+ * @param socket the socket associated with the remote client
+ */
+ default void opened(Socket socket)
+ {
+ }
+
+ /**
+ * <p>Callback method invoked when bytes sent by a remote client arrived on the server.</p>
+ *
+ * @param socket the socket associated with the remote client
+ * @param bytes the read-only buffer containing the incoming bytes
+ */
+ default void incoming(Socket socket, ByteBuffer bytes)
+ {
+ }
+
+ /**
+ * <p>Callback method invoked when bytes are sent to a remote client from the server.</p>
+ * <p>This method is invoked after the bytes have been actually written to the remote client.</p>
+ *
+ * @param socket the socket associated with the remote client
+ * @param bytes the read-only buffer containing the outgoing bytes
+ */
+ default void outgoing(Socket socket, ByteBuffer bytes)
+ {
+ }
+
+ /**
+ * <p>Callback method invoked when a connection to a remote client has been closed.</p>
+ * <p>The {@code socket} parameter is already closed when this method is called, so it
+ * cannot be queried for socket address information of the remote client.<br>
+ * However, the {@code socket} parameter is the same object passed to {@link #opened(Socket)},
+ * so it is possible to map socket information in {@link #opened(Socket)} and retrieve it
+ * in this method.
+ *
+ * @param socket the (closed) socket associated with the remote client
+ */
+ default void closed(Socket socket)
+ {
+ }
+
+ /**
+ * <p>A commodity class that implements {@link NetworkTrafficListener} with empty methods.</p>
+ * @deprecated use {@link NetworkTrafficListener} instead
+ */
+ @Deprecated
+ class Adapter implements NetworkTrafficListener
+ {
+ @Override
+ public void opened(Socket socket)
+ {
+ }
+
+ @Override
+ public void incoming(Socket socket, ByteBuffer bytes)
+ {
+ }
+
+ @Override
+ public void outgoing(Socket socket, ByteBuffer bytes)
+ {
+ }
+
+ @Override
+ public void closed(Socket socket)
+ {
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSelectChannelEndPoint.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSelectChannelEndPoint.java
new file mode 100644
index 0000000..37e3126
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSelectChannelEndPoint.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.util.List;
+
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * @deprecated use {@link NetworkTrafficSocketChannelEndPoint} instead
+ */
+@Deprecated
+public class NetworkTrafficSelectChannelEndPoint extends NetworkTrafficSocketChannelEndPoint
+{
+ public NetworkTrafficSelectChannelEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key, Scheduler scheduler, long idleTimeout, List<NetworkTrafficListener> listeners)
+ {
+ super(channel, selectSet, key, scheduler, idleTimeout, listeners);
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSocketChannelEndPoint.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSocketChannelEndPoint.java
new file mode 100644
index 0000000..4dbca17
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSocketChannelEndPoint.java
@@ -0,0 +1,154 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.util.List;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * <p>A specialized version of {@link SocketChannelEndPoint} that supports {@link NetworkTrafficListener}s.</p>
+ */
+public class NetworkTrafficSocketChannelEndPoint extends SocketChannelEndPoint
+{
+ private static final Logger LOG = Log.getLogger(NetworkTrafficSocketChannelEndPoint.class);
+
+ private final List<NetworkTrafficListener> listeners;
+
+ public NetworkTrafficSocketChannelEndPoint(SelectableChannel channel, ManagedSelector selectSet, SelectionKey key, Scheduler scheduler, long idleTimeout, List<NetworkTrafficListener> listeners)
+ {
+ super(channel, selectSet, key, scheduler);
+ setIdleTimeout(idleTimeout);
+ this.listeners = listeners;
+ }
+
+ @Override
+ public int fill(ByteBuffer buffer) throws IOException
+ {
+ int read = super.fill(buffer);
+ notifyIncoming(buffer, read);
+ return read;
+ }
+
+ @Override
+ public boolean flush(ByteBuffer... buffers) throws IOException
+ {
+ boolean flushed = true;
+ for (ByteBuffer b : buffers)
+ {
+ if (b.hasRemaining())
+ {
+ int position = b.position();
+ ByteBuffer view = b.slice();
+ flushed = super.flush(b);
+ int l = b.position() - position;
+ view.limit(view.position() + l);
+ notifyOutgoing(view);
+ if (!flushed)
+ break;
+ }
+ }
+ return flushed;
+ }
+
+ @Override
+ public void onOpen()
+ {
+ super.onOpen();
+ if (listeners != null && !listeners.isEmpty())
+ {
+ for (NetworkTrafficListener listener : listeners)
+ {
+ try
+ {
+ listener.opened(getSocket());
+ }
+ catch (Exception x)
+ {
+ LOG.warn(x);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onClose()
+ {
+ super.onClose();
+ if (listeners != null && !listeners.isEmpty())
+ {
+ for (NetworkTrafficListener listener : listeners)
+ {
+ try
+ {
+ listener.closed(getSocket());
+ }
+ catch (Exception x)
+ {
+ LOG.warn(x);
+ }
+ }
+ }
+ }
+
+ public void notifyIncoming(ByteBuffer buffer, int read)
+ {
+ if (listeners != null && !listeners.isEmpty() && read > 0)
+ {
+ for (NetworkTrafficListener listener : listeners)
+ {
+ try
+ {
+ ByteBuffer view = buffer.asReadOnlyBuffer();
+ listener.incoming(getSocket(), view);
+ }
+ catch (Exception x)
+ {
+ LOG.warn(x);
+ }
+ }
+ }
+ }
+
+ public void notifyOutgoing(ByteBuffer view)
+ {
+ if (listeners != null && !listeners.isEmpty() && view.hasRemaining())
+ {
+ Socket socket = getSocket();
+ for (NetworkTrafficListener listener : listeners)
+ {
+ try
+ {
+ listener.outgoing(socket, view);
+ }
+ catch (Exception x)
+ {
+ LOG.warn(x);
+ }
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NullByteBufferPool.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NullByteBufferPool.java
new file mode 100644
index 0000000..00240fb
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/NullByteBufferPool.java
@@ -0,0 +1,41 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.BufferUtil;
+
+public class NullByteBufferPool implements ByteBufferPool
+{
+ @Override
+ public ByteBuffer acquire(int size, boolean direct)
+ {
+ if (direct)
+ return BufferUtil.allocateDirect(size);
+ else
+ return BufferUtil.allocate(size);
+ }
+
+ @Override
+ public void release(ByteBuffer buffer)
+ {
+ BufferUtil.clear(buffer);
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/QuietException.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/QuietException.java
new file mode 100644
index 0000000..8b15958
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/QuietException.java
@@ -0,0 +1,28 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+/**
+ * A Quiet Exception.
+ * <p> Exception classes that extend this interface will be logged
+ * less verbosely.
+ */
+public interface QuietException
+{
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java
new file mode 100644
index 0000000..a206e5d
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java
@@ -0,0 +1,109 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Retainable;
+
+/**
+ * A Retainable ByteBuffer.
+ * <p>Acquires a ByteBuffer from a {@link ByteBufferPool} and maintains a reference count that is
+ * initially 1, incremented with {@link #retain()} and decremented with {@link #release()}. The buffer
+ * is released to the pool when the reference count is decremented to 0.</p>
+ */
+public class RetainableByteBuffer implements Retainable
+{
+ private final ByteBufferPool pool;
+ private final ByteBuffer buffer;
+ private final AtomicInteger references;
+
+ public RetainableByteBuffer(ByteBufferPool pool, int size)
+ {
+ this(pool, size, false);
+ }
+
+ public RetainableByteBuffer(ByteBufferPool pool, int size, boolean direct)
+ {
+ this.pool = pool;
+ this.buffer = pool.acquire(size, direct);
+ this.references = new AtomicInteger(1);
+ }
+
+ public ByteBuffer getBuffer()
+ {
+ return buffer;
+ }
+
+ public int getReferences()
+ {
+ return references.get();
+ }
+
+ @Override
+ public void retain()
+ {
+ while (true)
+ {
+ int r = references.get();
+ if (r == 0)
+ throw new IllegalStateException("released " + this);
+ if (references.compareAndSet(r, r + 1))
+ break;
+ }
+ }
+
+ public int release()
+ {
+ int ref = references.decrementAndGet();
+ if (ref == 0)
+ pool.release(buffer);
+ else if (ref < 0)
+ throw new IllegalStateException("already released " + this);
+ return ref;
+ }
+
+ public int remaining()
+ {
+ return buffer.remaining();
+ }
+
+ public boolean hasRemaining()
+ {
+ return remaining() > 0;
+ }
+
+ public boolean isEmpty()
+ {
+ return !hasRemaining();
+ }
+
+ public void clear()
+ {
+ BufferUtil.clear(buffer);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{%s,r=%d}", getClass().getSimpleName(), hashCode(), BufferUtil.toDetailString(buffer), getReferences());
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/RuntimeIOException.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/RuntimeIOException.java
new file mode 100644
index 0000000..5d28d41
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/RuntimeIOException.java
@@ -0,0 +1,46 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+/**
+ * Subclass of {@link java.lang.RuntimeException} used to signal that there
+ * was an {@link java.io.IOException} thrown by underlying {@link java.io.Writer}
+ */
+public class RuntimeIOException extends RuntimeException
+{
+ public RuntimeIOException()
+ {
+ super();
+ }
+
+ public RuntimeIOException(String message)
+ {
+ super(message);
+ }
+
+ public RuntimeIOException(Throwable cause)
+ {
+ super(cause);
+ }
+
+ public RuntimeIOException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/SelectChannelEndPoint.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/SelectChannelEndPoint.java
new file mode 100644
index 0000000..08b8f98
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/SelectChannelEndPoint.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * @deprecated use {@link SocketChannelEndPoint} instead
+ */
+@Deprecated
+public class SelectChannelEndPoint extends SocketChannelEndPoint
+{
+ public SelectChannelEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler, long idleTimeout)
+ {
+ super(channel, selector, key, scheduler);
+ setIdleTimeout(idleTimeout);
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java
new file mode 100644
index 0000000..f619975
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java
@@ -0,0 +1,522 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EventListener;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.IntUnaryOperator;
+
+import org.eclipse.jetty.util.ProcessorUtils;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.eclipse.jetty.util.thread.ThreadPool;
+import org.eclipse.jetty.util.thread.ThreadPoolBudget;
+
+/**
+ * <p>{@link SelectorManager} manages a number of {@link ManagedSelector}s that
+ * simplify the non-blocking primitives provided by the JVM via the {@code java.nio} package.</p>
+ * <p>{@link SelectorManager} subclasses implement methods to return protocol-specific
+ * {@link EndPoint}s and {@link Connection}s.</p>
+ */
+
+@ManagedObject("Manager of the NIO Selectors")
+public abstract class SelectorManager extends ContainerLifeCycle implements Dumpable
+{
+ public static final int DEFAULT_CONNECT_TIMEOUT = 15000;
+ protected static final Logger LOG = Log.getLogger(SelectorManager.class);
+
+ private final Executor executor;
+ private final Scheduler scheduler;
+ private final ManagedSelector[] _selectors;
+ private final AtomicInteger _selectorIndex = new AtomicInteger();
+ private final IntUnaryOperator _selectorIndexUpdate;
+ private final List<AcceptListener> _acceptListeners = new ArrayList<>();
+ private long _connectTimeout = DEFAULT_CONNECT_TIMEOUT;
+ private ThreadPoolBudget.Lease _lease;
+
+ private static int defaultSelectors(Executor executor)
+ {
+ if (executor instanceof ThreadPool.SizedThreadPool)
+ {
+ int threads = ((ThreadPool.SizedThreadPool)executor).getMaxThreads();
+ int cpus = ProcessorUtils.availableProcessors();
+ return Math.max(1, Math.min(cpus / 2, threads / 16));
+ }
+ return Math.max(1, ProcessorUtils.availableProcessors() / 2);
+ }
+
+ protected SelectorManager(Executor executor, Scheduler scheduler)
+ {
+ this(executor, scheduler, -1);
+ }
+
+ /**
+ * @param executor The executor to use for handling selected {@link EndPoint}s
+ * @param scheduler The scheduler to use for timing events
+ * @param selectors The number of selectors to use, or -1 for a default derived
+ * from a heuristic over available CPUs and thread pool size.
+ */
+ protected SelectorManager(Executor executor, Scheduler scheduler, int selectors)
+ {
+ if (selectors <= 0)
+ selectors = defaultSelectors(executor);
+ this.executor = executor;
+ this.scheduler = scheduler;
+ _selectors = new ManagedSelector[selectors];
+ _selectorIndexUpdate = index -> (index + 1) % _selectors.length;
+ }
+
+ @ManagedAttribute("The Executor")
+ public Executor getExecutor()
+ {
+ return executor;
+ }
+
+ @ManagedAttribute("The Scheduler")
+ public Scheduler getScheduler()
+ {
+ return scheduler;
+ }
+
+ /**
+ * Get the connect timeout
+ *
+ * @return the connect timeout (in milliseconds)
+ */
+ @ManagedAttribute("The Connection timeout (ms)")
+ public long getConnectTimeout()
+ {
+ return _connectTimeout;
+ }
+
+ /**
+ * Set the connect timeout (in milliseconds)
+ *
+ * @param milliseconds the number of milliseconds for the timeout
+ */
+ public void setConnectTimeout(long milliseconds)
+ {
+ _connectTimeout = milliseconds;
+ }
+
+ /**
+ * @return -1
+ * @deprecated
+ */
+ @Deprecated
+ public int getReservedThreads()
+ {
+ return -1;
+ }
+
+ /**
+ * @param threads ignored
+ * @deprecated
+ */
+ @Deprecated
+ public void setReservedThreads(int threads)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Executes the given task in a different thread.
+ *
+ * @param task the task to execute
+ */
+ protected void execute(Runnable task)
+ {
+ executor.execute(task);
+ }
+
+ /**
+ * @return the number of selectors in use
+ */
+ @ManagedAttribute("The number of NIO Selectors")
+ public int getSelectorCount()
+ {
+ return _selectors.length;
+ }
+
+ private ManagedSelector chooseSelector()
+ {
+ return _selectors[_selectorIndex.updateAndGet(_selectorIndexUpdate)];
+ }
+
+ /**
+ * <p>Registers a channel to perform a non-blocking connect.</p>
+ * <p>The channel must be set in non-blocking mode, {@link SocketChannel#connect(SocketAddress)}
+ * must be called prior to calling this method, and the connect operation must not be completed
+ * (the return value of {@link SocketChannel#connect(SocketAddress)} must be false).</p>
+ *
+ * @param channel the channel to register
+ * @param attachment the attachment object
+ * @see #accept(SelectableChannel, Object)
+ */
+ public void connect(SelectableChannel channel, Object attachment)
+ {
+ ManagedSelector set = chooseSelector();
+ set.submit(set.new Connect(channel, attachment));
+ }
+
+ /**
+ * @param channel the channel to accept
+ * @see #accept(SelectableChannel, Object)
+ */
+ public void accept(SelectableChannel channel)
+ {
+ accept(channel, null);
+ }
+
+ /**
+ * <p>Registers a channel to perform non-blocking read/write operations.</p>
+ * <p>This method is called just after a channel has been accepted by {@link ServerSocketChannel#accept()},
+ * or just after having performed a blocking connect via {@link Socket#connect(SocketAddress, int)}, or
+ * just after a non-blocking connect via {@link SocketChannel#connect(SocketAddress)} that completed
+ * successfully.</p>
+ *
+ * @param channel the channel to register
+ * @param attachment the attachment object
+ */
+ public void accept(SelectableChannel channel, Object attachment)
+ {
+ ManagedSelector selector = chooseSelector();
+ selector.submit(selector.new Accept(channel, attachment));
+ }
+
+ /**
+ * <p>Registers a server channel for accept operations.
+ * When a {@link SocketChannel} is accepted from the given {@link ServerSocketChannel}
+ * then the {@link #accepted(SelectableChannel)} method is called, which must be
+ * overridden by a derivation of this class to handle the accepted channel
+ *
+ * @param server the server channel to register
+ * @return A Closable that allows the acceptor to be cancelled
+ */
+ public Closeable acceptor(SelectableChannel server)
+ {
+ ManagedSelector selector = chooseSelector();
+ ManagedSelector.Acceptor acceptor = selector.new Acceptor(server);
+ selector.submit(acceptor);
+ return acceptor;
+ }
+
+ /**
+ * Callback method when a channel is accepted from the {@link ServerSocketChannel}
+ * passed to {@link #acceptor(SelectableChannel)}.
+ * The default impl throws an {@link UnsupportedOperationException}, so it must
+ * be overridden by subclasses if a server channel is provided.
+ *
+ * @param channel the
+ * @throws IOException if unable to accept channel
+ */
+ protected void accepted(SelectableChannel channel) throws IOException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ _lease = ThreadPoolBudget.leaseFrom(getExecutor(), this, _selectors.length);
+ for (int i = 0; i < _selectors.length; i++)
+ {
+ ManagedSelector selector = newSelector(i);
+ _selectors[i] = selector;
+ addBean(selector);
+ }
+ super.doStart();
+ }
+
+ /**
+ * <p>Factory method for {@link ManagedSelector}.</p>
+ *
+ * @param id an identifier for the {@link ManagedSelector to create}
+ * @return a new {@link ManagedSelector}
+ */
+ protected ManagedSelector newSelector(int id)
+ {
+ return new ManagedSelector(this, id);
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ try
+ {
+ super.doStop();
+ }
+ finally
+ {
+ // Cleanup
+ for (ManagedSelector selector : _selectors)
+ {
+ if (selector != null)
+ removeBean(selector);
+ }
+ Arrays.fill(_selectors, null);
+ if (_lease != null)
+ _lease.close();
+ }
+ }
+
+ /**
+ * <p>Callback method invoked when an endpoint is opened.</p>
+ *
+ * @param endpoint the endpoint being opened
+ */
+ protected void endPointOpened(EndPoint endpoint)
+ {
+ }
+
+ /**
+ * <p>Callback method invoked when an endpoint is closed.</p>
+ *
+ * @param endpoint the endpoint being closed
+ */
+ protected void endPointClosed(EndPoint endpoint)
+ {
+ }
+
+ /**
+ * <p>Callback method invoked when a connection is opened.</p>
+ *
+ * @param connection the connection just opened
+ */
+ public void connectionOpened(Connection connection)
+ {
+ try
+ {
+ connection.onOpen();
+ }
+ catch (Throwable x)
+ {
+ if (isRunning())
+ LOG.warn("Exception while notifying connection " + connection, x);
+ else
+ LOG.debug("Exception while notifying connection " + connection, x);
+ throw x;
+ }
+ }
+
+ /**
+ * <p>Callback method invoked when a connection is closed.</p>
+ *
+ * @param connection the connection just closed
+ */
+ public void connectionClosed(Connection connection)
+ {
+ try
+ {
+ connection.onClose();
+ }
+ catch (Throwable x)
+ {
+ LOG.debug("Exception while notifying connection " + connection, x);
+ }
+ }
+
+ protected boolean doFinishConnect(SelectableChannel channel) throws IOException
+ {
+ return ((SocketChannel)channel).finishConnect();
+ }
+
+ protected boolean isConnectionPending(SelectableChannel channel)
+ {
+ return ((SocketChannel)channel).isConnectionPending();
+ }
+
+ protected SelectableChannel doAccept(SelectableChannel server) throws IOException
+ {
+ return ((ServerSocketChannel)server).accept();
+ }
+
+ /**
+ * <p>Callback method invoked when a non-blocking connect cannot be completed.</p>
+ * <p>By default it just logs with level warning.</p>
+ *
+ * @param channel the channel that attempted the connect
+ * @param ex the exception that caused the connect to fail
+ * @param attachment the attachment object associated at registration
+ */
+ protected void connectionFailed(SelectableChannel channel, Throwable ex, Object attachment)
+ {
+ LOG.warn(String.format("%s - %s", channel, attachment), ex);
+ }
+
+ protected Selector newSelector() throws IOException
+ {
+ return Selector.open();
+ }
+
+ /**
+ * <p>Factory method to create {@link EndPoint}.</p>
+ * <p>This method is invoked as a result of the registration of a channel via {@link #connect(SelectableChannel, Object)}
+ * or {@link #accept(SelectableChannel)}.</p>
+ *
+ * @param channel the channel associated to the endpoint
+ * @param selector the selector the channel is registered to
+ * @param selectionKey the selection key
+ * @return a new endpoint
+ * @throws IOException if the endPoint cannot be created
+ * @see #newConnection(SelectableChannel, EndPoint, Object)
+ */
+ protected abstract EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) throws IOException;
+
+ /**
+ * <p>Factory method to create {@link Connection}.</p>
+ *
+ * @param channel the channel associated to the connection
+ * @param endpoint the endpoint
+ * @param attachment the attachment
+ * @return a new connection
+ * @throws IOException if unable to create new connection
+ */
+ public abstract Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment) throws IOException;
+
+ public void addEventListener(EventListener listener)
+ {
+ if (isRunning())
+ throw new IllegalStateException(this.toString());
+ if (listener instanceof AcceptListener)
+ addAcceptListener((AcceptListener)listener);
+ }
+
+ public void removeEventListener(EventListener listener)
+ {
+ if (isRunning())
+ throw new IllegalStateException(this.toString());
+ if (listener instanceof AcceptListener)
+ removeAcceptListener((AcceptListener)listener);
+ }
+
+ public void addAcceptListener(AcceptListener listener)
+ {
+ if (!_acceptListeners.contains(listener))
+ _acceptListeners.add(listener);
+ }
+
+ public void removeAcceptListener(AcceptListener listener)
+ {
+ _acceptListeners.remove(listener);
+ }
+
+ protected void onAccepting(SelectableChannel channel)
+ {
+ for (AcceptListener l : _acceptListeners)
+ {
+ try
+ {
+ l.onAccepting(channel);
+ }
+ catch (Throwable x)
+ {
+ LOG.warn(x);
+ }
+ }
+ }
+
+ protected void onAcceptFailed(SelectableChannel channel, Throwable cause)
+ {
+ for (AcceptListener l : _acceptListeners)
+ {
+ try
+ {
+ l.onAcceptFailed(channel, cause);
+ }
+ catch (Throwable x)
+ {
+ LOG.warn(x);
+ }
+ }
+ }
+
+ protected void onAccepted(SelectableChannel channel)
+ {
+ for (AcceptListener l : _acceptListeners)
+ {
+ try
+ {
+ l.onAccepted(channel);
+ }
+ catch (Throwable x)
+ {
+ LOG.warn(x);
+ }
+ }
+ }
+
+ /**
+ * <p>A listener for accept events.</p>
+ * <p>This listener is called from either the selector or acceptor thread
+ * and implementations must be non blocking and fast.</p>
+ */
+ public interface AcceptListener extends EventListener
+ {
+ /**
+ * Called immediately after a new SelectableChannel is accepted, but
+ * before it has been submitted to the {@link SelectorManager}.
+ *
+ * @param channel the accepted channel
+ */
+ default void onAccepting(SelectableChannel channel)
+ {
+ }
+
+ /**
+ * Called if the processing of the accepted channel fails prior to calling
+ * {@link #onAccepted(SelectableChannel)}.
+ *
+ * @param channel the accepted channel
+ * @param cause the cause of the failure
+ */
+ default void onAcceptFailed(SelectableChannel channel, Throwable cause)
+ {
+ }
+
+ /**
+ * Called after the accepted channel has been allocated an {@link EndPoint}
+ * and associated {@link Connection}, and after the onOpen notifications have
+ * been called on both endPoint and connection.
+ *
+ * @param channel the accepted channel
+ */
+ default void onAccepted(SelectableChannel channel)
+ {
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java
new file mode 100644
index 0000000..fe350af
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java
@@ -0,0 +1,44 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.net.Socket;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+
+import org.eclipse.jetty.util.thread.Scheduler;
+
+public class SocketChannelEndPoint extends ChannelEndPoint
+{
+ public SocketChannelEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler)
+ {
+ this((SocketChannel)channel, selector, key, scheduler);
+ }
+
+ public SocketChannelEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler)
+ {
+ super(channel, selector, key, scheduler);
+ }
+
+ public Socket getSocket()
+ {
+ return getChannel().socket();
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java
new file mode 100644
index 0000000..f22caec
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java
@@ -0,0 +1,599 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.WritePendingException;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Invocable;
+import org.eclipse.jetty.util.thread.Invocable.InvocationType;
+
+/**
+ * A Utility class to help implement {@link EndPoint#write(Callback, ByteBuffer...)} by calling
+ * {@link EndPoint#flush(ByteBuffer...)} until all content is written.
+ * The abstract method {@link #onIncompleteFlush()} is called when not all content has been written after a call to
+ * flush and should organize for the {@link #completeWrite()} method to be called when a subsequent call to flush
+ * should be able to make more progress.
+ */
+public abstract class WriteFlusher
+{
+ private static final Logger LOG = Log.getLogger(WriteFlusher.class);
+ private static final boolean DEBUG = LOG.isDebugEnabled(); // Easy for the compiler to remove the code if DEBUG==false
+ private static final ByteBuffer[] EMPTY_BUFFERS = new ByteBuffer[]{BufferUtil.EMPTY_BUFFER};
+ private static final EnumMap<StateType, Set<StateType>> __stateTransitions = new EnumMap<>(StateType.class);
+ private static final State __IDLE = new IdleState();
+ private static final State __WRITING = new WritingState();
+ private static final State __COMPLETING = new CompletingState();
+ private final EndPoint _endPoint;
+ private final AtomicReference<State> _state = new AtomicReference<>();
+
+ static
+ {
+ // fill the state machine
+ __stateTransitions.put(StateType.IDLE, EnumSet.of(StateType.WRITING));
+ __stateTransitions.put(StateType.WRITING, EnumSet.of(StateType.IDLE, StateType.PENDING, StateType.FAILED));
+ __stateTransitions.put(StateType.PENDING, EnumSet.of(StateType.COMPLETING, StateType.IDLE, StateType.FAILED));
+ __stateTransitions.put(StateType.COMPLETING, EnumSet.of(StateType.IDLE, StateType.PENDING, StateType.FAILED));
+ __stateTransitions.put(StateType.FAILED, EnumSet.noneOf(StateType.class));
+ }
+
+ // A write operation may either complete immediately:
+ // IDLE-->WRITING-->IDLE
+ // Or it may not completely flush and go via the PENDING state
+ // IDLE-->WRITING-->PENDING-->COMPLETING-->IDLE
+ // Or it may take several cycles to complete
+ // IDLE-->WRITING-->PENDING-->COMPLETING-->PENDING-->COMPLETING-->IDLE
+ //
+ // If a failure happens while in IDLE, it is a noop since there is no operation to tell of the failure.
+ // IDLE--(fail)-->IDLE
+ //
+ // From any other state than IDLE a failure will result in an FAILED state which is a terminal state, and
+ // the callback is failed with the Throwable which caused the failure.
+ // IDLE-->WRITING--(fail)-->FAILED
+
+ protected WriteFlusher(EndPoint endPoint)
+ {
+ _state.set(__IDLE);
+ _endPoint = endPoint;
+ }
+
+ private enum StateType
+ {
+ IDLE,
+ WRITING,
+ PENDING,
+ COMPLETING,
+ FAILED
+ }
+
+ /**
+ * Tries to update the current state to the given new state.
+ *
+ * @param previous the expected current state
+ * @param next the desired new state
+ * @return the previous state or null if the state transition failed
+ * @throws WritePendingException if currentState is WRITING and new state is WRITING (api usage error)
+ */
+ private boolean updateState(State previous, State next)
+ {
+ if (!isTransitionAllowed(previous, next))
+ throw new IllegalStateException();
+
+ boolean updated = _state.compareAndSet(previous, next);
+ if (DEBUG)
+ LOG.debug("update {}:{}{}{}", this, previous, updated ? "-->" : "!->", next);
+ return updated;
+ }
+
+ private boolean isTransitionAllowed(State currentState, State newState)
+ {
+ Set<StateType> allowedNewStateTypes = __stateTransitions.get(currentState.getType());
+ if (!allowedNewStateTypes.contains(newState.getType()))
+ {
+ LOG.warn("{}: {} -> {} not allowed", this, currentState, newState);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * State represents a State of WriteFlusher.
+ */
+ private static class State
+ {
+ private final StateType _type;
+
+ private State(StateType stateType)
+ {
+ _type = stateType;
+ }
+
+ public StateType getType()
+ {
+ return _type;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s", _type);
+ }
+ }
+
+ /**
+ * In IdleState WriteFlusher is idle and accepts new writes
+ */
+ private static class IdleState extends State
+ {
+ private IdleState()
+ {
+ super(StateType.IDLE);
+ }
+ }
+
+ /**
+ * In WritingState WriteFlusher is currently writing.
+ */
+ private static class WritingState extends State
+ {
+ private WritingState()
+ {
+ super(StateType.WRITING);
+ }
+ }
+
+ /**
+ * In FailedState no more operations are allowed. The current implementation will never recover from this state.
+ */
+ private static class FailedState extends State
+ {
+ private final Throwable _cause;
+
+ private FailedState(Throwable cause)
+ {
+ super(StateType.FAILED);
+ _cause = cause;
+ }
+
+ public Throwable getCause()
+ {
+ return _cause;
+ }
+ }
+
+ /**
+ * In CompletingState WriteFlusher is flushing buffers that have not been fully written in write(). If write()
+ * didn't flush all buffers in one go, it'll switch the State to PendingState. completeWrite() will then switch to
+ * this state and try to flush the remaining buffers.
+ */
+ private static class CompletingState extends State
+ {
+ private CompletingState()
+ {
+ super(StateType.COMPLETING);
+ }
+ }
+
+ /**
+ * In PendingState not all buffers could be written in one go. Then write() will switch to PendingState() and
+ * preserve the state by creating a new PendingState object with the given parameters.
+ */
+ private class PendingState extends State
+ {
+ private final Callback _callback;
+ private final ByteBuffer[] _buffers;
+
+ private PendingState(ByteBuffer[] buffers, Callback callback)
+ {
+ super(StateType.PENDING);
+ _buffers = buffers;
+ _callback = callback;
+ }
+
+ public ByteBuffer[] getBuffers()
+ {
+ return _buffers;
+ }
+
+ InvocationType getCallbackInvocationType()
+ {
+ return Invocable.getInvocationType(_callback);
+ }
+ }
+
+ public InvocationType getCallbackInvocationType()
+ {
+ State s = _state.get();
+ return (s instanceof PendingState)
+ ? ((PendingState)s).getCallbackInvocationType()
+ : Invocable.InvocationType.BLOCKING;
+ }
+
+ /**
+ * Abstract call to be implemented by specific WriteFlushers. It should schedule a call to {@link #completeWrite()}
+ * or {@link #onFail(Throwable)} when appropriate.
+ */
+ protected abstract void onIncompleteFlush();
+
+ /**
+ * Tries to switch state to WRITING. If successful it writes the given buffers to the EndPoint. If state transition
+ * fails it will fail the callback and leave the WriteFlusher in a terminal FAILED state.
+ *
+ * If not all buffers can be written in one go it creates a new {@code PendingState} object to preserve the state
+ * and then calls {@link #onIncompleteFlush()}. The remaining buffers will be written in {@link #completeWrite()}.
+ *
+ * If all buffers have been written it calls callback.complete().
+ *
+ * @param callback the callback to call on either failed or complete
+ * @param buffers the buffers to flush to the endpoint
+ * @throws WritePendingException if unable to write due to prior pending write
+ */
+ public void write(Callback callback, ByteBuffer... buffers) throws WritePendingException
+ {
+ Objects.requireNonNull(callback);
+
+ if (isFailed())
+ {
+ fail(callback);
+ return;
+ }
+
+ if (DEBUG)
+ LOG.debug("write: {} {}", this, BufferUtil.toDetailString(buffers));
+
+ if (!updateState(__IDLE, __WRITING))
+ throw new WritePendingException();
+
+ try
+ {
+ buffers = flush(buffers);
+
+ if (buffers != null)
+ {
+ if (DEBUG)
+ LOG.debug("flushed incomplete");
+ PendingState pending = new PendingState(buffers, callback);
+ if (updateState(__WRITING, pending))
+ onIncompleteFlush();
+ else
+ fail(callback);
+
+ return;
+ }
+
+ if (updateState(__WRITING, __IDLE))
+ callback.succeeded();
+ else
+ fail(callback);
+ }
+ catch (Throwable e)
+ {
+ if (DEBUG)
+ LOG.debug("write exception", e);
+ if (updateState(__WRITING, new FailedState(e)))
+ callback.failed(e);
+ else
+ fail(callback, e);
+ }
+ }
+
+ private void fail(Callback callback, Throwable... suppressed)
+ {
+ Throwable cause;
+ loop:
+ while (true)
+ {
+ State state = _state.get();
+
+ switch (state.getType())
+ {
+ case FAILED:
+ {
+ FailedState failed = (FailedState)state;
+ cause = failed.getCause();
+ break loop;
+ }
+
+ case IDLE:
+ for (Throwable t : suppressed)
+ {
+ LOG.warn(t);
+ }
+ return;
+
+ default:
+ Throwable t = new IllegalStateException();
+ if (!_state.compareAndSet(state, new FailedState(t)))
+ continue;
+
+ cause = t;
+ break loop;
+ }
+ }
+
+ for (Throwable t : suppressed)
+ {
+ if (t != cause)
+ cause.addSuppressed(t);
+ }
+
+ callback.failed(cause);
+ }
+
+ /**
+ * Complete a write that has not completed and that called {@link #onIncompleteFlush()} to request a call to this
+ * method when a call to {@link EndPoint#flush(ByteBuffer...)} is likely to be able to progress.
+ *
+ * It tries to switch from PENDING to COMPLETING. If state transition fails, then it does nothing as the callback
+ * should have been already failed. That's because the only way to switch from PENDING outside this method is
+ * {@link #onFail(Throwable)} or {@link #onClose()}
+ */
+ public void completeWrite()
+ {
+ if (DEBUG)
+ LOG.debug("completeWrite: {}", this);
+
+ State previous = _state.get();
+
+ if (previous.getType() != StateType.PENDING)
+ return; // failure already handled.
+
+ PendingState pending = (PendingState)previous;
+ if (!updateState(pending, __COMPLETING))
+ return; // failure already handled.
+
+ Callback callback = pending._callback;
+ try
+ {
+ ByteBuffer[] buffers = pending.getBuffers();
+
+ buffers = flush(buffers);
+
+ if (buffers != null)
+ {
+ if (DEBUG)
+ LOG.debug("flushed incomplete {}", BufferUtil.toDetailString(buffers));
+ if (buffers != pending.getBuffers())
+ pending = new PendingState(buffers, callback);
+ if (updateState(__COMPLETING, pending))
+ onIncompleteFlush();
+ else
+ fail(callback);
+ return;
+ }
+
+ if (updateState(__COMPLETING, __IDLE))
+ callback.succeeded();
+ else
+ fail(callback);
+ }
+ catch (Throwable e)
+ {
+ if (DEBUG)
+ LOG.debug("completeWrite exception", e);
+ if (updateState(__COMPLETING, new FailedState(e)))
+ callback.failed(e);
+ else
+ fail(callback, e);
+ }
+ }
+
+ /**
+ * Flushes the buffers iteratively until no progress is made.
+ *
+ * @param buffers The buffers to flush
+ * @return The unflushed buffers, or null if all flushed
+ * @throws IOException if unable to flush
+ */
+ protected ByteBuffer[] flush(ByteBuffer[] buffers) throws IOException
+ {
+ boolean progress = true;
+ while (progress && buffers != null)
+ {
+ long before = BufferUtil.remaining(buffers);
+ boolean flushed = _endPoint.flush(buffers);
+ long after = BufferUtil.remaining(buffers);
+ long written = before - after;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Flushed={} written={} remaining={} {}", flushed, written, after, this);
+
+ if (written > 0)
+ {
+ Connection connection = _endPoint.getConnection();
+ if (connection instanceof Listener)
+ ((Listener)connection).onFlushed(written);
+ }
+
+ if (flushed)
+ return null;
+
+ progress = written > 0;
+
+ int index = 0;
+ while (true)
+ {
+ if (index == buffers.length)
+ {
+ // All buffers consumed.
+ buffers = null;
+ index = 0;
+ break;
+ }
+ else
+ {
+ int remaining = buffers[index].remaining();
+ if (remaining > 0)
+ break;
+ ++index;
+ progress = true;
+ }
+ }
+ if (index > 0)
+ buffers = Arrays.copyOfRange(buffers, index, buffers.length);
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("!fully flushed {}", this);
+
+ // If buffers is null, then flush has returned false but has consumed all the data!
+ // This is probably SSL being unable to flush the encrypted buffer, so return EMPTY_BUFFERS
+ // and that will keep this WriteFlusher pending.
+ return buffers == null ? EMPTY_BUFFERS : buffers;
+ }
+
+ /**
+ * Notify the flusher of a failure
+ *
+ * @param cause The cause of the failure
+ * @return true if the flusher passed the failure to a {@link Callback} instance
+ */
+ public boolean onFail(Throwable cause)
+ {
+ // Keep trying to handle the failure until we get to IDLE or FAILED state
+ while (true)
+ {
+ State current = _state.get();
+ switch (current.getType())
+ {
+ case IDLE:
+ case FAILED:
+ if (DEBUG)
+ LOG.debug("ignored: " + this, cause);
+ return false;
+
+ case PENDING:
+ if (DEBUG)
+ LOG.debug("failed: " + this, cause);
+
+ PendingState pending = (PendingState)current;
+ if (updateState(pending, new FailedState(cause)))
+ {
+ pending._callback.failed(cause);
+ return true;
+ }
+ break;
+
+ case WRITING:
+ case COMPLETING:
+ if (DEBUG)
+ LOG.debug("failed: " + this, cause);
+ if (updateState(current, new FailedState(cause)))
+ return true;
+ break;
+
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ public void onClose()
+ {
+ switch (_state.get().getType())
+ {
+ case IDLE:
+ case FAILED:
+ return;
+
+ default:
+ onFail(new ClosedChannelException());
+ }
+ }
+
+ boolean isFailed()
+ {
+ return isState(StateType.FAILED);
+ }
+
+ boolean isIdle()
+ {
+ return isState(StateType.IDLE);
+ }
+
+ public boolean isPending()
+ {
+ return isState(StateType.PENDING);
+ }
+
+ private boolean isState(StateType type)
+ {
+ return _state.get().getType() == type;
+ }
+
+ public String toStateString()
+ {
+ switch (_state.get().getType())
+ {
+ case WRITING:
+ return "W";
+ case PENDING:
+ return "P";
+ case COMPLETING:
+ return "C";
+ case IDLE:
+ return "-";
+ case FAILED:
+ return "F";
+ default:
+ return "?";
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ State s = _state.get();
+ return String.format("WriteFlusher@%x{%s}->%s", hashCode(), s, s instanceof PendingState ? ((PendingState)s)._callback : null);
+ }
+
+ /**
+ * <p>A listener of {@link WriteFlusher} events.</p>
+ */
+ public interface Listener
+ {
+ /**
+ * <p>Invoked when a {@link WriteFlusher} flushed bytes in a non-blocking way,
+ * as part of a - possibly larger - write.</p>
+ * <p>This method may be invoked multiple times, for example when writing a large
+ * buffer: a first flush of bytes, then the connection became TCP congested, and
+ * a subsequent flush of bytes when the connection became writable again.</p>
+ * <p>This method is never invoked concurrently, but may be invoked by different
+ * threads, so implementations may not rely on thread-local variables.</p>
+ * <p>Implementations may throw an {@link IOException} to signal that the write
+ * should fail, for example if the implementation enforces a minimum data rate.</p>
+ *
+ * @param bytes the number of bytes flushed
+ * @throws IOException if the write should fail
+ */
+ void onFlushed(long bytes) throws IOException;
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/WriterOutputStream.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/WriterOutputStream.java
new file mode 100644
index 0000000..eeb155f
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/WriterOutputStream.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.nio.charset.Charset;
+
+/**
+ * Wrap a Writer as an OutputStream.
+ * When all you have is a Writer and only an OutputStream will do.
+ * Try not to use this as it indicates that your design is a dogs
+ * breakfast (JSP made me write it).
+ */
+public class WriterOutputStream extends OutputStream
+{
+ protected final Writer _writer;
+ protected final Charset _encoding;
+ private final byte[] _buf = new byte[1];
+
+ public WriterOutputStream(Writer writer, String encoding)
+ {
+ _writer = writer;
+ _encoding = encoding == null ? null : Charset.forName(encoding);
+ }
+
+ public WriterOutputStream(Writer writer)
+ {
+ _writer = writer;
+ _encoding = null;
+ }
+
+ @Override
+ public void close()
+ throws IOException
+ {
+ _writer.close();
+ }
+
+ @Override
+ public void flush()
+ throws IOException
+ {
+ _writer.flush();
+ }
+
+ @Override
+ public void write(byte[] b)
+ throws IOException
+ {
+ if (_encoding == null)
+ _writer.write(new String(b));
+ else
+ _writer.write(new String(b, _encoding));
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len)
+ throws IOException
+ {
+ if (_encoding == null)
+ _writer.write(new String(b, off, len));
+ else
+ _writer.write(new String(b, off, len, _encoding));
+ }
+
+ @Override
+ public synchronized void write(int b)
+ throws IOException
+ {
+ _buf[0] = (byte)b;
+ write(_buf);
+ }
+}
+
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/jmx/ConnectionStatisticsMBean.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/jmx/ConnectionStatisticsMBean.java
new file mode 100644
index 0000000..a2178b0
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/jmx/ConnectionStatisticsMBean.java
@@ -0,0 +1,50 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io.jmx;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.eclipse.jetty.io.ConnectionStatistics;
+import org.eclipse.jetty.jmx.ObjectMBean;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+
+@ManagedObject
+public class ConnectionStatisticsMBean extends ObjectMBean
+{
+ public ConnectionStatisticsMBean(Object object)
+ {
+ super(object);
+ }
+
+ @ManagedAttribute("ConnectionStatistics grouped by connection class")
+ public Collection<String> getConnectionStatisticsGroups()
+ {
+ ConnectionStatistics delegate = (ConnectionStatistics)getManagedObject();
+ Map<String, ConnectionStatistics.Stats> groups = delegate.getConnectionStatisticsGroups();
+ return groups.values().stream()
+ .sorted(Comparator.comparing(ConnectionStatistics.Stats::getName))
+ .map(stats -> stats.dump())
+ .map(dump -> dump.replaceAll("[\r\n]", " "))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/package-info.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/package-info.java
new file mode 100644
index 0000000..e466982
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty IO : Core classes for Jetty IO subsystem
+ */
+package org.eclipse.jetty.io;
+
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/ALPNProcessor.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/ALPNProcessor.java
new file mode 100644
index 0000000..e361571
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/ALPNProcessor.java
@@ -0,0 +1,71 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io.ssl;
+
+import javax.net.ssl.SSLEngine;
+
+import org.eclipse.jetty.io.Connection;
+
+public interface ALPNProcessor
+{
+ /**
+ * Initializes this ALPNProcessor
+ *
+ * @throws RuntimeException if this processor is unavailable (e.g. missing dependencies or wrong JVM)
+ */
+ default void init()
+ {
+ }
+
+ /**
+ * Tests if this processor can be applied to the given SSLEngine.
+ *
+ * @param sslEngine the SSLEngine to check
+ * @return true if the processor can be applied to the given SSLEngine
+ */
+ default boolean appliesTo(SSLEngine sslEngine)
+ {
+ return false;
+ }
+
+ /**
+ * Configures the given SSLEngine and the given Connection for ALPN.
+ *
+ * @param sslEngine the SSLEngine to configure
+ * @param connection the Connection to configure
+ * @throws RuntimeException if this processor cannot be configured
+ */
+ default void configure(SSLEngine sslEngine, Connection connection)
+ {
+ }
+
+ /**
+ * Server-side interface used by ServiceLoader.
+ */
+ interface Server extends ALPNProcessor
+ {
+ }
+
+ /**
+ * Client-side interface used by ServiceLoader.
+ */
+ interface Client extends ALPNProcessor
+ {
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java
new file mode 100644
index 0000000..ea7dd5f
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java
@@ -0,0 +1,215 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io.ssl;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.ClientConnectionFactory;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+/**
+ * <p>A ClientConnectionFactory that creates client-side {@link SslConnection} instances.</p>
+ */
+public class SslClientConnectionFactory implements ClientConnectionFactory
+{
+ public static final String SSL_CONTEXT_FACTORY_CONTEXT_KEY = "ssl.context.factory";
+ public static final String SSL_PEER_HOST_CONTEXT_KEY = "ssl.peer.host";
+ public static final String SSL_PEER_PORT_CONTEXT_KEY = "ssl.peer.port";
+ public static final String SSL_ENGINE_CONTEXT_KEY = "ssl.engine";
+
+ private final SslContextFactory sslContextFactory;
+ private final ByteBufferPool byteBufferPool;
+ private final Executor executor;
+ private final ClientConnectionFactory connectionFactory;
+ private boolean _directBuffersForEncryption = true;
+ private boolean _directBuffersForDecryption = true;
+ private boolean _requireCloseMessage;
+
+ public SslClientConnectionFactory(SslContextFactory sslContextFactory, ByteBufferPool byteBufferPool, Executor executor, ClientConnectionFactory connectionFactory)
+ {
+ this.sslContextFactory = Objects.requireNonNull(sslContextFactory, "Missing SslContextFactory");
+ this.byteBufferPool = byteBufferPool;
+ this.executor = executor;
+ this.connectionFactory = connectionFactory;
+ }
+
+ public void setDirectBuffersForEncryption(boolean useDirectBuffers)
+ {
+ this._directBuffersForEncryption = useDirectBuffers;
+ }
+
+ public void setDirectBuffersForDecryption(boolean useDirectBuffers)
+ {
+ this._directBuffersForDecryption = useDirectBuffers;
+ }
+
+ public boolean isDirectBuffersForDecryption()
+ {
+ return _directBuffersForDecryption;
+ }
+
+ public boolean isDirectBuffersForEncryption()
+ {
+ return _directBuffersForEncryption;
+ }
+
+ /**
+ * @return whether is not required that peers send the TLS {@code close_notify} message
+ * @deprecated use {@link #isRequireCloseMessage()} instead
+ */
+ @Deprecated
+ public boolean isAllowMissingCloseMessage()
+ {
+ return !isRequireCloseMessage();
+ }
+
+ /**
+ * @param allowMissingCloseMessage whether is not required that peers send the TLS {@code close_notify} message
+ * @deprecated use {@link #setRequireCloseMessage(boolean)} instead
+ */
+ @Deprecated
+ public void setAllowMissingCloseMessage(boolean allowMissingCloseMessage)
+ {
+ setRequireCloseMessage(!allowMissingCloseMessage);
+ }
+
+ /**
+ * @return whether peers must send the TLS {@code close_notify} message
+ * @see SslConnection#isRequireCloseMessage()
+ */
+ public boolean isRequireCloseMessage()
+ {
+ return _requireCloseMessage;
+ }
+
+ /**
+ * @param requireCloseMessage whether peers must send the TLS {@code close_notify} message
+ * @see SslConnection#setRequireCloseMessage(boolean)
+ */
+ public void setRequireCloseMessage(boolean requireCloseMessage)
+ {
+ _requireCloseMessage = requireCloseMessage;
+ }
+
+ @Override
+ public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map<String, Object> context) throws IOException
+ {
+ String host = (String)context.get(SSL_PEER_HOST_CONTEXT_KEY);
+ int port = (Integer)context.get(SSL_PEER_PORT_CONTEXT_KEY);
+
+ SSLEngine engine = sslContextFactory instanceof SslEngineFactory
+ ? ((SslEngineFactory)sslContextFactory).newSslEngine(host, port, context)
+ : sslContextFactory.newSSLEngine(host, port);
+ engine.setUseClientMode(true);
+ context.put(SSL_ENGINE_CONTEXT_KEY, engine);
+
+ SslConnection sslConnection = newSslConnection(byteBufferPool, executor, endPoint, engine);
+
+ EndPoint appEndPoint = sslConnection.getDecryptedEndPoint();
+ appEndPoint.setConnection(connectionFactory.newConnection(appEndPoint, context));
+
+ sslConnection.addHandshakeListener(new HTTPSHandshakeListener(context));
+ customize(sslConnection, context);
+
+ return sslConnection;
+ }
+
+ protected SslConnection newSslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine engine)
+ {
+ return new SslConnection(byteBufferPool, executor, endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption());
+ }
+
+ @Override
+ public Connection customize(Connection connection, Map<String, Object> context)
+ {
+ if (connection instanceof SslConnection)
+ {
+ SslConnection sslConnection = (SslConnection)connection;
+ sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed());
+ sslConnection.setRenegotiationLimit(sslContextFactory.getRenegotiationLimit());
+ sslConnection.setRequireCloseMessage(isRequireCloseMessage());
+ ContainerLifeCycle connector = (ContainerLifeCycle)context.get(ClientConnectionFactory.CONNECTOR_CONTEXT_KEY);
+ connector.getBeans(SslHandshakeListener.class).forEach(sslConnection::addHandshakeListener);
+ }
+ return ClientConnectionFactory.super.customize(connection, context);
+ }
+
+ /**
+ * <p>A factory for {@link SSLEngine} objects.</p>
+ * <p>Typically implemented by {@link SslContextFactory.Client}
+ * to support more flexible creation of SSLEngine instances.</p>
+ */
+ public interface SslEngineFactory
+ {
+ /**
+ * <p>Creates a new {@link SSLEngine} instance for the given peer host and port,
+ * and with the given context to help the creation of the SSLEngine.</p>
+ *
+ * @param host the peer host
+ * @param port the peer port
+ * @param context the {@link ClientConnectionFactory} context
+ * @return a new SSLEngine instance
+ */
+ public SSLEngine newSslEngine(String host, int port, Map<String, Object> context);
+ }
+
+ private class HTTPSHandshakeListener implements SslHandshakeListener
+ {
+ private final Map<String, Object> context;
+
+ private HTTPSHandshakeListener(Map<String, Object> context)
+ {
+ this.context = context;
+ }
+
+ @Override
+ public void handshakeSucceeded(Event event) throws SSLException
+ {
+ HostnameVerifier verifier = sslContextFactory.getHostnameVerifier();
+ if (verifier != null)
+ {
+ String host = (String)context.get(SSL_PEER_HOST_CONTEXT_KEY);
+ try
+ {
+ if (!verifier.verify(host, event.getSSLEngine().getSession()))
+ throw new SSLPeerUnverifiedException("Host name verification failed for host: " + host);
+ }
+ catch (SSLException x)
+ {
+ throw x;
+ }
+ catch (Throwable x)
+ {
+ throw (SSLException)new SSLPeerUnverifiedException("Host name verification failed for host: " + host).initCause(x);
+ }
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java
new file mode 100644
index 0000000..ae38bb7
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java
@@ -0,0 +1,1622 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io.ssl;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.ToIntFunction;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLEngineResult.Status;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSession;
+
+import org.eclipse.jetty.io.AbstractConnection;
+import org.eclipse.jetty.io.AbstractEndPoint;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.WriteFlusher;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Invocable;
+
+/**
+ * A Connection that acts as an interceptor between an EndPoint providing SSL encrypted data
+ * and another consumer of an EndPoint (typically an {@link Connection} like HttpConnection) that
+ * wants unencrypted data.
+ * <p>
+ * The connector uses an {@link EndPoint} (typically SocketChannelEndPoint) as
+ * it's source/sink of encrypted data. It then provides an endpoint via {@link #getDecryptedEndPoint()} to
+ * expose a source/sink of unencrypted data to another connection (eg HttpConnection).
+ * <p>
+ * The design of this class is based on a clear separation between the passive methods, which do not block nor schedule any
+ * asynchronous callbacks, and active methods that do schedule asynchronous callbacks.
+ * <p>
+ * The passive methods are {@link DecryptedEndPoint#fill(ByteBuffer)} and {@link DecryptedEndPoint#flush(ByteBuffer...)}. They make best
+ * effort attempts to progress the connection using only calls to the encrypted {@link EndPoint#fill(ByteBuffer)} and {@link EndPoint#flush(ByteBuffer...)}
+ * methods. They will never block nor schedule any readInterest or write callbacks. If a fill/flush cannot progress either because
+ * of network congestion or waiting for an SSL handshake message, then the fill/flush will simply return with zero bytes filled/flushed.
+ * Specifically, if a flush cannot proceed because it needs to receive a handshake message, then the flush will attempt to fill bytes from the
+ * encrypted endpoint, but if insufficient bytes are read it will NOT call {@link EndPoint#fillInterested(Callback)}.
+ * <p>
+ * It is only the active methods : {@link DecryptedEndPoint#fillInterested(Callback)} and
+ * {@link DecryptedEndPoint#write(Callback, ByteBuffer...)} that may schedule callbacks by calling the encrypted
+ * {@link EndPoint#fillInterested(Callback)} and {@link EndPoint#write(Callback, ByteBuffer...)}
+ * methods. For normal data handling, the decrypted fillInterest method will result in an encrypted fillInterest and a decrypted
+ * write will result in an encrypted write. However, due to SSL handshaking requirements, it is also possible for a decrypted fill
+ * to call the encrypted write and for the decrypted flush to call the encrypted fillInterested methods.
+ * <p>
+ * MOST IMPORTANTLY, the encrypted callbacks from the active methods (#onFillable() and WriteFlusher#completeWrite()) do no filling or flushing
+ * themselves. Instead they simple make the callbacks to the decrypted callbacks, so that the passive encrypted fill/flush will
+ * be called again and make another best effort attempt to progress the connection.
+ */
+public class SslConnection extends AbstractConnection implements Connection.UpgradeTo
+{
+ private static final Logger LOG = Log.getLogger(SslConnection.class);
+ private static final String TLS_1_3 = "TLSv1.3";
+
+ private enum HandshakeState
+ {
+ INITIAL,
+ HANDSHAKE,
+ SUCCEEDED,
+ FAILED
+ }
+
+ private enum FillState
+ {
+ IDLE, // Not Filling any data
+ INTERESTED, // We have a pending read interest
+ WAIT_FOR_FLUSH // Waiting for a flush to happen
+ }
+
+ private enum FlushState
+ {
+ IDLE, // Not flushing any data
+ WRITING, // We have a pending write of encrypted data
+ WAIT_FOR_FILL // Waiting for a fill to happen
+ }
+
+ private final AtomicReference<HandshakeState> _handshake = new AtomicReference<>(HandshakeState.INITIAL);
+ private final List<SslHandshakeListener> handshakeListeners = new ArrayList<>();
+ private final AtomicLong _bytesIn = new AtomicLong();
+ private final AtomicLong _bytesOut = new AtomicLong();
+ private final ByteBufferPool _bufferPool;
+ private final SSLEngine _sslEngine;
+ private final DecryptedEndPoint _decryptedEndPoint;
+ private ByteBuffer _decryptedInput;
+ private ByteBuffer _encryptedInput;
+ private ByteBuffer _encryptedOutput;
+ private final boolean _encryptedDirectBuffers;
+ private final boolean _decryptedDirectBuffers;
+ private boolean _renegotiationAllowed;
+ private int _renegotiationLimit = -1;
+ private boolean _closedOutbound;
+ private boolean _requireCloseMessage;
+ private FlushState _flushState = FlushState.IDLE;
+ private FillState _fillState = FillState.IDLE;
+ private boolean _underflown;
+
+ private abstract class RunnableTask implements Runnable, Invocable
+ {
+ private final String _operation;
+
+ protected RunnableTask(String op)
+ {
+ _operation = op;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("SSL:%s:%s:%s", SslConnection.this, _operation, getInvocationType());
+ }
+ }
+
+ private final Runnable _runFillable = new RunnableTask("runFillable")
+ {
+ @Override
+ public void run()
+ {
+ _decryptedEndPoint.getFillInterest().fillable();
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return _decryptedEndPoint.getFillInterest().getCallbackInvocationType();
+ }
+ };
+
+ private final Callback _sslReadCallback = new Callback()
+ {
+ @Override
+ public void succeeded()
+ {
+ onFillable();
+ }
+
+ @Override
+ public void failed(final Throwable x)
+ {
+ onFillInterestedFailed(x);
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return getDecryptedEndPoint().getFillInterest().getCallbackInvocationType();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("SSLC.NBReadCB@%x{%s}", SslConnection.this.hashCode(), SslConnection.this);
+ }
+ };
+
+ public SslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine sslEngine)
+ {
+ this(byteBufferPool, executor, endPoint, sslEngine, false, false);
+ }
+
+ public SslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine sslEngine,
+ boolean useDirectBuffersForEncryption, boolean useDirectBuffersForDecryption)
+ {
+ // This connection does not execute calls to onFillable(), so they will be called by the selector thread.
+ // onFillable() does not block and will only wakeup another thread to do the actual reading and handling.
+ super(endPoint, executor);
+ this._bufferPool = byteBufferPool;
+ this._sslEngine = sslEngine;
+ this._decryptedEndPoint = newDecryptedEndPoint();
+ this._encryptedDirectBuffers = useDirectBuffersForEncryption;
+ this._decryptedDirectBuffers = useDirectBuffersForDecryption;
+ }
+
+ @Override
+ public long getBytesIn()
+ {
+ return _bytesIn.get();
+ }
+
+ @Override
+ public long getBytesOut()
+ {
+ return _bytesOut.get();
+ }
+
+ public void addHandshakeListener(SslHandshakeListener listener)
+ {
+ handshakeListeners.add(listener);
+ }
+
+ public boolean removeHandshakeListener(SslHandshakeListener listener)
+ {
+ return handshakeListeners.remove(listener);
+ }
+
+ protected DecryptedEndPoint newDecryptedEndPoint()
+ {
+ return new DecryptedEndPoint();
+ }
+
+ public SSLEngine getSSLEngine()
+ {
+ return _sslEngine;
+ }
+
+ public DecryptedEndPoint getDecryptedEndPoint()
+ {
+ return _decryptedEndPoint;
+ }
+
+ public boolean isRenegotiationAllowed()
+ {
+ return _renegotiationAllowed;
+ }
+
+ public void setRenegotiationAllowed(boolean renegotiationAllowed)
+ {
+ _renegotiationAllowed = renegotiationAllowed;
+ }
+
+ /**
+ * @return The number of renegotiations allowed for this connection. When the limit
+ * is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied.
+ */
+ public int getRenegotiationLimit()
+ {
+ return _renegotiationLimit;
+ }
+
+ /**
+ * @param renegotiationLimit The number of renegotiations allowed for this connection.
+ * When the limit is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied.
+ * Default -1.
+ */
+ public void setRenegotiationLimit(int renegotiationLimit)
+ {
+ _renegotiationLimit = renegotiationLimit;
+ }
+
+ /**
+ * @return whether is not required that peers send the TLS {@code close_notify} message
+ * @deprecated use inverted {@link #isRequireCloseMessage()} instead
+ */
+ @Deprecated
+ public boolean isAllowMissingCloseMessage()
+ {
+ return !isRequireCloseMessage();
+ }
+
+ /**
+ * @param allowMissingCloseMessage whether is not required that peers send the TLS {@code close_notify} message
+ * @deprecated use inverted {@link #setRequireCloseMessage(boolean)} instead
+ */
+ @Deprecated
+ public void setAllowMissingCloseMessage(boolean allowMissingCloseMessage)
+ {
+ setRequireCloseMessage(!allowMissingCloseMessage);
+ }
+
+ /**
+ * @return whether peers must send the TLS {@code close_notify} message
+ */
+ public boolean isRequireCloseMessage()
+ {
+ return _requireCloseMessage;
+ }
+
+ /**
+ * <p>Sets whether it is required that a peer send the TLS {@code close_notify} message
+ * to indicate the will to close the connection, otherwise it may be interpreted as a
+ * truncation attack.</p>
+ * <p>This option is only useful on clients, since typically servers cannot accept
+ * connection-delimited content that may be truncated.</p>
+ *
+ * @param requireCloseMessage whether peers must send the TLS {@code close_notify} message
+ */
+ public void setRequireCloseMessage(boolean requireCloseMessage)
+ {
+ _requireCloseMessage = requireCloseMessage;
+ }
+
+ private boolean isHandshakeInitial()
+ {
+ return _handshake.get() == HandshakeState.INITIAL;
+ }
+
+ private boolean isHandshakeSucceeded()
+ {
+ return _handshake.get() == HandshakeState.SUCCEEDED;
+ }
+
+ private boolean isHandshakeComplete()
+ {
+ HandshakeState state = _handshake.get();
+ return state == HandshakeState.SUCCEEDED || state == HandshakeState.FAILED;
+ }
+
+ private int getApplicationBufferSize()
+ {
+ return getBufferSize(SSLSession::getApplicationBufferSize);
+ }
+
+ private int getPacketBufferSize()
+ {
+ return getBufferSize(SSLSession::getPacketBufferSize);
+ }
+
+ private int getBufferSize(ToIntFunction<SSLSession> bufferSizeFn)
+ {
+ SSLSession hsSession = _sslEngine.getHandshakeSession();
+ SSLSession session = _sslEngine.getSession();
+ int size = bufferSizeFn.applyAsInt(session);
+ if (hsSession == null || hsSession == session)
+ return size;
+ int hsSize = bufferSizeFn.applyAsInt(hsSession);
+ return Math.max(hsSize, size);
+ }
+
+ private void acquireEncryptedInput()
+ {
+ if (_encryptedInput == null)
+ _encryptedInput = _bufferPool.acquire(getPacketBufferSize(), _encryptedDirectBuffers);
+ }
+
+ private void acquireEncryptedOutput()
+ {
+ if (_encryptedOutput == null)
+ _encryptedOutput = _bufferPool.acquire(getPacketBufferSize(), _encryptedDirectBuffers);
+ }
+
+ @Override
+ public void onUpgradeTo(ByteBuffer buffer)
+ {
+ acquireEncryptedInput();
+ BufferUtil.append(_encryptedInput, buffer);
+ }
+
+ @Override
+ public void onOpen()
+ {
+ super.onOpen();
+ getDecryptedEndPoint().getConnection().onOpen();
+ }
+
+ @Override
+ public void onClose()
+ {
+ _decryptedEndPoint.getConnection().onClose();
+ super.onClose();
+ }
+
+ @Override
+ public void close()
+ {
+ getDecryptedEndPoint().getConnection().close();
+ }
+
+ @Override
+ public boolean onIdleExpired()
+ {
+ return getDecryptedEndPoint().getConnection().onIdleExpired();
+ }
+
+ @Override
+ public void onFillable()
+ {
+ // onFillable means that there are encrypted bytes ready to be filled.
+ // however we do not fill them here on this callback, but instead wakeup
+ // the decrypted readInterest and/or writeFlusher so that they will attempt
+ // to do the fill and/or flush again and these calls will do the actually
+ // filling.
+
+ if (LOG.isDebugEnabled())
+ LOG.debug(">c.onFillable {}", SslConnection.this);
+
+ // We have received a close handshake, close the end point to send FIN.
+ if (_decryptedEndPoint.isInputShutdown())
+ _decryptedEndPoint.close();
+
+ _decryptedEndPoint.onFillable();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("<c.onFillable {}", SslConnection.this);
+ }
+
+ @Override
+ public void onFillInterestedFailed(Throwable cause)
+ {
+ _decryptedEndPoint.onFillableFail(cause == null ? new IOException() : cause);
+ }
+
+ protected SSLEngineResult wrap(SSLEngine sslEngine, ByteBuffer[] input, ByteBuffer output) throws SSLException
+ {
+ return sslEngine.wrap(input, output);
+ }
+
+ protected SSLEngineResult unwrap(SSLEngine sslEngine, ByteBuffer input, ByteBuffer output) throws SSLException
+ {
+ return sslEngine.unwrap(input, output);
+ }
+
+ @Override
+ public String toConnectionString()
+ {
+ ByteBuffer b = _encryptedInput;
+ int ei = b == null ? -1 : b.remaining();
+ b = _encryptedOutput;
+ int eo = b == null ? -1 : b.remaining();
+ b = _decryptedInput;
+ int di = b == null ? -1 : b.remaining();
+
+ Connection connection = _decryptedEndPoint.getConnection();
+ return String.format("%s@%x{%s,eio=%d/%d,di=%d,fill=%s,flush=%s}~>%s=>%s",
+ getClass().getSimpleName(),
+ hashCode(),
+ _sslEngine.getHandshakeStatus(),
+ ei, eo, di,
+ _fillState, _flushState,
+ _decryptedEndPoint.toEndPointString(),
+ connection instanceof AbstractConnection ? ((AbstractConnection)connection).toConnectionString() : connection);
+ }
+
+ private void releaseEncryptedInputBuffer()
+ {
+ if (_encryptedInput != null && !_encryptedInput.hasRemaining())
+ {
+ _bufferPool.release(_encryptedInput);
+ _encryptedInput = null;
+ }
+ }
+
+ protected void releaseDecryptedInputBuffer()
+ {
+ if (_decryptedInput != null && !_decryptedInput.hasRemaining())
+ {
+ _bufferPool.release(_decryptedInput);
+ _decryptedInput = null;
+ }
+ }
+
+ private void releaseEncryptedOutputBuffer()
+ {
+ if (!Thread.holdsLock(_decryptedEndPoint))
+ throw new IllegalStateException();
+ if (_encryptedOutput != null && !_encryptedOutput.hasRemaining())
+ {
+ _bufferPool.release(_encryptedOutput);
+ _encryptedOutput = null;
+ }
+ }
+
+ protected int networkFill(ByteBuffer input) throws IOException
+ {
+ return getEndPoint().fill(input);
+ }
+
+ protected boolean networkFlush(ByteBuffer output) throws IOException
+ {
+ return getEndPoint().flush(output);
+ }
+
+ public class DecryptedEndPoint extends AbstractEndPoint
+ {
+ private final Callback _incompleteWriteCallback = new IncompleteWriteCallback();
+ private Throwable _failure;
+
+ public DecryptedEndPoint()
+ {
+ // Disable idle timeout checking: no scheduler and -1 timeout for this instance.
+ super(null);
+ super.setIdleTimeout(-1);
+ }
+
+ @Override
+ public long getIdleTimeout()
+ {
+ return getEndPoint().getIdleTimeout();
+ }
+
+ @Override
+ public void setIdleTimeout(long idleTimeout)
+ {
+ getEndPoint().setIdleTimeout(idleTimeout);
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ return getEndPoint().isOpen();
+ }
+
+ @Override
+ public InetSocketAddress getLocalAddress()
+ {
+ return getEndPoint().getLocalAddress();
+ }
+
+ @Override
+ public InetSocketAddress getRemoteAddress()
+ {
+ return getEndPoint().getRemoteAddress();
+ }
+
+ @Override
+ public WriteFlusher getWriteFlusher()
+ {
+ return super.getWriteFlusher();
+ }
+
+ protected void onFillable()
+ {
+ try
+ {
+ // If we are handshaking, then wake up any waiting write as well as it may have been blocked on the read
+ boolean waitingForFill;
+ synchronized (_decryptedEndPoint)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onFillable {}", SslConnection.this);
+
+ _fillState = FillState.IDLE;
+ waitingForFill = _flushState == FlushState.WAIT_FOR_FILL;
+ }
+
+ getFillInterest().fillable();
+
+ if (waitingForFill)
+ {
+ synchronized (_decryptedEndPoint)
+ {
+ waitingForFill = _flushState == FlushState.WAIT_FOR_FILL;
+ }
+ if (waitingForFill)
+ fill(BufferUtil.EMPTY_BUFFER);
+ }
+ }
+ catch (Throwable e)
+ {
+ close(e);
+ }
+ }
+
+ protected void onFillableFail(Throwable failure)
+ {
+ // If we are handshaking, then wake up any waiting write as well as it may have been blocked on the read
+ boolean fail = false;
+ synchronized (_decryptedEndPoint)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onFillableFail {}", SslConnection.this, failure);
+
+ _fillState = FillState.IDLE;
+ if (_flushState == FlushState.WAIT_FOR_FILL)
+ {
+ _flushState = FlushState.IDLE;
+ fail = true;
+ }
+ }
+
+ // wake up whoever is doing the fill
+ getFillInterest().onFail(failure);
+
+ // Try to complete the write
+ if (fail)
+ {
+ if (!getWriteFlusher().onFail(failure))
+ close(failure);
+ }
+ }
+
+ @Override
+ public void setConnection(Connection connection)
+ {
+ if (connection instanceof AbstractConnection)
+ {
+ // This is an optimization to avoid that upper layer connections use small
+ // buffers and we need to copy decrypted data rather than decrypting in place.
+ AbstractConnection c = (AbstractConnection)connection;
+ int appBufferSize = getApplicationBufferSize();
+ if (c.getInputBufferSize() < appBufferSize)
+ c.setInputBufferSize(appBufferSize);
+ }
+ super.setConnection(connection);
+ }
+
+ public SslConnection getSslConnection()
+ {
+ return SslConnection.this;
+ }
+
+ @Override
+ public int fill(ByteBuffer buffer) throws IOException
+ {
+ try
+ {
+ synchronized (_decryptedEndPoint)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(">fill {}", SslConnection.this);
+
+ int filled = -2;
+ try
+ {
+ if (_fillState != FillState.IDLE)
+ return filled = 0;
+
+ // Do we already have some decrypted data?
+ if (BufferUtil.hasContent(_decryptedInput))
+ return filled = BufferUtil.append(buffer, _decryptedInput);
+
+ // loop filling and unwrapping until we have something
+ while (true)
+ {
+ HandshakeStatus status = _sslEngine.getHandshakeStatus();
+ if (LOG.isDebugEnabled())
+ LOG.debug("fill {}", status);
+ switch (status)
+ {
+ case NEED_UNWRAP:
+ case NOT_HANDSHAKING:
+ break;
+
+ case NEED_TASK:
+ _sslEngine.getDelegatedTask().run();
+ continue;
+
+ case NEED_WRAP:
+ if (_flushState == FlushState.IDLE && flush(BufferUtil.EMPTY_BUFFER))
+ {
+ Throwable failure = _failure;
+ if (failure != null)
+ rethrow(failure);
+ if (_sslEngine.isInboundDone())
+ return filled = -1;
+ continue;
+ }
+ // Handle in needsFillInterest().
+ return filled = 0;
+
+ default:
+ throw new IllegalStateException("Unexpected HandshakeStatus " + status);
+ }
+
+ acquireEncryptedInput();
+
+ // can we use the passed buffer if it is big enough
+ ByteBuffer appIn;
+ int appBufferSize = getApplicationBufferSize();
+ if (_decryptedInput == null)
+ {
+ if (BufferUtil.space(buffer) > appBufferSize)
+ appIn = buffer;
+ else
+ appIn = _decryptedInput = _bufferPool.acquire(appBufferSize, _decryptedDirectBuffers);
+ }
+ else
+ {
+ appIn = _decryptedInput;
+ }
+
+ // Let's try reading some encrypted data... even if we have some already.
+ int netFilled = networkFill(_encryptedInput);
+ if (netFilled > 0)
+ _bytesIn.addAndGet(netFilled);
+ if (LOG.isDebugEnabled())
+ LOG.debug("net filled={}", netFilled);
+
+ // Workaround for Java 11 behavior.
+ if (netFilled < 0 && isHandshakeInitial() && BufferUtil.isEmpty(_encryptedInput))
+ closeInbound();
+
+ if (netFilled > 0 && !isHandshakeComplete() && isOutboundDone())
+ throw new SSLHandshakeException("Closed during handshake");
+
+ if (_handshake.compareAndSet(HandshakeState.INITIAL, HandshakeState.HANDSHAKE))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("fill starting handshake {}", SslConnection.this);
+ }
+
+ // Let's unwrap even if we have no net data because in that
+ // case we want to fall through to the handshake handling
+ int pos = BufferUtil.flipToFill(appIn);
+ SSLEngineResult unwrapResult;
+ try
+ {
+ _underflown = false;
+ unwrapResult = unwrap(_sslEngine, _encryptedInput, appIn);
+ }
+ finally
+ {
+ BufferUtil.flipToFlush(appIn, pos);
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("unwrap net_filled={} {} encryptedBuffer={} unwrapBuffer={} appBuffer={}",
+ netFilled,
+ StringUtil.replace(unwrapResult.toString(), '\n', ' '),
+ BufferUtil.toSummaryString(_encryptedInput),
+ BufferUtil.toDetailString(appIn),
+ BufferUtil.toDetailString(buffer));
+
+ SSLEngineResult.Status unwrap = unwrapResult.getStatus();
+
+ // Extra check on unwrapResultStatus == OK with zero bytes consumed
+ // or produced is due to an SSL client on Android (see bug #454773).
+ if (unwrap == Status.OK && unwrapResult.bytesConsumed() == 0 && unwrapResult.bytesProduced() == 0)
+ unwrap = Status.BUFFER_UNDERFLOW;
+
+ switch (unwrap)
+ {
+ case CLOSED:
+ Throwable failure = _failure;
+ if (failure != null)
+ rethrow(failure);
+ return filled = -1;
+
+ case BUFFER_UNDERFLOW:
+ // Continue if we can compact?
+ if (BufferUtil.compact(_encryptedInput))
+ continue;
+
+ // Are we out of space?
+ if (BufferUtil.space(_encryptedInput) == 0)
+ {
+ BufferUtil.clear(_encryptedInput);
+ throw new SSLHandshakeException("Encrypted buffer max length exceeded");
+ }
+
+ // if we just filled some
+ if (netFilled > 0)
+ continue; // try filling some more
+
+ _underflown = true;
+ if (netFilled < 0 && _sslEngine.getUseClientMode())
+ {
+ Throwable closeFailure = closeInbound();
+ if (_flushState == FlushState.WAIT_FOR_FILL)
+ {
+ Throwable handshakeFailure = new SSLHandshakeException("Abruptly closed by peer");
+ if (closeFailure != null)
+ handshakeFailure.addSuppressed(closeFailure);
+ throw handshakeFailure;
+ }
+ return filled = -1;
+ }
+ return filled = netFilled;
+
+ case BUFFER_OVERFLOW:
+ // It's possible that SSLSession.applicationBufferSize has been expanded
+ // by the SSLEngine implementation. Unwrapping a large encrypted buffer
+ // causes BUFFER_OVERFLOW because the (old) applicationBufferSize is
+ // too small. Release the decrypted input buffer so it will be re-acquired
+ // with the larger capacity.
+ // See also system property "jsse.SSLEngine.acceptLargeFragments".
+ if (BufferUtil.isEmpty(_decryptedInput) && appBufferSize < getApplicationBufferSize())
+ {
+ releaseDecryptedInputBuffer();
+ continue;
+ }
+ throw new IllegalStateException("Unexpected unwrap result " + unwrap);
+
+ case OK:
+ if (unwrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED)
+ handshakeSucceeded();
+
+ if (isRenegotiating() && !allowRenegotiate())
+ return filled = -1;
+
+ // If bytes were produced, don't bother with the handshake status;
+ // pass the decrypted data to the application, which will perform
+ // another call to fill() or flush().
+ if (unwrapResult.bytesProduced() > 0)
+ {
+ if (appIn == buffer)
+ return filled = unwrapResult.bytesProduced();
+ return filled = BufferUtil.append(buffer, _decryptedInput);
+ }
+
+ break;
+
+ default:
+ throw new IllegalStateException("Unexpected unwrap result " + unwrap);
+ }
+ }
+ }
+ catch (Throwable x)
+ {
+ Throwable f = handleException(x, "fill");
+ Throwable failure = handshakeFailed(f);
+ if (_flushState == FlushState.WAIT_FOR_FILL)
+ {
+ _flushState = FlushState.IDLE;
+ getExecutor().execute(() -> _decryptedEndPoint.getWriteFlusher().onFail(failure));
+ }
+ throw failure;
+ }
+ finally
+ {
+ releaseEncryptedInputBuffer();
+ releaseDecryptedInputBuffer();
+
+ if (_flushState == FlushState.WAIT_FOR_FILL)
+ {
+ _flushState = FlushState.IDLE;
+ getExecutor().execute(() -> _decryptedEndPoint.getWriteFlusher().completeWrite());
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("<fill f={} uf={} {}", filled, _underflown, SslConnection.this);
+ }
+ }
+ }
+ catch (Throwable x)
+ {
+ close(x);
+ rethrow(x);
+ // Never reached.
+ throw new AssertionError();
+ }
+ }
+
+ @Override
+ protected void needsFillInterest()
+ {
+ try
+ {
+ boolean fillable;
+ ByteBuffer write = null;
+ boolean interest = false;
+ synchronized (_decryptedEndPoint)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(">needFillInterest s={}/{} uf={} ei={} di={} {}",
+ _flushState,
+ _fillState,
+ _underflown,
+ BufferUtil.toDetailString(_encryptedInput),
+ BufferUtil.toDetailString(_decryptedInput),
+ SslConnection.this);
+
+ if (_fillState != FillState.IDLE)
+ return;
+
+ // Fillable if we have decrypted input OR enough encrypted input.
+ fillable = BufferUtil.hasContent(_decryptedInput) || (BufferUtil.hasContent(_encryptedInput) && !_underflown);
+
+ HandshakeStatus status = _sslEngine.getHandshakeStatus();
+ switch (status)
+ {
+ case NEED_TASK:
+ // Pretend we are fillable
+ fillable = true;
+ break;
+
+ case NEED_UNWRAP:
+ case NOT_HANDSHAKING:
+ if (!fillable)
+ {
+ interest = true;
+ _fillState = FillState.INTERESTED;
+ if (_flushState == FlushState.IDLE && BufferUtil.hasContent(_encryptedOutput))
+ {
+ _flushState = FlushState.WRITING;
+ write = _encryptedOutput;
+ }
+ }
+ break;
+
+ case NEED_WRAP:
+ if (!fillable)
+ {
+ _fillState = FillState.WAIT_FOR_FLUSH;
+ if (_flushState == FlushState.IDLE)
+ {
+ _flushState = FlushState.WRITING;
+ write = BufferUtil.hasContent(_encryptedOutput) ? _encryptedOutput : BufferUtil.EMPTY_BUFFER;
+ }
+ }
+ break;
+
+ default:
+ throw new IllegalStateException("Unexpected HandshakeStatus " + status);
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("<needFillInterest s={}/{} f={} i={} w={}", _flushState, _fillState, fillable, interest, BufferUtil.toDetailString(write));
+ }
+
+ if (write != null)
+ getEndPoint().write(_incompleteWriteCallback, write);
+ else if (fillable)
+ getExecutor().execute(_runFillable);
+ else if (interest)
+ ensureFillInterested();
+ }
+ catch (Throwable x)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(SslConnection.this.toString(), x);
+ close(x);
+ throw x;
+ }
+ }
+
+ private void handshakeSucceeded() throws SSLException
+ {
+ if (_handshake.compareAndSet(HandshakeState.HANDSHAKE, HandshakeState.SUCCEEDED))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("handshake succeeded {} {} {}/{}", SslConnection.this,
+ _sslEngine.getUseClientMode() ? "client" : "resumed server",
+ _sslEngine.getSession().getProtocol(), _sslEngine.getSession().getCipherSuite());
+ notifyHandshakeSucceeded(_sslEngine);
+ }
+ else if (isHandshakeSucceeded())
+ {
+ if (_renegotiationLimit > 0)
+ _renegotiationLimit--;
+ }
+ }
+
+ private Throwable handshakeFailed(Throwable failure)
+ {
+ if (_handshake.compareAndSet(HandshakeState.HANDSHAKE, HandshakeState.FAILED))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("handshake failed {} {}", SslConnection.this, failure);
+ if (!(failure instanceof SSLHandshakeException))
+ failure = new SSLHandshakeException(failure.getMessage()).initCause(failure);
+ notifyHandshakeFailed(_sslEngine, failure);
+ }
+ return failure;
+ }
+
+ private void terminateInput()
+ {
+ try
+ {
+ _sslEngine.closeInbound();
+ }
+ catch (Throwable x)
+ {
+ LOG.ignore(x);
+ }
+ }
+
+ private Throwable closeInbound() throws SSLException
+ {
+ HandshakeStatus handshakeStatus = _sslEngine.getHandshakeStatus();
+ try
+ {
+ _sslEngine.closeInbound();
+ return null;
+ }
+ catch (SSLException x)
+ {
+ if (handshakeStatus == HandshakeStatus.NOT_HANDSHAKING && isRequireCloseMessage())
+ throw x;
+ LOG.ignore(x);
+ return x;
+ }
+ catch (Throwable x)
+ {
+ LOG.ignore(x);
+ return x;
+ }
+ }
+
+ @Override
+ public boolean flush(ByteBuffer... appOuts) throws IOException
+ {
+ try
+ {
+ synchronized (_decryptedEndPoint)
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug(">flush {}", SslConnection.this);
+ int i = 0;
+ for (ByteBuffer b : appOuts)
+ {
+ LOG.debug("flush b[{}]={}", i++, BufferUtil.toDetailString(b));
+ }
+ }
+
+ // finish of any previous flushes
+ if (_encryptedOutput != null)
+ {
+ int remaining = _encryptedOutput.remaining();
+ if (remaining > 0)
+ {
+ boolean flushed = networkFlush(_encryptedOutput);
+ int written = remaining - _encryptedOutput.remaining();
+ if (written > 0)
+ _bytesOut.addAndGet(written);
+ if (!flushed)
+ return false;
+ }
+ }
+
+ boolean isEmpty = BufferUtil.isEmpty(appOuts);
+
+ Boolean result = null;
+ try
+ {
+ if (_flushState != FlushState.IDLE)
+ return result = false;
+
+ // Keep going while we can make progress or until we are done
+ while (true)
+ {
+ HandshakeStatus status = _sslEngine.getHandshakeStatus();
+ if (LOG.isDebugEnabled())
+ LOG.debug("flush {}", status);
+ switch (status)
+ {
+ case NEED_WRAP:
+ case NOT_HANDSHAKING:
+ break;
+
+ case NEED_TASK:
+ _sslEngine.getDelegatedTask().run();
+ continue;
+
+ case NEED_UNWRAP:
+ // Workaround for Java 11 behavior.
+ if (isHandshakeInitial() && isOutboundDone())
+ break;
+ if (_fillState == FillState.IDLE)
+ {
+ int filled = fill(BufferUtil.EMPTY_BUFFER);
+ if (_sslEngine.getHandshakeStatus() != status)
+ continue;
+ if (filled < 0)
+ throw new IOException("Broken pipe");
+ }
+ return result = isEmpty;
+
+ default:
+ throw new IllegalStateException("Unexpected HandshakeStatus " + status);
+ }
+
+ int packetBufferSize = getPacketBufferSize();
+ acquireEncryptedOutput();
+
+ if (_handshake.compareAndSet(HandshakeState.INITIAL, HandshakeState.HANDSHAKE))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("flush starting handshake {}", SslConnection.this);
+ }
+
+ // We call sslEngine.wrap to try to take bytes from appOuts
+ // buffers and encrypt them into the _encryptedOutput buffer.
+ BufferUtil.compact(_encryptedOutput);
+ int pos = BufferUtil.flipToFill(_encryptedOutput);
+ SSLEngineResult wrapResult;
+ try
+ {
+ wrapResult = wrap(_sslEngine, appOuts, _encryptedOutput);
+ }
+ finally
+ {
+ BufferUtil.flipToFlush(_encryptedOutput, pos);
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("wrap {} {} ioDone={}/{}",
+ StringUtil.replace(wrapResult.toString(), '\n', ' '),
+ BufferUtil.toSummaryString(_encryptedOutput),
+ _sslEngine.isInboundDone(),
+ _sslEngine.isOutboundDone());
+
+ // Was all the data consumed?
+ isEmpty = BufferUtil.isEmpty(appOuts);
+
+ // if we have net bytes, let's try to flush them
+ boolean flushed = true;
+ if (_encryptedOutput != null)
+ {
+ int remaining = _encryptedOutput.remaining();
+ if (remaining > 0)
+ {
+ flushed = networkFlush(_encryptedOutput);
+ int written = remaining - _encryptedOutput.remaining();
+ if (written > 0)
+ _bytesOut.addAndGet(written);
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("net flushed={}, ac={}", flushed, isEmpty);
+
+ // Now deal with the results returned from the wrap
+ Status wrap = wrapResult.getStatus();
+ switch (wrap)
+ {
+ case CLOSED:
+ {
+ // TODO: do we need to remember the CLOSED state or SSLEngine
+ // TODO: will produce CLOSED again if wrap() is called again?
+ if (!flushed)
+ return result = false;
+ getEndPoint().shutdownOutput();
+ if (isEmpty)
+ return result = true;
+ throw new IOException("Broken pipe");
+ }
+
+ case BUFFER_OVERFLOW:
+ if (!flushed)
+ return result = false;
+ // It's possible that SSLSession.packetBufferSize has been expanded
+ // by the SSLEngine implementation. Wrapping a large application buffer
+ // causes BUFFER_OVERFLOW because the (old) packetBufferSize is
+ // too small. Release the encrypted output buffer so that it will
+ // be re-acquired with the larger capacity.
+ // See also system property "jsse.SSLEngine.acceptLargeFragments".
+ if (packetBufferSize < getPacketBufferSize())
+ {
+ releaseEncryptedOutputBuffer();
+ continue;
+ }
+ throw new IllegalStateException("Unexpected wrap result " + wrap);
+
+ case OK:
+ if (wrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED)
+ handshakeSucceeded();
+
+ if (isRenegotiating() && !allowRenegotiate())
+ {
+ getEndPoint().shutdownOutput();
+ if (isEmpty && BufferUtil.isEmpty(_encryptedOutput))
+ return result = true;
+ throw new IOException("Broken pipe");
+ }
+
+ if (!flushed)
+ return result = false;
+
+ if (isEmpty)
+ {
+ if (wrapResult.getHandshakeStatus() != HandshakeStatus.NEED_WRAP ||
+ wrapResult.bytesProduced() == 0)
+ return result = true;
+ }
+ break;
+
+ default:
+ throw new IllegalStateException("Unexpected wrap result " + wrap);
+ }
+
+ if (getEndPoint().isOutputShutdown())
+ return false;
+ }
+ }
+ catch (Throwable x)
+ {
+ Throwable failure = handleException(x, "flush");
+ throw handshakeFailed(failure);
+ }
+ finally
+ {
+ releaseEncryptedOutputBuffer();
+ if (LOG.isDebugEnabled())
+ LOG.debug("<flush {} {}", result, SslConnection.this);
+ }
+ }
+ }
+ catch (Throwable x)
+ {
+ close(x);
+ rethrow(x);
+ // Never reached.
+ throw new AssertionError();
+ }
+ }
+
+ @Override
+ protected void onIncompleteFlush()
+ {
+ try
+ {
+ boolean fillInterest = false;
+ ByteBuffer write = null;
+ synchronized (_decryptedEndPoint)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(">onIncompleteFlush {} {}", SslConnection.this, BufferUtil.toDetailString(_encryptedOutput));
+
+ if (_flushState != FlushState.IDLE)
+ return;
+
+ while (true)
+ {
+ HandshakeStatus status = _sslEngine.getHandshakeStatus();
+ switch (status)
+ {
+ case NEED_TASK:
+ case NEED_WRAP:
+ case NOT_HANDSHAKING:
+ // write what we have or an empty buffer to reschedule a call to flush
+ write = BufferUtil.hasContent(_encryptedOutput) ? _encryptedOutput : BufferUtil.EMPTY_BUFFER;
+ _flushState = FlushState.WRITING;
+ break;
+
+ case NEED_UNWRAP:
+ // If we have something to write, then write it and ignore the needed unwrap for now.
+ if (BufferUtil.hasContent(_encryptedOutput))
+ {
+ write = _encryptedOutput;
+ _flushState = FlushState.WRITING;
+ break;
+ }
+
+ if (_fillState != FillState.IDLE)
+ {
+ // Wait for a fill that is happening anyway
+ _flushState = FlushState.WAIT_FOR_FILL;
+ break;
+ }
+
+ // Try filling ourselves
+ try
+ {
+ int filled = fill(BufferUtil.EMPTY_BUFFER);
+ // If this changed the status, let's try again
+ if (_sslEngine.getHandshakeStatus() != status)
+ continue;
+ if (filled < 0)
+ throw new IOException("Broken pipe");
+ }
+ catch (IOException e)
+ {
+ LOG.debug(e);
+ close(e);
+ write = BufferUtil.EMPTY_BUFFER;
+ _flushState = FlushState.WRITING;
+ break;
+ }
+
+ // Make sure we are fill interested.
+ fillInterest = true;
+ _fillState = FillState.INTERESTED;
+ _flushState = FlushState.WAIT_FOR_FILL;
+ break;
+
+ default:
+ throw new IllegalStateException("Unexpected HandshakeStatus " + status);
+ }
+ break;
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("<onIncompleteFlush s={}/{} fi={} w={}", _flushState, _fillState, fillInterest, BufferUtil.toDetailString(write));
+ }
+
+ if (write != null)
+ getEndPoint().write(_incompleteWriteCallback, write);
+ else if (fillInterest)
+ ensureFillInterested();
+ }
+ catch (Throwable x)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(SslConnection.this.toString(), x);
+ close(x);
+ throw x;
+ }
+ }
+
+ @Override
+ public void doShutdownOutput()
+ {
+ EndPoint endPoint = getEndPoint();
+ try
+ {
+ boolean close;
+ boolean flush = false;
+ synchronized (_decryptedEndPoint)
+ {
+ boolean ishut = endPoint.isInputShutdown();
+ boolean oshut = endPoint.isOutputShutdown();
+ if (LOG.isDebugEnabled())
+ LOG.debug("shutdownOutput: {} oshut={}, ishut={}", SslConnection.this, oshut, ishut);
+
+ closeOutbound();
+
+ if (!_closedOutbound)
+ {
+ _closedOutbound = true;
+ // Flush only once.
+ flush = !oshut;
+ }
+
+ close = ishut;
+ }
+
+ if (flush)
+ {
+ if (!flush(BufferUtil.EMPTY_BUFFER) && !close)
+ {
+ // If we still can't flush, but we are not closing the endpoint,
+ // let's just flush the encrypted output in the background.
+ ByteBuffer write = null;
+ synchronized (_decryptedEndPoint)
+ {
+ if (BufferUtil.hasContent(_encryptedOutput))
+ {
+ write = _encryptedOutput;
+ _flushState = FlushState.WRITING;
+ }
+ }
+ if (write != null)
+ {
+ endPoint.write(Callback.from(() ->
+ {
+ synchronized (_decryptedEndPoint)
+ {
+ _flushState = FlushState.IDLE;
+ releaseEncryptedOutputBuffer();
+ }
+ }, t -> endPoint.close()), write);
+ }
+ }
+ }
+
+ if (close)
+ endPoint.close();
+ else
+ ensureFillInterested();
+ }
+ catch (Throwable x)
+ {
+ LOG.ignore(x);
+ endPoint.close();
+ }
+ }
+
+ private void closeOutbound()
+ {
+ try
+ {
+ _sslEngine.closeOutbound();
+ }
+ catch (Throwable x)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(x);
+ }
+ }
+
+ private void ensureFillInterested()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("ensureFillInterested {}", SslConnection.this);
+ SslConnection.this.tryFillInterested(_sslReadCallback);
+ }
+
+ @Override
+ public boolean isOutputShutdown()
+ {
+ return isOutboundDone() || getEndPoint().isOutputShutdown();
+ }
+
+ private boolean isOutboundDone()
+ {
+ try
+ {
+ return _sslEngine.isOutboundDone();
+ }
+ catch (Throwable x)
+ {
+ LOG.ignore(x);
+ return true;
+ }
+ }
+
+ @Override
+ public void doClose()
+ {
+ // First send the TLS Close Alert, then the FIN.
+ doShutdownOutput();
+ getEndPoint().close();
+ super.doClose();
+ }
+
+ @Override
+ public Object getTransport()
+ {
+ return getEndPoint();
+ }
+
+ @Override
+ public boolean isInputShutdown()
+ {
+ return BufferUtil.isEmpty(_decryptedInput) && (getEndPoint().isInputShutdown() || isInboundDone());
+ }
+
+ private boolean isInboundDone()
+ {
+ try
+ {
+ return _sslEngine.isInboundDone();
+ }
+ catch (Throwable x)
+ {
+ LOG.ignore(x);
+ return true;
+ }
+ }
+
+ private void notifyHandshakeSucceeded(SSLEngine sslEngine) throws SSLException
+ {
+ SslHandshakeListener.Event event = null;
+ for (SslHandshakeListener listener : handshakeListeners)
+ {
+ if (event == null)
+ event = new SslHandshakeListener.Event(sslEngine);
+ try
+ {
+ listener.handshakeSucceeded(event);
+ }
+ catch (SSLException x)
+ {
+ throw x;
+ }
+ catch (Throwable x)
+ {
+ LOG.info("Exception while notifying listener " + listener, x);
+ }
+ }
+ }
+
+ private void notifyHandshakeFailed(SSLEngine sslEngine, Throwable failure)
+ {
+ SslHandshakeListener.Event event = null;
+ for (SslHandshakeListener listener : handshakeListeners)
+ {
+ if (event == null)
+ event = new SslHandshakeListener.Event(sslEngine);
+ try
+ {
+ listener.handshakeFailed(event, failure);
+ }
+ catch (Throwable x)
+ {
+ LOG.info("Exception while notifying listener " + listener, x);
+ }
+ }
+ }
+
+ private boolean isRenegotiating()
+ {
+ if (!isHandshakeComplete())
+ return false;
+ if (isTLS13())
+ return false;
+ return _sslEngine.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING;
+ }
+
+ private boolean allowRenegotiate()
+ {
+ if (!isRenegotiationAllowed())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Renegotiation denied {}", SslConnection.this);
+ terminateInput();
+ return false;
+ }
+
+ if (getRenegotiationLimit() == 0)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Renegotiation limit exceeded {}", SslConnection.this);
+ terminateInput();
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean isTLS13()
+ {
+ String protocol = _sslEngine.getSession().getProtocol();
+ return TLS_1_3.equals(protocol);
+ }
+
+ private Throwable handleException(Throwable x, String context)
+ {
+ synchronized (_decryptedEndPoint)
+ {
+ if (_failure == null)
+ {
+ _failure = x;
+ if (LOG.isDebugEnabled())
+ LOG.debug(this + " stored " + context + " exception", x);
+ }
+ else if (x != _failure)
+ {
+ _failure.addSuppressed(x);
+ if (LOG.isDebugEnabled())
+ LOG.debug(this + " suppressed " + context + " exception", x);
+ }
+ return _failure;
+ }
+ }
+
+ private void rethrow(Throwable x) throws IOException
+ {
+ if (x instanceof RuntimeException)
+ throw (RuntimeException)x;
+ if (x instanceof Error)
+ throw (Error)x;
+ if (x instanceof IOException)
+ throw (IOException)x;
+ throw new IOException(x);
+ }
+
+ @Override
+ public String toString()
+ {
+ return super.toEndPointString();
+ }
+
+ private final class IncompleteWriteCallback implements Callback, Invocable
+ {
+ @Override
+ public void succeeded()
+ {
+ boolean fillable;
+ boolean interested;
+ synchronized (_decryptedEndPoint)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("IncompleteWriteCB succeeded {}", SslConnection.this);
+ releaseEncryptedOutputBuffer();
+ _flushState = FlushState.IDLE;
+
+ interested = _fillState == FillState.INTERESTED;
+ fillable = _fillState == FillState.WAIT_FOR_FLUSH;
+ if (fillable)
+ _fillState = FillState.IDLE;
+ }
+
+ if (interested)
+ ensureFillInterested();
+ else if (fillable)
+ _decryptedEndPoint.getFillInterest().fillable();
+
+ _decryptedEndPoint.getWriteFlusher().completeWrite();
+ }
+
+ @Override
+ public void failed(final Throwable x)
+ {
+ boolean failFillInterest;
+ synchronized (_decryptedEndPoint)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("IncompleteWriteCB failed {}", SslConnection.this, x);
+
+ BufferUtil.clear(_encryptedOutput);
+ releaseEncryptedOutputBuffer();
+
+ _flushState = FlushState.IDLE;
+ failFillInterest = _fillState == FillState.WAIT_FOR_FLUSH ||
+ _fillState == FillState.INTERESTED;
+ if (failFillInterest)
+ _fillState = FillState.IDLE;
+ }
+
+ getExecutor().execute(() ->
+ {
+ if (failFillInterest)
+ _decryptedEndPoint.getFillInterest().onFail(x);
+ _decryptedEndPoint.getWriteFlusher().onFail(x);
+ });
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return _decryptedEndPoint.getWriteFlusher().getCallbackInvocationType();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("SSL@%h.DEP.writeCallback", SslConnection.this);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java
new file mode 100644
index 0000000..25b07a1
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java
@@ -0,0 +1,72 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io.ssl;
+
+import java.util.EventListener;
+import java.util.EventObject;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+
+/**
+ * <p>Implementations of this interface are notified of TLS handshake events.</p>
+ * <p>Similar to {@link javax.net.ssl.HandshakeCompletedListener}, but for {@link SSLEngine}.</p>
+ * <p>Typical usage if to add instances of this class as beans to a server connector, or
+ * to a client connector.</p>
+ */
+public interface SslHandshakeListener extends EventListener
+{
+ /**
+ * <p>Callback method invoked when the TLS handshake succeeds.</p>
+ *
+ * @param event the event object carrying information about the TLS handshake event
+ * @throws SSLException if any error happen during handshake
+ */
+ default void handshakeSucceeded(Event event) throws SSLException
+ {
+ }
+
+ /**
+ * <p>Callback method invoked when the TLS handshake fails.</p>
+ *
+ * @param event the event object carrying information about the TLS handshake event
+ * @param failure the failure that caused the TLS handshake to fail
+ */
+ default void handshakeFailed(Event event, Throwable failure)
+ {
+ }
+
+ /**
+ * <p>The event object carrying information about TLS handshake events.</p>
+ */
+ class Event extends EventObject
+ {
+ public Event(Object source)
+ {
+ super(source);
+ }
+
+ /**
+ * @return the SSLEngine associated to the TLS handshake event
+ */
+ public SSLEngine getSSLEngine()
+ {
+ return (SSLEngine)getSource();
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/package-info.java b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/package-info.java
new file mode 100644
index 0000000..0261d21
--- /dev/null
+++ b/third_party/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty IO : Core SSL Support
+ */
+package org.eclipse.jetty.io.ssl;
+
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/ArrayByteBufferPoolTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/ArrayByteBufferPoolTest.java
new file mode 100644
index 0000000..705f29e
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/ArrayByteBufferPoolTest.java
@@ -0,0 +1,230 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Objects;
+
+import org.eclipse.jetty.io.ByteBufferPool.Bucket;
+import org.eclipse.jetty.util.StringUtil;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.lessThan;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ArrayByteBufferPoolTest
+{
+ @Test
+ public void testMinimumRelease()
+ {
+ ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(10, 100, 1000);
+ ByteBufferPool.Bucket[] buckets = bufferPool.bucketsFor(true);
+
+ for (int size = 1; size <= 9; size++)
+ {
+ ByteBuffer buffer = bufferPool.acquire(size, true);
+
+ assertTrue(buffer.isDirect());
+ assertEquals(size, buffer.capacity());
+ for (ByteBufferPool.Bucket bucket : buckets)
+ {
+ if (bucket != null)
+ assertTrue(bucket.isEmpty());
+ }
+
+ bufferPool.release(buffer);
+
+ for (ByteBufferPool.Bucket bucket : buckets)
+ {
+ if (bucket != null)
+ assertTrue(bucket.isEmpty());
+ }
+ }
+ }
+
+ @Test
+ public void testMaxRelease()
+ {
+ ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(10, 100, 1000);
+ ByteBufferPool.Bucket[] buckets = bufferPool.bucketsFor(true);
+
+ for (int size = 999; size <= 1001; size++)
+ {
+ bufferPool.clear();
+ ByteBuffer buffer = bufferPool.acquire(size, true);
+
+ assertTrue(buffer.isDirect());
+ assertThat(buffer.capacity(), greaterThanOrEqualTo(size));
+ for (ByteBufferPool.Bucket bucket : buckets)
+ {
+ if (bucket != null)
+ assertTrue(bucket.isEmpty());
+ }
+
+ bufferPool.release(buffer);
+
+ int pooled = Arrays.stream(buckets)
+ .filter(Objects::nonNull)
+ .mapToInt(Bucket::size)
+ .sum();
+ assertEquals(size <= 1000, 1 == pooled);
+ }
+ }
+
+ @Test
+ public void testAcquireRelease()
+ {
+ ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(10, 100, 1000);
+ ByteBufferPool.Bucket[] buckets = bufferPool.bucketsFor(true);
+
+ for (int size = 390; size <= 510; size++)
+ {
+ bufferPool.clear();
+ ByteBuffer buffer = bufferPool.acquire(size, true);
+
+ assertTrue(buffer.isDirect());
+ assertThat(buffer.capacity(), greaterThanOrEqualTo(size));
+ for (ByteBufferPool.Bucket bucket : buckets)
+ {
+ if (bucket != null)
+ assertTrue(bucket.isEmpty());
+ }
+
+ bufferPool.release(buffer);
+
+ int pooled = Arrays.stream(buckets)
+ .filter(Objects::nonNull)
+ .mapToInt(Bucket::size)
+ .sum();
+ assertEquals(1, pooled);
+ }
+ }
+
+ @Test
+ public void testAcquireReleaseAcquire()
+ {
+ ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(10, 100, 1000);
+ ByteBufferPool.Bucket[] buckets = bufferPool.bucketsFor(true);
+
+ for (int size = 390; size <= 510; size++)
+ {
+ bufferPool.clear();
+ ByteBuffer buffer1 = bufferPool.acquire(size, true);
+ bufferPool.release(buffer1);
+ ByteBuffer buffer2 = bufferPool.acquire(size, true);
+ bufferPool.release(buffer2);
+ ByteBuffer buffer3 = bufferPool.acquire(size, false);
+ bufferPool.release(buffer3);
+
+ int pooled = Arrays.stream(buckets)
+ .filter(Objects::nonNull)
+ .mapToInt(Bucket::size)
+ .sum();
+ assertEquals(1, pooled);
+
+ assertSame(buffer1, buffer2);
+ assertNotSame(buffer1, buffer3);
+ }
+ }
+
+ @Test
+ public void testReleaseNonPooledBuffer()
+ {
+ ArrayByteBufferPool bufferPool = new ArrayByteBufferPool();
+
+ // Release a few small non-pool buffers
+ bufferPool.release(ByteBuffer.wrap(StringUtil.getUtf8Bytes("Hello")));
+
+ assertEquals(0, bufferPool.getHeapByteBufferCount());
+ }
+
+ @Test
+ public void testMaxQueue()
+ {
+ ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(-1, -1, -1, 2);
+
+ ByteBuffer buffer1 = bufferPool.acquire(512, false);
+ ByteBuffer buffer2 = bufferPool.acquire(512, false);
+ ByteBuffer buffer3 = bufferPool.acquire(512, false);
+
+ Bucket[] buckets = bufferPool.bucketsFor(false);
+ Arrays.stream(buckets)
+ .filter(Objects::nonNull)
+ .forEach(b -> assertEquals(0, b.size()));
+
+ bufferPool.release(buffer1);
+ Bucket bucket = Arrays.stream(buckets)
+ .filter(Objects::nonNull)
+ .filter(b -> b.size() > 0)
+ .findFirst()
+ .orElseThrow(AssertionError::new);
+ assertEquals(1, bucket.size());
+
+ bufferPool.release(buffer2);
+ assertEquals(2, bucket.size());
+
+ bufferPool.release(buffer3);
+ assertEquals(2, bucket.size());
+ }
+
+ @Test
+ public void testMaxMemory()
+ {
+ int factor = 1024;
+ int maxMemory = 11 * 1024;
+ ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(-1, factor, -1, -1, -1, maxMemory);
+ Bucket[] buckets = bufferPool.bucketsFor(true);
+
+ // Create the buckets - the oldest is the larger.
+ // 1+2+3+4=10 / maxMemory=11.
+ for (int i = 4; i >= 1; --i)
+ {
+ int capacity = factor * i;
+ ByteBuffer buffer = bufferPool.acquire(capacity, true);
+ bufferPool.release(buffer);
+ }
+
+ // Create and release a buffer to exceed the max memory.
+ ByteBuffer buffer = bufferPool.newByteBuffer(2 * factor, true);
+ bufferPool.release(buffer);
+
+ // Now the oldest buffer should be gone and we have: 1+2x2+3=8
+ long memory = bufferPool.getMemory(true);
+ assertThat(memory, lessThan((long)maxMemory));
+ assertNull(buckets[3]);
+
+ // Create and release a large buffer.
+ // Max memory is exceeded and buckets 3 and 1 are cleared.
+ // We will have 2x2+7=11.
+ buffer = bufferPool.newByteBuffer(7 * factor, true);
+ bufferPool.release(buffer);
+ memory = bufferPool.getMemory(true);
+ assertThat(memory, lessThanOrEqualTo((long)maxMemory));
+ assertNull(buckets[0]);
+ assertNull(buckets[2]);
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/ByteArrayEndPointTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/ByteArrayEndPointTest.java
new file mode 100644
index 0000000..9cf070b
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/ByteArrayEndPointTest.java
@@ -0,0 +1,306 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.FutureCallback;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.eclipse.jetty.util.thread.TimerScheduler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class ByteArrayEndPointTest
+{
+ private Scheduler _scheduler;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ _scheduler = new TimerScheduler();
+ _scheduler.start();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _scheduler.stop();
+ }
+
+ @Test
+ public void testFill() throws Exception
+ {
+ ByteArrayEndPoint endp = new ByteArrayEndPoint();
+ endp.addInput("test input");
+
+ ByteBuffer buffer = BufferUtil.allocate(1024);
+
+ assertEquals(10, endp.fill(buffer));
+ assertEquals("test input", BufferUtil.toString(buffer));
+
+ assertEquals(0, endp.fill(buffer));
+
+ endp.addInput(" more");
+ assertEquals(5, endp.fill(buffer));
+ assertEquals("test input more", BufferUtil.toString(buffer));
+
+ assertEquals(0, endp.fill(buffer));
+
+ endp.addInput((ByteBuffer)null);
+
+ assertEquals(-1, endp.fill(buffer));
+
+ endp.close();
+
+ try
+ {
+ endp.fill(buffer);
+ fail("Expected IOException");
+ }
+ catch (IOException e)
+ {
+ assertThat(e.getMessage(), containsString("CLOSED"));
+ }
+
+ endp.reset();
+ endp.addInput("and more");
+ buffer = BufferUtil.allocate(4);
+
+ assertEquals(4, endp.fill(buffer));
+ assertEquals("and ", BufferUtil.toString(buffer));
+ assertEquals(0, endp.fill(buffer));
+ BufferUtil.clear(buffer);
+ assertEquals(4, endp.fill(buffer));
+ assertEquals("more", BufferUtil.toString(buffer));
+ }
+
+ @Test
+ public void testGrowingFlush() throws Exception
+ {
+ ByteArrayEndPoint endp = new ByteArrayEndPoint((byte[])null, 15);
+ endp.setGrowOutput(true);
+
+ assertEquals(true, endp.flush(BufferUtil.toBuffer("some output")));
+ assertEquals("some output", endp.getOutputString());
+
+ assertEquals(true, endp.flush(BufferUtil.toBuffer(" some more")));
+ assertEquals("some output some more", endp.getOutputString());
+
+ assertEquals(true, endp.flush());
+ assertEquals("some output some more", endp.getOutputString());
+
+ assertEquals(true, endp.flush(BufferUtil.EMPTY_BUFFER));
+ assertEquals("some output some more", endp.getOutputString());
+
+ assertEquals(true, endp.flush(BufferUtil.EMPTY_BUFFER, BufferUtil.toBuffer(" and"), BufferUtil.toBuffer(" more")));
+ assertEquals("some output some more and more", endp.getOutputString());
+ endp.close();
+ }
+
+ @Test
+ public void testFlush() throws Exception
+ {
+ ByteArrayEndPoint endp = new ByteArrayEndPoint((byte[])null, 15);
+ endp.setGrowOutput(false);
+ endp.setOutput(BufferUtil.allocate(10));
+
+ ByteBuffer data = BufferUtil.toBuffer("Some more data.");
+ assertEquals(false, endp.flush(data));
+ assertEquals("Some more ", endp.getOutputString());
+ assertEquals("data.", BufferUtil.toString(data));
+
+ assertEquals("Some more ", endp.takeOutputString());
+
+ assertEquals(true, endp.flush(data));
+ assertEquals("data.", BufferUtil.toString(endp.takeOutput()));
+ endp.close();
+ }
+
+ @Test
+ public void testReadable() throws Exception
+ {
+ ByteArrayEndPoint endp = new ByteArrayEndPoint(_scheduler, 5000);
+ endp.addInput("test input");
+
+ ByteBuffer buffer = BufferUtil.allocate(1024);
+ FutureCallback fcb = new FutureCallback();
+
+ endp.fillInterested(fcb);
+ fcb.get(100, TimeUnit.MILLISECONDS);
+ assertTrue(fcb.isDone());
+ assertEquals(null, fcb.get());
+ assertEquals(10, endp.fill(buffer));
+ assertEquals("test input", BufferUtil.toString(buffer));
+
+ fcb = new FutureCallback();
+ endp.fillInterested(fcb);
+ Thread.sleep(100);
+ assertFalse(fcb.isDone());
+ assertEquals(0, endp.fill(buffer));
+
+ endp.addInput(" more");
+ fcb.get(1000, TimeUnit.MILLISECONDS);
+ assertTrue(fcb.isDone());
+ assertEquals(null, fcb.get());
+ assertEquals(5, endp.fill(buffer));
+ assertEquals("test input more", BufferUtil.toString(buffer));
+
+ fcb = new FutureCallback();
+ endp.fillInterested(fcb);
+ Thread.sleep(100);
+ assertFalse(fcb.isDone());
+ assertEquals(0, endp.fill(buffer));
+
+ endp.addInput((ByteBuffer)null);
+ assertTrue(fcb.isDone());
+ assertEquals(null, fcb.get());
+ assertEquals(-1, endp.fill(buffer));
+
+ fcb = new FutureCallback();
+ endp.fillInterested(fcb);
+ fcb.get(1000, TimeUnit.MILLISECONDS);
+ assertTrue(fcb.isDone());
+ assertEquals(null, fcb.get());
+ assertEquals(-1, endp.fill(buffer));
+
+ endp.close();
+
+ fcb = new FutureCallback();
+ endp.fillInterested(fcb);
+
+ try
+ {
+ fcb.get(1000, TimeUnit.MILLISECONDS);
+ fail("Expected ExecutionException");
+ }
+ catch (ExecutionException e)
+ {
+ assertThat(e.toString(), containsString("Closed"));
+ }
+ }
+
+ @Test
+ public void testWrite() throws Exception
+ {
+ ByteArrayEndPoint endp = new ByteArrayEndPoint(_scheduler, 5000, (byte[])null, 15);
+ endp.setGrowOutput(false);
+ endp.setOutput(BufferUtil.allocate(10));
+
+ ByteBuffer data = BufferUtil.toBuffer("Data.");
+ ByteBuffer more = BufferUtil.toBuffer(" Some more.");
+
+ FutureCallback fcb = new FutureCallback();
+ endp.write(fcb, data);
+ assertTrue(fcb.isDone());
+ assertEquals(null, fcb.get());
+ assertEquals("Data.", endp.getOutputString());
+
+ fcb = new FutureCallback();
+ endp.write(fcb, more);
+ assertFalse(fcb.isDone());
+
+ assertEquals("Data. Some", endp.getOutputString());
+ assertEquals("Data. Some", endp.takeOutputString());
+
+ assertTrue(fcb.isDone());
+ assertEquals(null, fcb.get());
+ assertEquals(" more.", endp.getOutputString());
+ endp.close();
+ }
+
+ /**
+ * Simulate AbstractConnection.ReadCallback.failed()
+ */
+ public static class Closer extends FutureCallback
+ {
+ private EndPoint endp;
+
+ public Closer(EndPoint endp)
+ {
+ this.endp = endp;
+ }
+
+ @Override
+ public void failed(Throwable cause)
+ {
+ endp.close();
+ super.failed(cause);
+ }
+ }
+
+ @Test
+ public void testIdle() throws Exception
+ {
+ long idleTimeout = 1500;
+ long halfIdleTimeout = idleTimeout / 2;
+ long oneAndHalfIdleTimeout = idleTimeout + halfIdleTimeout;
+
+ ByteArrayEndPoint endp = new ByteArrayEndPoint(_scheduler, idleTimeout);
+ endp.setGrowOutput(false);
+ endp.addInput("test");
+ endp.setOutput(BufferUtil.allocate(5));
+
+ assertTrue(endp.isOpen());
+ Thread.sleep(oneAndHalfIdleTimeout);
+ // Still open because it has not been oshut or closed explicitly
+ // and there are no callbacks, so idle timeout is ignored.
+ assertTrue(endp.isOpen());
+
+ // Normal read is immediate, since there is data to read.
+ ByteBuffer buffer = BufferUtil.allocate(1024);
+ FutureCallback fcb = new FutureCallback();
+ endp.fillInterested(fcb);
+ fcb.get(idleTimeout, TimeUnit.MILLISECONDS);
+ assertTrue(fcb.isDone());
+ assertEquals(4, endp.fill(buffer));
+ assertEquals("test", BufferUtil.toString(buffer));
+
+ // Wait for a read timeout.
+ long start = System.nanoTime();
+ fcb = new FutureCallback();
+ endp.fillInterested(fcb);
+ try
+ {
+ fcb.get();
+ fail("Expected ExecutionException");
+ }
+ catch (ExecutionException t)
+ {
+ assertThat(t.getCause(), instanceOf(TimeoutException.class));
+ }
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start), greaterThan(halfIdleTimeout));
+ assertThat("Endpoint open", endp.isOpen(), is(true));
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/ByteBufferAccumulatorTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/ByteBufferAccumulatorTest.java
new file mode 100644
index 0000000..aa37001
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/ByteBufferAccumulatorTest.java
@@ -0,0 +1,333 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class ByteBufferAccumulatorTest
+{
+ private CountingBufferPool byteBufferPool;
+ private ByteBufferAccumulator accumulator;
+
+ @BeforeEach
+ public void before()
+ {
+ byteBufferPool = new CountingBufferPool();
+ accumulator = new ByteBufferAccumulator(byteBufferPool, false);
+ }
+
+ @Test
+ public void testToBuffer()
+ {
+ int size = 1024 * 1024;
+ int allocationSize = 1024;
+ ByteBuffer content = randomBytes(size);
+ ByteBuffer slice = content.slice();
+
+ // We completely fill up the internal buffer with the first write.
+ ByteBuffer internalBuffer = accumulator.ensureBuffer(1, allocationSize);
+ assertThat(BufferUtil.space(internalBuffer), greaterThanOrEqualTo(allocationSize));
+ writeInFlushMode(slice, internalBuffer);
+ assertThat(BufferUtil.space(internalBuffer), is(0));
+
+ // If we ask for min size of 0 we get the same buffer which is full.
+ internalBuffer = accumulator.ensureBuffer(0, allocationSize);
+ assertThat(BufferUtil.space(internalBuffer), is(0));
+
+ // If we need at least 1 minSpace we must allocate a new buffer.
+ internalBuffer = accumulator.ensureBuffer(1, allocationSize);
+ assertThat(BufferUtil.space(internalBuffer), greaterThan(0));
+ assertThat(BufferUtil.space(internalBuffer), greaterThanOrEqualTo(allocationSize));
+
+ // Write 13 bytes from the end of the internal buffer.
+ int bytesToWrite = BufferUtil.space(internalBuffer) - 13;
+ ByteBuffer buffer = BufferUtil.toBuffer(new byte[bytesToWrite]);
+ BufferUtil.clear(buffer);
+ assertThat(writeInFlushMode(slice, buffer), is(bytesToWrite));
+ assertThat(writeInFlushMode(buffer, internalBuffer), is(bytesToWrite));
+ assertThat(BufferUtil.space(internalBuffer), is(13));
+
+ // If we request anything under the amount remaining we get back the same buffer.
+ for (int i = 0; i <= 13; i++)
+ {
+ internalBuffer = accumulator.ensureBuffer(i, allocationSize);
+ assertThat(BufferUtil.space(internalBuffer), is(13));
+ }
+
+ // If we request over 13 then we get a new buffer.
+ internalBuffer = accumulator.ensureBuffer(14, allocationSize);
+ assertThat(BufferUtil.space(internalBuffer), greaterThanOrEqualTo(1024));
+
+ // Copy the rest of the content.
+ while (slice.hasRemaining())
+ {
+ internalBuffer = accumulator.ensureBuffer(1, allocationSize);
+ assertThat(BufferUtil.space(internalBuffer), greaterThanOrEqualTo(1));
+ writeInFlushMode(slice, internalBuffer);
+ }
+
+ // Check we have the same content as the original buffer.
+ assertThat(accumulator.getLength(), is(size));
+ assertThat(byteBufferPool.getLeasedBuffers(), greaterThan(1L));
+ ByteBuffer combinedBuffer = accumulator.toByteBuffer();
+ assertThat(byteBufferPool.getLeasedBuffers(), is(1L));
+ assertThat(accumulator.getLength(), is(size));
+ assertThat(combinedBuffer, is(content));
+
+ // Close the accumulator and make sure all is returned to bufferPool.
+ accumulator.close();
+ byteBufferPool.verifyClosed();
+ }
+
+ @Test
+ public void testTakeBuffer()
+ {
+ int size = 1024 * 1024;
+ int allocationSize = 1024;
+ ByteBuffer content = randomBytes(size);
+ ByteBuffer slice = content.slice();
+
+ // Copy the content.
+ while (slice.hasRemaining())
+ {
+ ByteBuffer internalBuffer = accumulator.ensureBuffer(1, allocationSize);
+ assertThat(BufferUtil.space(internalBuffer), greaterThanOrEqualTo(1));
+ writeInFlushMode(slice, internalBuffer);
+ }
+
+ // Check we have the same content as the original buffer.
+ assertThat(accumulator.getLength(), is(size));
+ assertThat(byteBufferPool.getLeasedBuffers(), greaterThan(1L));
+ ByteBuffer combinedBuffer = accumulator.takeByteBuffer();
+ assertThat(byteBufferPool.getLeasedBuffers(), is(1L));
+ assertThat(accumulator.getLength(), is(0));
+ accumulator.close();
+ assertThat(byteBufferPool.getLeasedBuffers(), is(1L));
+ assertThat(combinedBuffer, is(content));
+
+ // Return the buffer and make sure all is returned to bufferPool.
+ byteBufferPool.release(combinedBuffer);
+ byteBufferPool.verifyClosed();
+ }
+
+ @Test
+ public void testToByteArray()
+ {
+ int size = 1024 * 1024;
+ int allocationSize = 1024;
+ ByteBuffer content = randomBytes(size);
+ ByteBuffer slice = content.slice();
+
+ // Copy the content.
+ while (slice.hasRemaining())
+ {
+ ByteBuffer internalBuffer = accumulator.ensureBuffer(1, allocationSize);
+ writeInFlushMode(slice, internalBuffer);
+ }
+
+ // Check we have the same content as the original buffer.
+ assertThat(accumulator.getLength(), is(size));
+ assertThat(byteBufferPool.getLeasedBuffers(), greaterThan(1L));
+ byte[] combinedBuffer = accumulator.toByteArray();
+ assertThat(byteBufferPool.getLeasedBuffers(), greaterThan(1L));
+ assertThat(accumulator.getLength(), is(size));
+ assertThat(BufferUtil.toBuffer(combinedBuffer), is(content));
+
+ // Close the accumulator and make sure all is returned to bufferPool.
+ accumulator.close();
+ byteBufferPool.verifyClosed();
+ }
+
+ @Test
+ public void testEmptyToBuffer()
+ {
+ ByteBuffer combinedBuffer = accumulator.toByteBuffer();
+ assertThat(combinedBuffer.remaining(), is(0));
+ assertThat(byteBufferPool.getLeasedBuffers(), is(1L));
+ accumulator.close();
+ byteBufferPool.verifyClosed();
+ }
+
+ @Test
+ public void testEmptyTakeBuffer()
+ {
+ ByteBuffer combinedBuffer = accumulator.takeByteBuffer();
+ assertThat(combinedBuffer.remaining(), is(0));
+ accumulator.close();
+ assertThat(byteBufferPool.getLeasedBuffers(), is(1L));
+ byteBufferPool.release(combinedBuffer);
+ byteBufferPool.verifyClosed();
+ }
+
+ @Test
+ public void testWriteTo()
+ {
+ int size = 1024 * 1024;
+ int allocationSize = 1024;
+ ByteBuffer content = randomBytes(size);
+ ByteBuffer slice = content.slice();
+
+ // Copy the content.
+ while (slice.hasRemaining())
+ {
+ ByteBuffer internalBuffer = accumulator.ensureBuffer(1, allocationSize);
+ writeInFlushMode(slice, internalBuffer);
+ }
+
+ // Check we have the same content as the original buffer.
+ assertThat(byteBufferPool.getLeasedBuffers(), greaterThan(1L));
+ ByteBuffer combinedBuffer = byteBufferPool.acquire(accumulator.getLength(), false);
+ accumulator.writeTo(combinedBuffer);
+ assertThat(accumulator.getLength(), is(size));
+ assertThat(combinedBuffer, is(content));
+ byteBufferPool.release(combinedBuffer);
+
+ // Close the accumulator and make sure all is returned to bufferPool.
+ accumulator.close();
+ byteBufferPool.verifyClosed();
+ }
+
+ @Test
+ public void testWriteToBufferTooSmall()
+ {
+ int size = 1024 * 1024;
+ int allocationSize = 1024;
+ ByteBuffer content = randomBytes(size);
+ ByteBuffer slice = content.slice();
+
+ // Copy the content.
+ while (slice.hasRemaining())
+ {
+ ByteBuffer internalBuffer = accumulator.ensureBuffer(1, allocationSize);
+ writeInFlushMode(slice, internalBuffer);
+ }
+
+ // Writing to a buffer too small gives buffer overflow.
+ assertThat(byteBufferPool.getLeasedBuffers(), greaterThan(1L));
+ ByteBuffer combinedBuffer = BufferUtil.toBuffer(new byte[accumulator.getLength() - 1]);
+ BufferUtil.clear(combinedBuffer);
+ assertThrows(BufferOverflowException.class, () -> accumulator.writeTo(combinedBuffer));
+
+ // Close the accumulator and make sure all is returned to bufferPool.
+ accumulator.close();
+ byteBufferPool.verifyClosed();
+ }
+
+ @Test
+ public void testCopy()
+ {
+ int size = 1024 * 1024;
+ ByteBuffer content = randomBytes(size);
+ ByteBuffer slice = content.slice();
+
+ // Copy the content.
+ int tmpBufferSize = 1024;
+ ByteBuffer tmpBuffer = BufferUtil.toBuffer(new byte[tmpBufferSize]);
+ BufferUtil.clear(tmpBuffer);
+ while (slice.hasRemaining())
+ {
+ writeInFlushMode(slice, tmpBuffer);
+ accumulator.copyBuffer(tmpBuffer);
+ }
+
+ // Check we have the same content as the original buffer.
+ assertThat(byteBufferPool.getLeasedBuffers(), greaterThan(1L));
+ ByteBuffer combinedBuffer = byteBufferPool.acquire(accumulator.getLength(), false);
+ accumulator.writeTo(combinedBuffer);
+ assertThat(accumulator.getLength(), is(size));
+ assertThat(combinedBuffer, is(content));
+ byteBufferPool.release(combinedBuffer);
+
+ // Close the accumulator and make sure all is returned to bufferPool.
+ accumulator.close();
+ byteBufferPool.verifyClosed();
+ }
+
+ private ByteBuffer randomBytes(int size)
+ {
+ byte[] data = new byte[size];
+ new Random().nextBytes(data);
+ return BufferUtil.toBuffer(data);
+ }
+
+ private int writeInFlushMode(ByteBuffer from, ByteBuffer to)
+ {
+ int pos = BufferUtil.flipToFill(to);
+ int written = BufferUtil.put(from, to);
+ BufferUtil.flipToFlush(to, pos);
+ return written;
+ }
+
+ public static class CountingBufferPool extends LeakTrackingByteBufferPool
+ {
+ private final AtomicLong _leasedBuffers = new AtomicLong(0);
+
+ public CountingBufferPool()
+ {
+ this(new MappedByteBufferPool());
+ }
+
+ public CountingBufferPool(ByteBufferPool delegate)
+ {
+ super(delegate);
+ }
+
+ @Override
+ public ByteBuffer acquire(int size, boolean direct)
+ {
+ _leasedBuffers.incrementAndGet();
+ return super.acquire(size, direct);
+ }
+
+ @Override
+ public void release(ByteBuffer buffer)
+ {
+ if (buffer != null)
+ _leasedBuffers.decrementAndGet();
+ super.release(buffer);
+ }
+
+ public long getLeasedBuffers()
+ {
+ return _leasedBuffers.get();
+ }
+
+ public void verifyClosed()
+ {
+ assertThat(_leasedBuffers.get(), is(0L));
+ assertThat(getLeakedAcquires(), is(0L));
+ assertThat(getLeakedReleases(), is(0L));
+ assertThat(getLeakedResources(), is(0L));
+ assertThat(getLeakedRemoves(), is(0L));
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/CyclicTimeoutTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/CyclicTimeoutTest.java
new file mode 100644
index 0000000..078db26
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/CyclicTimeoutTest.java
@@ -0,0 +1,166 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class CyclicTimeoutTest
+{
+ private volatile boolean _expired;
+ private ScheduledExecutorScheduler _timer = new ScheduledExecutorScheduler();
+ private CyclicTimeout _timeout;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ _expired = false;
+ _timer.start();
+
+ _timeout = new CyclicTimeout(_timer)
+ {
+ @Override
+ public void onTimeoutExpired()
+ {
+ _expired = true;
+ }
+ };
+
+ _timeout.schedule(1000, TimeUnit.MILLISECONDS);
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _timeout.destroy();
+ _timer.stop();
+ }
+
+ @Test
+ public void testReschedule() throws Exception
+ {
+ for (int i = 0; i < 20; i++)
+ {
+ Thread.sleep(100);
+ assertTrue(_timeout.schedule(1000, TimeUnit.MILLISECONDS));
+ }
+ assertFalse(_expired);
+ }
+
+ @Test
+ public void testExpire() throws Exception
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ Thread.sleep(100);
+ assertTrue(_timeout.schedule(1000, TimeUnit.MILLISECONDS));
+ }
+ Thread.sleep(1500);
+ assertTrue(_expired);
+ }
+
+ @Test
+ public void testCancel() throws Exception
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ Thread.sleep(100);
+ assertTrue(_timeout.schedule(1000, TimeUnit.MILLISECONDS));
+ }
+ _timeout.cancel();
+ Thread.sleep(1500);
+ assertFalse(_expired);
+ }
+
+ @Test
+ public void testShorten() throws Exception
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ Thread.sleep(100);
+ assertTrue(_timeout.schedule(1000, TimeUnit.MILLISECONDS));
+ }
+ assertTrue(_timeout.schedule(100, TimeUnit.MILLISECONDS));
+ Thread.sleep(400);
+ assertTrue(_expired);
+ }
+
+ @Test
+ public void testLengthen() throws Exception
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ Thread.sleep(100);
+ assertTrue(_timeout.schedule(1000, TimeUnit.MILLISECONDS));
+ }
+ assertTrue(_timeout.schedule(10000, TimeUnit.MILLISECONDS));
+ Thread.sleep(1500);
+ assertFalse(_expired);
+ }
+
+ @Test
+ public void testMultiple() throws Exception
+ {
+ Thread.sleep(1500);
+ assertTrue(_expired);
+ _expired = false;
+ assertFalse(_timeout.schedule(500, TimeUnit.MILLISECONDS));
+ Thread.sleep(1000);
+ assertTrue(_expired);
+ _expired = false;
+ _timeout.schedule(500, TimeUnit.MILLISECONDS);
+ Thread.sleep(1000);
+ assertTrue(_expired);
+ }
+
+ @Test
+ @Disabled
+ public void testBusy() throws Exception
+ {
+ QueuedThreadPool pool = new QueuedThreadPool(200);
+ pool.start();
+
+ long testUntil = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(1500);
+
+ assertTrue(_timeout.schedule(100, TimeUnit.MILLISECONDS));
+ while (System.nanoTime() < testUntil)
+ {
+ CountDownLatch latch = new CountDownLatch(1);
+ pool.execute(() ->
+ {
+ _timeout.schedule(100, TimeUnit.MILLISECONDS);
+ latch.countDown();
+ });
+ latch.await();
+ }
+
+ assertFalse(_expired);
+ pool.stop();
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/CyclicTimeoutsTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/CyclicTimeoutsTest.java
new file mode 100644
index 0000000..6276988
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/CyclicTimeoutsTest.java
@@ -0,0 +1,266 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class CyclicTimeoutsTest
+{
+ private Scheduler scheduler;
+ private CyclicTimeouts<ConstantExpirable> timeouts;
+
+ @BeforeEach
+ public void prepare()
+ {
+ scheduler = new ScheduledExecutorScheduler();
+ LifeCycle.start(scheduler);
+ }
+
+ @AfterEach
+ public void dispose()
+ {
+ if (timeouts != null)
+ timeouts.destroy();
+ LifeCycle.stop(scheduler);
+ }
+
+ @Test
+ public void testNoExpirationForNonExpiringEntity() throws Exception
+ {
+ CountDownLatch latch = new CountDownLatch(1);
+ timeouts = new CyclicTimeouts<ConstantExpirable>(scheduler)
+ {
+ @Override
+ protected Iterator<ConstantExpirable> iterator()
+ {
+ latch.countDown();
+ return null;
+ }
+
+ @Override
+ protected boolean onExpired(ConstantExpirable expirable)
+ {
+ return false;
+ }
+ };
+
+ // Schedule an entity that does not expire.
+ timeouts.schedule(ConstantExpirable.noExpire());
+
+ Assertions.assertFalse(latch.await(1, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void testScheduleZero() throws Exception
+ {
+ ConstantExpirable entity = ConstantExpirable.ofDelay(0, TimeUnit.SECONDS);
+ CountDownLatch iteratorLatch = new CountDownLatch(1);
+ CountDownLatch expiredLatch = new CountDownLatch(1);
+ timeouts = new CyclicTimeouts<ConstantExpirable>(scheduler)
+ {
+ @Override
+ protected Iterator<ConstantExpirable> iterator()
+ {
+ iteratorLatch.countDown();
+ return Collections.emptyIterator();
+ }
+
+ @Override
+ protected boolean onExpired(ConstantExpirable expirable)
+ {
+ expiredLatch.countDown();
+ return false;
+ }
+ };
+
+ timeouts.schedule(entity);
+
+ Assertions.assertTrue(iteratorLatch.await(1, TimeUnit.SECONDS));
+ Assertions.assertFalse(expiredLatch.await(1, TimeUnit.SECONDS));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testIterateAndExpire(boolean remove) throws Exception
+ {
+ ConstantExpirable zero = ConstantExpirable.ofDelay(0, TimeUnit.SECONDS);
+ ConstantExpirable one = ConstantExpirable.ofDelay(1, TimeUnit.SECONDS);
+ Collection<ConstantExpirable> collection = new ArrayList<>();
+ collection.add(one);
+ AtomicInteger iterations = new AtomicInteger();
+ CountDownLatch expiredLatch = new CountDownLatch(1);
+ timeouts = new CyclicTimeouts<ConstantExpirable>(scheduler)
+ {
+ @Override
+ protected Iterator<ConstantExpirable> iterator()
+ {
+ iterations.incrementAndGet();
+ return collection.iterator();
+ }
+
+ @Override
+ protected boolean onExpired(ConstantExpirable expirable)
+ {
+ assertSame(one, expirable);
+ expiredLatch.countDown();
+ return remove;
+ }
+ };
+
+ // Triggers immediate call to iterator(), which
+ // returns an entity that expires in 1 second.
+ timeouts.schedule(zero);
+
+ // After 1 second there is a second call to
+ // iterator(), which returns the now expired
+ // entity, which is passed to onExpired().
+ assertTrue(expiredLatch.await(2, TimeUnit.SECONDS));
+
+ // Wait for the collection to be processed
+ // with the return value of onExpired().
+ Thread.sleep(1000);
+
+ // Verify the processing of the return value of onExpired().
+ assertEquals(remove ? 0 : 1, collection.size());
+
+ // Wait to see if iterator() is called again (it should not).
+ Thread.sleep(1000);
+ assertEquals(2, iterations.get());
+ }
+
+ @Test
+ public void testScheduleOvertake() throws Exception
+ {
+ ConstantExpirable zero = ConstantExpirable.ofDelay(0, TimeUnit.SECONDS);
+ long delayMs = 2000;
+ ConstantExpirable two = ConstantExpirable.ofDelay(delayMs, TimeUnit.MILLISECONDS);
+ ConstantExpirable overtake = ConstantExpirable.ofDelay(delayMs / 2, TimeUnit.MILLISECONDS);
+ Collection<ConstantExpirable> collection = new ArrayList<>();
+ collection.add(two);
+ CountDownLatch expiredLatch = new CountDownLatch(2);
+ List<ConstantExpirable> expired = new ArrayList<>();
+ timeouts = new CyclicTimeouts<ConstantExpirable>(scheduler)
+ {
+ private final AtomicBoolean overtakeScheduled = new AtomicBoolean();
+
+ @Override
+ protected Iterator<ConstantExpirable> iterator()
+ {
+ return collection.iterator();
+ }
+
+ @Override
+ protected boolean onExpired(ConstantExpirable expirable)
+ {
+ expired.add(expirable);
+ expiredLatch.countDown();
+ return true;
+ }
+
+ @Override
+ boolean schedule(CyclicTimeout cyclicTimeout, long delay, TimeUnit unit)
+ {
+ if (delay <= 0)
+ return super.schedule(cyclicTimeout, delay, unit);
+
+ // Simulate that an entity with a shorter timeout
+ // overtakes the entity that is currently being scheduled.
+ // Only schedule the overtake once.
+ if (overtakeScheduled.compareAndSet(false, true))
+ {
+ collection.add(overtake);
+ schedule(overtake);
+ }
+ return super.schedule(cyclicTimeout, delay, unit);
+ }
+ };
+
+ // Trigger the initial call to iterator().
+ timeouts.schedule(zero);
+
+ // Make sure that the overtake entity expires properly.
+ assertTrue(expiredLatch.await(2 * delayMs, TimeUnit.MILLISECONDS));
+
+ // Make sure all entities expired properly.
+ assertSame(overtake, expired.get(0));
+ assertSame(two, expired.get(1));
+ }
+
+ private static class ConstantExpirable implements CyclicTimeouts.Expirable
+ {
+ private static ConstantExpirable noExpire()
+ {
+ return new ConstantExpirable();
+ }
+
+ private static ConstantExpirable ofDelay(long delay, TimeUnit unit)
+ {
+ return new ConstantExpirable(delay, unit);
+ }
+
+ private final long expireNanos;
+ private final String asString;
+
+ private ConstantExpirable()
+ {
+ this.expireNanos = Long.MAX_VALUE;
+ this.asString = "noexp";
+ }
+
+ public ConstantExpirable(long delay, TimeUnit unit)
+ {
+ this.expireNanos = System.nanoTime() + unit.toNanos(delay);
+ this.asString = String.valueOf(unit.toMillis(delay));
+ }
+
+ @Override
+ public long getExpireNanoTime()
+ {
+ return expireNanos;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x[%sms]", getClass().getSimpleName(), hashCode(), asString);
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/IOTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/IOTest.java
new file mode 100644
index 0000000..2e96a22
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/IOTest.java
@@ -0,0 +1,553 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousServerSocketChannel;
+import java.nio.channels.AsynchronousSocketChannel;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.IO;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(WorkDirExtension.class)
+public class IOTest
+{
+ public WorkDir workDir;
+
+ @Test
+ public void testIO() throws Exception
+ {
+ // Only a little test
+ ByteArrayInputStream in = new ByteArrayInputStream("The quick brown fox jumped over the lazy dog".getBytes());
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ IO.copy(in, out);
+
+ assertEquals(out.toString(), "The quick brown fox jumped over the lazy dog", "copyThread");
+ }
+
+ @Test
+ public void testHalfClose() throws Exception
+ {
+ try (ServerSocket connector = new ServerSocket(0);
+ Socket client = new Socket("localhost", connector.getLocalPort());
+ Socket server = connector.accept())
+ {
+ // we can write both ways
+ client.getOutputStream().write(1);
+ assertEquals(1, server.getInputStream().read());
+ server.getOutputStream().write(1);
+ assertEquals(1, client.getInputStream().read());
+
+ // shutdown output results in read -1
+ client.shutdownOutput();
+ assertEquals(-1, server.getInputStream().read());
+
+ // Even though EOF has been read, the server input is not seen as shutdown
+ assertFalse(server.isInputShutdown());
+
+ // and we can read -1 again
+ assertEquals(-1, server.getInputStream().read());
+
+ // but cannot write
+ Assertions.assertThrows(SocketException.class, () -> client.getOutputStream().write(1));
+
+ // but can still write in opposite direction.
+ server.getOutputStream().write(1);
+ assertEquals(1, client.getInputStream().read());
+
+ // server can shutdown input to match the shutdown out of client
+ server.shutdownInput();
+
+ // now we EOF instead of reading -1
+ Assertions.assertThrows(SocketException.class, () -> server.getInputStream().read());
+
+ // but can still write in opposite direction.
+ server.getOutputStream().write(1);
+ assertEquals(1, client.getInputStream().read());
+
+ // client can shutdown input
+ client.shutdownInput();
+
+ // now we EOF instead of reading -1
+ Assertions.assertThrows(SocketException.class, () -> client.getInputStream().read());
+
+ // But we can still write at the server (data which will never be read)
+ server.getOutputStream().write(1);
+
+ // and the server output is not shutdown
+ assertFalse(server.isOutputShutdown());
+
+ // until we explicitly shut it down
+ server.shutdownOutput();
+
+ // and now we can't write
+ Assertions.assertThrows(SocketException.class, () -> server.getOutputStream().write(1));
+
+ // but the sockets are still open
+ assertFalse(client.isClosed());
+ assertFalse(server.isClosed());
+
+ // but if we close one end
+ client.close();
+
+ // it is seen as closed.
+ assertTrue(client.isClosed());
+
+ // but not the other end
+ assertFalse(server.isClosed());
+
+ // which has to be closed explicitly
+ server.close();
+ assertTrue(server.isClosed());
+ }
+ }
+
+ @Test
+ public void testHalfCloseClientServer() throws Exception
+ {
+ try (ServerSocketChannel connector = ServerSocketChannel.open())
+ {
+ connector.socket().bind(null);
+ try (Socket client = SocketChannel.open(connector.socket().getLocalSocketAddress()).socket())
+ {
+ client.setSoTimeout(1000);
+ try (Socket server = connector.accept().socket())
+ {
+ server.setSoTimeout(1000);
+
+ // Write from client to server
+ client.getOutputStream().write(1);
+
+ // Server reads
+ assertEquals(1, server.getInputStream().read());
+
+ // Write from server to client with oshut
+ server.getOutputStream().write(1);
+ // System.err.println("OSHUT "+server);
+ server.shutdownOutput();
+
+ // Client reads response
+ assertEquals(1, client.getInputStream().read());
+
+ try
+ {
+ // Client reads -1 and does ishut
+ assertEquals(-1, client.getInputStream().read());
+ assertFalse(client.isInputShutdown());
+ //System.err.println("ISHUT "+client);
+ client.shutdownInput();
+
+ // Client ???
+ //System.err.println("OSHUT "+client);
+ client.shutdownOutput();
+ //System.err.println("CLOSE "+client);
+ client.close();
+
+ // Server reads -1, does ishut and then close
+ assertEquals(-1, server.getInputStream().read());
+ assertFalse(server.isInputShutdown());
+ //System.err.println("ISHUT "+server);
+
+ server.shutdownInput();
+ server.close();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ assertTrue(OS.MAC.isCurrentOs());
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testHalfCloseBadClient() throws Exception
+ {
+ try (ServerSocketChannel connector = ServerSocketChannel.open())
+ {
+ connector.socket().bind(null);
+
+ try (Socket client = SocketChannel.open(connector.socket().getLocalSocketAddress()).socket())
+ {
+ client.setSoTimeout(1000);
+ try (Socket server = connector.accept().socket())
+ {
+ server.setSoTimeout(1000);
+
+ // Write from client to server
+ client.getOutputStream().write(1);
+
+ // Server reads
+ assertEquals(1, server.getInputStream().read());
+
+ // Write from server to client with oshut
+ server.getOutputStream().write(1);
+ //System.err.println("OSHUT "+server);
+ server.shutdownOutput();
+
+ // Client reads response
+ assertEquals(1, client.getInputStream().read());
+
+ // Client reads -1
+ assertEquals(-1, client.getInputStream().read());
+ assertFalse(client.isInputShutdown());
+
+ // Client can still write as we are half closed
+ client.getOutputStream().write(1);
+
+ // Server can still read
+ assertEquals(1, server.getInputStream().read());
+
+ // Server now closes
+ server.close();
+
+ // Client still reads -1 (not broken pipe !!)
+ assertEquals(-1, client.getInputStream().read());
+ assertFalse(client.isInputShutdown());
+
+ Thread.sleep(100);
+
+ // Client still reads -1 (not broken pipe !!)
+ assertEquals(-1, client.getInputStream().read());
+ assertFalse(client.isInputShutdown());
+
+ // Client can still write data even though server is closed???
+ client.getOutputStream().write(1);
+
+ // Client eventually sees Broken Pipe
+ assertThrows(IOException.class, () ->
+ {
+ int i = 0;
+ for (i = 0; i < 100000; i++)
+ {
+ client.getOutputStream().write(1);
+ }
+ });
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testServerChannelInterrupt() throws Exception
+ {
+ try (ServerSocketChannel connector = ServerSocketChannel.open())
+ {
+ connector.configureBlocking(true);
+ connector.socket().bind(null);
+ try (Socket client = SocketChannel.open(connector.socket().getLocalSocketAddress()).socket())
+ {
+ client.setSoTimeout(2000);
+ try (Socket server = connector.accept().socket())
+ {
+ server.setSoTimeout(2000);
+
+ // Write from client to server
+ client.getOutputStream().write(1);
+ // Server reads
+ assertEquals(1, server.getInputStream().read());
+
+ // Write from server to client
+ server.getOutputStream().write(1);
+ // Client reads
+ assertEquals(1, client.getInputStream().read());
+
+ // block a thread in accept
+ final CountDownLatch latch = new CountDownLatch(2);
+ Thread acceptor = new Thread(() ->
+ {
+ try
+ {
+ latch.countDown();
+ connector.accept();
+ }
+ catch (Throwable ignored)
+ {
+ }
+ finally
+ {
+ latch.countDown();
+ }
+ });
+ acceptor.start();
+ while (latch.getCount() == 2)
+ {
+ Thread.sleep(10);
+ }
+
+ // interrupt the acceptor
+ acceptor.interrupt();
+
+ // wait for acceptor to exit
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+
+ // connector is closed
+ assertFalse(connector.isOpen());
+
+ // but connection is still open
+ assertFalse(client.isClosed());
+ assertFalse(server.isClosed());
+
+ // Write from client to server
+ client.getOutputStream().write(42);
+ // Server reads
+ assertEquals(42, server.getInputStream().read());
+
+ // Write from server to client
+ server.getOutputStream().write(43);
+ // Client reads
+ assertEquals(43, client.getInputStream().read());
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testReset() throws Exception
+ {
+ try (ServerSocket connector = new ServerSocket(0);
+ Socket client = new Socket("127.0.0.1", connector.getLocalPort());
+ Socket server = connector.accept())
+ {
+ client.setTcpNoDelay(true);
+ client.setSoLinger(true, 0);
+ server.setTcpNoDelay(true);
+ server.setSoLinger(true, 0);
+
+ client.getOutputStream().write(1);
+ assertEquals(1, server.getInputStream().read());
+ server.getOutputStream().write(1);
+ assertEquals(1, client.getInputStream().read());
+
+ // Server generator shutdowns output after non persistent sending response.
+ server.shutdownOutput();
+
+ // client endpoint reads EOF and shutdown input as result
+ assertEquals(-1, client.getInputStream().read());
+ client.shutdownInput();
+
+ // client connection see's EOF and shutsdown output as no more requests to be sent.
+ client.shutdownOutput();
+
+ // Since input already shutdown, client also closes socket.
+ client.close();
+
+ // Server reads the EOF from client oshut and shut's down it's input
+ assertEquals(-1, server.getInputStream().read());
+ server.shutdownInput();
+
+ // Since output was already shutdown, server
+ // closes in the try-with-resources block end.
+ }
+ }
+
+ @Test
+ public void testAsyncSocketChannel() throws Exception
+ {
+ AsynchronousServerSocketChannel connector = AsynchronousServerSocketChannel.open();
+ connector.bind(null);
+ InetSocketAddress addr = (InetSocketAddress)connector.getLocalAddress();
+ Future<AsynchronousSocketChannel> acceptor = connector.accept();
+
+ AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
+
+ client.connect(new InetSocketAddress("127.0.0.1", addr.getPort())).get(5, TimeUnit.SECONDS);
+
+ AsynchronousSocketChannel server = acceptor.get(5, TimeUnit.SECONDS);
+
+ ByteBuffer read = ByteBuffer.allocate(1024);
+ Future<Integer> reading = server.read(read);
+
+ byte[] data = "Testing 1 2 3".getBytes(StandardCharsets.UTF_8);
+ ByteBuffer write = BufferUtil.toBuffer(data);
+ Future<Integer> writing = client.write(write);
+
+ writing.get(5, TimeUnit.SECONDS);
+ reading.get(5, TimeUnit.SECONDS);
+ read.flip();
+
+ assertEquals(ByteBuffer.wrap(data), read);
+ }
+
+ @Test
+ public void testGatherWrite() throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Path file = Files.createTempFile(dir, "test", ".txt");
+ FileChannel out = FileChannel.open(file,
+ StandardOpenOption.CREATE,
+ StandardOpenOption.READ,
+ StandardOpenOption.WRITE,
+ StandardOpenOption.DELETE_ON_CLOSE);
+
+ ByteBuffer[] buffers = new ByteBuffer[4096];
+ long expected = 0;
+ for (int i = 0; i < buffers.length; i++)
+ {
+ buffers[i] = BufferUtil.toBuffer(i);
+ expected += buffers[i].remaining();
+ }
+
+ long wrote = IO.write(out, buffers, 0, buffers.length);
+
+ assertEquals(expected, wrote);
+
+ for (ByteBuffer buffer : buffers)
+ {
+ assertEquals(0, buffer.remaining());
+ }
+ }
+
+ @Test
+ public void testDeleteNull()
+ {
+ assertFalse(IO.delete(null));
+ }
+
+ @Test
+ public void testDeleteNonExistentFile(TestInfo testInfo)
+ {
+ File dir = MavenTestingUtils.getTargetTestingDir(testInfo.getDisplayName());
+ FS.ensureEmpty(dir);
+ File noFile = new File(dir, "nada");
+ assertFalse(IO.delete(noFile));
+ }
+
+ @Test
+ public void testIsEmptyNull()
+ {
+ assertTrue(IO.isEmptyDir(null));
+ }
+
+ @Test
+ public void testIsEmptyDoesNotExist(TestInfo testInfo)
+ {
+ File dir = MavenTestingUtils.getTargetTestingDir(testInfo.getDisplayName());
+ FS.ensureEmpty(dir);
+ File noFile = new File(dir, "nada");
+ assertTrue(IO.isEmptyDir(noFile));
+ }
+
+ @Test
+ public void testIsEmptyExistButAsFile(TestInfo testInfo) throws IOException
+ {
+ File dir = MavenTestingUtils.getTargetTestingDir(testInfo.getDisplayName());
+ FS.ensureEmpty(dir);
+ File file = new File(dir, "nada");
+ FS.touch(file);
+ assertFalse(IO.isEmptyDir(file));
+ }
+
+ @Test
+ public void testIsEmptyExistAndIsEmpty(TestInfo testInfo)
+ {
+ File dir = MavenTestingUtils.getTargetTestingDir(testInfo.getDisplayName());
+ FS.ensureEmpty(dir);
+ assertTrue(IO.isEmptyDir(dir));
+ }
+
+ @Test
+ public void testIsEmptyExistAndHasContent(TestInfo testInfo) throws IOException
+ {
+ File dir = MavenTestingUtils.getTargetTestingDir(testInfo.getDisplayName());
+ FS.ensureEmpty(dir);
+ File file = new File(dir, "nada");
+ FS.touch(file);
+ assertFalse(IO.isEmptyDir(dir));
+ }
+
+ @Test
+ public void testSelectorWakeup() throws Exception
+ {
+ try (ServerSocketChannel connector = ServerSocketChannel.open())
+ {
+ connector.bind(null);
+ InetSocketAddress addr = (InetSocketAddress)connector.getLocalAddress();
+ try (SocketChannel client = SocketChannel.open(new InetSocketAddress("127.0.0.1", addr.getPort()));
+ SocketChannel server = connector.accept())
+ {
+ server.configureBlocking(false);
+
+ Selector selector = Selector.open();
+ SelectionKey key = server.register(selector, SelectionKey.OP_READ);
+
+ assertThat(key, notNullValue());
+ assertThat(selector.selectNow(), is(0));
+
+ // Test wakeup before select
+ selector.wakeup();
+ assertThat(selector.select(), is(0));
+
+ // Test wakeup after select
+ new Thread(() ->
+ {
+ try
+ {
+ Thread.sleep(100);
+ selector.wakeup();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }).start();
+ assertThat(selector.select(), is(0));
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/IdleTimeoutTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/IdleTimeoutTest.java
new file mode 100644
index 0000000..1dc7cbb
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/IdleTimeoutTest.java
@@ -0,0 +1,165 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jetty.util.thread.TimerScheduler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class IdleTimeoutTest
+{
+ volatile boolean _open;
+ volatile TimeoutException _expired;
+
+ TimerScheduler _timer;
+ IdleTimeout _timeout;
+
+ @BeforeEach
+ public void setUp() throws Exception
+ {
+ _open = true;
+ _expired = null;
+ _timer = new TimerScheduler();
+ _timer.start();
+ _timeout = new IdleTimeout(_timer)
+ {
+ @Override
+ protected void onIdleExpired(TimeoutException timeout)
+ {
+ _expired = timeout;
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ return _open;
+ }
+ };
+ _timeout.setIdleTimeout(1000);
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception
+ {
+ _open = false;
+ _timer.stop();
+ }
+
+ @Test
+ public void testNotIdle() throws Exception
+ {
+ for (int i = 0; i < 20; i++)
+ {
+ Thread.sleep(100);
+ _timeout.notIdle();
+ }
+
+ assertNull(_expired);
+ }
+
+ @Test
+ public void testIdle() throws Exception
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ Thread.sleep(100);
+ _timeout.notIdle();
+ }
+ Thread.sleep(1500);
+ assertNotNull(_expired);
+ }
+
+ @Test
+ public void testClose() throws Exception
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ Thread.sleep(100);
+ _timeout.notIdle();
+ }
+ _timeout.onClose();
+ Thread.sleep(1500);
+ assertNull(_expired);
+ }
+
+ @Test
+ public void testClosed() throws Exception
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ Thread.sleep(100);
+ _timeout.notIdle();
+ }
+ _open = false;
+ Thread.sleep(1500);
+ assertNull(_expired);
+ }
+
+ @Test
+ public void testShorten() throws Exception
+ {
+ _timeout.setIdleTimeout(2000);
+
+ for (int i = 0; i < 30; i++)
+ {
+ Thread.sleep(100);
+ _timeout.notIdle();
+ }
+ assertNull(_expired);
+ _timeout.setIdleTimeout(100);
+
+ long start = System.nanoTime();
+ while (_expired == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5)
+ {
+ Thread.sleep(200);
+ }
+
+ assertNotNull(_expired);
+ }
+
+ @Test
+ public void testLengthen() throws Exception
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ Thread.sleep(100);
+ _timeout.notIdle();
+ }
+ _timeout.setIdleTimeout(10000);
+ Thread.sleep(1500);
+ assertNull(_expired);
+ }
+
+ @Test
+ public void testMultiple() throws Exception
+ {
+ Thread.sleep(1500);
+ assertNotNull(_expired);
+ _expired = null;
+ Thread.sleep(1000);
+ assertNotNull(_expired);
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/MappedByteBufferPoolTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/MappedByteBufferPoolTest.java
new file mode 100644
index 0000000..27ec5eb
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/MappedByteBufferPoolTest.java
@@ -0,0 +1,179 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.ConcurrentMap;
+
+import org.eclipse.jetty.io.ByteBufferPool.Bucket;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.StringUtil;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.lessThan;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class MappedByteBufferPoolTest
+{
+ @Test
+ public void testAcquireRelease()
+ {
+ MappedByteBufferPool bufferPool = new MappedByteBufferPool();
+ ConcurrentMap<Integer, Bucket> buckets = bufferPool.bucketsFor(true);
+
+ int size = 512;
+ ByteBuffer buffer = bufferPool.acquire(size, true);
+
+ assertTrue(buffer.isDirect());
+ assertThat(buffer.capacity(), greaterThanOrEqualTo(size));
+ assertTrue(buckets.isEmpty());
+
+ bufferPool.release(buffer);
+
+ assertEquals(1, buckets.size());
+ assertEquals(1, buckets.values().iterator().next().size());
+ }
+
+ @Test
+ public void testAcquireReleaseAcquire()
+ {
+ MappedByteBufferPool bufferPool = new MappedByteBufferPool();
+ ConcurrentMap<Integer, Bucket> buckets = bufferPool.bucketsFor(false);
+
+ ByteBuffer buffer1 = bufferPool.acquire(512, false);
+ bufferPool.release(buffer1);
+ ByteBuffer buffer2 = bufferPool.acquire(512, false);
+
+ assertSame(buffer1, buffer2);
+ assertEquals(1, buckets.size());
+ assertEquals(0, buckets.values().iterator().next().size());
+
+ bufferPool.release(buffer2);
+
+ assertEquals(1, buckets.size());
+ assertEquals(1, buckets.values().iterator().next().size());
+ }
+
+ @Test
+ public void testAcquireReleaseClear()
+ {
+ MappedByteBufferPool bufferPool = new MappedByteBufferPool();
+ ConcurrentMap<Integer, Bucket> buckets = bufferPool.bucketsFor(true);
+
+ ByteBuffer buffer = bufferPool.acquire(512, true);
+ bufferPool.release(buffer);
+
+ assertEquals(1, buckets.size());
+ assertEquals(1, buckets.values().iterator().next().size());
+
+ bufferPool.clear();
+
+ assertTrue(buckets.isEmpty());
+ }
+
+ @Test
+ public void testReleaseNonPooledBuffer()
+ {
+ MappedByteBufferPool bufferPool = new MappedByteBufferPool();
+
+ // Release a few small non-pool buffers
+ bufferPool.release(ByteBuffer.wrap(StringUtil.getUtf8Bytes("Hello")));
+
+ assertEquals(0, bufferPool.getHeapByteBufferCount());
+ }
+
+ @Test
+ public void testTagged()
+ {
+ MappedByteBufferPool pool = new MappedByteBufferPool.Tagged();
+
+ ByteBuffer buffer = pool.acquire(1024, false);
+
+ assertThat(BufferUtil.toDetailString(buffer), containsString("@T00000001"));
+ buffer = pool.acquire(1024, false);
+ assertThat(BufferUtil.toDetailString(buffer), containsString("@T00000002"));
+ }
+
+ @Test
+ public void testMaxQueue()
+ {
+ MappedByteBufferPool bufferPool = new MappedByteBufferPool(-1, 2);
+ ConcurrentMap<Integer, Bucket> buckets = bufferPool.bucketsFor(false);
+
+ ByteBuffer buffer1 = bufferPool.acquire(512, false);
+ ByteBuffer buffer2 = bufferPool.acquire(512, false);
+ ByteBuffer buffer3 = bufferPool.acquire(512, false);
+ assertEquals(0, buckets.size());
+
+ bufferPool.release(buffer1);
+ assertEquals(1, buckets.size());
+ Bucket bucket = buckets.values().iterator().next();
+ assertEquals(1, bucket.size());
+
+ bufferPool.release(buffer2);
+ assertEquals(2, bucket.size());
+
+ bufferPool.release(buffer3);
+ assertEquals(2, bucket.size());
+ }
+
+ @Test
+ public void testMaxMemory()
+ {
+ int factor = 1024;
+ int maxMemory = 11 * 1024;
+ MappedByteBufferPool bufferPool = new MappedByteBufferPool(factor, -1, null, -1, maxMemory);
+ ConcurrentMap<Integer, Bucket> buckets = bufferPool.bucketsFor(true);
+
+ // Create the buckets - the oldest is the larger.
+ // 1+2+3+4=10 / maxMemory=11.
+ for (int i = 4; i >= 1; --i)
+ {
+ int capacity = factor * i;
+ ByteBuffer buffer = bufferPool.acquire(capacity, true);
+ bufferPool.release(buffer);
+ }
+
+ // Create and release a buffer to exceed the max memory.
+ ByteBuffer buffer = bufferPool.newByteBuffer(2 * factor, true);
+ bufferPool.release(buffer);
+
+ // Now the oldest buffer should be gone and we have: 1+2x2+3=8
+ long memory = bufferPool.getMemory(true);
+ assertThat(memory, lessThan((long)maxMemory));
+ assertNull(buckets.get(4));
+
+ // Create and release a large buffer.
+ // Max memory is exceeded and buckets 3 and 1 are cleared.
+ // We will have 2x2+7=11.
+ buffer = bufferPool.newByteBuffer(7 * factor, true);
+ bufferPool.release(buffer);
+ memory = bufferPool.getMemory(true);
+ assertThat(memory, lessThanOrEqualTo((long)maxMemory));
+ assertNull(buckets.get(1));
+ assertNull(buckets.get(3));
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/NIOTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/NIOTest.java
new file mode 100644
index 0000000..82b0a7d
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/NIOTest.java
@@ -0,0 +1,134 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ *
+ */
+public class NIOTest
+{
+ @Test
+ public void testSelector() throws Exception
+ {
+ try (ServerSocket acceptor = new ServerSocket(0);
+ Selector selector = Selector.open();
+ SocketChannel client = SocketChannel.open(acceptor.getLocalSocketAddress());
+ Socket server = acceptor.accept())
+ {
+ server.setTcpNoDelay(true);
+
+ // Make the client non blocking and register it with selector for reads
+ client.configureBlocking(false);
+ SelectionKey key = client.register(selector, SelectionKey.OP_READ);
+
+ // assert it is not selected
+ assertTrue(key.isValid());
+ assertFalse(key.isReadable());
+ assertEquals(0, key.readyOps());
+
+ // try selecting and assert nothing selected
+ int selected = selector.selectNow();
+ assertEquals(0, selected);
+ assertEquals(0, selector.selectedKeys().size());
+ assertTrue(key.isValid());
+ assertFalse(key.isReadable());
+ assertEquals(0, key.readyOps());
+
+ // Write a byte from server to client
+ server.getOutputStream().write(42);
+ server.getOutputStream().flush();
+
+ // select again and assert selection found for read
+ selected = selector.select(1000);
+ assertEquals(1, selected);
+ assertEquals(1, selector.selectedKeys().size());
+ assertTrue(key.isValid());
+ assertTrue(key.isReadable());
+ assertEquals(1, key.readyOps());
+
+ // select again and see that it is not reselect, but stays selected
+ selected = selector.select(100);
+ assertEquals(0, selected);
+ assertEquals(1, selector.selectedKeys().size());
+ assertTrue(key.isValid());
+ assertTrue(key.isReadable());
+ assertEquals(1, key.readyOps());
+
+ // read the byte
+ ByteBuffer buf = ByteBuffer.allocate(1024);
+ int len = client.read(buf);
+ assertEquals(1, len);
+ buf.flip();
+ assertEquals(42, buf.get());
+ buf.clear();
+
+ // But this does not change the key
+ assertTrue(key.isValid());
+ assertTrue(key.isReadable());
+ assertEquals(1, key.readyOps());
+
+ // Even if we select again ?
+ selected = selector.select(100);
+ assertEquals(0, selected);
+ assertEquals(1, selector.selectedKeys().size());
+ assertTrue(key.isValid());
+ assertTrue(key.isReadable());
+ assertEquals(1, key.readyOps());
+
+ // Unless we remove the key from the select set
+ // and then it is still flagged as isReadable()
+ selector.selectedKeys().clear();
+ assertEquals(0, selector.selectedKeys().size());
+ assertTrue(key.isValid());
+ assertTrue(key.isReadable());
+ assertEquals(1, key.readyOps());
+
+ // Now if we select again - it is still flagged as readable!!!
+ selected = selector.select(100);
+ assertEquals(0, selected);
+ assertEquals(0, selector.selectedKeys().size());
+ assertTrue(key.isValid());
+ assertTrue(key.isReadable());
+ assertEquals(1, key.readyOps());
+
+ // Only when it is selected for something else does that state change.
+ key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
+ selected = selector.select(1000);
+ assertEquals(1, selected);
+ assertEquals(1, selector.selectedKeys().size());
+ assertTrue(key.isValid());
+ assertTrue(key.isWritable());
+ assertFalse(key.isReadable());
+ assertEquals(SelectionKey.OP_WRITE, key.readyOps());
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SelectorManagerTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SelectorManagerTest.java
new file mode 100644
index 0000000..92a668e
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SelectorManagerTest.java
@@ -0,0 +1,166 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.TimerScheduler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SelectorManagerTest
+{
+ private QueuedThreadPool executor = new QueuedThreadPool();
+ private TimerScheduler scheduler = new TimerScheduler();
+
+ @BeforeEach
+ public void prepare() throws Exception
+ {
+ executor.start();
+ scheduler.start();
+ }
+
+ @AfterEach
+ public void dispose() throws Exception
+ {
+ scheduler.stop();
+ executor.stop();
+ }
+
+ @Test
+ @DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review
+ public void testConnectTimeoutBeforeSuccessfulConnect() throws Exception
+ {
+ ServerSocketChannel server = ServerSocketChannel.open();
+ server.bind(new InetSocketAddress("localhost", 0));
+ SocketAddress address = server.getLocalAddress();
+
+ final AtomicLong timeoutConnection = new AtomicLong();
+ final long connectTimeout = 1000;
+ SelectorManager selectorManager = new SelectorManager(executor, scheduler)
+ {
+ @Override
+ protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key) throws IOException
+ {
+ SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, key, getScheduler());
+ endp.setIdleTimeout(connectTimeout / 2);
+ return endp;
+ }
+
+ @Override
+ protected boolean doFinishConnect(SelectableChannel channel) throws IOException
+ {
+ try
+ {
+ long timeout = timeoutConnection.get();
+ if (timeout > 0)
+ TimeUnit.MILLISECONDS.sleep(timeout);
+ return super.doFinishConnect(channel);
+ }
+ catch (InterruptedException e)
+ {
+ return false;
+ }
+ }
+
+ @Override
+ public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment) throws IOException
+ {
+ ((Callback)attachment).succeeded();
+ return new AbstractConnection(endpoint, executor)
+ {
+ @Override
+ public void onFillable()
+ {
+ }
+ };
+ }
+
+ @Override
+ protected void connectionFailed(SelectableChannel channel, Throwable ex, Object attachment)
+ {
+ ((Callback)attachment).failed(ex);
+ }
+ };
+ selectorManager.setConnectTimeout(connectTimeout);
+ selectorManager.start();
+
+ try
+ {
+ SocketChannel client1 = SocketChannel.open();
+ client1.configureBlocking(false);
+ client1.connect(address);
+ long timeout = connectTimeout * 2;
+ timeoutConnection.set(timeout);
+ final CountDownLatch latch1 = new CountDownLatch(1);
+ selectorManager.connect(client1, new Callback()
+ {
+ @Override
+ public void failed(Throwable x)
+ {
+ latch1.countDown();
+ }
+ });
+ assertTrue(latch1.await(connectTimeout * 3, TimeUnit.MILLISECONDS));
+ assertFalse(client1.isOpen());
+
+ // Wait for the first connect to finish, as the selector thread is waiting in finishConnect().
+ Thread.sleep(timeout);
+
+ // Verify that after the failure we can connect successfully.
+ try (SocketChannel client2 = SocketChannel.open())
+ {
+ client2.configureBlocking(false);
+ client2.connect(address);
+ timeoutConnection.set(0);
+ final CountDownLatch latch2 = new CountDownLatch(1);
+ selectorManager.connect(client2, new Callback()
+ {
+ @Override
+ public void succeeded()
+ {
+ latch2.countDown();
+ }
+ });
+ assertTrue(latch2.await(connectTimeout * 5, TimeUnit.MILLISECONDS));
+ assertTrue(client2.isOpen());
+ }
+ }
+ finally
+ {
+ selectorManager.stop();
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointInterestsTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointInterestsTest.java
new file mode 100644
index 0000000..50c9030
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointInterestsTest.java
@@ -0,0 +1,222 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.eclipse.jetty.util.thread.TimerScheduler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SocketChannelEndPointInterestsTest
+{
+ private QueuedThreadPool threadPool;
+ private Scheduler scheduler;
+ private ServerSocketChannel connector;
+ private SelectorManager selectorManager;
+
+ public void init(final Interested interested) throws Exception
+ {
+ threadPool = new QueuedThreadPool();
+ threadPool.start();
+
+ scheduler = new TimerScheduler();
+ scheduler.start();
+
+ connector = ServerSocketChannel.open();
+ connector.bind(new InetSocketAddress("localhost", 0));
+
+ selectorManager = new SelectorManager(threadPool, scheduler)
+ {
+
+ @Override
+ protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key)
+ {
+ SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, key, getScheduler())
+ {
+ @Override
+ protected void onIncompleteFlush()
+ {
+ super.onIncompleteFlush();
+ interested.onIncompleteFlush();
+ }
+ };
+
+ endp.setIdleTimeout(60000);
+ return endp;
+ }
+
+ @Override
+ public Connection newConnection(SelectableChannel channel, final EndPoint endPoint, Object attachment)
+ {
+ return new AbstractConnection(endPoint, getExecutor())
+ {
+ @Override
+ public void onOpen()
+ {
+ super.onOpen();
+ fillInterested();
+ }
+
+ @Override
+ public void onFillable()
+ {
+ interested.onFillable(endPoint, this);
+ }
+ };
+ }
+ };
+ selectorManager.start();
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ if (scheduler != null)
+ scheduler.stop();
+ if (selectorManager != null)
+ selectorManager.stop();
+ if (connector != null)
+ connector.close();
+ if (threadPool != null)
+ threadPool.stop();
+ }
+
+ @Test
+ public void testReadBlockedThenWriteBlockedThenReadableThenWritable() throws Exception
+ {
+ final AtomicInteger size = new AtomicInteger(1024 * 1024);
+ final AtomicReference<Exception> failure = new AtomicReference<>();
+ final CountDownLatch latch1 = new CountDownLatch(1);
+ final CountDownLatch latch2 = new CountDownLatch(1);
+ final AtomicBoolean writeBlocked = new AtomicBoolean();
+ init(new Interested()
+ {
+ @Override
+ public void onFillable(EndPoint endPoint, AbstractConnection connection)
+ {
+ ByteBuffer input = BufferUtil.allocate(2);
+ int read = fill(endPoint, input);
+
+ if (read == 1)
+ {
+ byte b = input.get();
+ if (b == 1)
+ {
+ connection.fillInterested();
+
+ ByteBuffer output = ByteBuffer.allocate(size.get());
+ endPoint.write(new Callback() {}, output);
+
+ latch1.countDown();
+ }
+ else
+ {
+ latch2.countDown();
+ }
+ }
+ else
+ {
+ failure.set(new Exception("Unexpectedly read " + read + " bytes"));
+ }
+ }
+
+ @Override
+ public void onIncompleteFlush()
+ {
+ writeBlocked.set(true);
+ }
+
+ private int fill(EndPoint endPoint, ByteBuffer buffer)
+ {
+ try
+ {
+ return endPoint.fill(buffer);
+ }
+ catch (IOException x)
+ {
+ failure.set(x);
+ return 0;
+ }
+ }
+ });
+
+ try (Socket client = new Socket())
+ {
+ client.connect(connector.getLocalAddress());
+ client.setSoTimeout(5000);
+ try (SocketChannel server = connector.accept())
+ {
+ server.configureBlocking(false);
+ selectorManager.accept(server);
+
+ OutputStream clientOutput = client.getOutputStream();
+ clientOutput.write(1);
+ clientOutput.flush();
+ assertTrue(latch1.await(5, TimeUnit.SECONDS));
+
+ // We do not read to keep the socket write blocked
+
+ clientOutput.write(2);
+ clientOutput.flush();
+ assertTrue(latch2.await(5, TimeUnit.SECONDS));
+
+ // Sleep before reading to allow waking up the server only for read
+ Thread.sleep(1000);
+
+ // Now read what was written, waking up the server for write
+ InputStream clientInput = client.getInputStream();
+ while (size.getAndDecrement() > 0)
+ {
+ clientInput.read();
+ }
+
+ assertNull(failure.get());
+ }
+ }
+ }
+
+ private interface Interested
+ {
+ void onFillable(EndPoint endPoint, AbstractConnection connection);
+
+ void onIncompleteFlush();
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointOpenCloseTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointOpenCloseTest.java
new file mode 100644
index 0000000..728d233
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointOpenCloseTest.java
@@ -0,0 +1,179 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.nio.ByteBuffer;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SocketChannelEndPointOpenCloseTest
+{
+ public static class EndPointPair
+ {
+ public SocketChannelEndPoint client;
+ public SocketChannelEndPoint server;
+ }
+
+ static ServerSocketChannel connector;
+
+ @BeforeAll
+ public static void open() throws Exception
+ {
+ connector = ServerSocketChannel.open();
+ connector.socket().bind(null);
+ }
+
+ @AfterAll
+ public static void close() throws Exception
+ {
+ connector.close();
+ connector = null;
+ }
+
+ private EndPointPair newConnection() throws Exception
+ {
+ EndPointPair c = new EndPointPair();
+
+ c.client = new SocketChannelEndPoint(SocketChannel.open(connector.socket().getLocalSocketAddress()), null, null, null);
+ c.server = new SocketChannelEndPoint(connector.accept(), null, null, null);
+ return c;
+ }
+
+ @Test
+ public void testClientServerExchange() throws Exception
+ {
+ EndPointPair c = newConnection();
+ ByteBuffer buffer = BufferUtil.allocate(4096);
+
+ // Client sends a request
+ c.client.flush(BufferUtil.toBuffer("request"));
+
+ // Server receives the request
+ int len = c.server.fill(buffer);
+ assertEquals(7, len);
+ assertEquals("request", BufferUtil.toString(buffer));
+
+ // Client and server are open
+ assertTrue(c.client.isOpen());
+ assertFalse(c.client.isOutputShutdown());
+ assertTrue(c.server.isOpen());
+ assertFalse(c.server.isOutputShutdown());
+
+ // Server sends response and closes output
+ c.server.flush(BufferUtil.toBuffer("response"));
+ c.server.shutdownOutput();
+
+ // client server are open, server is oshut
+ assertTrue(c.client.isOpen());
+ assertFalse(c.client.isOutputShutdown());
+ assertTrue(c.server.isOpen());
+ assertTrue(c.server.isOutputShutdown());
+
+ // Client reads response
+ BufferUtil.clear(buffer);
+ len = c.client.fill(buffer);
+ assertEquals(8, len);
+ assertEquals("response", BufferUtil.toString(buffer));
+
+ // Client and server are open, server is oshut
+ assertTrue(c.client.isOpen());
+ assertFalse(c.client.isOutputShutdown());
+ assertTrue(c.server.isOpen());
+ assertTrue(c.server.isOutputShutdown());
+
+ // Client reads -1
+ BufferUtil.clear(buffer);
+ len = c.client.fill(buffer);
+ assertEquals(-1, len);
+
+ // Client and server are open, server is oshut, client is ishut
+ assertTrue(c.client.isOpen());
+ assertFalse(c.client.isOutputShutdown());
+ assertTrue(c.server.isOpen());
+ assertTrue(c.server.isOutputShutdown());
+
+ // Client shutsdown output, which is a close because already ishut
+ c.client.shutdownOutput();
+
+ // Client is closed. Server is open and oshut
+ assertFalse(c.client.isOpen());
+ assertTrue(c.client.isOutputShutdown());
+ assertTrue(c.server.isOpen());
+ assertTrue(c.server.isOutputShutdown());
+
+ // Server reads close
+ BufferUtil.clear(buffer);
+ len = c.server.fill(buffer);
+ assertEquals(-1, len);
+
+ // Client and Server are closed
+ assertFalse(c.client.isOpen());
+ assertTrue(c.client.isOutputShutdown());
+ assertFalse(c.server.isOpen());
+ assertTrue(c.server.isOutputShutdown());
+ }
+
+ @Test
+ public void testClientClose() throws Exception
+ {
+ EndPointPair c = newConnection();
+ ByteBuffer buffer = BufferUtil.allocate(4096);
+
+ c.client.flush(BufferUtil.toBuffer("request"));
+ int len = c.server.fill(buffer);
+ assertEquals(7, len);
+ assertEquals("request", BufferUtil.toString(buffer));
+
+ assertTrue(c.client.isOpen());
+ assertFalse(c.client.isOutputShutdown());
+ assertTrue(c.server.isOpen());
+ assertFalse(c.server.isOutputShutdown());
+
+ c.client.close();
+
+ assertFalse(c.client.isOpen());
+ assertTrue(c.client.isOutputShutdown());
+ assertTrue(c.server.isOpen());
+ assertFalse(c.server.isOutputShutdown());
+
+ len = c.server.fill(buffer);
+ assertEquals(-1, len);
+
+ assertFalse(c.client.isOpen());
+ assertTrue(c.client.isOutputShutdown());
+ assertTrue(c.server.isOpen());
+ assertFalse(c.server.isOutputShutdown());
+
+ c.server.shutdownOutput();
+
+ assertFalse(c.client.isOpen());
+ assertTrue(c.client.isOutputShutdown());
+ assertFalse(c.server.isOpen());
+ assertTrue(c.server.isOutputShutdown());
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java
new file mode 100644
index 0000000..66b621c
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java
@@ -0,0 +1,828 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLSocket;
+
+import org.eclipse.jetty.io.ssl.SslConnection;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.FutureCallback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.eclipse.jetty.util.thread.TimerScheduler;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@SuppressWarnings("Duplicates")
+public class SocketChannelEndPointTest
+{
+ private static final Logger LOG = Log.getLogger(SocketChannelEndPoint.class);
+
+ public interface Scenario
+ {
+ Socket newClient(ServerSocketChannel connector) throws IOException;
+
+ Connection newConnection(SelectableChannel channel, EndPoint endPoint, Executor executor, AtomicInteger blockAt, AtomicInteger writeCount);
+
+ boolean supportsHalfCloses();
+ }
+
+ public static Stream<Arguments> scenarios() throws Exception
+ {
+ NormalScenario normalScenario = new NormalScenario();
+ SslScenario sslScenario = new SslScenario(normalScenario);
+
+ return Stream.of(normalScenario, sslScenario).map(Arguments::of);
+ }
+
+ private Scenario _scenario;
+
+ private ServerSocketChannel _connector;
+ private QueuedThreadPool _threadPool;
+ private Scheduler _scheduler;
+ private SelectorManager _manager;
+ private volatile EndPoint _lastEndPoint;
+ private CountDownLatch _lastEndPointLatch;
+
+ // Must be volatile or the test may fail spuriously
+ private AtomicInteger _blockAt = new AtomicInteger(0);
+ private AtomicInteger _writeCount = new AtomicInteger(1);
+
+ public void init(Scenario scenario) throws Exception
+ {
+ _scenario = scenario;
+ _threadPool = new QueuedThreadPool();
+ _scheduler = new TimerScheduler();
+ _manager = new ScenarioSelectorManager(_threadPool, _scheduler);
+
+ _lastEndPointLatch = new CountDownLatch(1);
+ _connector = ServerSocketChannel.open();
+ _connector.socket().bind(null);
+ _scheduler.start();
+ _threadPool.start();
+ _manager.start();
+ }
+
+ @AfterEach
+ public void stopManager() throws Exception
+ {
+ _scheduler.stop();
+ _manager.stop();
+ _threadPool.stop();
+ _connector.close();
+ }
+
+ @ParameterizedTest
+ @MethodSource("scenarios")
+ public void testEcho(Scenario scenario) throws Exception
+ {
+ init(scenario);
+ try (Socket client = _scenario.newClient(_connector))
+ {
+ client.setSoTimeout(60000);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ // Write client to server
+ client.getOutputStream().write("HelloWorld".getBytes(StandardCharsets.UTF_8));
+
+ // Verify echo server to client
+ for (char c : "HelloWorld".toCharArray())
+ {
+ int b = client.getInputStream().read();
+ assertTrue(b > 0);
+ assertEquals(c, (char)b);
+ }
+
+ // wait for read timeout
+ client.setSoTimeout(500);
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ assertThrows(SocketTimeoutException.class, () -> client.getInputStream().read());
+ long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start;
+ assertThat("timeout duration", duration, greaterThanOrEqualTo(400L));
+
+ // write then shutdown
+ client.getOutputStream().write("Goodbye Cruel TLS".getBytes(StandardCharsets.UTF_8));
+
+ // Verify echo server to client
+ for (char c : "Goodbye Cruel TLS".toCharArray())
+ {
+ int b = client.getInputStream().read();
+ assertThat("expect valid char integer", b, greaterThan(0));
+ assertEquals(c, (char)b, "expect characters to be same");
+ }
+ client.close();
+
+ for (int i = 0; i < 10; ++i)
+ {
+ if (server.isOpen())
+ Thread.sleep(10);
+ else
+ break;
+ }
+ assertFalse(server.isOpen());
+ }
+ }
+ }
+
+ @Test
+ public void testShutdown() throws Exception
+ {
+ // We don't test SSL as JVM SSL doesn't support half-close
+ init(new NormalScenario());
+
+ try (Socket client = _scenario.newClient(_connector))
+ {
+ client.setSoTimeout(500);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ // Write client to server
+ client.getOutputStream().write("HelloWorld".getBytes(StandardCharsets.UTF_8));
+
+ // Verify echo server to client
+ for (char c : "HelloWorld".toCharArray())
+ {
+ int b = client.getInputStream().read();
+ assertTrue(b > 0);
+ assertEquals(c, (char)b);
+ }
+
+ // wait for read timeout
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ assertThrows(SocketTimeoutException.class, () -> client.getInputStream().read());
+ assertTrue(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start >= 400);
+
+ // write then shutdown
+ client.getOutputStream().write("Goodbye Cruel TLS".getBytes(StandardCharsets.UTF_8));
+ client.shutdownOutput();
+
+ // Verify echo server to client
+ for (char c : "Goodbye Cruel TLS".toCharArray())
+ {
+ int b = client.getInputStream().read();
+ assertTrue(b > 0);
+ assertEquals(c, (char)b);
+ }
+
+ // Read close
+ assertEquals(-1, client.getInputStream().read());
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("scenarios")
+ public void testReadBlocked(Scenario scenario) throws Exception
+ {
+ init(scenario);
+ try (Socket client = _scenario.newClient(_connector);
+ SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ OutputStream clientOutputStream = client.getOutputStream();
+ InputStream clientInputStream = client.getInputStream();
+
+ int specifiedTimeout = 1000;
+ client.setSoTimeout(specifiedTimeout);
+
+ // Write 8 and cause block waiting for 10
+ _blockAt.set(10);
+ clientOutputStream.write("12345678".getBytes(StandardCharsets.UTF_8));
+ clientOutputStream.flush();
+
+ assertTrue(_lastEndPointLatch.await(1, TimeUnit.SECONDS));
+ _lastEndPoint.setIdleTimeout(10 * specifiedTimeout);
+ Thread.sleep((11 * specifiedTimeout) / 10);
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ assertThrows(SocketTimeoutException.class, () -> clientInputStream.read());
+ int elapsed = Long.valueOf(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start).intValue();
+ assertThat("Expected timeout", elapsed, greaterThanOrEqualTo(3 * specifiedTimeout / 4));
+
+ // write remaining characters
+ clientOutputStream.write("90ABCDEF".getBytes(StandardCharsets.UTF_8));
+ clientOutputStream.flush();
+
+ // Verify echo server to client
+ for (char c : "1234567890ABCDEF".toCharArray())
+ {
+ int b = clientInputStream.read();
+ assertTrue(b > 0);
+ assertEquals(c, (char)b);
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("scenarios")
+ public void testStress(Scenario scenario) throws Exception
+ {
+ init(scenario);
+ try (Socket client = _scenario.newClient(_connector))
+ {
+ client.setSoTimeout(30000);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+ final int writes = 200000;
+
+ final byte[] bytes = "HelloWorld-".getBytes(StandardCharsets.UTF_8);
+ byte[] count = "0\n".getBytes(StandardCharsets.UTF_8);
+ BufferedOutputStream out = new BufferedOutputStream(client.getOutputStream());
+ final CountDownLatch latch = new CountDownLatch(writes);
+ final InputStream in = new BufferedInputStream(client.getInputStream());
+ final long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ out.write(bytes);
+ out.write(count);
+ out.flush();
+
+ assertTrue(_lastEndPointLatch.await(1, TimeUnit.SECONDS));
+ _lastEndPoint.setIdleTimeout(5000);
+
+ new Thread(() ->
+ {
+ Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
+ long last = -1;
+ int count1 = -1;
+ try
+ {
+ while (latch.getCount() > 0)
+ {
+ // Verify echo server to client
+ for (byte b0 : bytes)
+ {
+ int b = in.read();
+ assertThat(b, greaterThan(0));
+ assertEquals(0xff & b0, b);
+ }
+
+ count1 = 0;
+ int b = in.read();
+ while (b > 0 && b != '\n')
+ {
+ count1 = count1 * 10 + (b - '0');
+ b = in.read();
+ }
+ last = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+
+ //if (latch.getCount()%1000==0)
+ // System.out.println(writes-latch.getCount());
+
+ latch.countDown();
+ }
+ }
+ catch (Throwable e)
+ {
+
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ System.err.println("count=" + count1);
+ System.err.println("latch=" + latch.getCount());
+ System.err.println("time=" + (now - start));
+ System.err.println("last=" + (now - last));
+ System.err.println("endp=" + _lastEndPoint);
+ System.err.println("conn=" + _lastEndPoint.getConnection());
+
+ e.printStackTrace();
+ }
+ }).start();
+
+ // Write client to server
+ for (int i = 1; i < writes; i++)
+ {
+ out.write(bytes);
+ out.write(Integer.toString(i).getBytes(StandardCharsets.ISO_8859_1));
+ out.write('\n');
+ if (i % 1000 == 0)
+ {
+ //System.err.println(i+"/"+writes);
+ out.flush();
+ }
+ Thread.yield();
+ }
+ out.flush();
+
+ long last = latch.getCount();
+ while (!latch.await(5, TimeUnit.SECONDS))
+ {
+ //System.err.println(latch.getCount());
+ if (latch.getCount() == last)
+ fail("Latch failure");
+ last = latch.getCount();
+ }
+
+ assertEquals(0, latch.getCount());
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("scenarios")
+ public void testWriteBlocked(Scenario scenario) throws Exception
+ {
+ init(scenario);
+ try (Socket client = _scenario.newClient(_connector))
+ {
+ client.setSoTimeout(10000);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ // Write client to server
+ _writeCount.set(10000);
+ String data = "Now is the time for all good men to come to the aid of the party";
+ client.getOutputStream().write(data.getBytes(StandardCharsets.UTF_8));
+ BufferedInputStream in = new BufferedInputStream(client.getInputStream());
+
+ int byteNum = 0;
+ try
+ {
+ for (int i = 0; i < _writeCount.get(); i++)
+ {
+ if (i % 1000 == 0)
+ TimeUnit.MILLISECONDS.sleep(200);
+
+ // Verify echo server to client
+ for (int j = 0; j < data.length(); j++)
+ {
+ char c = data.charAt(j);
+ int b = in.read();
+ byteNum++;
+ assertTrue(b > 0);
+ assertEquals(c, (char)b, "test-" + i + "/" + j);
+ }
+
+ if (i == 0)
+ _lastEndPoint.setIdleTimeout(60000);
+ }
+ }
+ catch (SocketTimeoutException e)
+ {
+ System.err.println("SelectorManager.dump() = " + _manager.dump());
+ LOG.warn("Server: " + server);
+ LOG.warn("Error reading byte #" + byteNum, e);
+ throw e;
+ }
+
+ client.close();
+
+ for (int i = 0; i < 10; ++i)
+ {
+ if (server.isOpen())
+ Thread.sleep(10);
+ else
+ break;
+ }
+ assertFalse(server.isOpen());
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("scenarios")
+ @Tag("Unstable")
+ @Disabled
+ public void testRejectedExecution(Scenario scenario) throws Exception
+ {
+ init(scenario);
+ _manager.stop();
+ _threadPool.stop();
+
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ BlockingQueue<Runnable> q = new ArrayBlockingQueue<>(4);
+ _threadPool = new QueuedThreadPool(4, 4, 60000, q);
+ _manager = new SelectorManager(_threadPool, _scheduler, 1)
+ {
+
+ @Override
+ protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey)
+ {
+ SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, selectionKey, getScheduler());
+ _lastEndPoint = endp;
+ _lastEndPointLatch.countDown();
+ return endp;
+ }
+
+ @Override
+ public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment)
+ {
+ return new TestConnection(endpoint, latch, getExecutor(), _blockAt, _writeCount);
+ }
+ };
+
+ _threadPool.start();
+ _manager.start();
+
+ AtomicInteger timeout = new AtomicInteger();
+ AtomicInteger rejections = new AtomicInteger();
+ AtomicInteger echoed = new AtomicInteger();
+
+ CountDownLatch closed = new CountDownLatch(20);
+ for (int i = 0; i < 20; i++)
+ {
+ new Thread(() ->
+ {
+ try (Socket client = _scenario.newClient(_connector))
+ {
+ client.setSoTimeout(5000);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ // Write client to server
+ client.getOutputStream().write("HelloWorld".getBytes(StandardCharsets.UTF_8));
+ client.getOutputStream().flush();
+ client.shutdownOutput();
+
+ // Verify echo server to client
+ for (char c : "HelloWorld".toCharArray())
+ {
+ int b = client.getInputStream().read();
+ assertTrue(b > 0);
+ assertEquals(c, (char)b);
+ }
+ assertEquals(-1, client.getInputStream().read());
+ echoed.incrementAndGet();
+ }
+ }
+ catch (SocketTimeoutException x)
+ {
+ x.printStackTrace();
+ timeout.incrementAndGet();
+ }
+ catch (Throwable x)
+ {
+ rejections.incrementAndGet();
+ }
+ finally
+ {
+ closed.countDown();
+ }
+ }).start();
+ }
+
+ // unblock the handling
+ latch.countDown();
+
+ // wait for all clients to complete or fail
+ closed.await();
+
+ // assert some clients must have been rejected
+ assertThat(rejections.get(), Matchers.greaterThan(0));
+ // but not all of them
+ assertThat(rejections.get(), Matchers.lessThan(20));
+ // none should have timed out
+ assertThat(timeout.get(), Matchers.equalTo(0));
+ // and the rest should have worked
+ assertThat(echoed.get(), Matchers.equalTo(20 - rejections.get()));
+
+ // and the selector is still working for new requests
+ try (Socket client = _scenario.newClient(_connector))
+ {
+ client.setSoTimeout(5000);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ // Write client to server
+ client.getOutputStream().write("HelloWorld".getBytes(StandardCharsets.UTF_8));
+ client.getOutputStream().flush();
+ client.shutdownOutput();
+
+ // Verify echo server to client
+ for (char c : "HelloWorld".toCharArray())
+ {
+ int b = client.getInputStream().read();
+ assertTrue(b > 0);
+ assertEquals(c, (char)b);
+ }
+ assertEquals(-1, client.getInputStream().read());
+ }
+ }
+ }
+
+ public class ScenarioSelectorManager extends SelectorManager
+ {
+ protected ScenarioSelectorManager(Executor executor, Scheduler scheduler)
+ {
+ super(executor, scheduler);
+ }
+
+ protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key)
+ {
+ SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, key, getScheduler());
+ endp.setIdleTimeout(60000);
+ _lastEndPoint = endp;
+ _lastEndPointLatch.countDown();
+ return endp;
+ }
+
+ @Override
+ public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment)
+ {
+ return _scenario.newConnection(channel, endpoint, getExecutor(), _blockAt, _writeCount);
+ }
+ }
+
+ public static class NormalScenario implements Scenario
+ {
+ @Override
+ public Socket newClient(ServerSocketChannel connector) throws IOException
+ {
+ return new Socket(connector.socket().getInetAddress(), connector.socket().getLocalPort());
+ }
+
+ @Override
+ public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Executor executor, AtomicInteger blockAt, AtomicInteger writeCount)
+ {
+ return new TestConnection(endpoint, executor, blockAt, writeCount);
+ }
+
+ @Override
+ public boolean supportsHalfCloses()
+ {
+ return true;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "normal";
+ }
+ }
+
+ public static class SslScenario implements Scenario
+ {
+ private final NormalScenario _normalScenario;
+ private final SslContextFactory _sslCtxFactory = new SslContextFactory.Server();
+ private final ByteBufferPool _byteBufferPool = new MappedByteBufferPool();
+
+ public SslScenario(NormalScenario normalScenario) throws Exception
+ {
+ _normalScenario = normalScenario;
+ File keystore = MavenTestingUtils.getTestResourceFile("keystore");
+ _sslCtxFactory.setKeyStorePath(keystore.getAbsolutePath());
+ _sslCtxFactory.setKeyStorePassword("storepwd");
+ _sslCtxFactory.setKeyManagerPassword("keypwd");
+ _sslCtxFactory.start();
+ }
+
+ @Override
+ public Socket newClient(ServerSocketChannel connector) throws IOException
+ {
+ SSLSocket socket = _sslCtxFactory.newSslSocket();
+ socket.connect(connector.socket().getLocalSocketAddress());
+ return socket;
+ }
+
+ @Override
+ public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Executor executor, AtomicInteger blockAt, AtomicInteger writeCount)
+ {
+ SSLEngine engine = _sslCtxFactory.newSSLEngine();
+ engine.setUseClientMode(false);
+ SslConnection sslConnection = new SslConnection(_byteBufferPool, executor, endpoint, engine);
+ sslConnection.setRenegotiationAllowed(_sslCtxFactory.isRenegotiationAllowed());
+ sslConnection.setRenegotiationLimit(_sslCtxFactory.getRenegotiationLimit());
+ Connection appConnection = _normalScenario.newConnection(channel, sslConnection.getDecryptedEndPoint(), executor, blockAt, writeCount);
+ sslConnection.getDecryptedEndPoint().setConnection(appConnection);
+ return sslConnection;
+ }
+
+ @Override
+ public boolean supportsHalfCloses()
+ {
+ return false;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ssl";
+ }
+ }
+
+ @SuppressWarnings("Duplicates")
+ public static class TestConnection extends AbstractConnection
+ {
+ private static final Logger LOG = Log.getLogger(TestConnection.class);
+
+ volatile FutureCallback _blockingRead;
+ final AtomicInteger _blockAt;
+ final AtomicInteger _writeCount;
+ // volatile int _blockAt = 0;
+ ByteBuffer _in = BufferUtil.allocate(32 * 1024);
+ ByteBuffer _out = BufferUtil.allocate(32 * 1024);
+ long _last = -1;
+ final CountDownLatch _latch;
+
+ public TestConnection(EndPoint endp, Executor executor, AtomicInteger blockAt, AtomicInteger writeCount)
+ {
+ super(endp, executor);
+ _latch = null;
+ this._blockAt = blockAt;
+ this._writeCount = writeCount;
+ }
+
+ public TestConnection(EndPoint endp, CountDownLatch latch, Executor executor, AtomicInteger blockAt, AtomicInteger writeCount)
+ {
+ super(endp, executor);
+ _latch = latch;
+ this._blockAt = blockAt;
+ this._writeCount = writeCount;
+ }
+
+ @Override
+ public void onOpen()
+ {
+ super.onOpen();
+ fillInterested();
+ }
+
+ @Override
+ public void onFillInterestedFailed(Throwable cause)
+ {
+ Callback blocking = _blockingRead;
+ if (blocking != null)
+ {
+ _blockingRead = null;
+ blocking.failed(cause);
+ return;
+ }
+ super.onFillInterestedFailed(cause);
+ }
+
+ @Override
+ public void onFillable()
+ {
+ if (_latch != null)
+ {
+ try
+ {
+ _latch.await();
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ Callback blocking = _blockingRead;
+ if (blocking != null)
+ {
+ _blockingRead = null;
+ blocking.succeeded();
+ return;
+ }
+
+ EndPoint endp = getEndPoint();
+ try
+ {
+ _last = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ boolean progress = true;
+ while (progress)
+ {
+ progress = false;
+
+ // Fill the input buffer with everything available
+ BufferUtil.compact(_in);
+ if (BufferUtil.isFull(_in))
+ throw new IllegalStateException("FULL " + BufferUtil.toDetailString(_in));
+ int filled = endp.fill(_in);
+ if (filled > 0)
+ progress = true;
+
+ // If the tests wants to block, then block
+ while (_blockAt.get() > 0 && endp.isOpen() && _in.remaining() < _blockAt.get())
+ {
+ FutureCallback future = _blockingRead = new FutureCallback();
+ fillInterested();
+ future.get();
+ filled = endp.fill(_in);
+ progress |= filled > 0;
+ }
+
+ // Copy to the out buffer
+ if (BufferUtil.hasContent(_in) && BufferUtil.append(_out, _in) > 0)
+ progress = true;
+
+ // Blocking writes
+ if (BufferUtil.hasContent(_out))
+ {
+ ByteBuffer out = _out.duplicate();
+ BufferUtil.clear(_out);
+ for (int i = 0; i < _writeCount.get(); i++)
+ {
+ FutureCallback blockingWrite = new FutureCallback();
+ endp.write(blockingWrite, out.asReadOnlyBuffer());
+ blockingWrite.get();
+ }
+ progress = true;
+ }
+
+ // are we done?
+ if (endp.isInputShutdown())
+ endp.shutdownOutput();
+ }
+
+ if (endp.isOpen())
+ fillInterested();
+ }
+ catch (ExecutionException e)
+ {
+ // Timeout does not close, so echo exception then shutdown
+ try
+ {
+ FutureCallback blockingWrite = new FutureCallback();
+ endp.write(blockingWrite, BufferUtil.toBuffer("EE: " + BufferUtil.toString(_in)));
+ blockingWrite.get();
+ endp.shutdownOutput();
+ }
+ catch (Exception e2)
+ {
+ // e2.printStackTrace();
+ }
+ }
+ catch (InterruptedException | EofException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(e);
+ else
+ LOG.info(e.getClass().getName());
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java
new file mode 100644
index 0000000..0e4e4b4
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java
@@ -0,0 +1,568 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSocket;
+
+import org.eclipse.jetty.io.ssl.SslConnection;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.FutureCallback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.eclipse.jetty.util.thread.TimerScheduler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SslConnectionTest
+{
+ private static final int TIMEOUT = 1000000;
+ private static ByteBufferPool __byteBufferPool = new LeakTrackingByteBufferPool(new MappedByteBufferPool.Tagged());
+
+ private final SslContextFactory _sslCtxFactory = new SslContextFactory.Server();
+ protected volatile EndPoint _lastEndp;
+ private volatile boolean _testFill = true;
+ private volatile boolean _onXWriteThenShutdown = false;
+
+ private volatile FutureCallback _writeCallback;
+ protected ServerSocketChannel _connector;
+ final AtomicInteger _dispatches = new AtomicInteger();
+ protected QueuedThreadPool _threadPool = new QueuedThreadPool()
+ {
+ @Override
+ public void execute(Runnable job)
+ {
+ _dispatches.incrementAndGet();
+ super.execute(job);
+ }
+ };
+ protected Scheduler _scheduler = new TimerScheduler();
+ protected SelectorManager _manager = new SelectorManager(_threadPool, _scheduler)
+ {
+ @Override
+ public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment)
+ {
+ SSLEngine engine = _sslCtxFactory.newSSLEngine();
+ engine.setUseClientMode(false);
+ SslConnection sslConnection = new SslConnection(__byteBufferPool, getExecutor(), endpoint, engine);
+ sslConnection.setRenegotiationAllowed(_sslCtxFactory.isRenegotiationAllowed());
+ sslConnection.setRenegotiationLimit(_sslCtxFactory.getRenegotiationLimit());
+ Connection appConnection = new TestConnection(sslConnection.getDecryptedEndPoint());
+ sslConnection.getDecryptedEndPoint().setConnection(appConnection);
+ return sslConnection;
+ }
+
+ @Override
+ protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey)
+ {
+ SocketChannelEndPoint endp = new TestEP(channel, selector, selectionKey, getScheduler());
+ endp.setIdleTimeout(TIMEOUT);
+ _lastEndp = endp;
+ return endp;
+ }
+ };
+
+ static final AtomicInteger __startBlocking = new AtomicInteger();
+ static final AtomicInteger __blockFor = new AtomicInteger();
+ static final AtomicBoolean __onIncompleteFlush = new AtomicBoolean();
+
+ private static class TestEP extends SocketChannelEndPoint
+ {
+ public TestEP(SelectableChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler)
+ {
+ super((SocketChannel)channel, selector, key, scheduler);
+ }
+
+ @Override
+ protected void onIncompleteFlush()
+ {
+ __onIncompleteFlush.set(true);
+ // super.onIncompleteFlush();
+ }
+
+ @Override
+ public boolean flush(ByteBuffer... buffers) throws IOException
+ {
+ __onIncompleteFlush.set(false);
+ if (__startBlocking.get() == 0 || __startBlocking.decrementAndGet() == 0)
+ {
+ if (__blockFor.get() > 0 && __blockFor.getAndDecrement() > 0)
+ {
+ return false;
+ }
+ }
+ return super.flush(buffers);
+ }
+ }
+
+ @BeforeEach
+ public void initSSL() throws Exception
+ {
+ File keystore = MavenTestingUtils.getTestResourceFile("keystore");
+ _sslCtxFactory.setKeyStorePath(keystore.getAbsolutePath());
+ _sslCtxFactory.setKeyStorePassword("storepwd");
+ _sslCtxFactory.setKeyManagerPassword("keypwd");
+ _sslCtxFactory.setRenegotiationAllowed(true);
+ _sslCtxFactory.setRenegotiationLimit(-1);
+ startManager();
+ }
+
+ public void startManager() throws Exception
+ {
+ _testFill = true;
+ _writeCallback = null;
+ _lastEndp = null;
+ _connector = ServerSocketChannel.open();
+ _connector.socket().bind(null);
+ _threadPool.start();
+ _scheduler.start();
+ _manager.start();
+ }
+
+ private void startSSL() throws Exception
+ {
+ _sslCtxFactory.start();
+ }
+
+ @AfterEach
+ public void stopSSL() throws Exception
+ {
+ stopManager();
+ _sslCtxFactory.stop();
+ }
+
+ private void stopManager() throws Exception
+ {
+ if (_lastEndp != null && _lastEndp.isOpen())
+ _lastEndp.close();
+ _manager.stop();
+ _scheduler.stop();
+ _threadPool.stop();
+ _connector.close();
+ }
+
+ public class TestConnection extends AbstractConnection
+ {
+ ByteBuffer _in = BufferUtil.allocate(8 * 1024);
+
+ public TestConnection(EndPoint endp)
+ {
+ super(endp, _threadPool);
+ }
+
+ @Override
+ public void onOpen()
+ {
+ super.onOpen();
+ if (_testFill)
+ fillInterested();
+ else
+ {
+ getExecutor().execute(() -> getEndPoint().write(_writeCallback, BufferUtil.toBuffer("Hello Client")));
+ }
+ }
+
+ @Override
+ public void onClose()
+ {
+ super.onClose();
+ }
+
+ @Override
+ public synchronized void onFillable()
+ {
+ EndPoint endp = getEndPoint();
+ try
+ {
+ boolean progress = true;
+ while (progress)
+ {
+ progress = false;
+
+ // Fill the input buffer with everything available
+ int filled = endp.fill(_in);
+ while (filled > 0)
+ {
+ progress = true;
+ filled = endp.fill(_in);
+ }
+
+ boolean shutdown = _onXWriteThenShutdown && BufferUtil.toString(_in).contains("X");
+
+ // Write everything
+ int l = _in.remaining();
+ if (l > 0)
+ {
+ FutureCallback blockingWrite = new FutureCallback();
+
+ endp.write(blockingWrite, _in);
+ blockingWrite.get();
+ if (shutdown)
+ endp.shutdownOutput();
+ }
+
+ // are we done?
+ if (endp.isInputShutdown() || shutdown)
+ endp.shutdownOutput();
+ }
+ }
+ catch (InterruptedException | EofException e)
+ {
+ Log.getRootLogger().ignore(e);
+ }
+ catch (Exception e)
+ {
+ Log.getRootLogger().warn(e);
+ }
+ finally
+ {
+ if (endp.isOpen())
+ fillInterested();
+ }
+ }
+ }
+
+ protected SSLSocket newClient() throws IOException
+ {
+ SSLSocket socket = _sslCtxFactory.newSslSocket();
+ socket.connect(_connector.socket().getLocalSocketAddress());
+ return socket;
+ }
+
+ @Test
+ public void testHelloWorld() throws Exception
+ {
+ startSSL();
+ try (Socket client = newClient())
+ {
+ client.setSoTimeout(TIMEOUT);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ client.getOutputStream().write("Hello".getBytes(StandardCharsets.UTF_8));
+ byte[] buffer = new byte[1024];
+ int len = client.getInputStream().read(buffer);
+ assertEquals(5, len);
+ assertEquals("Hello", new String(buffer, 0, len, StandardCharsets.UTF_8));
+
+ _dispatches.set(0);
+ client.getOutputStream().write("World".getBytes(StandardCharsets.UTF_8));
+ len = 5;
+ while (len > 0)
+ {
+ len -= client.getInputStream().read(buffer);
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testRenegotiate() throws Exception
+ {
+ startSSL();
+ try (SSLSocket client = newClient())
+ {
+ client.setSoTimeout(TIMEOUT);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ client.getOutputStream().write("Hello".getBytes(StandardCharsets.UTF_8));
+ byte[] buffer = new byte[1024];
+ int len = client.getInputStream().read(buffer);
+ assertEquals(5, len);
+ assertEquals("Hello", new String(buffer, 0, len, StandardCharsets.UTF_8));
+
+ client.startHandshake();
+
+ client.getOutputStream().write("World".getBytes(StandardCharsets.UTF_8));
+ len = client.getInputStream().read(buffer);
+ assertEquals(5, len);
+ assertEquals("World", new String(buffer, 0, len, StandardCharsets.UTF_8));
+ }
+ }
+ }
+
+ @Test
+ public void testRenegotiateNotAllowed() throws Exception
+ {
+ // TLS 1.3 and beyond do not support renegotiation.
+ _sslCtxFactory.setIncludeProtocols("TLSv1.2");
+ _sslCtxFactory.setRenegotiationAllowed(false);
+ startSSL();
+ try (SSLSocket client = newClient())
+ {
+ client.setSoTimeout(TIMEOUT);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ client.getOutputStream().write("Hello".getBytes(StandardCharsets.UTF_8));
+ byte[] buffer = new byte[1024];
+ int len = client.getInputStream().read(buffer);
+ assertEquals(5, len);
+ assertEquals("Hello", new String(buffer, 0, len, StandardCharsets.UTF_8));
+
+ // Try to renegotiate, must fail.
+ client.startHandshake();
+
+ client.getOutputStream().write("World".getBytes(StandardCharsets.UTF_8));
+ assertThrows(SSLException.class, () -> client.getInputStream().read(buffer));
+ }
+ }
+ }
+
+ @Test
+ public void testRenegotiateLimit() throws Exception
+ {
+ // TLS 1.3 and beyond do not support renegotiation.
+ _sslCtxFactory.setIncludeProtocols("TLSv1.2");
+ _sslCtxFactory.setRenegotiationAllowed(true);
+ _sslCtxFactory.setRenegotiationLimit(2);
+ startSSL();
+ try (SSLSocket client = newClient())
+ {
+ client.setSoTimeout(TIMEOUT);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ client.getOutputStream().write("Good".getBytes(StandardCharsets.UTF_8));
+ byte[] buffer = new byte[1024];
+ int len = client.getInputStream().read(buffer);
+ assertEquals(4, len);
+ assertEquals("Good", new String(buffer, 0, len, StandardCharsets.UTF_8));
+
+ client.startHandshake();
+
+ client.getOutputStream().write("Bye".getBytes(StandardCharsets.UTF_8));
+ len = client.getInputStream().read(buffer);
+ assertEquals(3, len);
+ assertEquals("Bye", new String(buffer, 0, len, StandardCharsets.UTF_8));
+
+ client.startHandshake();
+
+ client.getOutputStream().write("Cruel".getBytes(StandardCharsets.UTF_8));
+ len = client.getInputStream().read(buffer);
+ assertEquals(5, len);
+ assertEquals("Cruel", new String(buffer, 0, len, StandardCharsets.UTF_8));
+
+ client.startHandshake();
+
+ client.getOutputStream().write("World".getBytes(StandardCharsets.UTF_8));
+ assertThrows(SSLException.class, () -> client.getInputStream().read(buffer));
+ }
+ }
+ }
+
+ @Test
+ public void testWriteOnConnect() throws Exception
+ {
+ _testFill = false;
+ _writeCallback = new FutureCallback();
+ startSSL();
+ try (SSLSocket client = newClient())
+ {
+ client.setSoTimeout(TIMEOUT);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ // The server side will write something, and in order
+ // to proceed with the initial TLS handshake we need
+ // to start reading before waiting for the callback.
+
+ byte[] buffer = new byte[1024];
+ int len = client.getInputStream().read(buffer);
+ assertEquals("Hello Client", new String(buffer, 0, len, StandardCharsets.UTF_8));
+
+ assertNull(_writeCallback.get(1, TimeUnit.SECONDS));
+ }
+ }
+ }
+
+ @Test
+ public void testBlockedWrite() throws Exception
+ {
+ startSSL();
+ try (SSLSocket client = newClient())
+ {
+ client.setSoTimeout(5000);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ client.getOutputStream().write("Hello".getBytes(StandardCharsets.UTF_8));
+ byte[] buffer = new byte[1024];
+ int len = client.getInputStream().read(buffer);
+ assertEquals("Hello", new String(buffer, 0, len, StandardCharsets.UTF_8));
+
+ __startBlocking.set(0);
+ __blockFor.set(2);
+ _dispatches.set(0);
+ client.getOutputStream().write("World".getBytes(StandardCharsets.UTF_8));
+
+ try
+ {
+ client.setSoTimeout(500);
+ client.getInputStream().read(buffer);
+ throw new IllegalStateException();
+ }
+ catch (SocketTimeoutException e)
+ {
+ // no op
+ }
+
+ assertTrue(__onIncompleteFlush.get());
+ ((TestEP)_lastEndp).getWriteFlusher().completeWrite();
+
+ len = client.getInputStream().read(buffer);
+ assertEquals("World", new String(buffer, 0, len, StandardCharsets.UTF_8));
+ }
+ }
+ }
+
+ @Test
+ public void testBlockedClose() throws Exception
+ {
+ startSSL();
+ try (SSLSocket client = newClient())
+ {
+ client.setSoTimeout(5000);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ client.getOutputStream().write("Short".getBytes(StandardCharsets.UTF_8));
+ byte[] buffer = new byte[1024];
+ int len = client.getInputStream().read(buffer);
+ assertEquals("Short", new String(buffer, 0, len, StandardCharsets.UTF_8));
+
+ _onXWriteThenShutdown = true;
+ __startBlocking.set(2); // block on the close handshake flush
+ __blockFor.set(Integer.MAX_VALUE); // > retry loops in SslConnection + 1
+ client.getOutputStream().write("This is a much longer example with X".getBytes(StandardCharsets.UTF_8));
+ len = client.getInputStream().read(buffer);
+ assertEquals("This is a much longer example with X", new String(buffer, 0, len, StandardCharsets.UTF_8));
+
+ try
+ {
+ client.setSoTimeout(500);
+ client.getInputStream().read(buffer);
+ throw new IllegalStateException();
+ }
+ catch (SocketTimeoutException e)
+ {
+ // no op
+ }
+
+ __blockFor.set(0);
+ assertTrue(__onIncompleteFlush.get());
+ ((TestEP)_lastEndp).getWriteFlusher().completeWrite();
+ len = client.getInputStream().read(buffer);
+ assertThat(len, is(-1));
+ }
+ }
+ }
+
+ @Test
+ public void testManyLines() throws Exception
+ {
+ startSSL();
+ try (Socket client = newClient())
+ {
+ client.setSoTimeout(10000);
+ try (SocketChannel server = _connector.accept())
+ {
+ server.configureBlocking(false);
+ _manager.accept(server);
+
+ final int LINES = 20;
+ final CountDownLatch count = new CountDownLatch(LINES);
+
+ new Thread(() ->
+ {
+ try
+ {
+ BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8));
+ while (count.getCount() > 0)
+ {
+ String line = in.readLine();
+ if (line == null)
+ break;
+ count.countDown();
+ }
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+ }).start();
+
+ for (int i = 0; i < LINES; i++)
+ {
+ client.getOutputStream().write(("HelloWorld " + i + "\n").getBytes(StandardCharsets.UTF_8));
+ if (i % 1000 == 0)
+ {
+ client.getOutputStream().flush();
+ Thread.sleep(10);
+ }
+ }
+
+ assertTrue(count.await(20, TimeUnit.SECONDS));
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/WriteFlusherTest.java b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/WriteFlusherTest.java
new file mode 100644
index 0000000..1e66665
--- /dev/null
+++ b/third_party/jetty-io/src/test/java/org/eclipse/jetty/io/WriteFlusherTest.java
@@ -0,0 +1,509 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.io;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritePendingException;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.FutureCallback;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class WriteFlusherTest
+{
+ @Test
+ public void testCompleteNoBlocking() throws Exception
+ {
+ testCompleteWrite(false);
+ }
+
+ @Test
+ public void testIgnorePreviousFailures() throws Exception
+ {
+ testCompleteWrite(true);
+ }
+
+ private void testCompleteWrite(boolean failBefore) throws Exception
+ {
+ ByteArrayEndPoint endPoint = new ByteArrayEndPoint(new byte[0], 16);
+ endPoint.setGrowOutput(true);
+
+ AtomicBoolean incompleteFlush = new AtomicBoolean();
+ WriteFlusher flusher = new WriteFlusher(endPoint)
+ {
+ @Override
+ protected void onIncompleteFlush()
+ {
+ incompleteFlush.set(true);
+ }
+ };
+
+ if (failBefore)
+ flusher.onFail(new IOException("Ignored because no operation in progress"));
+
+ FutureCallback callback = new FutureCallback();
+ flusher.write(callback, BufferUtil.toBuffer("How "), BufferUtil.toBuffer("now "), BufferUtil.toBuffer("brown "), BufferUtil.toBuffer("cow!"));
+
+ assertTrue(callback.isDone());
+ assertFalse(incompleteFlush.get());
+ assertEquals("How now brown cow!", endPoint.takeOutputString());
+ assertTrue(flusher.isIdle());
+ }
+
+ @Test
+ public void testClosedNoBlocking() throws Exception
+ {
+ ByteArrayEndPoint endPoint = new ByteArrayEndPoint(new byte[0], 16);
+ endPoint.close();
+
+ AtomicBoolean incompleteFlush = new AtomicBoolean();
+ WriteFlusher flusher = new WriteFlusher(endPoint)
+ {
+ @Override
+ protected void onIncompleteFlush()
+ {
+ incompleteFlush.set(true);
+ }
+ };
+
+ FutureCallback callback = new FutureCallback();
+ flusher.write(callback, BufferUtil.toBuffer("foo"));
+
+ assertTrue(callback.isDone());
+ assertFalse(incompleteFlush.get());
+
+ ExecutionException e = assertThrows(ExecutionException.class, () ->
+ {
+ callback.get();
+ });
+ assertThat(e.getCause(), instanceOf(IOException.class));
+ assertThat(e.getCause().getMessage(), containsString("CLOSED"));
+
+ assertEquals("", endPoint.takeOutputString());
+ assertTrue(flusher.isFailed());
+ }
+
+ @Test
+ public void testCompleteBlocking() throws Exception
+ {
+ ByteArrayEndPoint endPoint = new ByteArrayEndPoint(new byte[0], 10);
+
+ AtomicBoolean incompleteFlush = new AtomicBoolean();
+ WriteFlusher flusher = new WriteFlusher(endPoint)
+ {
+ @Override
+ protected void onIncompleteFlush()
+ {
+ incompleteFlush.set(true);
+ }
+ };
+
+ FutureCallback callback = new FutureCallback();
+ flusher.write(callback, BufferUtil.toBuffer("How now brown cow!"));
+
+ assertFalse(callback.isDone());
+ assertFalse(callback.isCancelled());
+
+ assertTrue(incompleteFlush.get());
+
+ assertThrows(TimeoutException.class, () ->
+ {
+ callback.get(100, TimeUnit.MILLISECONDS);
+ });
+
+ incompleteFlush.set(false);
+
+ assertEquals("How now br", endPoint.takeOutputString());
+
+ flusher.completeWrite();
+
+ assertTrue(callback.isDone());
+ assertEquals("own cow!", endPoint.takeOutputString());
+ assertFalse(incompleteFlush.get());
+ assertTrue(flusher.isIdle());
+ }
+
+ @Test
+ public void testCallbackThrows() throws Exception
+ {
+ ByteArrayEndPoint endPoint = new ByteArrayEndPoint(new byte[0], 100);
+
+ AtomicBoolean incompleteFlush = new AtomicBoolean(false);
+ WriteFlusher flusher = new WriteFlusher(endPoint)
+ {
+ @Override
+ protected void onIncompleteFlush()
+ {
+ incompleteFlush.set(true);
+ }
+ };
+
+ FutureCallback callback = new FutureCallback()
+ {
+ @Override
+ public void succeeded()
+ {
+ super.succeeded();
+ throw new IllegalStateException();
+ }
+ };
+
+ try (StacklessLogging stacklessLogging = new StacklessLogging(WriteFlusher.class))
+ {
+ flusher.write(callback, BufferUtil.toBuffer("How now brown cow!"));
+ callback.get(100, TimeUnit.MILLISECONDS);
+ }
+
+ assertEquals("How now brown cow!", endPoint.takeOutputString());
+ assertTrue(callback.isDone());
+ assertFalse(incompleteFlush.get());
+ assertTrue(flusher.isIdle());
+ }
+
+ @Test
+ public void testCloseWhileBlocking() throws Exception
+ {
+ ByteArrayEndPoint endPoint = new ByteArrayEndPoint(new byte[0], 10);
+
+ AtomicBoolean incompleteFlush = new AtomicBoolean();
+ WriteFlusher flusher = new WriteFlusher(endPoint)
+ {
+ @Override
+ protected void onIncompleteFlush()
+ {
+ incompleteFlush.set(true);
+ }
+ };
+
+ FutureCallback callback = new FutureCallback();
+ flusher.write(callback, BufferUtil.toBuffer("How now brown cow!"));
+
+ assertFalse(callback.isDone());
+ assertFalse(callback.isCancelled());
+
+ assertTrue(incompleteFlush.get());
+ incompleteFlush.set(false);
+
+ assertEquals("How now br", endPoint.takeOutputString());
+
+ endPoint.close();
+ flusher.completeWrite();
+
+ assertTrue(callback.isDone());
+ assertFalse(incompleteFlush.get());
+
+ ExecutionException e = assertThrows(ExecutionException.class, () -> callback.get());
+ assertThat(e.getCause(), instanceOf(IOException.class));
+ assertThat(e.getCause().getMessage(), containsString("CLOSED"));
+
+ assertEquals("", endPoint.takeOutputString());
+ assertTrue(flusher.isFailed());
+ }
+
+ @Test
+ public void testFailWhileBlocking() throws Exception
+ {
+ ByteArrayEndPoint endPoint = new ByteArrayEndPoint(new byte[0], 10);
+
+ AtomicBoolean incompleteFlush = new AtomicBoolean();
+ WriteFlusher flusher = new WriteFlusher(endPoint)
+ {
+ @Override
+ protected void onIncompleteFlush()
+ {
+ incompleteFlush.set(true);
+ }
+ };
+
+ FutureCallback callback = new FutureCallback();
+ flusher.write(callback, BufferUtil.toBuffer("How now brown cow!"));
+
+ assertFalse(callback.isDone());
+ assertFalse(callback.isCancelled());
+
+ assertTrue(incompleteFlush.get());
+ incompleteFlush.set(false);
+
+ assertEquals("How now br", endPoint.takeOutputString());
+
+ String reason = "Failure";
+ flusher.onFail(new IOException(reason));
+ flusher.completeWrite();
+
+ assertTrue(callback.isDone());
+ assertFalse(incompleteFlush.get());
+
+ ExecutionException e = assertThrows(ExecutionException.class, () -> callback.get());
+ assertThat(e.getCause(), instanceOf(IOException.class));
+ assertThat(e.getCause().getMessage(), containsString(reason));
+
+ assertEquals("", endPoint.takeOutputString());
+ assertTrue(flusher.isFailed());
+ }
+
+ @Test
+ public void testConcurrent() throws Exception
+ {
+ Random random = new Random();
+ ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(100);
+ try
+ {
+ String reason = "THE_CAUSE";
+ ConcurrentWriteFlusher[] flushers = new ConcurrentWriteFlusher[50000];
+ FutureCallback[] futures = new FutureCallback[flushers.length];
+ for (int i = 0; i < flushers.length; ++i)
+ {
+ int size = 5 + random.nextInt(15);
+ ByteArrayEndPoint endPoint = new ByteArrayEndPoint(new byte[0], size);
+ ConcurrentWriteFlusher flusher = new ConcurrentWriteFlusher(endPoint, scheduler, random);
+ flushers[i] = flusher;
+ FutureCallback callback = new FutureCallback();
+ futures[i] = callback;
+ scheduler.schedule(() -> flusher.onFail(new Throwable(reason)), random.nextInt(75) + 1, TimeUnit.MILLISECONDS);
+ flusher.write(callback, BufferUtil.toBuffer("How Now Brown Cow."), BufferUtil.toBuffer(" The quick brown fox jumped over the lazy dog!"));
+ }
+
+ int completed = 0;
+ int failed = 0;
+ for (int i = 0; i < flushers.length; ++i)
+ {
+ try
+ {
+ futures[i].get(15, TimeUnit.SECONDS);
+ assertEquals("How Now Brown Cow. The quick brown fox jumped over the lazy dog!", flushers[i].getContent());
+ completed++;
+ }
+ catch (ExecutionException x)
+ {
+ assertEquals(reason, x.getCause().getMessage());
+ failed++;
+ }
+ }
+ assertThat(completed, Matchers.greaterThan(0));
+ assertThat(failed, Matchers.greaterThan(0));
+ assertEquals(flushers.length, completed + failed);
+ }
+ finally
+ {
+ scheduler.shutdown();
+ }
+ }
+
+ @Test
+ public void testPendingWriteDoesNotStoreConsumedBuffers() throws Exception
+ {
+ ByteArrayEndPoint endPoint = new ByteArrayEndPoint(new byte[0], 10);
+
+ int toWrite = endPoint.getOutput().capacity();
+ byte[] chunk1 = new byte[toWrite / 2];
+ Arrays.fill(chunk1, (byte)1);
+ ByteBuffer buffer1 = ByteBuffer.wrap(chunk1);
+ byte[] chunk2 = new byte[toWrite];
+ Arrays.fill(chunk1, (byte)2);
+ ByteBuffer buffer2 = ByteBuffer.wrap(chunk2);
+
+ AtomicBoolean incompleteFlush = new AtomicBoolean();
+ WriteFlusher flusher = new WriteFlusher(endPoint)
+ {
+ @Override
+ protected void onIncompleteFlush()
+ {
+ incompleteFlush.set(true);
+ }
+ };
+
+ flusher.write(Callback.NOOP, buffer1, buffer2);
+ assertTrue(incompleteFlush.get());
+ assertFalse(buffer1.hasRemaining());
+
+ // Reuse buffer1
+ buffer1.clear();
+ Arrays.fill(chunk1, (byte)3);
+ int remaining1 = buffer1.remaining();
+
+ // Complete the write
+ endPoint.takeOutput();
+ flusher.completeWrite();
+
+ // Make sure buffer1 is unchanged
+ assertEquals(remaining1, buffer1.remaining());
+ }
+
+ @Test
+ public void testConcurrentWrites() throws Exception
+ {
+ ByteArrayEndPoint endPoint = new ByteArrayEndPoint(new byte[0], 16);
+
+ CountDownLatch flushLatch = new CountDownLatch(1);
+ WriteFlusher flusher = new WriteFlusher(endPoint)
+ {
+ @Override
+ protected ByteBuffer[] flush(ByteBuffer[] buffers) throws IOException
+ {
+ try
+ {
+ flushLatch.countDown();
+ Thread.sleep(2000);
+ return super.flush(buffers);
+ }
+ catch (InterruptedException x)
+ {
+ throw new InterruptedIOException();
+ }
+ }
+
+ @Override
+ protected void onIncompleteFlush()
+ {
+ }
+ };
+
+ // Two concurrent writes.
+ new Thread(() -> flusher.write(Callback.NOOP, BufferUtil.toBuffer("foo"))).start();
+ assertTrue(flushLatch.await(1, TimeUnit.SECONDS));
+
+ assertThrows(WritePendingException.class, () ->
+ {
+ // The second write throws WritePendingException.
+ flusher.write(Callback.NOOP, BufferUtil.toBuffer("bar"));
+ });
+ }
+
+ @Test
+ public void testConcurrentWriteAndOnFail() throws Exception
+ {
+ assertThrows(ExecutionException.class, () ->
+ {
+ ByteArrayEndPoint endPoint = new ByteArrayEndPoint(new byte[0], 16);
+
+ WriteFlusher flusher = new WriteFlusher(endPoint)
+ {
+ @Override
+ protected ByteBuffer[] flush(ByteBuffer[] buffers)
+ throws IOException
+ {
+ ByteBuffer[] result = super.flush(buffers);
+ boolean notified = onFail(new Throwable());
+ assertTrue(notified);
+ return result;
+ }
+
+ @Override
+ protected void onIncompleteFlush()
+ {
+ }
+ };
+
+ FutureCallback callback = new FutureCallback();
+ flusher.write(callback, BufferUtil.toBuffer("foo"));
+
+ assertTrue(flusher.isFailed());
+
+ callback.get(1, TimeUnit.SECONDS);
+ });
+ }
+
+ @Test
+ public void testConcurrentIncompleteFlushAndOnFail() throws Exception
+ {
+ int capacity = 8;
+ ByteArrayEndPoint endPoint = new ByteArrayEndPoint(new byte[0], capacity);
+ String reason = "the_reason";
+
+ WriteFlusher flusher = new WriteFlusher(endPoint)
+ {
+ @Override
+ protected void onIncompleteFlush()
+ {
+ onFail(new Throwable(reason));
+ }
+ };
+
+ FutureCallback callback = new FutureCallback();
+ byte[] content = new byte[capacity * 2];
+ flusher.write(callback, BufferUtil.toBuffer(content));
+
+ try
+ {
+ // Callback must be failed.
+ callback.get(1, TimeUnit.SECONDS);
+ }
+ catch (ExecutionException x)
+ {
+ assertEquals(reason, x.getCause().getMessage());
+ }
+ }
+
+ private static class ConcurrentWriteFlusher extends WriteFlusher implements Runnable
+ {
+ private final ByteArrayEndPoint endPoint;
+ private final ScheduledExecutorService scheduler;
+ private final Random random;
+ private String content = "";
+
+ private ConcurrentWriteFlusher(ByteArrayEndPoint endPoint, ScheduledThreadPoolExecutor scheduler, Random random)
+ {
+ super(endPoint);
+ this.endPoint = endPoint;
+ this.scheduler = scheduler;
+ this.random = random;
+ }
+
+ @Override
+ protected void onIncompleteFlush()
+ {
+ scheduler.schedule(this, 1 + random.nextInt(9), TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void run()
+ {
+ content += endPoint.takeOutputString();
+ completeWrite();
+ }
+
+ private String getContent()
+ {
+ content += endPoint.takeOutputString();
+ return content;
+ }
+ }
+}
diff --git a/third_party/jetty-io/src/test/resources/jetty-logging.properties b/third_party/jetty-io/src/test/resources/jetty-logging.properties
new file mode 100644
index 0000000..0e7fd71
--- /dev/null
+++ b/third_party/jetty-io/src/test/resources/jetty-logging.properties
@@ -0,0 +1,5 @@
+org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
+#org.eclipse.jetty.LEVEL=DEBUG
+#org.eclipse.jetty.io.AbstractConnection.LEVEL=DEBUG
+#org.eclipse.jetty.io.ManagedSelector.LEVEL=DEBUG
+#org.eclipse.jetty.io.ssl.SslConnection.LEVEL=DEBUG
diff --git a/third_party/jetty-io/src/test/resources/keystore b/third_party/jetty-io/src/test/resources/keystore
new file mode 100644
index 0000000..b727bd0
--- /dev/null
+++ b/third_party/jetty-io/src/test/resources/keystore
Binary files differ
diff --git a/third_party/jetty-security/pom.xml b/third_party/jetty-security/pom.xml
new file mode 100644
index 0000000..92f10b7
--- /dev/null
+++ b/third_party/jetty-security/pom.xml
@@ -0,0 +1,55 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-project</artifactId>
+ <version>9.4.44.v20210927</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>jetty-security</artifactId>
+ <name>Jetty :: Security</name>
+ <description>Jetty security infrastructure</description>
+ <properties>
+ <bundle-symbolic-name>${project.groupId}.security</bundle-symbolic-name>
+ <spotbugs.onlyAnalyze>org.eclipse.jetty.security.*</spotbugs.onlyAnalyze>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <executions>
+ <execution>
+ <goals>
+ <goal>manifest</goal>
+ </goals>
+ <configuration>
+ <instructions>
+ <Require-Capability>osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.security.Authenticator$Factory)";resolution:=optional;cardinality:=multiple, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional</Require-Capability>
+ </instructions>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-http</artifactId>
+ <version>${project.version}</version>
+ <classifier>tests</classifier>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.toolchain</groupId>
+ <artifactId>jetty-test-helper</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/third_party/jetty-security/src/main/config/etc/README.spnego b/third_party/jetty-security/src/main/config/etc/README.spnego
new file mode 100644
index 0000000..85917c8
--- /dev/null
+++ b/third_party/jetty-security/src/main/config/etc/README.spnego
@@ -0,0 +1,62 @@
+This setup will enable you to authenticate a user via SPNEGO into your
+webapp.
+
+To run with SPNEGO enabled the following command line options are required:
+
+-Djava.security.krb5.conf=/path/to/jetty/etc/krb5.ini
+-Djava.security.auth.login.config=/path/to/jetty/etc/spnego.conf
+-Djavax.security.auth.useSubjectCredsOnly=false
+
+The easiest place to put these lines are in the start.ini file.
+
+For debugging the SPNEGO authentication the following options are helpful:
+
+-Dorg.eclipse.jetty.LEVEL=debug
+-Dsun.security.spnego.debug=true
+
+
+SPNEGO Authentication is enabled in the webapp with the following setup.
+
+ <security-constraint>
+ <web-resource-collection>
+ <web-resource-name>Secure Area</web-resource-name>
+ <url-pattern>/secure/me/*</url-pattern>
+ </web-resource-collection>
+ <auth-constraint>
+ <role-name>MORTBAY.ORG</role-name> <-- this is the domain that the user is a member of
+ </auth-constraint>
+ </security-constraint>
+
+ <login-config>
+ <auth-method>SPNEGO</auth-method>
+ <realm-name>Test Realm</realm-name>
+ (optionally to add custom error page)
+ <spnego-login-config>
+ <spnego-error-page>/loginError.html?param=foo</spnego-error-page>
+ </spnego-login-config>
+ </login-config>
+
+A corresponding UserRealm needs to be created either programmatically if
+embedded, via the jetty.xml or in a context file for the webapp.
+
+(in the jetty.xml)
+
+ <Call name="addBean">
+ <Arg>
+ <New class="org.eclipse.jetty.security.SpnegoLoginService">
+ <Set name="name">Test Realm</Set>
+ <Set name="config"><Property name="jetty.home" default="."/>/etc/spnego.properties</Set>
+ </New>
+ </Arg>
+ </Call>
+
+(context file)
+ <Get name="securityHandler">
+ <Set name="loginService">
+ <New class="org.eclipse.jetty.security.SpnegoLoginService">
+ <Set name="name">Test Realm</Set>
+ <Set name="config"><SystemProperty name="jetty.home" default="."/>/etc/spnego.properties</Set>
+ </New>
+ </Set>
+ <Set name="checkWelcomeFiles">true</Set>
+ </Get>
diff --git a/third_party/jetty-security/src/main/config/etc/krb5.ini b/third_party/jetty-security/src/main/config/etc/krb5.ini
new file mode 100644
index 0000000..283880c
--- /dev/null
+++ b/third_party/jetty-security/src/main/config/etc/krb5.ini
@@ -0,0 +1,23 @@
+[libdefaults]
+default_realm = MORTBAY.ORG
+default_keytab_name = FILE:/path/to/jetty/etc/krb5.keytab
+permitted_enctypes = aes128-cts aes256-cts arcfour-hmac-md5
+default_tgs_enctypes = aes128-cts aes256-cts arcfour-hmac-md5
+default_tkt_enctypes = aes128-cts aes256-cts arcfour-hmac-md5
+
+
+
+[realms]
+MORTBAY.ORG = {
+ kdc = 192.168.2.30
+ admin_server = 192.168.2.30
+ default_domain = MORTBAY.ORG
+}
+
+[domain_realm]
+mortbay.org= MORTBAY.ORG
+.mortbay.org = MORTBAY.ORG
+
+[appdefaults]
+autologin = true
+forwardable = true
diff --git a/third_party/jetty-security/src/main/config/etc/spnego.conf b/third_party/jetty-security/src/main/config/etc/spnego.conf
new file mode 100644
index 0000000..3d5caf8
--- /dev/null
+++ b/third_party/jetty-security/src/main/config/etc/spnego.conf
@@ -0,0 +1,19 @@
+com.sun.security.jgss.initiate {
+ com.sun.security.auth.module.Krb5LoginModule required
+ principal="HTTP/vm.mortbay.org@MORTBAY.ORG"
+ keyTab="/path/to/jetty/etc/krb5.keytab"
+ useKeyTab=true
+ storeKey=true
+ debug=true
+ isInitiator=false;
+};
+
+com.sun.security.jgss.accept {
+ com.sun.security.auth.module.Krb5LoginModule required
+ principal="HTTP/vm.mortbay.org@MORTBAY.ORG"
+ useKeyTab=true
+ keyTab="/path/to/jetty/etc/krb5.keytab"
+ storeKey=true
+ debug=true
+ isInitiator=false;
+};
diff --git a/third_party/jetty-security/src/main/config/etc/spnego.properties b/third_party/jetty-security/src/main/config/etc/spnego.properties
new file mode 100644
index 0000000..86862ea
--- /dev/null
+++ b/third_party/jetty-security/src/main/config/etc/spnego.properties
@@ -0,0 +1 @@
+targetName = HTTP/vm.mortbay.org
\ No newline at end of file
diff --git a/third_party/jetty-security/src/main/config/modules/security.mod b/third_party/jetty-security/src/main/config/modules/security.mod
new file mode 100644
index 0000000..4b87e26
--- /dev/null
+++ b/third_party/jetty-security/src/main/config/modules/security.mod
@@ -0,0 +1,10 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Adds servlet standard security handling to the classpath.
+
+[depend]
+server
+
+[lib]
+lib/jetty-security-${jetty.version}.jar
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractLoginService.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractLoginService.java
new file mode 100644
index 0000000..6810e6d
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractLoginService.java
@@ -0,0 +1,231 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.Serializable;
+import java.security.Principal;
+import javax.security.auth.Subject;
+import javax.servlet.ServletRequest;
+
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.security.Credential;
+
+/**
+ * AbstractLoginService
+ */
+public abstract class AbstractLoginService extends ContainerLifeCycle implements LoginService
+{
+ private static final Logger LOG = Log.getLogger(AbstractLoginService.class);
+
+ protected IdentityService _identityService = new DefaultIdentityService();
+ protected String _name;
+ protected boolean _fullValidate = false;
+
+ /**
+ * RolePrincipal
+ */
+ public static class RolePrincipal implements Principal, Serializable
+ {
+ private static final long serialVersionUID = 2998397924051854402L;
+ private final String _roleName;
+
+ public RolePrincipal(String name)
+ {
+ _roleName = name;
+ }
+
+ @Override
+ public String getName()
+ {
+ return _roleName;
+ }
+ }
+
+ /**
+ * UserPrincipal
+ */
+ public static class UserPrincipal implements Principal, Serializable
+ {
+ private static final long serialVersionUID = -6226920753748399662L;
+ private final String _name;
+ private final Credential _credential;
+
+ public UserPrincipal(String name, Credential credential)
+ {
+ _name = name;
+ _credential = credential;
+ }
+
+ public boolean authenticate(Object credentials)
+ {
+ return _credential != null && _credential.check(credentials);
+ }
+
+ public boolean authenticate(Credential c)
+ {
+ return (_credential != null && c != null && _credential.equals(c));
+ }
+
+ @Override
+ public String getName()
+ {
+ return _name;
+ }
+
+ @Override
+ public String toString()
+ {
+ return _name;
+ }
+ }
+
+ protected abstract String[] loadRoleInfo(UserPrincipal user);
+
+ protected abstract UserPrincipal loadUserInfo(String username);
+
+ protected AbstractLoginService()
+ {
+ addBean(_identityService);
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.LoginService#getName()
+ */
+ @Override
+ public String getName()
+ {
+ return _name;
+ }
+
+ /**
+ * Set the identityService.
+ *
+ * @param identityService the identityService to set
+ */
+ @Override
+ public void setIdentityService(IdentityService identityService)
+ {
+ if (isRunning())
+ throw new IllegalStateException("Running");
+ updateBean(_identityService, identityService);
+ _identityService = identityService;
+ }
+
+ /**
+ * Set the name.
+ *
+ * @param name the name to set
+ */
+ public void setName(String name)
+ {
+ if (isRunning())
+ throw new IllegalStateException("Running");
+ _name = name;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x[%s]", this.getClass().getSimpleName(), hashCode(), _name);
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.LoginService#login(java.lang.String, java.lang.Object, javax.servlet.ServletRequest)
+ */
+ @Override
+ public UserIdentity login(String username, Object credentials, ServletRequest request)
+ {
+ if (username == null)
+ return null;
+
+ UserPrincipal userPrincipal = loadUserInfo(username);
+ if (userPrincipal != null && userPrincipal.authenticate(credentials))
+ {
+ //safe to load the roles
+ String[] roles = loadRoleInfo(userPrincipal);
+
+ Subject subject = new Subject();
+ subject.getPrincipals().add(userPrincipal);
+ subject.getPrivateCredentials().add(userPrincipal._credential);
+ if (roles != null)
+ for (String role : roles)
+ {
+ subject.getPrincipals().add(new RolePrincipal(role));
+ }
+ subject.setReadOnly();
+ return _identityService.newUserIdentity(subject, userPrincipal, roles);
+ }
+
+ return null;
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.LoginService#validate(org.eclipse.jetty.server.UserIdentity)
+ */
+ @Override
+ public boolean validate(UserIdentity user)
+ {
+ if (!isFullValidate())
+ return true; //if we have a user identity it must be valid
+
+ //Do a full validation back against the user store
+ UserPrincipal fresh = loadUserInfo(user.getUserPrincipal().getName());
+ if (fresh == null)
+ return false; //user no longer exists
+
+ if (user.getUserPrincipal() instanceof UserPrincipal)
+ {
+ return fresh.authenticate(((UserPrincipal)user.getUserPrincipal())._credential);
+ }
+
+ throw new IllegalStateException("UserPrincipal not KnownUser"); //can't validate
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.LoginService#getIdentityService()
+ */
+ @Override
+ public IdentityService getIdentityService()
+ {
+ return _identityService;
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.LoginService#logout(org.eclipse.jetty.server.UserIdentity)
+ */
+ @Override
+ public void logout(UserIdentity user)
+ {
+ //Override in subclasses
+
+ }
+
+ public boolean isFullValidate()
+ {
+ return _fullValidate;
+ }
+
+ public void setFullValidate(boolean fullValidate)
+ {
+ _fullValidate = fullValidate;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractUserAuthentication.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractUserAuthentication.java
new file mode 100644
index 0000000..0c44f41
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractUserAuthentication.java
@@ -0,0 +1,116 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.Serializable;
+import java.util.Set;
+import javax.servlet.ServletRequest;
+
+import org.eclipse.jetty.security.authentication.LoginAuthenticator;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Authentication.User;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.server.UserIdentity.Scope;
+
+/**
+ * AbstractUserAuthentication
+ *
+ *
+ * Base class for representing an authenticated user.
+ */
+public abstract class AbstractUserAuthentication implements User, Serializable
+{
+ private static final long serialVersionUID = -6290411814232723403L;
+ protected String _method;
+ protected transient UserIdentity _userIdentity;
+
+ public AbstractUserAuthentication(String method, UserIdentity userIdentity)
+ {
+ _method = method;
+ _userIdentity = userIdentity;
+ }
+
+ @Override
+ public String getAuthMethod()
+ {
+ return _method;
+ }
+
+ @Override
+ public UserIdentity getUserIdentity()
+ {
+ return _userIdentity;
+ }
+
+ @Override
+ public boolean isUserInRole(Scope scope, String role)
+ {
+ String roleToTest = null;
+ if (scope != null && scope.getRoleRefMap() != null)
+ roleToTest = scope.getRoleRefMap().get(role);
+ if (roleToTest == null)
+ roleToTest = role;
+ //Servlet Spec 3.1 pg 125 if testing special role **
+ if ("**".equals(roleToTest.trim()))
+ {
+ //if ** is NOT a declared role name, the we return true
+ //as the user is authenticated. If ** HAS been declared as a
+ //role name, then we have to check if the user has that role
+ if (!declaredRolesContains("**"))
+ return true;
+ else
+ return _userIdentity.isUserInRole(role, scope);
+ }
+
+ return _userIdentity.isUserInRole(role, scope);
+ }
+
+ public boolean declaredRolesContains(String roleName)
+ {
+ SecurityHandler security = SecurityHandler.getCurrentSecurityHandler();
+ if (security == null)
+ return false;
+
+ if (security instanceof ConstraintAware)
+ {
+ Set<String> declaredRoles = ((ConstraintAware)security).getRoles();
+ return (declaredRoles != null) && declaredRoles.contains(roleName);
+ }
+
+ return false;
+ }
+
+ @Override
+ public Authentication logout(ServletRequest request)
+ {
+ SecurityHandler security = SecurityHandler.getCurrentSecurityHandler();
+ if (security != null)
+ {
+ security.logout(this);
+ Authenticator authenticator = security.getAuthenticator();
+ if (authenticator instanceof LoginAuthenticator)
+ {
+ ((LoginAuthenticator)authenticator).logout(request);
+ return new LoggedOutAuthentication((LoginAuthenticator)authenticator);
+ }
+ }
+
+ return Authentication.UNAUTHENTICATED;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java
new file mode 100644
index 0000000..af06323
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java
@@ -0,0 +1,135 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.util.Set;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Authentication.User;
+import org.eclipse.jetty.server.Server;
+
+/**
+ * Authenticator Interface
+ * <p>
+ * An Authenticator is responsible for checking requests and sending
+ * response challenges in order to authenticate a request.
+ * Various types of {@link Authentication} are returned in order to
+ * signal the next step in authentication.
+ *
+ * @version $Rev: 4793 $ $Date: 2009-03-19 00:00:01 +0100 (Thu, 19 Mar 2009) $
+ */
+public interface Authenticator
+{
+
+ /**
+ * Configure the Authenticator
+ *
+ * @param configuration the configuration
+ */
+ void setConfiguration(AuthConfiguration configuration);
+
+ /**
+ * @return The name of the authentication method
+ */
+ String getAuthMethod();
+
+ /**
+ * Called prior to validateRequest. The authenticator can
+ * manipulate the request to update it with information that
+ * can be inspected prior to validateRequest being called.
+ * The primary purpose of this method is to satisfy the Servlet
+ * Spec 3.1 section 13.6.3 on handling Form authentication
+ * where the http method of the original request causing authentication
+ * is not the same as the http method resulting from the redirect
+ * after authentication.
+ *
+ * @param request the request to manipulate
+ */
+ void prepareRequest(ServletRequest request);
+
+ /**
+ * Validate a request
+ *
+ * @param request The request
+ * @param response The response
+ * @param mandatory True if authentication is mandatory.
+ * @return An Authentication. If Authentication is successful, this will be a {@link org.eclipse.jetty.server.Authentication.User}. If a response has
+ * been sent by the Authenticator (which can be done for both successful and unsuccessful authentications), then the result will
+ * implement {@link org.eclipse.jetty.server.Authentication.ResponseSent}. If Authentication is not mandatory, then a
+ * {@link org.eclipse.jetty.server.Authentication.Deferred} may be returned.
+ * @throws ServerAuthException if unable to validate request
+ */
+ Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException;
+
+ /**
+ * is response secure
+ *
+ * @param request the request
+ * @param response the response
+ * @param mandatory if security is mandator
+ * @param validatedUser the user that was validated
+ * @return true if response is secure
+ * @throws ServerAuthException if unable to test response
+ */
+ boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, User validatedUser) throws ServerAuthException;
+
+ /**
+ * Authenticator Configuration
+ */
+ interface AuthConfiguration
+ {
+ String getAuthMethod();
+
+ String getRealmName();
+
+ /**
+ * Get a SecurityHandler init parameter
+ *
+ * @param param parameter name
+ * @return Parameter value or null
+ * @see SecurityHandler#getInitParameter(String)
+ */
+ String getInitParameter(String param);
+
+ /**
+ * Get a SecurityHandler init parameter names
+ *
+ * @return Set of parameter names
+ * @see SecurityHandler#getInitParameterNames()
+ */
+ Set<String> getInitParameterNames();
+
+ LoginService getLoginService();
+
+ IdentityService getIdentityService();
+
+ boolean isSessionRenewedOnAuthentication();
+ }
+
+ /**
+ * Authenticator Factory
+ */
+ interface Factory
+ {
+ Authenticator getAuthenticator(Server server, ServletContext context, AuthConfiguration configuration, IdentityService identityService, LoginService loginService);
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java
new file mode 100644
index 0000000..2969225
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java
@@ -0,0 +1,330 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.Serializable;
+import java.net.InetAddress;
+import java.nio.file.Path;
+import java.security.PrivilegedAction;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import javax.security.auth.Subject;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+import org.eclipse.jetty.security.authentication.AuthorizationService;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+
+/**
+ * <p>A configurable (as opposed to using system properties) SPNEGO LoginService.</p>
+ * <p>At startup, this LoginService will login via JAAS the service principal, composed
+ * of the {@link #getServiceName() service name} and the {@link #getHostName() host name},
+ * for example {@code HTTP/wonder.com}, using a {@code keyTab} file as the service principal
+ * credentials.</p>
+ * <p>Upon receiving an HTTP request, the server tries to authenticate the client
+ * calling {@link #login(String, Object, ServletRequest)} where the GSS APIs are used to
+ * verify client tokens and (perhaps after a few round-trips) a {@code GSSContext} is
+ * established.</p>
+ */
+public class ConfigurableSpnegoLoginService extends ContainerLifeCycle implements LoginService
+{
+ private static final Logger LOG = Log.getLogger(ConfigurableSpnegoLoginService.class);
+
+ private final GSSManager _gssManager = GSSManager.getInstance();
+ private final String _realm;
+ private final AuthorizationService _authorizationService;
+ private IdentityService _identityService = new DefaultIdentityService();
+ private String _serviceName;
+ private Path _keyTabPath;
+ private String _hostName;
+ private SpnegoContext _context;
+
+ public ConfigurableSpnegoLoginService(String realm, AuthorizationService authorizationService)
+ {
+ _realm = realm;
+ _authorizationService = authorizationService;
+ }
+
+ /**
+ * @return the realm name
+ */
+ @Override
+ public String getName()
+ {
+ return _realm;
+ }
+
+ /**
+ * @return the path of the keyTab file containing service credentials
+ */
+ public Path getKeyTabPath()
+ {
+ return _keyTabPath;
+ }
+
+ /**
+ * @param keyTabFile the path of the keyTab file containing service credentials
+ */
+ public void setKeyTabPath(Path keyTabFile)
+ {
+ _keyTabPath = keyTabFile;
+ }
+
+ /**
+ * @return the service name, typically "HTTP"
+ * @see #getHostName()
+ */
+ public String getServiceName()
+ {
+ return _serviceName;
+ }
+
+ /**
+ * @param serviceName the service name
+ * @see #setHostName(String)
+ */
+ public void setServiceName(String serviceName)
+ {
+ _serviceName = serviceName;
+ }
+
+ /**
+ * @return the host name of the service
+ * @see #setServiceName(String)
+ */
+ public String getHostName()
+ {
+ return _hostName;
+ }
+
+ /**
+ * @param hostName the host name of the service
+ */
+ public void setHostName(String hostName)
+ {
+ _hostName = hostName;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (_hostName == null)
+ _hostName = InetAddress.getLocalHost().getCanonicalHostName();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Retrieving credentials for service {}/{}", getServiceName(), getHostName());
+ LoginContext loginContext = new LoginContext("", null, null, new SpnegoConfiguration());
+ loginContext.login();
+ Subject subject = loginContext.getSubject();
+ _context = Subject.doAs(subject, newSpnegoContext(subject));
+ super.doStart();
+ }
+
+ private PrivilegedAction<SpnegoContext> newSpnegoContext(Subject subject)
+ {
+ return () ->
+ {
+ try
+ {
+ GSSName serviceName = _gssManager.createName(getServiceName() + "@" + getHostName(), GSSName.NT_HOSTBASED_SERVICE);
+ Oid kerberosOid = new Oid("1.2.840.113554.1.2.2");
+ Oid spnegoOid = new Oid("1.3.6.1.5.5.2");
+ Oid[] mechanisms = new Oid[]{kerberosOid, spnegoOid};
+ GSSCredential serviceCredential = _gssManager.createCredential(serviceName, GSSCredential.DEFAULT_LIFETIME, mechanisms, GSSCredential.ACCEPT_ONLY);
+ SpnegoContext context = new SpnegoContext();
+ context._subject = subject;
+ context._serviceCredential = serviceCredential;
+ return context;
+ }
+ catch (GSSException x)
+ {
+ throw new RuntimeException(x);
+ }
+ };
+ }
+
+ @Override
+ public UserIdentity login(String username, Object credentials, ServletRequest req)
+ {
+ Subject subject = _context._subject;
+ HttpServletRequest request = (HttpServletRequest)req;
+ HttpSession httpSession = request.getSession(false);
+ GSSContext gssContext = null;
+ if (httpSession != null)
+ {
+ GSSContextHolder holder = (GSSContextHolder)httpSession.getAttribute(GSSContextHolder.ATTRIBUTE);
+ gssContext = holder == null ? null : holder.gssContext;
+ }
+ if (gssContext == null)
+ gssContext = Subject.doAs(subject, newGSSContext());
+
+ byte[] input = Base64.getDecoder().decode((String)credentials);
+ byte[] output = Subject.doAs(_context._subject, acceptGSSContext(gssContext, input));
+ String token = Base64.getEncoder().encodeToString(output);
+
+ String userName = toUserName(gssContext);
+ // Save the token in the principal so it can be sent in the response.
+ SpnegoUserPrincipal principal = new SpnegoUserPrincipal(userName, token);
+ if (gssContext.isEstablished())
+ {
+ if (httpSession != null)
+ httpSession.removeAttribute(GSSContextHolder.ATTRIBUTE);
+
+ UserIdentity roles = _authorizationService.getUserIdentity(request, userName);
+ return new SpnegoUserIdentity(subject, principal, roles);
+ }
+ else
+ {
+ // The GSS context is not established yet, save it into the HTTP session.
+ if (httpSession == null)
+ httpSession = request.getSession(true);
+ GSSContextHolder holder = new GSSContextHolder(gssContext);
+ httpSession.setAttribute(GSSContextHolder.ATTRIBUTE, holder);
+
+ // Return an unestablished UserIdentity.
+ return new SpnegoUserIdentity(subject, principal, null);
+ }
+ }
+
+ private PrivilegedAction<GSSContext> newGSSContext()
+ {
+ return () ->
+ {
+ try
+ {
+ return _gssManager.createContext(_context._serviceCredential);
+ }
+ catch (GSSException x)
+ {
+ throw new RuntimeException(x);
+ }
+ };
+ }
+
+ private PrivilegedAction<byte[]> acceptGSSContext(GSSContext gssContext, byte[] token)
+ {
+ return () ->
+ {
+ try
+ {
+ return gssContext.acceptSecContext(token, 0, token.length);
+ }
+ catch (GSSException x)
+ {
+ throw new RuntimeException(x);
+ }
+ };
+ }
+
+ private String toUserName(GSSContext gssContext)
+ {
+ try
+ {
+ String name = gssContext.getSrcName().toString();
+ int at = name.indexOf('@');
+ if (at < 0)
+ return name;
+ return name.substring(0, at);
+ }
+ catch (GSSException x)
+ {
+ throw new RuntimeException(x);
+ }
+ }
+
+ @Override
+ public boolean validate(UserIdentity user)
+ {
+ return false;
+ }
+
+ @Override
+ public IdentityService getIdentityService()
+ {
+ return _identityService;
+ }
+
+ @Override
+ public void setIdentityService(IdentityService identityService)
+ {
+ _identityService = identityService;
+ }
+
+ @Override
+ public void logout(UserIdentity user)
+ {
+ }
+
+ private class SpnegoConfiguration extends Configuration
+ {
+ @Override
+ public AppConfigurationEntry[] getAppConfigurationEntry(String name)
+ {
+ String principal = getServiceName() + "/" + getHostName();
+ Map<String, Object> options = new HashMap<>();
+ if (LOG.isDebugEnabled())
+ options.put("debug", "true");
+ options.put("doNotPrompt", "true");
+ options.put("refreshKrb5Config", "true");
+ options.put("principal", principal);
+ options.put("useKeyTab", "true");
+ Path keyTabPath = getKeyTabPath();
+ if (keyTabPath != null)
+ options.put("keyTab", keyTabPath.toAbsolutePath().toString());
+ // This option is required to store the service credentials in
+ // the Subject, so that it can be later used by acceptSecContext().
+ options.put("storeKey", "true");
+ options.put("isInitiator", "false");
+ String moduleClass = "com.sun.security.auth.module.Krb5LoginModule";
+ AppConfigurationEntry config = new AppConfigurationEntry(moduleClass, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
+ return new AppConfigurationEntry[]{config};
+ }
+ }
+
+ private static class SpnegoContext
+ {
+ private Subject _subject;
+ private GSSCredential _serviceCredential;
+ }
+
+ private static class GSSContextHolder implements Serializable
+ {
+ public static final String ATTRIBUTE = GSSContextHolder.class.getName();
+
+ private final transient GSSContext gssContext;
+
+ private GSSContextHolder(GSSContext gssContext)
+ {
+ this.gssContext = gssContext;
+ }
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintAware.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintAware.java
new file mode 100644
index 0000000..018a77f
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintAware.java
@@ -0,0 +1,73 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.util.List;
+import java.util.Set;
+
+public interface ConstraintAware
+{
+ List<ConstraintMapping> getConstraintMappings();
+
+ Set<String> getRoles();
+
+ /**
+ * Set Constraint Mappings and roles.
+ * Can only be called during initialization.
+ *
+ * @param constraintMappings the mappings
+ * @param roles the roles
+ */
+ void setConstraintMappings(List<ConstraintMapping> constraintMappings, Set<String> roles);
+
+ /**
+ * Add a Constraint Mapping.
+ * May be called for running webapplication as an annotated servlet is instantiated.
+ *
+ * @param mapping the mapping
+ */
+ void addConstraintMapping(ConstraintMapping mapping);
+
+ /**
+ * Add a Role definition.
+ * May be called on running webapplication as an annotated servlet is instantiated.
+ *
+ * @param role the role
+ */
+ void addRole(String role);
+
+ /**
+ * See Servlet Spec 31, sec 13.8.4, pg 145
+ * When true, requests with http methods not explicitly covered either by inclusion or omissions
+ * in constraints, will have access denied.
+ *
+ * @param deny true for denied method access
+ */
+ void setDenyUncoveredHttpMethods(boolean deny);
+
+ boolean isDenyUncoveredHttpMethods();
+
+ /**
+ * See Servlet Spec 31, sec 13.8.4, pg 145
+ * Container must check if there are urls with uncovered http methods
+ *
+ * @return true if urls with uncovered http methods
+ */
+ boolean checkPathsWithUncoveredHttpMethods();
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintMapping.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintMapping.java
new file mode 100644
index 0000000..1f4e15d
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintMapping.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import org.eclipse.jetty.util.security.Constraint;
+
+public class ConstraintMapping
+{
+ String _method;
+ String[] _methodOmissions;
+
+ String _pathSpec;
+
+ Constraint _constraint;
+
+ /**
+ * @return Returns the constraint.
+ */
+ public Constraint getConstraint()
+ {
+ return _constraint;
+ }
+
+ /**
+ * @param constraint The constraint to set.
+ */
+ public void setConstraint(Constraint constraint)
+ {
+ this._constraint = constraint;
+ }
+
+ /**
+ * @return Returns the method.
+ */
+ public String getMethod()
+ {
+ return _method;
+ }
+
+ /**
+ * @param method The method to set.
+ */
+ public void setMethod(String method)
+ {
+ this._method = method;
+ }
+
+ /**
+ * @return Returns the pathSpec.
+ */
+ public String getPathSpec()
+ {
+ return _pathSpec;
+ }
+
+ /**
+ * @param pathSpec The pathSpec to set.
+ */
+ public void setPathSpec(String pathSpec)
+ {
+ this._pathSpec = pathSpec;
+ }
+
+ /**
+ * @param omissions The http-method-omission
+ */
+ public void setMethodOmissions(String[] omissions)
+ {
+ _methodOmissions = omissions;
+ }
+
+ public String[] getMethodOmissions()
+ {
+ return _methodOmissions;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java
new file mode 100644
index 0000000..2e82516
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java
@@ -0,0 +1,879 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CopyOnWriteArraySet;
+import javax.servlet.HttpConstraintElement;
+import javax.servlet.HttpMethodConstraintElement;
+import javax.servlet.ServletSecurityElement;
+import javax.servlet.annotation.ServletSecurity.EmptyRoleSemantic;
+import javax.servlet.annotation.ServletSecurity.TransportGuarantee;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.PathMap;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.component.DumpableCollection;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.security.Constraint;
+
+/**
+ * ConstraintSecurityHandler
+ * <p>
+ * Handler to enforce SecurityConstraints. This implementation is servlet spec
+ * 3.1 compliant and pre-computes the constraint combinations for runtime
+ * efficiency.
+ */
+public class ConstraintSecurityHandler extends SecurityHandler implements ConstraintAware
+{
+ private static final Logger LOG = Log.getLogger(SecurityHandler.class); //use same as SecurityHandler
+
+ private static final String OMISSION_SUFFIX = ".omission";
+ private static final String ALL_METHODS = "*";
+ private final List<ConstraintMapping> _constraintMappings = new CopyOnWriteArrayList<>();
+ private final List<ConstraintMapping> _durableConstraintMappings = new CopyOnWriteArrayList<>();
+ private final Set<String> _roles = new CopyOnWriteArraySet<>();
+ private final PathMap<Map<String, RoleInfo>> _constraintMap = new PathMap<>();
+ private boolean _denyUncoveredMethods = false;
+
+ public static Constraint createConstraint()
+ {
+ return new Constraint();
+ }
+
+ public static Constraint createConstraint(Constraint constraint)
+ {
+ try
+ {
+ return (Constraint)constraint.clone();
+ }
+ catch (CloneNotSupportedException e)
+ {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Create a security constraint
+ *
+ * @param name the name of the constraint
+ * @param authenticate true to authenticate
+ * @param roles list of roles
+ * @param dataConstraint the data constraint
+ * @return the constraint
+ */
+ public static Constraint createConstraint(String name, boolean authenticate, String[] roles, int dataConstraint)
+ {
+ Constraint constraint = createConstraint();
+ if (name != null)
+ constraint.setName(name);
+ constraint.setAuthenticate(authenticate);
+ constraint.setRoles(roles);
+ constraint.setDataConstraint(dataConstraint);
+ return constraint;
+ }
+
+ /**
+ * Create a Constraint
+ *
+ * @param name the name
+ * @param element the http constraint element
+ * @return the created constraint
+ */
+ public static Constraint createConstraint(String name, HttpConstraintElement element)
+ {
+ return createConstraint(name, element.getRolesAllowed(), element.getEmptyRoleSemantic(), element.getTransportGuarantee());
+ }
+
+ /**
+ * Create Constraint
+ *
+ * @param name the name
+ * @param rolesAllowed the list of allowed roles
+ * @param permitOrDeny the permission semantic
+ * @param transport the transport guarantee
+ * @return the created constraint
+ */
+ public static Constraint createConstraint(String name, String[] rolesAllowed, EmptyRoleSemantic permitOrDeny, TransportGuarantee transport)
+ {
+ Constraint constraint = createConstraint();
+
+ if (rolesAllowed == null || rolesAllowed.length == 0)
+ {
+ if (permitOrDeny.equals(EmptyRoleSemantic.DENY))
+ {
+ //Equivalent to <auth-constraint> with no roles
+ constraint.setName(name + "-Deny");
+ constraint.setAuthenticate(true);
+ }
+ else
+ {
+ //Equivalent to no <auth-constraint>
+ constraint.setName(name + "-Permit");
+ constraint.setAuthenticate(false);
+ }
+ }
+ else
+ {
+ //Equivalent to <auth-constraint> with list of <security-role-name>s
+ constraint.setAuthenticate(true);
+ constraint.setRoles(rolesAllowed);
+ constraint.setName(name + "-RolesAllowed");
+ }
+
+ //Equivalent to //<user-data-constraint><transport-guarantee>CONFIDENTIAL</transport-guarantee></user-data-constraint>
+ constraint.setDataConstraint((transport.equals(TransportGuarantee.CONFIDENTIAL) ? Constraint.DC_CONFIDENTIAL : Constraint.DC_NONE));
+ return constraint;
+ }
+
+ public static List<ConstraintMapping> getConstraintMappingsForPath(String pathSpec, List<ConstraintMapping> constraintMappings)
+ {
+ if (pathSpec == null || "".equals(pathSpec.trim()) || constraintMappings == null || constraintMappings.size() == 0)
+ return Collections.emptyList();
+
+ List<ConstraintMapping> mappings = new ArrayList<>();
+ for (ConstraintMapping mapping : constraintMappings)
+ {
+ if (pathSpec.equals(mapping.getPathSpec()))
+ {
+ mappings.add(mapping);
+ }
+ }
+ return mappings;
+ }
+
+ /**
+ * Take out of the constraint mappings those that match the
+ * given path.
+ *
+ * @param pathSpec the path spec
+ * @param constraintMappings a new list minus the matching constraints
+ * @return the list of constraint mappings
+ */
+ public static List<ConstraintMapping> removeConstraintMappingsForPath(String pathSpec, List<ConstraintMapping> constraintMappings)
+ {
+ if (pathSpec == null || "".equals(pathSpec.trim()) || constraintMappings == null || constraintMappings.size() == 0)
+ return Collections.emptyList();
+
+ List<ConstraintMapping> mappings = new ArrayList<>();
+ for (ConstraintMapping mapping : constraintMappings)
+ {
+ //Remove the matching mappings by only copying in non-matching mappings
+ if (!pathSpec.equals(mapping.getPathSpec()))
+ {
+ mappings.add(mapping);
+ }
+ }
+ return mappings;
+ }
+
+ /**
+ * Generate Constraints and ContraintMappings for the given url pattern and ServletSecurityElement
+ *
+ * @param name the name
+ * @param pathSpec the path spec
+ * @param securityElement the servlet security element
+ * @return the list of constraint mappings
+ */
+ public static List<ConstraintMapping> createConstraintsWithMappingsForPath(String name, String pathSpec, ServletSecurityElement securityElement)
+ {
+ List<ConstraintMapping> mappings = new ArrayList<>();
+
+ //Create a constraint that will describe the default case (ie if not overridden by specific HttpMethodConstraints)
+ Constraint httpConstraint;
+ ConstraintMapping httpConstraintMapping = null;
+
+ if (securityElement.getEmptyRoleSemantic() != EmptyRoleSemantic.PERMIT ||
+ securityElement.getRolesAllowed().length != 0 ||
+ securityElement.getTransportGuarantee() != TransportGuarantee.NONE)
+ {
+ httpConstraint = ConstraintSecurityHandler.createConstraint(name, securityElement);
+
+ //Create a mapping for the pathSpec for the default case
+ httpConstraintMapping = new ConstraintMapping();
+ httpConstraintMapping.setPathSpec(pathSpec);
+ httpConstraintMapping.setConstraint(httpConstraint);
+ mappings.add(httpConstraintMapping);
+ }
+
+ //See Spec 13.4.1.2 p127
+ List<String> methodOmissions = new ArrayList<>();
+
+ //make constraint mappings for this url for each of the HttpMethodConstraintElements
+ Collection<HttpMethodConstraintElement> methodConstraintElements = securityElement.getHttpMethodConstraints();
+ if (methodConstraintElements != null)
+ {
+ for (HttpMethodConstraintElement methodConstraintElement : methodConstraintElements)
+ {
+ //Make a Constraint that captures the <auth-constraint> and <user-data-constraint> elements supplied for the HttpMethodConstraintElement
+ Constraint methodConstraint = ConstraintSecurityHandler.createConstraint(name, methodConstraintElement);
+ ConstraintMapping mapping = new ConstraintMapping();
+ mapping.setConstraint(methodConstraint);
+ mapping.setPathSpec(pathSpec);
+ if (methodConstraintElement.getMethodName() != null)
+ {
+ mapping.setMethod(methodConstraintElement.getMethodName());
+ //See spec 13.4.1.2 p127 - add an omission for every method name to the default constraint
+ methodOmissions.add(methodConstraintElement.getMethodName());
+ }
+ mappings.add(mapping);
+ }
+ }
+ //See spec 13.4.1.2 p127 - add an omission for every method name to the default constraint
+ //UNLESS the default constraint contains all default values. In that case, we won't add it. See Servlet Spec 3.1 pg 129
+ if (methodOmissions.size() > 0 && httpConstraintMapping != null)
+ httpConstraintMapping.setMethodOmissions(methodOmissions.toArray(new String[0]));
+
+ return mappings;
+ }
+
+ @Override
+ public List<ConstraintMapping> getConstraintMappings()
+ {
+ return _constraintMappings;
+ }
+
+ @Override
+ public Set<String> getRoles()
+ {
+ return _roles;
+ }
+
+ /**
+ * Process the constraints following the combining rules in Servlet 3.0 EA
+ * spec section 13.7.1 Note that much of the logic is in the RoleInfo class.
+ *
+ * @param constraintMappings The constraintMappings to set, from which the set of known roles
+ * is determined.
+ */
+ public void setConstraintMappings(List<ConstraintMapping> constraintMappings)
+ {
+ setConstraintMappings(constraintMappings, null);
+ }
+
+ /**
+ * Process the constraints following the combining rules in Servlet 3.0 EA
+ * spec section 13.7.1 Note that much of the logic is in the RoleInfo class.
+ *
+ * @param constraintMappings The constraintMappings to set as array, from which the set of known roles
+ * is determined. Needed to retain API compatibility for 7.x
+ */
+ public void setConstraintMappings(ConstraintMapping[] constraintMappings)
+ {
+ setConstraintMappings(Arrays.asList(constraintMappings), null);
+ }
+
+ /**
+ * Process the constraints following the combining rules in Servlet 3.0 EA
+ * spec section 13.7.1 Note that much of the logic is in the RoleInfo class.
+ *
+ * @param constraintMappings The constraintMappings to set.
+ * @param roles The known roles (or null to determine them from the mappings)
+ */
+ @Override
+ public void setConstraintMappings(List<ConstraintMapping> constraintMappings, Set<String> roles)
+ {
+
+ _constraintMappings.clear();
+ _constraintMappings.addAll(constraintMappings);
+
+ _durableConstraintMappings.clear();
+ if (isInDurableState())
+ {
+ _durableConstraintMappings.addAll(constraintMappings);
+ }
+
+ if (roles == null)
+ {
+ roles = new HashSet<>();
+ for (ConstraintMapping cm : constraintMappings)
+ {
+ String[] cmr = cm.getConstraint().getRoles();
+ if (cmr != null)
+ {
+ for (String r : cmr)
+ {
+ if (!ALL_METHODS.equals(r))
+ roles.add(r);
+ }
+ }
+ }
+ }
+ setRoles(roles);
+
+ if (isStarted())
+ {
+ _constraintMappings.stream().forEach(m -> processConstraintMapping(m));
+ }
+ }
+
+ /**
+ * Set the known roles.
+ * This may be overridden by a subsequent call to {@link #setConstraintMappings(ConstraintMapping[])} or
+ * {@link #setConstraintMappings(List, Set)}.
+ *
+ * @param roles The known roles (or null to determine them from the mappings)
+ */
+ public void setRoles(Set<String> roles)
+ {
+ _roles.clear();
+ _roles.addAll(roles);
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.ConstraintAware#addConstraintMapping(org.eclipse.jetty.security.ConstraintMapping)
+ */
+ @Override
+ public void addConstraintMapping(ConstraintMapping mapping)
+ {
+ _constraintMappings.add(mapping);
+
+ if (isInDurableState())
+ _durableConstraintMappings.add(mapping);
+
+ if (mapping.getConstraint() != null && mapping.getConstraint().getRoles() != null)
+ {
+ //allow for lazy role naming: if a role is named in a security constraint, try and
+ //add it to the list of declared roles (ie as if it was declared with a security-role
+ for (String role : mapping.getConstraint().getRoles())
+ {
+ if ("*".equals(role) || "**".equals(role))
+ continue;
+ addRole(role);
+ }
+ }
+
+ if (isStarted())
+ processConstraintMapping(mapping);
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.ConstraintAware#addRole(java.lang.String)
+ */
+ @Override
+ public void addRole(String role)
+ {
+ //add to list of declared roles
+ boolean modified = _roles.add(role);
+ if (isStarted() && modified)
+ {
+ // Add the new role to currently defined any role role infos
+ for (Map<String, RoleInfo> map : _constraintMap.values())
+ {
+ for (RoleInfo info : map.values())
+ {
+ if (info.isAnyRole())
+ info.addRole(role);
+ }
+ }
+ }
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.SecurityHandler#doStart()
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ _constraintMappings.stream().forEach(m -> processConstraintMapping(m));
+
+ //Servlet Spec 3.1 pg 147 sec 13.8.4.2 log paths for which there are uncovered http methods
+ checkPathsWithUncoveredHttpMethods();
+
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ super.doStop();
+ _constraintMap.clear();
+ _constraintMappings.clear();
+ _constraintMappings.addAll(_durableConstraintMappings);
+ }
+
+ /**
+ * Create and combine the constraint with the existing processed
+ * constraints.
+ *
+ * @param mapping the constraint mapping
+ */
+ protected void processConstraintMapping(ConstraintMapping mapping)
+ {
+ Map<String, RoleInfo> mappings = _constraintMap.get(mapping.getPathSpec());
+
+ if (mappings == null)
+ {
+ mappings = new HashMap<>();
+ _constraintMap.put(mapping.getPathSpec(), mappings);
+ }
+ RoleInfo allMethodsRoleInfo = mappings.get(ALL_METHODS);
+ if (allMethodsRoleInfo != null && allMethodsRoleInfo.isForbidden())
+ return;
+
+ if (mapping.getMethodOmissions() != null && mapping.getMethodOmissions().length > 0)
+ {
+ processConstraintMappingWithMethodOmissions(mapping, mappings);
+ return;
+ }
+
+ String httpMethod = mapping.getMethod();
+ if (httpMethod == null)
+ httpMethod = ALL_METHODS;
+ RoleInfo roleInfo = mappings.get(httpMethod);
+ if (roleInfo == null)
+ {
+ roleInfo = new RoleInfo();
+ mappings.put(httpMethod, roleInfo);
+ if (allMethodsRoleInfo != null)
+ {
+ roleInfo.combine(allMethodsRoleInfo);
+ }
+ }
+ if (roleInfo.isForbidden())
+ return;
+
+ //add in info from the constraint
+ configureRoleInfo(roleInfo, mapping);
+
+ if (roleInfo.isForbidden())
+ {
+ if (httpMethod.equals(ALL_METHODS))
+ {
+ mappings.clear();
+ mappings.put(ALL_METHODS, roleInfo);
+ }
+ }
+ }
+
+ /**
+ * Constraints that name method omissions are dealt with differently.
+ * We create an entry in the mappings with key "<method>.omission". This entry
+ * is only ever combined with other omissions for the same method to produce a
+ * consolidated RoleInfo. Then, when we wish to find the relevant constraints for
+ * a given Request (in prepareConstraintInfo()), we consult 3 types of entries in
+ * the mappings: an entry that names the method of the Request specifically, an
+ * entry that names constraints that apply to all methods, entries of the form
+ * <method>.omission, where the method of the Request is not named in the omission.
+ *
+ * @param mapping the constraint mapping
+ * @param mappings the mappings of roles
+ */
+ protected void processConstraintMappingWithMethodOmissions(ConstraintMapping mapping, Map<String, RoleInfo> mappings)
+ {
+ String[] omissions = mapping.getMethodOmissions();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < omissions.length; i++)
+ {
+ if (i > 0)
+ sb.append(".");
+ sb.append(omissions[i]);
+ }
+ sb.append(OMISSION_SUFFIX);
+ RoleInfo ri = new RoleInfo();
+ mappings.put(sb.toString(), ri);
+ configureRoleInfo(ri, mapping);
+ }
+
+ /**
+ * Initialize or update the RoleInfo from the constraint
+ *
+ * @param ri the role info
+ * @param mapping the constraint mapping
+ */
+ protected void configureRoleInfo(RoleInfo ri, ConstraintMapping mapping)
+ {
+ Constraint constraint = mapping.getConstraint();
+ boolean forbidden = constraint.isForbidden();
+ ri.setForbidden(forbidden);
+
+ //set up the data constraint (NOTE: must be done after setForbidden, as it nulls out the data constraint
+ //which we need in order to do combining of omissions in prepareConstraintInfo
+ UserDataConstraint userDataConstraint = UserDataConstraint.get(mapping.getConstraint().getDataConstraint());
+ ri.setUserDataConstraint(userDataConstraint);
+
+ //if forbidden, no point setting up roles
+ if (!ri.isForbidden())
+ {
+ //add in the roles
+ boolean checked = mapping.getConstraint().getAuthenticate();
+ ri.setChecked(checked);
+
+ if (ri.isChecked())
+ {
+ if (mapping.getConstraint().isAnyRole())
+ {
+ // * means matches any defined role
+ for (String role : _roles)
+ {
+ ri.addRole(role);
+ }
+ ri.setAnyRole(true);
+ }
+ else if (mapping.getConstraint().isAnyAuth())
+ {
+ //being authenticated is sufficient, not necessary to check roles
+ ri.setAnyAuth(true);
+ }
+ else
+ {
+ //user must be in one of the named roles
+ String[] newRoles = mapping.getConstraint().getRoles();
+ for (String role : newRoles)
+ {
+ //check role has been defined
+ if (!_roles.contains(role))
+ throw new IllegalArgumentException("Attempt to use undeclared role: " + role + ", known roles: " + _roles);
+ ri.addRole(role);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Find constraints that apply to the given path.
+ * In order to do this, we consult 3 different types of information stored in the mappings for each path - each mapping
+ * represents a merged set of user data constraints, roles etc -:
+ * <ol>
+ * <li>A mapping of an exact method name </li>
+ * <li>A mapping with key * that matches every method name</li>
+ * <li>Mappings with keys of the form "<method>.<method>.<method>.omission" that indicates it will match every method name EXCEPT those given</li>
+ * </ol>
+ *
+ * @see org.eclipse.jetty.security.SecurityHandler#prepareConstraintInfo(java.lang.String, org.eclipse.jetty.server.Request)
+ */
+ @Override
+ protected RoleInfo prepareConstraintInfo(String pathInContext, Request request)
+ {
+ Map<String, RoleInfo> mappings = _constraintMap.match(pathInContext);
+
+ if (mappings != null)
+ {
+ String httpMethod = request.getMethod();
+ RoleInfo roleInfo = mappings.get(httpMethod);
+ if (roleInfo == null)
+ {
+ //No specific http-method names matched
+ List<RoleInfo> applicableConstraints = new ArrayList<>();
+
+ //Get info for constraint that matches all methods if it exists
+ RoleInfo all = mappings.get(ALL_METHODS);
+ if (all != null)
+ applicableConstraints.add(all);
+
+ //Get info for constraints that name method omissions where target method name is not omitted
+ //(ie matches because target method is not omitted, hence considered covered by the constraint)
+ for (Entry<String, RoleInfo> entry : mappings.entrySet())
+ {
+ if (entry.getKey() != null && entry.getKey().endsWith(OMISSION_SUFFIX) && !entry.getKey().contains(httpMethod))
+ applicableConstraints.add(entry.getValue());
+ }
+
+ if (applicableConstraints.size() == 0 && isDenyUncoveredHttpMethods())
+ {
+ roleInfo = new RoleInfo();
+ roleInfo.setForbidden(true);
+ }
+ else if (applicableConstraints.size() == 1)
+ roleInfo = applicableConstraints.get(0);
+ else
+ {
+ roleInfo = new RoleInfo();
+ roleInfo.setUserDataConstraint(UserDataConstraint.None);
+
+ for (RoleInfo r : applicableConstraints)
+ {
+ roleInfo.combine(r);
+ }
+ }
+ }
+
+ return roleInfo;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected boolean checkUserDataPermissions(String pathInContext, Request request, Response response, RoleInfo roleInfo) throws IOException
+ {
+ if (roleInfo == null)
+ return true;
+
+ if (roleInfo.isForbidden())
+ return false;
+
+ UserDataConstraint dataConstraint = roleInfo.getUserDataConstraint();
+ if (dataConstraint == null || dataConstraint == UserDataConstraint.None)
+ return true;
+
+ Request baseRequest = Request.getBaseRequest(request);
+ HttpConfiguration httpConfig = baseRequest.getHttpChannel().getHttpConfiguration();
+
+ if (dataConstraint == UserDataConstraint.Confidential || dataConstraint == UserDataConstraint.Integral)
+ {
+ if (request.isSecure())
+ return true;
+
+ if (httpConfig.getSecurePort() > 0)
+ {
+ String scheme = httpConfig.getSecureScheme();
+ int port = httpConfig.getSecurePort();
+
+ String url = URIUtil.newURI(scheme, request.getServerName(), port, request.getRequestURI(), request.getQueryString());
+ response.setContentLength(0);
+ response.sendRedirect(url, true);
+ }
+ else
+ response.sendError(HttpStatus.FORBIDDEN_403, "!Secure");
+
+ request.setHandled(true);
+ return false;
+ }
+ else
+ {
+ throw new IllegalArgumentException("Invalid dataConstraint value: " + dataConstraint);
+ }
+ }
+
+ @Override
+ protected boolean isAuthMandatory(Request baseRequest, Response baseResponse, Object constraintInfo)
+ {
+ return constraintInfo != null && ((RoleInfo)constraintInfo).isChecked();
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.SecurityHandler#checkWebResourcePermissions(java.lang.String, org.eclipse.jetty.server.Request, org.eclipse.jetty.server.Response, java.lang.Object, org.eclipse.jetty.server.UserIdentity)
+ */
+ @Override
+ protected boolean checkWebResourcePermissions(String pathInContext, Request request, Response response, Object constraintInfo, UserIdentity userIdentity)
+ throws IOException
+ {
+ if (constraintInfo == null)
+ {
+ return true;
+ }
+ RoleInfo roleInfo = (RoleInfo)constraintInfo;
+
+ if (!roleInfo.isChecked())
+ {
+ return true;
+ }
+
+ //handle ** role constraint
+ if (roleInfo.isAnyAuth() && request.getUserPrincipal() != null)
+ {
+ return true;
+ }
+
+ //check if user is any of the allowed roles
+ boolean isUserInRole = false;
+ for (String role : roleInfo.getRoles())
+ {
+ if (userIdentity.isUserInRole(role, null))
+ {
+ isUserInRole = true;
+ break;
+ }
+ }
+
+ //handle * role constraint
+ if (roleInfo.isAnyRole() && request.getUserPrincipal() != null && isUserInRole)
+ {
+ return true;
+ }
+
+ //normal role check
+ return isUserInRole;
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ dumpObjects(out, indent,
+ DumpableCollection.from("roles", _roles),
+ DumpableCollection.from("constraints", _constraintMap.entrySet()));
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.ConstraintAware#setDenyUncoveredHttpMethods(boolean)
+ */
+ @Override
+ public void setDenyUncoveredHttpMethods(boolean deny)
+ {
+ _denyUncoveredMethods = deny;
+ }
+
+ @Override
+ public boolean isDenyUncoveredHttpMethods()
+ {
+ return _denyUncoveredMethods;
+ }
+
+ /**
+ * Servlet spec 3.1 pg. 147.
+ */
+ @Override
+ public boolean checkPathsWithUncoveredHttpMethods()
+ {
+ Set<String> paths = getPathsWithUncoveredHttpMethods();
+ if (paths != null && !paths.isEmpty())
+ {
+ for (String p : paths)
+ {
+ LOG.warn("{} has uncovered http methods for path: {}", ContextHandler.getCurrentContext(), p);
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug(new Throwable());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Servlet spec 3.1 pg. 147.
+ * The container must check all the combined security constraint
+ * information and log any methods that are not protected and the
+ * urls at which they are not protected
+ *
+ * @return list of paths for which there are uncovered methods
+ */
+ public Set<String> getPathsWithUncoveredHttpMethods()
+ {
+ //if automatically denying uncovered methods, there are no uncovered methods
+ if (_denyUncoveredMethods)
+ return Collections.emptySet();
+
+ Set<String> uncoveredPaths = new HashSet<>();
+ for (Entry<String, Map<String, RoleInfo>> entry : _constraintMap.entrySet())
+ {
+ Map<String, RoleInfo> methodMappings = entry.getValue();
+
+ //Each key is either:
+ // : an exact method name
+ // : * which means that the constraint applies to every method
+ // : a name of the form <method>.<method>.<method>.omission, which means it applies to every method EXCEPT those named
+ if (methodMappings.get(ALL_METHODS) != null)
+ continue; //can't be any uncovered methods for this url path
+
+ boolean hasOmissions = omissionsExist(entry.getKey(), methodMappings);
+
+ for (String method : methodMappings.keySet())
+ {
+ if (method.endsWith(OMISSION_SUFFIX))
+ {
+ Set<String> omittedMethods = getOmittedMethods(method);
+ for (String m : omittedMethods)
+ {
+ if (!methodMappings.containsKey(m))
+ uncoveredPaths.add(entry.getKey());
+ }
+ }
+ else
+ {
+ //an exact method name
+ if (!hasOmissions)
+ //an http-method does not have http-method-omission to cover the other method names
+ uncoveredPaths.add(entry.getKey());
+ }
+ }
+ }
+ return uncoveredPaths;
+ }
+
+ /**
+ * Check if any http method omissions exist in the list of method
+ * to auth info mappings.
+ *
+ * @param path the path
+ * @param methodMappings the method mappings
+ * @return true if omission exist
+ */
+ protected boolean omissionsExist(String path, Map<String, RoleInfo> methodMappings)
+ {
+ if (methodMappings == null)
+ return false;
+ boolean hasOmissions = false;
+ for (String m : methodMappings.keySet())
+ {
+ if (m.endsWith(OMISSION_SUFFIX))
+ hasOmissions = true;
+ }
+ return hasOmissions;
+ }
+
+ /**
+ * Given a string of the form <code><method>.<method>.omission</code>
+ * split out the individual method names.
+ *
+ * @param omission the method
+ * @return the list of strings
+ */
+ protected Set<String> getOmittedMethods(String omission)
+ {
+ if (omission == null || !omission.endsWith(OMISSION_SUFFIX))
+ return Collections.emptySet();
+
+ String[] strings = omission.split("\\.");
+ Set<String> methods = new HashSet<>();
+ for (int i = 0; i < strings.length - 1; i++)
+ {
+ methods.add(strings[i]);
+ }
+ return methods;
+ }
+
+ /**
+ * Constraints can be added to the ConstraintSecurityHandler before the
+ * associated context is started. These constraints should persist across
+ * a stop/start. Others can be added after the associated context is starting
+ * (eg by a web.xml/web-fragment.xml, annotation or javax.servlet api call) -
+ * these should not be persisted across a stop/start as they will be re-added on
+ * the restart.
+ *
+ * @return true if the context with which this ConstraintSecurityHandler
+ * has not yet started, or if there is no context, the server has not yet started.
+ */
+ private boolean isInDurableState()
+ {
+ ContextHandler context = ContextHandler.getContextHandler(null);
+ Server server = getServer();
+
+ return (context == null && server == null) || (context != null && !context.isRunning()) || (context == null && server != null && !server.isRunning());
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultAuthenticatorFactory.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultAuthenticatorFactory.java
new file mode 100644
index 0000000..c53e1b9
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultAuthenticatorFactory.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import javax.servlet.ServletContext;
+
+import org.eclipse.jetty.security.Authenticator.AuthConfiguration;
+import org.eclipse.jetty.security.authentication.BasicAuthenticator;
+import org.eclipse.jetty.security.authentication.ClientCertAuthenticator;
+import org.eclipse.jetty.security.authentication.DigestAuthenticator;
+import org.eclipse.jetty.security.authentication.FormAuthenticator;
+import org.eclipse.jetty.security.authentication.SpnegoAuthenticator;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.security.Constraint;
+
+/**
+ * The Default Authenticator Factory.
+ * Uses the {@link AuthConfiguration#getAuthMethod()} to select an {@link Authenticator} from: <ul>
+ * <li>{@link org.eclipse.jetty.security.authentication.BasicAuthenticator}</li>
+ * <li>{@link org.eclipse.jetty.security.authentication.DigestAuthenticator}</li>
+ * <li>{@link org.eclipse.jetty.security.authentication.FormAuthenticator}</li>
+ * <li>{@link org.eclipse.jetty.security.authentication.ClientCertAuthenticator}</li>
+ * </ul>
+ * All authenticators derived from {@link org.eclipse.jetty.security.authentication.LoginAuthenticator} are
+ * wrapped with a {@link org.eclipse.jetty.security.authentication.DeferredAuthentication}
+ * instance, which is used if authentication is not mandatory.
+ *
+ * The Authentications from the {@link org.eclipse.jetty.security.authentication.FormAuthenticator} are always wrapped in a
+ * {@link org.eclipse.jetty.security.authentication.SessionAuthentication}
+ * <p>
+ * If a {@link LoginService} has not been set on this factory, then
+ * the service is selected by searching the {@link Server#getBeans(Class)} results for
+ * a service that matches the realm name, else the first LoginService found is used.
+ */
+public class DefaultAuthenticatorFactory implements Authenticator.Factory
+{
+ LoginService _loginService;
+
+ @Override
+ public Authenticator getAuthenticator(Server server, ServletContext context, AuthConfiguration configuration, IdentityService identityService, LoginService loginService)
+ {
+ String auth = configuration.getAuthMethod();
+ Authenticator authenticator = null;
+
+ if (Constraint.__BASIC_AUTH.equalsIgnoreCase(auth))
+ authenticator = new BasicAuthenticator();
+ else if (Constraint.__DIGEST_AUTH.equalsIgnoreCase(auth))
+ authenticator = new DigestAuthenticator();
+ else if (Constraint.__FORM_AUTH.equalsIgnoreCase(auth))
+ authenticator = new FormAuthenticator();
+ else if (Constraint.__SPNEGO_AUTH.equalsIgnoreCase(auth))
+ authenticator = new SpnegoAuthenticator();
+ else if (Constraint.__NEGOTIATE_AUTH.equalsIgnoreCase(auth)) // see Bug #377076
+ authenticator = new SpnegoAuthenticator(Constraint.__NEGOTIATE_AUTH);
+ if (Constraint.__CERT_AUTH.equalsIgnoreCase(auth) || Constraint.__CERT_AUTH2.equalsIgnoreCase(auth))
+ authenticator = new ClientCertAuthenticator();
+
+ return authenticator;
+ }
+
+ /**
+ * @return the loginService
+ */
+ public LoginService getLoginService()
+ {
+ return _loginService;
+ }
+
+ /**
+ * @param loginService the loginService to set
+ */
+ public void setLoginService(LoginService loginService)
+ {
+ _loginService = loginService;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultIdentityService.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultIdentityService.java
new file mode 100644
index 0000000..a6324e2
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultIdentityService.java
@@ -0,0 +1,85 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.security.Principal;
+import javax.security.auth.Subject;
+
+import org.eclipse.jetty.server.UserIdentity;
+
+/**
+ * Default Identity Service implementation.
+ * This service handles only role reference maps passed in an
+ * associated {@link org.eclipse.jetty.server.UserIdentity.Scope}. If there are roles
+ * refs present, then associate will wrap the UserIdentity with one
+ * that uses the role references in the
+ * {@link org.eclipse.jetty.server.UserIdentity#isUserInRole(String, org.eclipse.jetty.server.UserIdentity.Scope)}
+ * implementation. All other operations are effectively noops.
+ */
+public class DefaultIdentityService implements IdentityService
+{
+
+ public DefaultIdentityService()
+ {
+ }
+
+ /**
+ * If there are roles refs present in the scope, then wrap the UserIdentity
+ * with one that uses the role references in the {@link UserIdentity#isUserInRole(String, org.eclipse.jetty.server.UserIdentity.Scope)}
+ */
+ @Override
+ public Object associate(UserIdentity user)
+ {
+ return null;
+ }
+
+ @Override
+ public void disassociate(Object previous)
+ {
+ }
+
+ @Override
+ public Object setRunAs(UserIdentity user, RunAsToken token)
+ {
+ return token;
+ }
+
+ @Override
+ public void unsetRunAs(Object lastToken)
+ {
+ }
+
+ @Override
+ public RunAsToken newRunAsToken(String runAsName)
+ {
+ return new RoleRunAsToken(runAsName);
+ }
+
+ @Override
+ public UserIdentity getSystemUserIdentity()
+ {
+ return null;
+ }
+
+ @Override
+ public UserIdentity newUserIdentity(final Subject subject, final Principal userPrincipal, final String[] roles)
+ {
+ return new DefaultUserIdentity(subject, userPrincipal, roles);
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultUserIdentity.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultUserIdentity.java
new file mode 100644
index 0000000..627ae5f
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultUserIdentity.java
@@ -0,0 +1,82 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.security.Principal;
+import javax.security.auth.Subject;
+
+import org.eclipse.jetty.server.UserIdentity;
+
+/**
+ * The default implementation of UserIdentity.
+ */
+public class DefaultUserIdentity implements UserIdentity
+{
+ private final Subject _subject;
+ private final Principal _userPrincipal;
+ private final String[] _roles;
+
+ public DefaultUserIdentity(Subject subject, Principal userPrincipal, String[] roles)
+ {
+ _subject = subject;
+ _userPrincipal = userPrincipal;
+ _roles = roles;
+ }
+
+ @Override
+ public Subject getSubject()
+ {
+ return _subject;
+ }
+
+ @Override
+ public Principal getUserPrincipal()
+ {
+ return _userPrincipal;
+ }
+
+ @Override
+ public boolean isUserInRole(String role, Scope scope)
+ {
+ //Servlet Spec 3.1, pg 125
+ if ("*".equals(role))
+ return false;
+
+ String roleToTest = null;
+ if (scope != null && scope.getRoleRefMap() != null)
+ roleToTest = scope.getRoleRefMap().get(role);
+
+ //Servlet Spec 3.1, pg 125
+ if (roleToTest == null)
+ roleToTest = role;
+
+ for (String r : _roles)
+ {
+ if (r.equals(roleToTest))
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString()
+ {
+ return DefaultUserIdentity.class.getSimpleName() + "('" + _userPrincipal + "')";
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/HashLoginService.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/HashLoginService.java
new file mode 100644
index 0000000..3b2f3ee
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/HashLoginService.java
@@ -0,0 +1,214 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+
+/**
+ * Properties User Realm.
+ * <p>
+ * An implementation of UserRealm that stores users and roles in-memory in HashMaps.
+ * <p>
+ * Typically these maps are populated by calling the load() method or passing a properties resource to the constructor. The format of the properties file is:
+ *
+ * <pre>
+ * username: password [,rolename ...]
+ * </pre>
+ *
+ * Passwords may be clear text, obfuscated or checksummed. The class com.eclipse.Util.Password should be used to generate obfuscated passwords or password
+ * checksums.
+ * <p>
+ * If DIGEST Authentication is used, the password must be in a recoverable format, either plain text or OBF:.
+ */
+public class HashLoginService extends AbstractLoginService
+{
+ private static final Logger LOG = Log.getLogger(HashLoginService.class);
+
+ private String _config;
+ private boolean hotReload = false; // default is not to reload
+ private UserStore _userStore;
+ private boolean _userStoreAutoCreate = false;
+
+ public HashLoginService()
+ {
+ }
+
+ public HashLoginService(String name)
+ {
+ setName(name);
+ }
+
+ public HashLoginService(String name, String config)
+ {
+ setName(name);
+ setConfig(config);
+ }
+
+ public String getConfig()
+ {
+ return _config;
+ }
+
+ @Deprecated
+ public Resource getConfigResource()
+ {
+ return null;
+ }
+
+ /**
+ * Load realm users from properties file.
+ * <p>
+ * The property file maps usernames to password specs followed by an optional comma separated list of role names.
+ * </p>
+ *
+ * @param config uri or url or path to realm properties file
+ */
+ public void setConfig(String config)
+ {
+ _config = config;
+ }
+
+ /**
+ * Is hot reload enabled on this user store
+ *
+ * @return true if hot reload was enabled before startup
+ */
+ public boolean isHotReload()
+ {
+ return hotReload;
+ }
+
+ /**
+ * Enable Hot Reload of the Property File
+ *
+ * @param enable true to enable, false to disable
+ */
+ public void setHotReload(boolean enable)
+ {
+ if (isRunning())
+ {
+ throw new IllegalStateException("Cannot set hot reload while user store is running");
+ }
+ this.hotReload = enable;
+ }
+
+ /**
+ * Configure the {@link UserStore} implementation to use.
+ * If none, for backward compat if none the {@link PropertyUserStore} will be used
+ *
+ * @param userStore the {@link UserStore} implementation to use
+ */
+ public void setUserStore(UserStore userStore)
+ {
+ updateBean(_userStore, userStore);
+ _userStore = userStore;
+ }
+
+ @Override
+ protected String[] loadRoleInfo(UserPrincipal user)
+ {
+ UserIdentity id = _userStore.getUserIdentity(user.getName());
+ if (id == null)
+ return null;
+
+ Set<RolePrincipal> roles = id.getSubject().getPrincipals(RolePrincipal.class);
+ if (roles == null)
+ return null;
+
+ List<String> list = roles.stream()
+ .map(rolePrincipal -> rolePrincipal.getName())
+ .collect(Collectors.toList());
+
+ return list.toArray(new String[roles.size()]);
+ }
+
+ @Override
+ protected UserPrincipal loadUserInfo(String userName)
+ {
+ UserIdentity id = _userStore.getUserIdentity(userName);
+ if (id != null)
+ {
+ return (UserPrincipal)id.getUserPrincipal();
+ }
+
+ return null;
+ }
+
+ /**
+ * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+
+ // can be null so we switch to previous behaviour using PropertyUserStore
+ if (_userStore == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("doStart: Starting new PropertyUserStore. PropertiesFile: " + _config + " hotReload: " + hotReload);
+ PropertyUserStore propertyUserStore = new PropertyUserStore();
+ propertyUserStore.setHotReload(hotReload);
+ propertyUserStore.setConfigPath(_config);
+ setUserStore(propertyUserStore);
+ _userStoreAutoCreate = true;
+ }
+ }
+
+ /**
+ * To facilitate testing.
+ *
+ * @return the UserStore
+ */
+ UserStore getUserStore()
+ {
+ return _userStore;
+ }
+
+ /**
+ * To facilitate testing.
+ *
+ * @return true if a UserStore has been created from a config, false if a UserStore was provided.
+ */
+ boolean isUserStoreAutoCreate()
+ {
+ return _userStoreAutoCreate;
+ }
+
+ /**
+ * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
+ */
+ @Override
+ protected void doStop() throws Exception
+ {
+ super.doStop();
+ if (_userStoreAutoCreate)
+ {
+ setUserStore(null);
+ _userStoreAutoCreate = false;
+ }
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/IdentityService.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/IdentityService.java
new file mode 100644
index 0000000..877aef5
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/IdentityService.java
@@ -0,0 +1,89 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.security.Principal;
+import javax.security.auth.Subject;
+
+import org.eclipse.jetty.server.UserIdentity;
+
+/**
+ * Associates UserIdentities from with threads and UserIdentity.Contexts.
+ */
+public interface IdentityService
+{
+ String[] NO_ROLES = new String[]{};
+
+ /**
+ * Associate a user identity with the current thread.
+ * This is called with as a thread enters the
+ * {@link SecurityHandler#handle(String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)}
+ * method and then again with a null argument as that call exits.
+ *
+ * @param user The current user or null for no user to associated.
+ * @return an object representing the previous associated state
+ */
+ Object associate(UserIdentity user);
+
+ /**
+ * Disassociate the user identity from the current thread
+ * and restore previous identity.
+ *
+ * @param previous The opaque object returned from a call to {@link IdentityService#associate(UserIdentity)}
+ */
+ void disassociate(Object previous);
+
+ /**
+ * Associate a runas Token with the current user and thread.
+ *
+ * @param user The UserIdentity
+ * @param token The runAsToken to associate.
+ * @return The previous runAsToken or null.
+ */
+ Object setRunAs(UserIdentity user, RunAsToken token);
+
+ /**
+ * Disassociate the current runAsToken from the thread
+ * and reassociate the previous token.
+ *
+ * @param token RUNAS returned from previous associateRunAs call
+ */
+ void unsetRunAs(Object token);
+
+ /**
+ * Create a new UserIdentity for use with this identity service.
+ * The UserIdentity should be immutable and able to be cached.
+ *
+ * @param subject Subject to include in UserIdentity
+ * @param userPrincipal Principal to include in UserIdentity. This will be returned from getUserPrincipal calls
+ * @param roles set of roles to include in UserIdentity.
+ * @return A new immutable UserIdententity
+ */
+ UserIdentity newUserIdentity(Subject subject, Principal userPrincipal, String[] roles);
+
+ /**
+ * Create a new RunAsToken from a runAsName (normally a role).
+ *
+ * @param runAsName Normally a role name
+ * @return A new immutable RunAsToken
+ */
+ RunAsToken newRunAsToken(String runAsName);
+
+ UserIdentity getSystemUserIdentity();
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/JDBCLoginService.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/JDBCLoginService.java
new file mode 100644
index 0000000..5cad8e0
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/JDBCLoginService.java
@@ -0,0 +1,297 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import javax.servlet.ServletRequest;
+
+import org.eclipse.jetty.util.Loader;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.security.Credential;
+
+/**
+ * HashMapped User Realm with JDBC as data source.
+ * The {@link #login(String, Object, ServletRequest)} method checks the inherited Map for the user. If the user is not
+ * found, it will fetch details from the database and populate the inherited
+ * Map. It then calls the superclass {@link #login(String, Object, ServletRequest)} method to perform the actual
+ * authentication. Periodically (controlled by configuration parameter),
+ * internal hashes are cleared. Caching can be disabled by setting cache refresh
+ * interval to zero. Uses one database connection that is initialized at
+ * startup. Reconnect on failures.
+ * <p>
+ * An example properties file for configuration is in
+ * <code>${jetty.home}/etc/jdbcRealm.properties</code>
+ */
+public class JDBCLoginService extends AbstractLoginService
+{
+ private static final Logger LOG = Log.getLogger(JDBCLoginService.class);
+
+ protected String _config;
+ protected String _jdbcDriver;
+ protected String _url;
+ protected String _userName;
+ protected String _password;
+ protected String _userTableKey;
+ protected String _userTablePasswordField;
+ protected String _roleTableRoleField;
+ protected Connection _con;
+ protected String _userSql;
+ protected String _roleSql;
+
+ /**
+ * JDBCKnownUser
+ */
+ public class JDBCUserPrincipal extends UserPrincipal
+ {
+ int _userKey;
+
+ public JDBCUserPrincipal(String name, Credential credential, int key)
+ {
+ super(name, credential);
+ _userKey = key;
+ }
+
+ public int getUserKey()
+ {
+ return _userKey;
+ }
+ }
+
+ public JDBCLoginService()
+ throws IOException
+ {
+ }
+
+ public JDBCLoginService(String name)
+ throws IOException
+ {
+ setName(name);
+ }
+
+ public JDBCLoginService(String name, String config)
+ throws IOException
+ {
+ setName(name);
+ setConfig(config);
+ }
+
+ public JDBCLoginService(String name, IdentityService identityService, String config)
+ throws IOException
+ {
+ setName(name);
+ setIdentityService(identityService);
+ setConfig(config);
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ Properties properties = new Properties();
+ Resource resource = Resource.newResource(_config);
+ try (InputStream in = resource.getInputStream())
+ {
+ properties.load(in);
+ }
+ _jdbcDriver = properties.getProperty("jdbcdriver");
+ _url = properties.getProperty("url");
+ _userName = properties.getProperty("username");
+ _password = properties.getProperty("password");
+ String userTable = properties.getProperty("usertable");
+ _userTableKey = properties.getProperty("usertablekey");
+ String userTableUserField = properties.getProperty("usertableuserfield");
+ _userTablePasswordField = properties.getProperty("usertablepasswordfield");
+ String roleTable = properties.getProperty("roletable");
+ String roleTableKey = properties.getProperty("roletablekey");
+ _roleTableRoleField = properties.getProperty("roletablerolefield");
+ String userRoleTable = properties.getProperty("userroletable");
+ String userRoleTableUserKey = properties.getProperty("userroletableuserkey");
+ String userRoleTableRoleKey = properties.getProperty("userroletablerolekey");
+
+ if (_jdbcDriver == null || _jdbcDriver.equals("") ||
+ _url == null ||
+ _url.equals("") ||
+ _userName == null ||
+ _userName.equals("") ||
+ _password == null)
+ {
+ LOG.warn("UserRealm " + getName() + " has not been properly configured");
+ }
+
+ _userSql = "select " + _userTableKey + "," + _userTablePasswordField + " from " + userTable + " where " + userTableUserField + " = ?";
+ _roleSql = "select r." + _roleTableRoleField +
+ " from " + roleTable +
+ " r, " + userRoleTable +
+ " u where u." + userRoleTableUserKey + " = ?" +
+ " and r." + roleTableKey + " = u." + userRoleTableRoleKey;
+
+ Loader.loadClass(_jdbcDriver).getDeclaredConstructor().newInstance();
+ super.doStart();
+ }
+
+ public String getConfig()
+ {
+ return _config;
+ }
+
+ /**
+ * Load JDBC connection configuration from properties file.
+ *
+ * @param config Filename or url of user properties file.
+ */
+ public void setConfig(String config)
+ {
+ if (isRunning())
+ throw new IllegalStateException("Running");
+ _config = config;
+ }
+
+ /**
+ * (re)Connect to database with parameters setup by loadConfig()
+ */
+ public void connectDatabase()
+ {
+ try
+ {
+ Class.forName(_jdbcDriver);
+ _con = DriverManager.getConnection(_url, _userName, _password);
+ }
+ catch (SQLException e)
+ {
+ LOG.warn("UserRealm " + getName() + " could not connect to database; will try later", e);
+ }
+ catch (ClassNotFoundException e)
+ {
+ LOG.warn("UserRealm " + getName() + " could not connect to database; will try later", e);
+ }
+ }
+
+ @Override
+ public UserPrincipal loadUserInfo(String username)
+ {
+ try
+ {
+ if (null == _con)
+ connectDatabase();
+
+ if (null == _con)
+ throw new SQLException("Can't connect to database");
+
+ try (PreparedStatement stat1 = _con.prepareStatement(_userSql))
+ {
+ stat1.setObject(1, username);
+ try (ResultSet rs1 = stat1.executeQuery())
+ {
+ if (rs1.next())
+ {
+ int key = rs1.getInt(_userTableKey);
+ String credentials = rs1.getString(_userTablePasswordField);
+
+ return new JDBCUserPrincipal(username, Credential.getCredential(credentials), key);
+ }
+ }
+ }
+ }
+ catch (SQLException e)
+ {
+ LOG.warn("UserRealm " + getName() + " could not load user information from database", e);
+ closeConnection();
+ }
+
+ return null;
+ }
+
+ @Override
+ public String[] loadRoleInfo(UserPrincipal user)
+ {
+ JDBCUserPrincipal jdbcUser = (JDBCUserPrincipal)user;
+
+ try
+ {
+ if (null == _con)
+ connectDatabase();
+
+ if (null == _con)
+ throw new SQLException("Can't connect to database");
+
+ List<String> roles = new ArrayList<String>();
+
+ try (PreparedStatement stat2 = _con.prepareStatement(_roleSql))
+ {
+ stat2.setInt(1, jdbcUser.getUserKey());
+ try (ResultSet rs2 = stat2.executeQuery())
+ {
+ while (rs2.next())
+ {
+ roles.add(rs2.getString(_roleTableRoleField));
+ }
+ return roles.toArray(new String[roles.size()]);
+ }
+ }
+ }
+ catch (SQLException e)
+ {
+ LOG.warn("UserRealm " + getName() + " could not load user information from database", e);
+ closeConnection();
+ }
+
+ return null;
+ }
+
+ /**
+ * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
+ */
+ @Override
+ protected void doStop() throws Exception
+ {
+ closeConnection();
+ super.doStop();
+ }
+
+ /**
+ * Close an existing connection
+ */
+ private void closeConnection()
+ {
+ if (_con != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Closing db connection for JDBCUserRealm");
+ try
+ {
+ _con.close();
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+ }
+ _con = null;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/LoggedOutAuthentication.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/LoggedOutAuthentication.java
new file mode 100644
index 0000000..21ff91c
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/LoggedOutAuthentication.java
@@ -0,0 +1,59 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import javax.servlet.ServletRequest;
+
+import org.eclipse.jetty.security.authentication.LoginAuthenticator;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.UserIdentity;
+
+/**
+ * LoggedOutAuthentication
+ *
+ * An Authentication indicating that a user has been previously, but is not currently logged in,
+ * but may be capable of logging in after a call to Request.login(String,String)
+ */
+public class LoggedOutAuthentication implements Authentication.NonAuthenticated
+{
+ private LoginAuthenticator _authenticator;
+
+ public LoggedOutAuthentication(LoginAuthenticator authenticator)
+ {
+ _authenticator = authenticator;
+ }
+
+ @Override
+ public Authentication login(String username, Object password, ServletRequest request)
+ {
+ if (username == null)
+ return null;
+
+ UserIdentity identity = _authenticator.login(username, password, request);
+ if (identity != null)
+ {
+ IdentityService identityService = _authenticator.getLoginService().getIdentityService();
+ UserAuthentication authentication = new UserAuthentication("API", identity);
+ if (identityService != null)
+ identityService.associate(identity);
+ return authentication;
+ }
+ return null;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/LoginService.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/LoginService.java
new file mode 100644
index 0000000..733d01d
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/LoginService.java
@@ -0,0 +1,75 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import javax.servlet.ServletRequest;
+
+import org.eclipse.jetty.server.UserIdentity;
+
+/**
+ * Login Service Interface.
+ * <p>
+ * The Login service provides an abstract mechanism for an {@link Authenticator}
+ * to check credentials and to create a {@link UserIdentity} using the
+ * set {@link IdentityService}.
+ */
+public interface LoginService
+{
+
+ /**
+ * @return Get the name of the login service (aka Realm name)
+ */
+ String getName();
+
+ /**
+ * Login a user.
+ *
+ * @param username The user name
+ * @param credentials The users credentials
+ * @param request TODO
+ * @return A UserIdentity if the credentials matched, otherwise null
+ */
+ UserIdentity login(String username, Object credentials, ServletRequest request);
+
+ /**
+ * Validate a user identity.
+ * Validate that a UserIdentity previously created by a call
+ * to {@link #login(String, Object, ServletRequest)} is still valid.
+ *
+ * @param user The user to validate
+ * @return true if authentication has not been revoked for the user.
+ */
+ boolean validate(UserIdentity user);
+
+ /**
+ * Get the IdentityService associated with this Login Service.
+ *
+ * @return the IdentityService associated with this Login Service.
+ */
+ IdentityService getIdentityService();
+
+ /**
+ * Set the IdentityService associated with this Login Service.
+ *
+ * @param service the IdentityService associated with this Login Service.
+ */
+ void setIdentityService(IdentityService service);
+
+ void logout(UserIdentity user);
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/PropertyUserStore.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/PropertyUserStore.java
new file mode 100644
index 0000000..c6e1982
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/PropertyUserStore.java
@@ -0,0 +1,395 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.PathWatcher;
+import org.eclipse.jetty.util.PathWatcher.PathWatchEvent;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.JarFileResource;
+import org.eclipse.jetty.util.resource.PathResource;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.security.Credential;
+
+/**
+ * <p>This class monitors a property file of the format mentioned below
+ * and notifies registered listeners of the changes to the the given file.</p>
+ *
+ * <pre>
+ * username: password [,rolename ...]
+ * </pre>
+ *
+ * <p>Passwords may be clear text, obfuscated or checksummed.
+ * The class {@link org.eclipse.jetty.util.security.Password} should be used
+ * to generate obfuscated passwords or password checksums.</p>
+ *
+ * <p>If DIGEST Authentication is used, the password must be in a recoverable
+ * format, either plain text or obfuscated.</p>
+ */
+public class PropertyUserStore extends UserStore implements PathWatcher.Listener
+{
+ private static final Logger LOG = Log.getLogger(PropertyUserStore.class);
+
+ protected Path _configPath;
+ protected PathWatcher _pathWatcher;
+ protected boolean _hotReload = false; // default is not to reload
+ protected boolean _firstLoad = true; // true if first load, false from that point on
+ protected List<UserListener> _listeners;
+
+ /**
+ * Get the config (as a string)
+ *
+ * @return the config path as a string
+ */
+ public String getConfig()
+ {
+ if (_configPath != null)
+ return _configPath.toString();
+ return null;
+ }
+
+ /**
+ * Set the Config Path from a String reference to a file
+ *
+ * @param config the config file
+ */
+ public void setConfig(String config)
+ {
+ if (config == null)
+ {
+ _configPath = null;
+ return;
+ }
+
+ try
+ {
+ Resource configResource = Resource.newResource(config);
+
+ if (configResource instanceof JarFileResource)
+ _configPath = extractPackedFile((JarFileResource)configResource);
+ else if (configResource instanceof PathResource)
+ _configPath = ((PathResource)configResource).getPath();
+ else if (configResource.getFile() != null)
+ setConfigFile(configResource.getFile());
+ else
+ throw new IllegalArgumentException(config);
+ }
+ catch (Exception e)
+ {
+ _configPath = null;
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Get the Config {@link Path} reference.
+ *
+ * @return the config path
+ */
+ public Path getConfigPath()
+ {
+ return _configPath;
+ }
+
+ /**
+ * Set the Config Path from a String reference to a file
+ *
+ * @param configFile the config file can a be a file path or a reference to a file within a jar file {@code jar:file:}
+ */
+ @Deprecated
+ public void setConfigPath(String configFile)
+ {
+ setConfig(configFile);
+ }
+
+ private Path extractPackedFile(JarFileResource configResource) throws IOException
+ {
+ String uri = configResource.getURI().toASCIIString();
+ int colon = uri.lastIndexOf(":");
+ int bangSlash = uri.indexOf("!/");
+ if (colon < 0 || bangSlash < 0 || colon > bangSlash)
+ throw new IllegalArgumentException("Not resolved JarFile resource: " + uri);
+
+ String entryPath = StringUtil.sanitizeFileSystemName(uri.substring(colon + 2));
+
+ Path tmpDirectory = Files.createTempDirectory("users_store");
+ tmpDirectory.toFile().deleteOnExit();
+ Path extractedPath = Paths.get(tmpDirectory.toString(), entryPath);
+ Files.deleteIfExists(extractedPath);
+ extractedPath.toFile().deleteOnExit();
+ IO.copy(configResource.getInputStream(), new FileOutputStream(extractedPath.toFile()));
+ if (isHotReload())
+ {
+ LOG.warn("Cannot hot reload from packed configuration: {}", configResource);
+ setHotReload(false);
+ }
+ return extractedPath;
+ }
+
+ /**
+ * Set the Config Path from a {@link File} reference
+ *
+ * @param configFile the config file
+ */
+ @Deprecated
+ public void setConfigPath(File configFile)
+ {
+ setConfigFile(configFile);
+ }
+
+ /**
+ * Set the Config Path from a {@link File} reference
+ *
+ * @param configFile the config file
+ */
+ public void setConfigFile(File configFile)
+ {
+ if (configFile == null)
+ _configPath = null;
+ else
+ _configPath = configFile.toPath();
+ }
+
+ /**
+ * Set the Config Path
+ *
+ * @param configPath the config path
+ */
+ public void setConfigPath(Path configPath)
+ {
+ _configPath = configPath;
+ }
+
+ /**
+ * @return the resource associated with the configured properties file, creating it if necessary
+ */
+ public Resource getConfigResource()
+ {
+ if (_configPath == null)
+ return null;
+ return new PathResource(_configPath);
+ }
+
+ /**
+ * Is hot reload enabled on this user store
+ *
+ * @return true if hot reload was enabled before startup
+ */
+ public boolean isHotReload()
+ {
+ return _hotReload;
+ }
+
+ /**
+ * Enable Hot Reload of the Property File
+ *
+ * @param enable true to enable, false to disable
+ */
+ public void setHotReload(boolean enable)
+ {
+ if (isRunning())
+ {
+ throw new IllegalStateException("Cannot set hot reload while user store is running");
+ }
+ this._hotReload = enable;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x[users.count=%d,identityService=%s]", getClass().getSimpleName(), hashCode(), getKnownUserIdentities().size(), getIdentityService());
+ }
+
+ protected void loadUsers() throws IOException
+ {
+ if (_configPath == null)
+ throw new IllegalStateException("No config path set");
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Loading {} from {}", this, _configPath);
+
+ Resource config = getConfigResource();
+
+ if (!config.exists())
+ throw new IllegalStateException("Config does not exist: " + config);
+
+ Properties properties = new Properties();
+ properties.load(config.getInputStream());
+
+ Set<String> known = new HashSet<>();
+
+ for (Map.Entry<Object, Object> entry : properties.entrySet())
+ {
+ String username = ((String)entry.getKey()).trim();
+ String credentials = ((String)entry.getValue()).trim();
+ String roles = null;
+ int c = credentials.indexOf(',');
+ if (c >= 0)
+ {
+ roles = credentials.substring(c + 1).trim();
+ credentials = credentials.substring(0, c).trim();
+ }
+
+ if (username.length() > 0)
+ {
+ String[] roleArray = IdentityService.NO_ROLES;
+ if (roles != null && roles.length() > 0)
+ roleArray = StringUtil.csvSplit(roles);
+ known.add(username);
+ Credential credential = Credential.getCredential(credentials);
+ addUser(username, credential, roleArray);
+ notifyUpdate(username, credential, roleArray);
+ }
+ }
+
+ List<String> currentlyKnownUsers = new ArrayList<>(getKnownUserIdentities().keySet());
+ // if its not the initial load then we want to process removed users
+ if (!_firstLoad)
+ {
+ for (String user : currentlyKnownUsers)
+ {
+ if (!known.contains(user))
+ {
+ removeUser(user);
+ notifyRemove(user);
+ }
+ }
+ }
+
+ // set initial load to false as there should be no more initial loads
+ _firstLoad = false;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Loaded " + this + " from " + _configPath);
+ }
+
+ /**
+ * Depending on the value of the refresh interval, this method will either start
+ * up a scanner thread that will monitor the properties file for changes after
+ * it has initially loaded it. Otherwise the users will be loaded and there will
+ * be no active monitoring thread so changes will not be detected.
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+
+ loadUsers();
+ if (isHotReload() && (_configPath != null))
+ {
+ this._pathWatcher = new PathWatcher();
+ this._pathWatcher.watch(_configPath);
+ this._pathWatcher.addListener(this);
+ this._pathWatcher.setNotifyExistingOnStart(false);
+ this._pathWatcher.start();
+ }
+ }
+
+ @Override
+ public void onPathWatchEvent(PathWatchEvent event)
+ {
+ try
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Path watch event: {}", event.getType());
+ loadUsers();
+ }
+ catch (IOException e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ super.doStop();
+ if (this._pathWatcher != null)
+ this._pathWatcher.stop();
+ this._pathWatcher = null;
+ }
+
+ /**
+ * Notifies the registered listeners of potential updates to a user
+ *
+ * @param username the user that was updated
+ * @param credential the updated credentials
+ * @param roleArray the updated roles
+ */
+ private void notifyUpdate(String username, Credential credential, String[] roleArray)
+ {
+ if (_listeners != null)
+ {
+ for (UserListener listener : _listeners)
+ {
+ listener.update(username, credential, roleArray);
+ }
+ }
+ }
+
+ /**
+ * Notifies the registered listeners that a user has been removed.
+ *
+ * @param username the user that was removed
+ */
+ private void notifyRemove(String username)
+ {
+ if (_listeners != null)
+ {
+ for (UserListener listener : _listeners)
+ {
+ listener.remove(username);
+ }
+ }
+ }
+
+ /**
+ * Registers a listener to be notified of the contents of the property file
+ *
+ * @param listener the user listener
+ */
+ public void registerUserListener(UserListener listener)
+ {
+ if (_listeners == null)
+ _listeners = new ArrayList<>();
+ _listeners.add(listener);
+ }
+
+ public interface UserListener
+ {
+ void update(String username, Credential credential, String[] roleArray);
+
+ void remove(String username);
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/RoleInfo.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/RoleInfo.java
new file mode 100644
index 0000000..eba7b98
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/RoleInfo.java
@@ -0,0 +1,167 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * RoleInfo
+ *
+ * Badly named class that holds the role and user data constraint info for a
+ * path/http method combination, extracted and combined from security
+ * constraints.
+ *
+ * @version $Rev: 4793 $ $Date: 2009-03-19 00:00:01 +0100 (Thu, 19 Mar 2009) $
+ */
+public class RoleInfo
+{
+ private boolean _isAnyAuth;
+ private boolean _isAnyRole;
+ private boolean _checked;
+ private boolean _forbidden;
+ private UserDataConstraint _userDataConstraint;
+
+ /**
+ * List of permitted roles
+ */
+ private final Set<String> _roles = new CopyOnWriteArraySet<>();
+
+ public RoleInfo()
+ {
+ }
+
+ public boolean isChecked()
+ {
+ return _checked;
+ }
+
+ public void setChecked(boolean checked)
+ {
+ this._checked = checked;
+ if (!checked)
+ {
+ _forbidden = false;
+ _roles.clear();
+ _isAnyRole = false;
+ _isAnyAuth = false;
+ }
+ }
+
+ public boolean isForbidden()
+ {
+ return _forbidden;
+ }
+
+ public void setForbidden(boolean forbidden)
+ {
+ this._forbidden = forbidden;
+ if (forbidden)
+ {
+ _checked = true;
+ _userDataConstraint = null;
+ _isAnyRole = false;
+ _isAnyAuth = false;
+ _roles.clear();
+ }
+ }
+
+ public boolean isAnyRole()
+ {
+ return _isAnyRole;
+ }
+
+ public void setAnyRole(boolean anyRole)
+ {
+ this._isAnyRole = anyRole;
+ if (anyRole)
+ _checked = true;
+ }
+
+ public boolean isAnyAuth()
+ {
+ return _isAnyAuth;
+ }
+
+ public void setAnyAuth(boolean anyAuth)
+ {
+ this._isAnyAuth = anyAuth;
+ if (anyAuth)
+ _checked = true;
+ }
+
+ public UserDataConstraint getUserDataConstraint()
+ {
+ return _userDataConstraint;
+ }
+
+ public void setUserDataConstraint(UserDataConstraint userDataConstraint)
+ {
+ if (userDataConstraint == null)
+ throw new NullPointerException("Null UserDataConstraint");
+ if (this._userDataConstraint == null)
+ {
+
+ this._userDataConstraint = userDataConstraint;
+ }
+ else
+ {
+ this._userDataConstraint = this._userDataConstraint.combine(userDataConstraint);
+ }
+ }
+
+ public Set<String> getRoles()
+ {
+ return _roles;
+ }
+
+ public void addRole(String role)
+ {
+ _roles.add(role);
+ }
+
+ public void combine(RoleInfo other)
+ {
+ if (other._forbidden)
+ setForbidden(true);
+ else if (other._checked)
+ {
+ setChecked(true);
+ if (other._isAnyAuth)
+ setAnyAuth(true);
+ if (other._isAnyRole)
+ setAnyRole(true);
+
+ _roles.addAll(other._roles);
+ }
+ setUserDataConstraint(other._userDataConstraint);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("RoleInfo@%x{%s%s%s%s,%s}",
+ hashCode(),
+ (_forbidden ? "Forbidden," : ""),
+ (_checked ? "Checked," : ""),
+ (_isAnyAuth ? "AnyAuth," : ""),
+ (_isAnyRole ? "*" : _roles),
+ _userDataConstraint);
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/RoleRunAsToken.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/RoleRunAsToken.java
new file mode 100644
index 0000000..3eccd54
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/RoleRunAsToken.java
@@ -0,0 +1,43 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+/**
+ * @version $Rev: 4701 $ $Date: 2009-03-03 13:01:26 +0100 (Tue, 03 Mar 2009) $
+ */
+public class RoleRunAsToken implements RunAsToken
+{
+ private final String _runAsRole;
+
+ public RoleRunAsToken(String runAsRole)
+ {
+ this._runAsRole = runAsRole;
+ }
+
+ public String getRunAsRole()
+ {
+ return _runAsRole;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "RoleRunAsToken(" + _runAsRole + ")";
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/RunAsToken.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/RunAsToken.java
new file mode 100644
index 0000000..697f25f
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/RunAsToken.java
@@ -0,0 +1,28 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+/**
+ * marker interface for run-as-role tokens
+ *
+ * @version $Rev: 4701 $ $Date: 2009-03-03 13:01:26 +0100 (Tue, 03 Mar 2009) $
+ */
+public interface RunAsToken
+{
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java
new file mode 100644
index 0000000..1e42f29
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java
@@ -0,0 +1,717 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+import java.util.Set;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.security.authentication.DeferredAuthentication;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandler.Context;
+import org.eclipse.jetty.server.handler.HandlerWrapper;
+import org.eclipse.jetty.util.component.DumpableCollection;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Abstract SecurityHandler.
+ * <p>
+ * Select and apply an {@link Authenticator} to a request.
+ * <p>
+ * The Authenticator may either be directly set on the handler
+ * or will be create during {@link #start()} with a call to
+ * either the default or set AuthenticatorFactory.
+ * <p>
+ * SecurityHandler has a set of initparameters that are used by the
+ * Authentication.Configuration. At startup, any context init parameters
+ * that start with "org.eclipse.jetty.security." that do not have
+ * values in the SecurityHandler init parameters, are copied.
+ */
+public abstract class SecurityHandler extends HandlerWrapper implements Authenticator.AuthConfiguration
+{
+ private static final Logger LOG = Log.getLogger(SecurityHandler.class);
+ private static final List<Authenticator.Factory> __knownAuthenticatorFactories = new ArrayList<>();
+
+ private boolean _checkWelcomeFiles = false;
+ private Authenticator _authenticator;
+ private Authenticator.Factory _authenticatorFactory;
+ private String _realmName;
+ private String _authMethod;
+ private final Map<String, String> _initParameters = new HashMap<>();
+ private LoginService _loginService;
+ private IdentityService _identityService;
+ private boolean _renewSession = true;
+
+ static
+ {
+ Iterator<Authenticator.Factory> serviceLoaderIterator = ServiceLoader.load(Authenticator.Factory.class).iterator();
+ while (true)
+ {
+ try
+ {
+ if (!serviceLoaderIterator.hasNext())
+ break;
+ __knownAuthenticatorFactories.add(serviceLoaderIterator.next());
+ }
+ catch (ServiceConfigurationError error)
+ {
+ LOG.warn("Error while loading AuthenticatorFactory with ServiceLoader", error);
+ }
+ }
+
+ __knownAuthenticatorFactories.add(new DefaultAuthenticatorFactory());
+ }
+
+ protected SecurityHandler()
+ {
+ addBean(new DumpableCollection("knownAuthenticatorFactories", __knownAuthenticatorFactories));
+ }
+
+ /**
+ * Get the identityService.
+ *
+ * @return the identityService
+ */
+ @Override
+ public IdentityService getIdentityService()
+ {
+ return _identityService;
+ }
+
+ /**
+ * Set the identityService.
+ *
+ * @param identityService the identityService to set
+ */
+ public void setIdentityService(IdentityService identityService)
+ {
+ if (isStarted())
+ throw new IllegalStateException("Started");
+ updateBean(_identityService, identityService);
+ _identityService = identityService;
+ }
+
+ /**
+ * Get the loginService.
+ *
+ * @return the loginService
+ */
+ @Override
+ public LoginService getLoginService()
+ {
+ return _loginService;
+ }
+
+ /**
+ * Set the loginService.
+ *
+ * @param loginService the loginService to set
+ */
+ public void setLoginService(LoginService loginService)
+ {
+ if (isStarted())
+ throw new IllegalStateException("Started");
+ updateBean(_loginService, loginService);
+ _loginService = loginService;
+ }
+
+ public Authenticator getAuthenticator()
+ {
+ return _authenticator;
+ }
+
+ /**
+ * Set the authenticator.
+ *
+ * @param authenticator the authenticator
+ * @throws IllegalStateException if the SecurityHandler is running
+ */
+ public void setAuthenticator(Authenticator authenticator)
+ {
+ if (isStarted())
+ throw new IllegalStateException("Started");
+ updateBean(_authenticator, authenticator);
+ _authenticator = authenticator;
+ if (_authenticator != null)
+ _authMethod = _authenticator.getAuthMethod();
+ }
+
+ /**
+ * @return the authenticatorFactory
+ */
+ public Authenticator.Factory getAuthenticatorFactory()
+ {
+ return _authenticatorFactory;
+ }
+
+ /**
+ * @param authenticatorFactory the authenticatorFactory to set
+ * @throws IllegalStateException if the SecurityHandler is running
+ */
+ public void setAuthenticatorFactory(Authenticator.Factory authenticatorFactory)
+ {
+ if (isRunning())
+ throw new IllegalStateException("running");
+ updateBean(_authenticatorFactory, authenticatorFactory);
+ _authenticatorFactory = authenticatorFactory;
+ }
+
+ /**
+ * @return the list of discovered authenticatorFactories
+ */
+ public List<Authenticator.Factory> getKnownAuthenticatorFactories()
+ {
+ return __knownAuthenticatorFactories;
+ }
+
+ /**
+ * @return the realmName
+ */
+ @Override
+ public String getRealmName()
+ {
+ return _realmName;
+ }
+
+ /**
+ * @param realmName the realmName to set
+ * @throws IllegalStateException if the SecurityHandler is running
+ */
+ public void setRealmName(String realmName)
+ {
+ if (isRunning())
+ throw new IllegalStateException("running");
+ _realmName = realmName;
+ }
+
+ /**
+ * @return the authMethod
+ */
+ @Override
+ public String getAuthMethod()
+ {
+ return _authMethod;
+ }
+
+ /**
+ * @param authMethod the authMethod to set
+ * @throws IllegalStateException if the SecurityHandler is running
+ */
+ public void setAuthMethod(String authMethod)
+ {
+ if (isRunning())
+ throw new IllegalStateException("running");
+ _authMethod = authMethod;
+ }
+
+ /**
+ * @return True if forwards to welcome files are authenticated
+ */
+ public boolean isCheckWelcomeFiles()
+ {
+ return _checkWelcomeFiles;
+ }
+
+ /**
+ * @param authenticateWelcomeFiles True if forwards to welcome files are
+ * authenticated
+ * @throws IllegalStateException if the SecurityHandler is running
+ */
+ public void setCheckWelcomeFiles(boolean authenticateWelcomeFiles)
+ {
+ if (isRunning())
+ throw new IllegalStateException("running");
+ _checkWelcomeFiles = authenticateWelcomeFiles;
+ }
+
+ @Override
+ public String getInitParameter(String key)
+ {
+ return _initParameters.get(key);
+ }
+
+ @Override
+ public Set<String> getInitParameterNames()
+ {
+ return _initParameters.keySet();
+ }
+
+ /**
+ * Set an initialization parameter.
+ *
+ * @param key the init key
+ * @param value the init value
+ * @return previous value
+ * @throws IllegalStateException if the SecurityHandler is started
+ */
+ public String setInitParameter(String key, String value)
+ {
+ if (isStarted())
+ throw new IllegalStateException("started");
+ return _initParameters.put(key, value);
+ }
+
+ protected LoginService findLoginService() throws Exception
+ {
+ Collection<LoginService> list = getServer().getBeans(LoginService.class);
+ LoginService service = null;
+ String realm = getRealmName();
+ if (realm != null)
+ {
+ for (LoginService s : list)
+ {
+ if (s.getName() != null && s.getName().equals(realm))
+ {
+ service = s;
+ break;
+ }
+ }
+ }
+ else if (list.size() == 1)
+ service = list.iterator().next();
+
+ return service;
+ }
+
+ protected IdentityService findIdentityService()
+ {
+ return getServer().getBean(IdentityService.class);
+ }
+
+ @Override
+ protected void doStart()
+ throws Exception
+ {
+ // copy security init parameters
+ ContextHandler.Context context = ContextHandler.getCurrentContext();
+ if (context != null)
+ {
+ Enumeration<String> names = context.getInitParameterNames();
+ while (names != null && names.hasMoreElements())
+ {
+ String name = names.nextElement();
+ if (name.startsWith("org.eclipse.jetty.security.") &&
+ getInitParameter(name) == null)
+ setInitParameter(name, context.getInitParameter(name));
+ }
+ }
+
+ // complicated resolution of login and identity service to handle
+ // many different ways these can be constructed and injected.
+
+ if (_loginService == null)
+ {
+ setLoginService(findLoginService());
+ if (_loginService != null)
+ unmanage(_loginService);
+ }
+
+ if (_identityService == null)
+ {
+ if (_loginService != null)
+ setIdentityService(_loginService.getIdentityService());
+
+ if (_identityService == null)
+ setIdentityService(findIdentityService());
+
+ if (_identityService == null)
+ {
+ setIdentityService(new DefaultIdentityService());
+ manage(_identityService);
+ }
+ else
+ unmanage(_identityService);
+ }
+
+ if (_loginService != null)
+ {
+ if (_loginService.getIdentityService() == null)
+ _loginService.setIdentityService(_identityService);
+ else if (_loginService.getIdentityService() != _identityService)
+ throw new IllegalStateException("LoginService has different IdentityService to " + this);
+ }
+
+ if (_authenticator == null)
+ {
+ // If someone has set an authenticator factory only use that, otherwise try the list of discovered factories.
+ if (_authenticatorFactory != null)
+ {
+ Authenticator authenticator = _authenticatorFactory.getAuthenticator(getServer(), ContextHandler.getCurrentContext(),
+ this, _identityService, _loginService);
+
+ if (authenticator != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Created authenticator {} with {}", authenticator, _authenticatorFactory);
+
+ setAuthenticator(authenticator);
+ }
+ }
+ else
+ {
+ for (Authenticator.Factory factory : getKnownAuthenticatorFactories())
+ {
+ Authenticator authenticator = factory.getAuthenticator(getServer(), ContextHandler.getCurrentContext(),
+ this, _identityService, _loginService);
+
+ if (authenticator != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Created authenticator {} with {}", authenticator, factory);
+
+ setAuthenticator(authenticator);
+ break;
+ }
+ }
+ }
+ }
+
+ if (_authenticator != null)
+ _authenticator.setConfiguration(this);
+ else if (_realmName != null)
+ {
+ LOG.warn("No Authenticator for " + this);
+ throw new IllegalStateException("No Authenticator");
+ }
+
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ //if we discovered the services (rather than had them explicitly configured), remove them.
+ if (!isManaged(_identityService))
+ {
+ removeBean(_identityService);
+ _identityService = null;
+ }
+
+ if (!isManaged(_loginService))
+ {
+ removeBean(_loginService);
+ _loginService = null;
+ }
+
+ super.doStop();
+ }
+
+ protected boolean checkSecurity(Request request)
+ {
+ switch (request.getDispatcherType())
+ {
+ case REQUEST:
+ case ASYNC:
+ return true;
+ case FORWARD:
+ if (isCheckWelcomeFiles() && request.getAttribute("org.eclipse.jetty.server.welcome") != null)
+ {
+ request.removeAttribute("org.eclipse.jetty.server.welcome");
+ return true;
+ }
+ return false;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.Authenticator.AuthConfiguration#isSessionRenewedOnAuthentication()
+ */
+ @Override
+ public boolean isSessionRenewedOnAuthentication()
+ {
+ return _renewSession;
+ }
+
+ /**
+ * Set renew the session on Authentication.
+ * <p>
+ * If set to true, then on authentication, the session associated with a reqeuest is invalidated and replaced with a new session.
+ *
+ * @param renew true to renew the authentication on session
+ * @see org.eclipse.jetty.security.Authenticator.AuthConfiguration#isSessionRenewedOnAuthentication()
+ */
+ public void setSessionRenewedOnAuthentication(boolean renew)
+ {
+ _renewSession = renew;
+ }
+
+ /*
+ * @see org.eclipse.jetty.server.Handler#handle(java.lang.String,
+ * javax.servlet.http.HttpServletRequest,
+ * javax.servlet.http.HttpServletResponse, int)
+ */
+ @Override
+ public void handle(String pathInContext, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ final Response base_response = baseRequest.getResponse();
+ final Handler handler = getHandler();
+
+ if (handler == null)
+ return;
+
+ final Authenticator authenticator = _authenticator;
+
+ if (checkSecurity(baseRequest))
+ {
+ //See Servlet Spec 3.1 sec 13.6.3
+ if (authenticator != null)
+ authenticator.prepareRequest(baseRequest);
+
+ RoleInfo roleInfo = prepareConstraintInfo(pathInContext, baseRequest);
+
+ // Check data constraints
+ if (!checkUserDataPermissions(pathInContext, baseRequest, base_response, roleInfo))
+ {
+ if (!baseRequest.isHandled())
+ {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ baseRequest.setHandled(true);
+ }
+ return;
+ }
+
+ // is Auth mandatory?
+ boolean isAuthMandatory =
+ isAuthMandatory(baseRequest, base_response, roleInfo);
+
+ if (isAuthMandatory && authenticator == null)
+ {
+ LOG.warn("No authenticator for: " + roleInfo);
+ if (!baseRequest.isHandled())
+ {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ baseRequest.setHandled(true);
+ }
+ return;
+ }
+
+ // check authentication
+ Object previousIdentity = null;
+ try
+ {
+ Authentication authentication = baseRequest.getAuthentication();
+ if (authentication == null || authentication == Authentication.NOT_CHECKED)
+ authentication = authenticator == null ? Authentication.UNAUTHENTICATED : authenticator.validateRequest(request, response, isAuthMandatory);
+
+ if (authentication instanceof Authentication.Wrapped)
+ {
+ request = ((Authentication.Wrapped)authentication).getHttpServletRequest();
+ response = ((Authentication.Wrapped)authentication).getHttpServletResponse();
+ }
+
+ if (authentication instanceof Authentication.ResponseSent)
+ {
+ baseRequest.setHandled(true);
+ }
+ else if (authentication instanceof Authentication.User)
+ {
+ Authentication.User userAuth = (Authentication.User)authentication;
+ baseRequest.setAuthentication(authentication);
+ if (_identityService != null)
+ previousIdentity = _identityService.associate(userAuth.getUserIdentity());
+
+ if (isAuthMandatory)
+ {
+ boolean authorized = checkWebResourcePermissions(pathInContext, baseRequest, base_response, roleInfo, userAuth.getUserIdentity());
+ if (!authorized)
+ {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN, "!role");
+ baseRequest.setHandled(true);
+ return;
+ }
+ }
+
+ handler.handle(pathInContext, baseRequest, request, response);
+ if (authenticator != null)
+ authenticator.secureResponse(request, response, isAuthMandatory, userAuth);
+ }
+ else if (authentication instanceof Authentication.Deferred)
+ {
+ DeferredAuthentication deferred = (DeferredAuthentication)authentication;
+ baseRequest.setAuthentication(authentication);
+
+ try
+ {
+ handler.handle(pathInContext, baseRequest, request, response);
+ }
+ finally
+ {
+ previousIdentity = deferred.getPreviousAssociation();
+ }
+
+ if (authenticator != null)
+ {
+ Authentication auth = baseRequest.getAuthentication();
+ if (auth instanceof Authentication.User)
+ {
+ Authentication.User userAuth = (Authentication.User)auth;
+ authenticator.secureResponse(request, response, isAuthMandatory, userAuth);
+ }
+ else
+ authenticator.secureResponse(request, response, isAuthMandatory, null);
+ }
+ }
+ else if (isAuthMandatory)
+ {
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "unauthenticated");
+ baseRequest.setHandled(true);
+ }
+ else
+ {
+ baseRequest.setAuthentication(authentication);
+ if (_identityService != null)
+ previousIdentity = _identityService.associate(null);
+ handler.handle(pathInContext, baseRequest, request, response);
+ if (authenticator != null)
+ authenticator.secureResponse(request, response, isAuthMandatory, null);
+ }
+ }
+ catch (ServerAuthException e)
+ {
+ // jaspi 3.8.3 send HTTP 500 internal server error, with message
+ // from AuthException
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ }
+ finally
+ {
+ if (_identityService != null)
+ _identityService.disassociate(previousIdentity);
+ }
+ }
+ else
+ handler.handle(pathInContext, baseRequest, request, response);
+ }
+
+ public static SecurityHandler getCurrentSecurityHandler()
+ {
+ Context context = ContextHandler.getCurrentContext();
+ if (context == null)
+ return null;
+
+ return context.getContextHandler().getChildHandlerByClass(SecurityHandler.class);
+ }
+
+ public void logout(Authentication.User user)
+ {
+ LOG.debug("logout {}", user);
+ if (user == null)
+ return;
+
+ LoginService loginService = getLoginService();
+ if (loginService != null)
+ {
+ loginService.logout(user.getUserIdentity());
+ }
+
+ IdentityService identityService = getIdentityService();
+ if (identityService != null)
+ {
+ // TODO recover previous from threadlocal (or similar)
+ Object previous = null;
+ identityService.disassociate(previous);
+ }
+ }
+
+ protected abstract RoleInfo prepareConstraintInfo(String pathInContext, Request request);
+
+ protected abstract boolean checkUserDataPermissions(String pathInContext, Request request, Response response, RoleInfo constraintInfo) throws IOException;
+
+ protected abstract boolean isAuthMandatory(Request baseRequest, Response baseResponse, Object constraintInfo);
+
+ protected abstract boolean checkWebResourcePermissions(String pathInContext, Request request, Response response, Object constraintInfo,
+ UserIdentity userIdentity) throws IOException;
+
+ public class NotChecked implements Principal
+ {
+ @Override
+ public String getName()
+ {
+ return null;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "NOT CHECKED";
+ }
+
+ public SecurityHandler getSecurityHandler()
+ {
+ return SecurityHandler.this;
+ }
+ }
+
+ public static final Principal __NO_USER = new Principal()
+ {
+ @Override
+ public String getName()
+ {
+ return null;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "No User";
+ }
+ };
+
+ /**
+ * Nobody user. The Nobody UserPrincipal is used to indicate a partial state
+ * of authentication. A request with a Nobody UserPrincipal will be allowed
+ * past all authentication constraints - but will not be considered an
+ * authenticated request. It can be used by Authenticators such as
+ * FormAuthenticator to allow access to logon and error pages within an
+ * authenticated URI tree.
+ */
+ public static final Principal __NOBODY = new Principal()
+ {
+ @Override
+ public String getName()
+ {
+ return "Nobody";
+ }
+
+ @Override
+ public String toString()
+ {
+ return getName();
+ }
+ };
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ServerAuthException.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ServerAuthException.java
new file mode 100644
index 0000000..b32b778
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/ServerAuthException.java
@@ -0,0 +1,47 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * @version $Rev: 4466 $ $Date: 2009-02-10 23:42:54 +0100 (Tue, 10 Feb 2009) $
+ */
+public class ServerAuthException extends GeneralSecurityException
+{
+
+ public ServerAuthException()
+ {
+ }
+
+ public ServerAuthException(String s)
+ {
+ super(s);
+ }
+
+ public ServerAuthException(String s, Throwable throwable)
+ {
+ super(s, throwable);
+ }
+
+ public ServerAuthException(Throwable throwable)
+ {
+ super(throwable);
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoLoginService.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoLoginService.java
new file mode 100644
index 0000000..b17baa7
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoLoginService.java
@@ -0,0 +1,191 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.util.Base64;
+import java.util.Properties;
+import javax.security.auth.Subject;
+import javax.servlet.ServletRequest;
+
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+
+/**
+ * @deprecated use {@link ConfigurableSpnegoLoginService} instead
+ */
+@Deprecated
+public class SpnegoLoginService extends AbstractLifeCycle implements LoginService
+{
+ private static final Logger LOG = Log.getLogger(SpnegoLoginService.class);
+
+ protected IdentityService _identityService;
+ protected String _name;
+ private String _config;
+
+ private String _targetName;
+
+ public SpnegoLoginService()
+ {
+
+ }
+
+ public SpnegoLoginService(String name)
+ {
+ setName(name);
+ }
+
+ public SpnegoLoginService(String name, String config)
+ {
+ setName(name);
+ setConfig(config);
+ }
+
+ @Override
+ public String getName()
+ {
+ return _name;
+ }
+
+ public void setName(String name)
+ {
+ if (isRunning())
+ {
+ throw new IllegalStateException("Running");
+ }
+
+ _name = name;
+ }
+
+ public String getConfig()
+ {
+ return _config;
+ }
+
+ public void setConfig(String config)
+ {
+ if (isRunning())
+ {
+ throw new IllegalStateException("Running");
+ }
+
+ _config = config;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ Properties properties = new Properties();
+ Resource resource = Resource.newResource(_config);
+ properties.load(resource.getInputStream());
+
+ _targetName = properties.getProperty("targetName");
+
+ LOG.debug("Target Name {}", _targetName);
+
+ super.doStart();
+ }
+
+ /**
+ * username will be null since the credentials will contain all the relevant info
+ */
+ @Override
+ public UserIdentity login(String username, Object credentials, ServletRequest request)
+ {
+ String encodedAuthToken = (String)credentials;
+
+ byte[] authToken = Base64.getDecoder().decode(encodedAuthToken);
+
+ GSSManager manager = GSSManager.getInstance();
+ try
+ {
+ Oid krb5Oid = new Oid("1.3.6.1.5.5.2"); // http://java.sun.com/javase/6/docs/technotes/guides/security/jgss/jgss-features.html
+ GSSName gssName = manager.createName(_targetName, null);
+ GSSCredential serverCreds = manager.createCredential(gssName, GSSCredential.INDEFINITE_LIFETIME, krb5Oid, GSSCredential.ACCEPT_ONLY);
+ GSSContext gContext = manager.createContext(serverCreds);
+
+ if (gContext == null)
+ {
+ LOG.debug("SpnegoUserRealm: failed to establish GSSContext");
+ }
+ else
+ {
+ while (!gContext.isEstablished())
+ {
+ authToken = gContext.acceptSecContext(authToken, 0, authToken.length);
+ }
+ if (gContext.isEstablished())
+ {
+ String clientName = gContext.getSrcName().toString();
+ String role = clientName.substring(clientName.indexOf('@') + 1);
+
+ LOG.debug("SpnegoUserRealm: established a security context");
+ LOG.debug("Client Principal is: " + gContext.getSrcName());
+ LOG.debug("Server Principal is: " + gContext.getTargName());
+ LOG.debug("Client Default Role: " + role);
+
+ SpnegoUserPrincipal user = new SpnegoUserPrincipal(clientName, authToken);
+
+ Subject subject = new Subject();
+ subject.getPrincipals().add(user);
+
+ return _identityService.newUserIdentity(subject, user, new String[]{role});
+ }
+ }
+ }
+ catch (GSSException gsse)
+ {
+ LOG.warn(gsse);
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean validate(UserIdentity user)
+ {
+ return false;
+ }
+
+ @Override
+ public IdentityService getIdentityService()
+ {
+ return _identityService;
+ }
+
+ @Override
+ public void setIdentityService(IdentityService service)
+ {
+ _identityService = service;
+ }
+
+ @Override
+ public void logout(UserIdentity user)
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserIdentity.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserIdentity.java
new file mode 100644
index 0000000..010c939
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserIdentity.java
@@ -0,0 +1,61 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.security.Principal;
+import javax.security.auth.Subject;
+
+import org.eclipse.jetty.server.UserIdentity;
+
+public class SpnegoUserIdentity implements UserIdentity
+{
+ private final Subject _subject;
+ private final Principal _principal;
+ private final UserIdentity _roleDelegate;
+
+ public SpnegoUserIdentity(Subject subject, Principal principal, UserIdentity roleDelegate)
+ {
+ _subject = subject;
+ _principal = principal;
+ _roleDelegate = roleDelegate;
+ }
+
+ @Override
+ public Subject getSubject()
+ {
+ return _subject;
+ }
+
+ @Override
+ public Principal getUserPrincipal()
+ {
+ return _principal;
+ }
+
+ @Override
+ public boolean isUserInRole(String role, Scope scope)
+ {
+ return _roleDelegate != null && _roleDelegate.isUserInRole(role, scope);
+ }
+
+ public boolean isEstablished()
+ {
+ return _roleDelegate != null;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserPrincipal.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserPrincipal.java
new file mode 100644
index 0000000..2c7cd9e
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserPrincipal.java
@@ -0,0 +1,61 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.security.Principal;
+import java.util.Base64;
+
+public class SpnegoUserPrincipal implements Principal
+{
+ private final String _name;
+ private byte[] _token;
+ private String _encodedToken;
+
+ public SpnegoUserPrincipal(String name, String encodedToken)
+ {
+ _name = name;
+ _encodedToken = encodedToken;
+ }
+
+ public SpnegoUserPrincipal(String name, byte[] token)
+ {
+ _name = name;
+ _token = token;
+ }
+
+ @Override
+ public String getName()
+ {
+ return _name;
+ }
+
+ public byte[] getToken()
+ {
+ if (_token == null)
+ _token = Base64.getDecoder().decode(_encodedToken);
+ return _token;
+ }
+
+ public String getEncodedToken()
+ {
+ if (_encodedToken == null)
+ _encodedToken = new String(Base64.getEncoder().encode(_token));
+ return _encodedToken;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/UserAuthentication.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/UserAuthentication.java
new file mode 100644
index 0000000..77805cf
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/UserAuthentication.java
@@ -0,0 +1,45 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import org.eclipse.jetty.server.UserIdentity;
+
+/**
+ * @version $Rev: 4793 $ $Date: 2009-03-19 00:00:01 +0100 (Thu, 19 Mar 2009) $
+ */
+public class UserAuthentication extends AbstractUserAuthentication
+{
+
+ public UserAuthentication(String method, UserIdentity userIdentity)
+ {
+ super(method, userIdentity);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "{User," + getAuthMethod() + "," + _userIdentity + "}";
+ }
+
+ @Override
+ @Deprecated
+ public void logout()
+ {
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/UserDataConstraint.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/UserDataConstraint.java
new file mode 100644
index 0000000..a7634ce
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/UserDataConstraint.java
@@ -0,0 +1,43 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+/**
+ * @version $Rev: 4466 $ $Date: 2009-02-10 23:42:54 +0100 (Tue, 10 Feb 2009) $
+ */
+public enum UserDataConstraint
+{
+ None, Integral, Confidential;
+
+ public static UserDataConstraint get(int dataConstraint)
+ {
+ if (dataConstraint < -1 || dataConstraint > 2)
+ throw new IllegalArgumentException("Expected -1, 0, 1, or 2, not: " + dataConstraint);
+ if (dataConstraint == -1)
+ return None;
+ return values()[dataConstraint];
+ }
+
+ public UserDataConstraint combine(UserDataConstraint other)
+ {
+ if (this.compareTo(other) < 0)
+ return this;
+ return other;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/UserStore.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/UserStore.java
new file mode 100644
index 0000000..99e6f78
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/UserStore.java
@@ -0,0 +1,76 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.security.Principal;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.security.auth.Subject;
+
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.security.Credential;
+
+/**
+ * Base class to store User
+ */
+public class UserStore extends AbstractLifeCycle
+{
+ private IdentityService _identityService = new DefaultIdentityService();
+ private final Map<String, UserIdentity> _knownUserIdentities = new ConcurrentHashMap<>();
+
+ public void addUser(String username, Credential credential, String[] roles)
+ {
+ Principal userPrincipal = new AbstractLoginService.UserPrincipal(username, credential);
+ Subject subject = new Subject();
+ subject.getPrincipals().add(userPrincipal);
+ subject.getPrivateCredentials().add(credential);
+
+ if (roles != null)
+ {
+ for (String role : roles)
+ {
+ subject.getPrincipals().add(new AbstractLoginService.RolePrincipal(role));
+ }
+ }
+
+ subject.setReadOnly();
+ _knownUserIdentities.put(username, _identityService.newUserIdentity(subject, userPrincipal, roles));
+ }
+
+ public void removeUser(String username)
+ {
+ _knownUserIdentities.remove(username);
+ }
+
+ public UserIdentity getUserIdentity(String userName)
+ {
+ return _knownUserIdentities.get(userName);
+ }
+
+ public IdentityService getIdentityService()
+ {
+ return _identityService;
+ }
+
+ public Map<String, UserIdentity> getKnownUserIdentities()
+ {
+ return _knownUserIdentities;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/AuthorizationService.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/AuthorizationService.java
new file mode 100644
index 0000000..0ee6b0a
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/AuthorizationService.java
@@ -0,0 +1,49 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security.authentication;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.security.LoginService;
+import org.eclipse.jetty.server.UserIdentity;
+
+/**
+ * <p>A service to query for user roles.</p>
+ */
+@FunctionalInterface
+public interface AuthorizationService
+{
+ /**
+ * @param request the current HTTP request
+ * @param name the user name
+ * @return a {@link UserIdentity} to query for roles of the given user
+ */
+ UserIdentity getUserIdentity(HttpServletRequest request, String name);
+
+ /**
+ * <p>Wraps a {@link LoginService} as an AuthorizationService</p>
+ *
+ * @param loginService the {@link LoginService} to wrap
+ * @return an AuthorizationService that delegates the query for roles to the given {@link LoginService}
+ */
+ static AuthorizationService from(LoginService loginService, Object credentials)
+ {
+ return (request, name) -> loginService.login(name, credentials, request);
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/BasicAuthenticator.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/BasicAuthenticator.java
new file mode 100644
index 0000000..dca6f93
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/BasicAuthenticator.java
@@ -0,0 +1,119 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security.authentication;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.security.ServerAuthException;
+import org.eclipse.jetty.security.UserAuthentication;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Authentication.User;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.security.Constraint;
+
+public class BasicAuthenticator extends LoginAuthenticator
+{
+ private Charset _charset;
+
+ public Charset getCharset()
+ {
+ return _charset;
+ }
+
+ public void setCharset(Charset charset)
+ {
+ this._charset = charset;
+ }
+
+ @Override
+ public String getAuthMethod()
+ {
+ return Constraint.__BASIC_AUTH;
+ }
+
+ @Override
+ public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
+ {
+ HttpServletRequest request = (HttpServletRequest)req;
+ HttpServletResponse response = (HttpServletResponse)res;
+ String credentials = request.getHeader(HttpHeader.AUTHORIZATION.asString());
+
+ try
+ {
+ if (!mandatory)
+ return new DeferredAuthentication(this);
+
+ if (credentials != null)
+ {
+ int space = credentials.indexOf(' ');
+ if (space > 0)
+ {
+ String method = credentials.substring(0, space);
+ if ("basic".equalsIgnoreCase(method))
+ {
+ credentials = credentials.substring(space + 1);
+ Charset charset = getCharset();
+ if (charset == null)
+ charset = StandardCharsets.ISO_8859_1;
+ credentials = new String(Base64.getDecoder().decode(credentials), charset);
+ int i = credentials.indexOf(':');
+ if (i > 0)
+ {
+ String username = credentials.substring(0, i);
+ String password = credentials.substring(i + 1);
+
+ UserIdentity user = login(username, password, request);
+ if (user != null)
+ return new UserAuthentication(getAuthMethod(), user);
+ }
+ }
+ }
+ }
+
+ if (DeferredAuthentication.isDeferred(response))
+ return Authentication.UNAUTHENTICATED;
+
+ String value = "basic realm=\"" + _loginService.getName() + "\"";
+ Charset charset = getCharset();
+ if (charset != null)
+ value += ", charset=\"" + charset.name() + "\"";
+ response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), value);
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return Authentication.SEND_CONTINUE;
+ }
+ catch (IOException e)
+ {
+ throw new ServerAuthException(e);
+ }
+ }
+
+ @Override
+ public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException
+ {
+ return true;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ClientCertAuthenticator.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ClientCertAuthenticator.java
new file mode 100644
index 0000000..fe2774a
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ClientCertAuthenticator.java
@@ -0,0 +1,375 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security.authentication;
+
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.Principal;
+import java.security.cert.CRL;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.Collection;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.security.ServerAuthException;
+import org.eclipse.jetty.security.UserAuthentication;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Authentication.User;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.security.CertificateUtils;
+import org.eclipse.jetty.util.security.CertificateValidator;
+import org.eclipse.jetty.util.security.Constraint;
+import org.eclipse.jetty.util.security.Password;
+
+public class ClientCertAuthenticator extends LoginAuthenticator
+{
+ /**
+ * String name of keystore password property.
+ */
+ private static final String PASSWORD_PROPERTY = "org.eclipse.jetty.ssl.password";
+
+ /**
+ * Truststore path
+ */
+ private String _trustStorePath;
+ /**
+ * Truststore provider name
+ */
+ private String _trustStoreProvider;
+ /**
+ * Truststore type
+ */
+ private String _trustStoreType = "JKS";
+ /**
+ * Truststore password
+ */
+ private transient Password _trustStorePassword;
+
+ /**
+ * Set to true if SSL certificate validation is required
+ */
+ private boolean _validateCerts;
+ /**
+ * Path to file that contains Certificate Revocation List
+ */
+ private String _crlPath;
+ /**
+ * Maximum certification path length (n - number of intermediate certs, -1 for unlimited)
+ */
+ private int _maxCertPathLength = -1;
+ /**
+ * CRL Distribution Points (CRLDP) support
+ */
+ private boolean _enableCRLDP = false;
+ /**
+ * On-Line Certificate Status Protocol (OCSP) support
+ */
+ private boolean _enableOCSP = false;
+ /**
+ * Location of OCSP Responder
+ */
+ private String _ocspResponderURL;
+
+ public ClientCertAuthenticator()
+ {
+ super();
+ }
+
+ @Override
+ public String getAuthMethod()
+ {
+ return Constraint.__CERT_AUTH;
+ }
+
+ @Override
+ public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
+ {
+ if (!mandatory)
+ return new DeferredAuthentication(this);
+
+ HttpServletRequest request = (HttpServletRequest)req;
+ HttpServletResponse response = (HttpServletResponse)res;
+ X509Certificate[] certs = (X509Certificate[])request.getAttribute("javax.servlet.request.X509Certificate");
+
+ try
+ {
+ // Need certificates.
+ if (certs != null && certs.length > 0)
+ {
+
+ if (_validateCerts)
+ {
+ KeyStore trustStore = getKeyStore(
+ _trustStorePath, _trustStoreType, _trustStoreProvider,
+ _trustStorePassword == null ? null : _trustStorePassword.toString());
+ Collection<? extends CRL> crls = loadCRL(_crlPath);
+ CertificateValidator validator = new CertificateValidator(trustStore, crls);
+ validator.validate(certs);
+ }
+
+ for (X509Certificate cert : certs)
+ {
+ if (cert == null)
+ continue;
+
+ Principal principal = cert.getSubjectDN();
+ if (principal == null)
+ principal = cert.getIssuerDN();
+ final String username = principal == null ? "clientcert" : principal.getName();
+
+ // TODO: investigate if using a raw byte[] is better vs older char[]
+ final char[] credential = Base64.getEncoder().encodeToString(cert.getSignature()).toCharArray();
+
+ UserIdentity user = login(username, credential, req);
+ if (user != null)
+ {
+ return new UserAuthentication(getAuthMethod(), user);
+ }
+ }
+ }
+
+ if (!DeferredAuthentication.isDeferred(response))
+ {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return Authentication.SEND_FAILURE;
+ }
+
+ return Authentication.UNAUTHENTICATED;
+ }
+ catch (Exception e)
+ {
+ throw new ServerAuthException(e.getMessage());
+ }
+ }
+
+ @Deprecated
+ protected KeyStore getKeyStore(InputStream storeStream, String storePath, String storeType, String storeProvider, String storePassword) throws Exception
+ {
+ return getKeyStore(storePath, storeType, storeProvider, storePassword);
+ }
+
+ /**
+ * Loads keystore using an input stream or a file path in the same
+ * order of precedence.
+ *
+ * Required for integrations to be able to override the mechanism
+ * used to load a keystore in order to provide their own implementation.
+ *
+ * @param storePath path of keystore file
+ * @param storeType keystore type
+ * @param storeProvider keystore provider
+ * @param storePassword keystore password
+ * @return created keystore
+ * @throws Exception if unable to get keystore
+ */
+ protected KeyStore getKeyStore(String storePath, String storeType, String storeProvider, String storePassword) throws Exception
+ {
+ return CertificateUtils.getKeyStore(Resource.newResource(storePath), storeType, storeProvider, storePassword);
+ }
+
+ /**
+ * Loads certificate revocation list (CRL) from a file.
+ *
+ * Required for integrations to be able to override the mechanism used to
+ * load CRL in order to provide their own implementation.
+ *
+ * @param crlPath path of certificate revocation list file
+ * @return a (possibly empty) collection view of java.security.cert.CRL objects initialized with the data from the
+ * input stream.
+ * @throws Exception if unable to load CRL
+ */
+ protected Collection<? extends CRL> loadCRL(String crlPath) throws Exception
+ {
+ return CertificateUtils.loadCRL(crlPath);
+ }
+
+ @Override
+ public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException
+ {
+ return true;
+ }
+
+ /**
+ * @return true if SSL certificate has to be validated
+ */
+ public boolean isValidateCerts()
+ {
+ return _validateCerts;
+ }
+
+ /**
+ * @param validateCerts true if SSL certificates have to be validated
+ */
+ public void setValidateCerts(boolean validateCerts)
+ {
+ _validateCerts = validateCerts;
+ }
+
+ /**
+ * @return The file name or URL of the trust store location
+ */
+ public String getTrustStore()
+ {
+ return _trustStorePath;
+ }
+
+ /**
+ * @param trustStorePath The file name or URL of the trust store location
+ */
+ public void setTrustStore(String trustStorePath)
+ {
+ _trustStorePath = trustStorePath;
+ }
+
+ /**
+ * @return The provider of the trust store
+ */
+ public String getTrustStoreProvider()
+ {
+ return _trustStoreProvider;
+ }
+
+ /**
+ * @param trustStoreProvider The provider of the trust store
+ */
+ public void setTrustStoreProvider(String trustStoreProvider)
+ {
+ _trustStoreProvider = trustStoreProvider;
+ }
+
+ /**
+ * @return The type of the trust store (default "JKS")
+ */
+ public String getTrustStoreType()
+ {
+ return _trustStoreType;
+ }
+
+ /**
+ * @param trustStoreType The type of the trust store (default "JKS")
+ */
+ public void setTrustStoreType(String trustStoreType)
+ {
+ _trustStoreType = trustStoreType;
+ }
+
+ /**
+ * @param password The password for the trust store
+ */
+ public void setTrustStorePassword(String password)
+ {
+ _trustStorePassword = Password.getPassword(PASSWORD_PROPERTY, password, null);
+ }
+
+ /**
+ * Get the crlPath.
+ *
+ * @return the crlPath
+ */
+ public String getCrlPath()
+ {
+ return _crlPath;
+ }
+
+ /**
+ * Set the crlPath.
+ *
+ * @param crlPath the crlPath to set
+ */
+ public void setCrlPath(String crlPath)
+ {
+ _crlPath = crlPath;
+ }
+
+ /**
+ * @return Maximum number of intermediate certificates in
+ * the certification path (-1 for unlimited)
+ */
+ public int getMaxCertPathLength()
+ {
+ return _maxCertPathLength;
+ }
+
+ /**
+ * @param maxCertPathLength maximum number of intermediate certificates in
+ * the certification path (-1 for unlimited)
+ */
+ public void setMaxCertPathLength(int maxCertPathLength)
+ {
+ _maxCertPathLength = maxCertPathLength;
+ }
+
+ /**
+ * @return true if CRL Distribution Points support is enabled
+ */
+ public boolean isEnableCRLDP()
+ {
+ return _enableCRLDP;
+ }
+
+ /**
+ * Enables CRL Distribution Points Support
+ *
+ * @param enableCRLDP true - turn on, false - turns off
+ */
+ public void setEnableCRLDP(boolean enableCRLDP)
+ {
+ _enableCRLDP = enableCRLDP;
+ }
+
+ /**
+ * @return true if On-Line Certificate Status Protocol support is enabled
+ */
+ public boolean isEnableOCSP()
+ {
+ return _enableOCSP;
+ }
+
+ /**
+ * Enables On-Line Certificate Status Protocol support
+ *
+ * @param enableOCSP true - turn on, false - turn off
+ */
+ public void setEnableOCSP(boolean enableOCSP)
+ {
+ _enableOCSP = enableOCSP;
+ }
+
+ /**
+ * @return Location of the OCSP Responder
+ */
+ public String getOcspResponderURL()
+ {
+ return _ocspResponderURL;
+ }
+
+ /**
+ * Set the location of the OCSP Responder.
+ *
+ * @param ocspResponderURL location of the OCSP Responder
+ */
+ public void setOcspResponderURL(String ocspResponderURL)
+ {
+ _ocspResponderURL = ocspResponderURL;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java
new file mode 100644
index 0000000..92e7897
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java
@@ -0,0 +1,248 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security.authentication;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.time.Duration;
+import java.time.Instant;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.security.ServerAuthException;
+import org.eclipse.jetty.security.SpnegoUserIdentity;
+import org.eclipse.jetty.security.SpnegoUserPrincipal;
+import org.eclipse.jetty.security.UserAuthentication;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Authentication.User;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.security.Constraint;
+
+/**
+ * <p>A LoginAuthenticator that uses SPNEGO and the GSS API to authenticate requests.</p>
+ * <p>A successful authentication from a client is cached for a configurable
+ * {@link #getAuthenticationDuration() duration} using the HTTP session; this avoids
+ * that the client is asked to authenticate for every request.</p>
+ *
+ * @see org.eclipse.jetty.security.ConfigurableSpnegoLoginService
+ */
+public class ConfigurableSpnegoAuthenticator extends LoginAuthenticator
+{
+ private static final Logger LOG = Log.getLogger(ConfigurableSpnegoAuthenticator.class);
+
+ private final String _authMethod;
+ private Duration _authenticationDuration = Duration.ofNanos(-1);
+
+ public ConfigurableSpnegoAuthenticator()
+ {
+ this(Constraint.__SPNEGO_AUTH);
+ }
+
+ /**
+ * Allow for a custom authMethod value to be set for instances where SPNEGO may not be appropriate
+ *
+ * @param authMethod the auth method
+ */
+ public ConfigurableSpnegoAuthenticator(String authMethod)
+ {
+ _authMethod = authMethod;
+ }
+
+ @Override
+ public String getAuthMethod()
+ {
+ return _authMethod;
+ }
+
+ /**
+ * @return the authentication duration
+ */
+ public Duration getAuthenticationDuration()
+ {
+ return _authenticationDuration;
+ }
+
+ /**
+ * <p>Sets the duration of the authentication.</p>
+ * <p>A negative duration means that the authentication is only valid for the current request.</p>
+ * <p>A zero duration means that the authentication is valid forever.</p>
+ * <p>A positive value means that the authentication is valid for the specified duration.</p>
+ *
+ * @param authenticationDuration the authentication duration
+ */
+ public void setAuthenticationDuration(Duration authenticationDuration)
+ {
+ _authenticationDuration = authenticationDuration;
+ }
+
+ /**
+ * Only renew the session id if the user has been fully authenticated, don't
+ * renew the session for any of the intermediate request/response handshakes.
+ */
+ @Override
+ public UserIdentity login(String username, Object password, ServletRequest servletRequest)
+ {
+ SpnegoUserIdentity user = (SpnegoUserIdentity)_loginService.login(username, password, servletRequest);
+ if (user != null && user.isEstablished())
+ {
+ Request request = Request.getBaseRequest(servletRequest);
+ renewSession(request, request == null ? null : request.getResponse());
+ }
+ return user;
+ }
+
+ @Override
+ public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
+ {
+ if (!mandatory)
+ return new DeferredAuthentication(this);
+
+ HttpServletRequest request = (HttpServletRequest)req;
+ HttpServletResponse response = (HttpServletResponse)res;
+
+ String header = request.getHeader(HttpHeader.AUTHORIZATION.asString());
+ String spnegoToken = getSpnegoToken(header);
+ HttpSession httpSession = request.getSession(false);
+
+ // We have a token from the client, so run the login.
+ if (header != null && spnegoToken != null)
+ {
+ SpnegoUserIdentity identity = (SpnegoUserIdentity)login(null, spnegoToken, request);
+ if (identity.isEstablished())
+ {
+ if (!DeferredAuthentication.isDeferred(response))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Sending final token");
+ // Send to the client the final token so that the
+ // client can establish the GSS context with the server.
+ SpnegoUserPrincipal principal = (SpnegoUserPrincipal)identity.getUserPrincipal();
+ setSpnegoToken(response, principal.getEncodedToken());
+ }
+
+ Duration authnDuration = getAuthenticationDuration();
+ if (!authnDuration.isNegative())
+ {
+ if (httpSession == null)
+ httpSession = request.getSession(true);
+ httpSession.setAttribute(UserIdentityHolder.ATTRIBUTE, new UserIdentityHolder(identity));
+ }
+ return new UserAuthentication(getAuthMethod(), identity);
+ }
+ else
+ {
+ if (DeferredAuthentication.isDeferred(response))
+ return Authentication.UNAUTHENTICATED;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Sending intermediate challenge");
+ SpnegoUserPrincipal principal = (SpnegoUserPrincipal)identity.getUserPrincipal();
+ sendChallenge(response, principal.getEncodedToken());
+ return Authentication.SEND_CONTINUE;
+ }
+ }
+ // No token from the client; check if the client has logged in
+ // successfully before and the authentication has not expired.
+ else if (httpSession != null)
+ {
+ UserIdentityHolder holder = (UserIdentityHolder)httpSession.getAttribute(UserIdentityHolder.ATTRIBUTE);
+ if (holder != null)
+ {
+ UserIdentity identity = holder._userIdentity;
+ if (identity != null)
+ {
+ Duration authnDuration = getAuthenticationDuration();
+ if (!authnDuration.isNegative())
+ {
+ boolean expired = !authnDuration.isZero() && Instant.now().isAfter(holder._validFrom.plus(authnDuration));
+ // Allow non-GET requests even if they're expired, so that
+ // the client does not need to send the request content again.
+ if (!expired || !HttpMethod.GET.is(request.getMethod()))
+ return new UserAuthentication(getAuthMethod(), identity);
+ }
+ }
+ }
+ }
+
+ if (DeferredAuthentication.isDeferred(response))
+ return Authentication.UNAUTHENTICATED;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Sending initial challenge");
+ sendChallenge(response, null);
+ return Authentication.SEND_CONTINUE;
+ }
+
+ private void sendChallenge(HttpServletResponse response, String token) throws ServerAuthException
+ {
+ try
+ {
+ setSpnegoToken(response, token);
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ }
+ catch (IOException x)
+ {
+ throw new ServerAuthException(x);
+ }
+ }
+
+ private void setSpnegoToken(HttpServletResponse response, String token)
+ {
+ String value = HttpHeader.NEGOTIATE.asString();
+ if (token != null)
+ value += " " + token;
+ response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), value);
+ }
+
+ private String getSpnegoToken(String header)
+ {
+ if (header == null)
+ return null;
+ String scheme = HttpHeader.NEGOTIATE.asString() + " ";
+ if (header.regionMatches(true, 0, scheme, 0, scheme.length()))
+ return header.substring(scheme.length()).trim();
+ return null;
+ }
+
+ @Override
+ public boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, User validatedUser)
+ {
+ return true;
+ }
+
+ private static class UserIdentityHolder implements Serializable
+ {
+ private static final String ATTRIBUTE = UserIdentityHolder.class.getName();
+
+ private final transient Instant _validFrom = Instant.now();
+ private final transient UserIdentity _userIdentity;
+
+ private UserIdentityHolder(UserIdentity userIdentity)
+ {
+ _userIdentity = userIdentity;
+ }
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DeferredAuthentication.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DeferredAuthentication.java
new file mode 100644
index 0000000..ba627d6
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DeferredAuthentication.java
@@ -0,0 +1,395 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security.authentication;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Locale;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.WriteListener;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.security.IdentityService;
+import org.eclipse.jetty.security.LoggedOutAuthentication;
+import org.eclipse.jetty.security.LoginService;
+import org.eclipse.jetty.security.SecurityHandler;
+import org.eclipse.jetty.security.ServerAuthException;
+import org.eclipse.jetty.security.UserAuthentication;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class DeferredAuthentication implements Authentication.Deferred
+{
+ private static final Logger LOG = Log.getLogger(DeferredAuthentication.class);
+ protected final LoginAuthenticator _authenticator;
+ private Object _previousAssociation;
+
+ public DeferredAuthentication(LoginAuthenticator authenticator)
+ {
+ if (authenticator == null)
+ throw new NullPointerException("No Authenticator");
+ this._authenticator = authenticator;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.Authentication.Deferred#authenticate(ServletRequest)
+ */
+ @Override
+ public Authentication authenticate(ServletRequest request)
+ {
+ try
+ {
+ Authentication authentication = _authenticator.validateRequest(request, __deferredResponse, true);
+ if (authentication != null && (authentication instanceof Authentication.User) && !(authentication instanceof Authentication.ResponseSent))
+ {
+ LoginService loginService = _authenticator.getLoginService();
+ IdentityService identityService = loginService.getIdentityService();
+
+ if (identityService != null)
+ _previousAssociation = identityService.associate(((Authentication.User)authentication).getUserIdentity());
+
+ return authentication;
+ }
+ }
+ catch (ServerAuthException e)
+ {
+ LOG.debug(e);
+ }
+
+ return this;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.Authentication.Deferred#authenticate(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
+ */
+ @Override
+ public Authentication authenticate(ServletRequest request, ServletResponse response)
+ {
+ try
+ {
+ LoginService loginService = _authenticator.getLoginService();
+ IdentityService identityService = loginService.getIdentityService();
+
+ Authentication authentication = _authenticator.validateRequest(request, response, true);
+ if (authentication instanceof Authentication.User && identityService != null)
+ _previousAssociation = identityService.associate(((Authentication.User)authentication).getUserIdentity());
+ return authentication;
+ }
+ catch (ServerAuthException e)
+ {
+ LOG.debug(e);
+ }
+ return this;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.Authentication.Deferred#login(String, Object, ServletRequest)
+ */
+ @Override
+ public Authentication login(String username, Object password, ServletRequest request)
+ {
+ if (username == null)
+ return null;
+
+ UserIdentity identity = _authenticator.login(username, password, request);
+ if (identity != null)
+ {
+ IdentityService identityService = _authenticator.getLoginService().getIdentityService();
+ UserAuthentication authentication = new UserAuthentication("API", identity);
+ if (identityService != null)
+ _previousAssociation = identityService.associate(identity);
+ return authentication;
+ }
+ return null;
+ }
+
+ @Override
+ public Authentication logout(ServletRequest request)
+ {
+ SecurityHandler security = SecurityHandler.getCurrentSecurityHandler();
+ if (security != null)
+ {
+ security.logout(null);
+ if (_authenticator instanceof LoginAuthenticator)
+ {
+ _authenticator.logout(request);
+ return new LoggedOutAuthentication(_authenticator);
+ }
+ }
+
+ return Authentication.UNAUTHENTICATED;
+ }
+
+ public Object getPreviousAssociation()
+ {
+ return _previousAssociation;
+ }
+
+ /**
+ * @param response the response
+ * @return true if this response is from a deferred call to {@link #authenticate(ServletRequest)}
+ */
+ public static boolean isDeferred(HttpServletResponse response)
+ {
+ return response == __deferredResponse;
+ }
+
+ static final HttpServletResponse __deferredResponse = new HttpServletResponse()
+ {
+ @Override
+ public void addCookie(Cookie cookie)
+ {
+ }
+
+ @Override
+ public void addDateHeader(String name, long date)
+ {
+ }
+
+ @Override
+ public void addHeader(String name, String value)
+ {
+ }
+
+ @Override
+ public void addIntHeader(String name, int value)
+ {
+ }
+
+ @Override
+ public boolean containsHeader(String name)
+ {
+ return false;
+ }
+
+ @Override
+ public String encodeRedirectURL(String url)
+ {
+ return null;
+ }
+
+ @Override
+ public String encodeRedirectUrl(String url)
+ {
+ return null;
+ }
+
+ @Override
+ public String encodeURL(String url)
+ {
+ return null;
+ }
+
+ @Override
+ public String encodeUrl(String url)
+ {
+ return null;
+ }
+
+ @Override
+ public void sendError(int sc) throws IOException
+ {
+ }
+
+ @Override
+ public void sendError(int sc, String msg) throws IOException
+ {
+ }
+
+ @Override
+ public void sendRedirect(String location) throws IOException
+ {
+ }
+
+ @Override
+ public void setDateHeader(String name, long date)
+ {
+ }
+
+ @Override
+ public void setHeader(String name, String value)
+ {
+ }
+
+ @Override
+ public void setIntHeader(String name, int value)
+ {
+ }
+
+ @Override
+ public void setStatus(int sc)
+ {
+ }
+
+ @Override
+ public void setStatus(int sc, String sm)
+ {
+ }
+
+ @Override
+ public void flushBuffer() throws IOException
+ {
+ }
+
+ @Override
+ public int getBufferSize()
+ {
+ return 1024;
+ }
+
+ @Override
+ public String getCharacterEncoding()
+ {
+ return null;
+ }
+
+ @Override
+ public String getContentType()
+ {
+ return null;
+ }
+
+ @Override
+ public Locale getLocale()
+ {
+ return null;
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() throws IOException
+ {
+ return __nullOut;
+ }
+
+ @Override
+ public PrintWriter getWriter() throws IOException
+ {
+ return IO.getNullPrintWriter();
+ }
+
+ @Override
+ public boolean isCommitted()
+ {
+ return true;
+ }
+
+ @Override
+ public void reset()
+ {
+ }
+
+ @Override
+ public void resetBuffer()
+ {
+ }
+
+ @Override
+ public void setBufferSize(int size)
+ {
+ }
+
+ @Override
+ public void setCharacterEncoding(String charset)
+ {
+ }
+
+ @Override
+ public void setContentLength(int len)
+ {
+ }
+
+ @Override
+ public void setContentLengthLong(long len)
+ {
+
+ }
+
+ @Override
+ public void setContentType(String type)
+ {
+ }
+
+ @Override
+ public void setLocale(Locale loc)
+ {
+ }
+
+ @Override
+ public Collection<String> getHeaderNames()
+ {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public String getHeader(String arg0)
+ {
+ return null;
+ }
+
+ @Override
+ public Collection<String> getHeaders(String arg0)
+ {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public int getStatus()
+ {
+ return 0;
+ }
+ };
+
+ private static ServletOutputStream __nullOut = new ServletOutputStream()
+ {
+ @Override
+ public void write(int b) throws IOException
+ {
+ }
+
+ @Override
+ public void print(String s) throws IOException
+ {
+ }
+
+ @Override
+ public void println(String s) throws IOException
+ {
+ }
+
+ @Override
+ public void setWriteListener(WriteListener writeListener)
+ {
+
+ }
+
+ @Override
+ public boolean isReady()
+ {
+ return false;
+ }
+ };
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DigestAuthenticator.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DigestAuthenticator.java
new file mode 100644
index 0000000..bf2f7e6
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DigestAuthenticator.java
@@ -0,0 +1,399 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security.authentication;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.BitSet;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ConcurrentMap;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.security.SecurityHandler;
+import org.eclipse.jetty.security.ServerAuthException;
+import org.eclipse.jetty.security.UserAuthentication;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Authentication.User;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.QuotedStringTokenizer;
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.security.Constraint;
+import org.eclipse.jetty.util.security.Credential;
+
+/**
+ * The nonce max age in ms can be set with the {@link SecurityHandler#setInitParameter(String, String)}
+ * using the name "maxNonceAge". The nonce max count can be set with {@link SecurityHandler#setInitParameter(String, String)}
+ * using the name "maxNonceCount". When the age or count is exceeded, the nonce is considered stale.
+ */
+public class DigestAuthenticator extends LoginAuthenticator
+{
+ private static final Logger LOG = Log.getLogger(DigestAuthenticator.class);
+
+ private final SecureRandom _random = new SecureRandom();
+ private long _maxNonceAgeMs = 60 * 1000;
+ private int _maxNC = 1024;
+ private ConcurrentMap<String, Nonce> _nonceMap = new ConcurrentHashMap<>();
+ private Queue<Nonce> _nonceQueue = new ConcurrentLinkedQueue<>();
+
+ @Override
+ public void setConfiguration(AuthConfiguration configuration)
+ {
+ super.setConfiguration(configuration);
+
+ String mna = configuration.getInitParameter("maxNonceAge");
+ if (mna != null)
+ setMaxNonceAge(Long.parseLong(mna));
+ String mnc = configuration.getInitParameter("maxNonceCount");
+ if (mnc != null)
+ setMaxNonceCount(Integer.parseInt(mnc));
+ }
+
+ public int getMaxNonceCount()
+ {
+ return _maxNC;
+ }
+
+ public void setMaxNonceCount(int maxNC)
+ {
+ _maxNC = maxNC;
+ }
+
+ public long getMaxNonceAge()
+ {
+ return _maxNonceAgeMs;
+ }
+
+ public void setMaxNonceAge(long maxNonceAgeInMillis)
+ {
+ _maxNonceAgeMs = maxNonceAgeInMillis;
+ }
+
+ @Override
+ public String getAuthMethod()
+ {
+ return Constraint.__DIGEST_AUTH;
+ }
+
+ @Override
+ public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException
+ {
+ return true;
+ }
+
+ @Override
+ public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
+ {
+ if (!mandatory)
+ return new DeferredAuthentication(this);
+
+ HttpServletRequest request = (HttpServletRequest)req;
+ HttpServletResponse response = (HttpServletResponse)res;
+ String credentials = request.getHeader(HttpHeader.AUTHORIZATION.asString());
+
+ try
+ {
+ Request baseRequest = Request.getBaseRequest(request);
+
+ boolean stale = false;
+ if (credentials != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Credentials: " + credentials);
+ QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials, "=, ", true, false);
+ final Digest digest = new Digest(request.getMethod());
+ String last = null;
+ String name = null;
+
+ while (tokenizer.hasMoreTokens())
+ {
+ String tok = tokenizer.nextToken();
+ char c = (tok.length() == 1) ? tok.charAt(0) : '\0';
+
+ switch (c)
+ {
+ case '=':
+ name = last;
+ last = tok;
+ break;
+ case ',':
+ name = null;
+ break;
+ case ' ':
+ break;
+
+ default:
+ last = tok;
+ if (name != null)
+ {
+ if ("username".equalsIgnoreCase(name))
+ digest.username = tok;
+ else if ("realm".equalsIgnoreCase(name))
+ digest.realm = tok;
+ else if ("nonce".equalsIgnoreCase(name))
+ digest.nonce = tok;
+ else if ("nc".equalsIgnoreCase(name))
+ digest.nc = tok;
+ else if ("cnonce".equalsIgnoreCase(name))
+ digest.cnonce = tok;
+ else if ("qop".equalsIgnoreCase(name))
+ digest.qop = tok;
+ else if ("uri".equalsIgnoreCase(name))
+ digest.uri = tok;
+ else if ("response".equalsIgnoreCase(name))
+ digest.response = tok;
+ name = null;
+ }
+ }
+ }
+
+ int n = checkNonce(digest, baseRequest);
+
+ if (n > 0)
+ {
+ //UserIdentity user = _loginService.login(digest.username,digest);
+ UserIdentity user = login(digest.username, digest, req);
+ if (user != null)
+ {
+ return new UserAuthentication(getAuthMethod(), user);
+ }
+ }
+ else if (n == 0)
+ stale = true;
+ }
+
+ if (!DeferredAuthentication.isDeferred(response))
+ {
+ String domain = request.getContextPath();
+ if (domain == null)
+ domain = "/";
+ response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), "Digest realm=\"" + _loginService.getName() +
+ "\", domain=\"" + domain +
+ "\", nonce=\"" + newNonce(baseRequest) +
+ "\", algorithm=MD5" +
+ ", qop=\"auth\"" +
+ ", stale=" + stale);
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+
+ return Authentication.SEND_CONTINUE;
+ }
+
+ return Authentication.UNAUTHENTICATED;
+ }
+ catch (IOException e)
+ {
+ throw new ServerAuthException(e);
+ }
+ }
+
+ @Override
+ public UserIdentity login(String username, Object credentials, ServletRequest request)
+ {
+ Digest digest = (Digest)credentials;
+ if (!Objects.equals(digest.realm, _loginService.getName()))
+ return null;
+ return super.login(username, credentials, request);
+ }
+
+ public String newNonce(Request request)
+ {
+ Nonce nonce;
+
+ do
+ {
+ byte[] nounce = new byte[24];
+ _random.nextBytes(nounce);
+
+ nonce = new Nonce(Base64.getEncoder().encodeToString(nounce), request.getTimeStamp(), getMaxNonceCount());
+ }
+ while (_nonceMap.putIfAbsent(nonce._nonce, nonce) != null);
+ _nonceQueue.add(nonce);
+
+ return nonce._nonce;
+ }
+
+ /**
+ * @param digest the digest data to check
+ * @param request the request object
+ * @return -1 for a bad nonce, 0 for a stale none, 1 for a good nonce
+ */
+ private int checkNonce(Digest digest, Request request)
+ {
+ // firstly let's expire old nonces
+ long expired = request.getTimeStamp() - getMaxNonceAge();
+ Nonce nonce = _nonceQueue.peek();
+ while (nonce != null && nonce._ts < expired)
+ {
+ _nonceQueue.remove(nonce);
+ _nonceMap.remove(nonce._nonce);
+ nonce = _nonceQueue.peek();
+ }
+
+ // Now check the requested nonce
+ try
+ {
+ nonce = _nonceMap.get(digest.nonce);
+ if (nonce == null)
+ return 0;
+
+ long count = Long.parseLong(digest.nc, 16);
+ if (count >= _maxNC)
+ return 0;
+
+ if (nonce.seen((int)count))
+ return -1;
+
+ return 1;
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+ return -1;
+ }
+
+ private static class Nonce
+ {
+ final String _nonce;
+ final long _ts;
+ final BitSet _seen;
+
+ public Nonce(String nonce, long ts, int size)
+ {
+ _nonce = nonce;
+ _ts = ts;
+ _seen = new BitSet(size);
+ }
+
+ public boolean seen(int count)
+ {
+ synchronized (this)
+ {
+ if (count >= _seen.size())
+ return true;
+ boolean s = _seen.get(count);
+ _seen.set(count);
+ return s;
+ }
+ }
+ }
+
+ private static class Digest extends Credential
+ {
+ private static final long serialVersionUID = -2484639019549527724L;
+ final String method;
+ String username = "";
+ String realm = "";
+ String nonce = "";
+ String nc = "";
+ String cnonce = "";
+ String qop = "";
+ String uri = "";
+ String response = "";
+
+ Digest(String m)
+ {
+ method = m;
+ }
+
+ @Override
+ public boolean check(Object credentials)
+ {
+ if (credentials instanceof char[])
+ credentials = new String((char[])credentials);
+ String password = (credentials instanceof String) ? (String)credentials : credentials.toString();
+
+ try
+ {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] ha1;
+ if (credentials instanceof Credential.MD5)
+ {
+ // Credentials are already a MD5 digest - assume it's in
+ // form user:realm:password (we have no way to know since
+ // it's a digest, alright?)
+ ha1 = ((Credential.MD5)credentials).getDigest();
+ }
+ else
+ {
+ // calc A1 digest
+ md.update(username.getBytes(StandardCharsets.ISO_8859_1));
+ md.update((byte)':');
+ md.update(realm.getBytes(StandardCharsets.ISO_8859_1));
+ md.update((byte)':');
+ md.update(password.getBytes(StandardCharsets.ISO_8859_1));
+ ha1 = md.digest();
+ }
+ // calc A2 digest
+ md.reset();
+ md.update(method.getBytes(StandardCharsets.ISO_8859_1));
+ md.update((byte)':');
+ md.update(uri.getBytes(StandardCharsets.ISO_8859_1));
+ byte[] ha2 = md.digest();
+
+ // calc digest
+ // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":"
+ // nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) )
+ // <">
+ // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2)
+ // ) > <">
+
+ md.update(TypeUtil.toString(ha1, 16).getBytes(StandardCharsets.ISO_8859_1));
+ md.update((byte)':');
+ md.update(nonce.getBytes(StandardCharsets.ISO_8859_1));
+ md.update((byte)':');
+ md.update(nc.getBytes(StandardCharsets.ISO_8859_1));
+ md.update((byte)':');
+ md.update(cnonce.getBytes(StandardCharsets.ISO_8859_1));
+ md.update((byte)':');
+ md.update(qop.getBytes(StandardCharsets.ISO_8859_1));
+ md.update((byte)':');
+ md.update(TypeUtil.toString(ha2, 16).getBytes(StandardCharsets.ISO_8859_1));
+ byte[] digest = md.digest();
+
+ // check digest
+ return stringEquals(TypeUtil.toString(digest, 16).toLowerCase(), response == null ? null : response.toLowerCase());
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+
+ return false;
+ }
+
+ @Override
+ public String toString()
+ {
+ return username + "," + response;
+ }
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java
new file mode 100644
index 0000000..3b879b1
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java
@@ -0,0 +1,546 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security.authentication;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Locale;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+import javax.servlet.http.HttpSession;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpHeaderValue;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.security.ServerAuthException;
+import org.eclipse.jetty.security.UserAuthentication;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Authentication.User;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.security.Constraint;
+
+/**
+ * FORM Authenticator.
+ *
+ * <p>This authenticator implements form authentication will use dispatchers to
+ * the login page if the {@link #__FORM_DISPATCH} init parameter is set to true.
+ * Otherwise it will redirect.</p>
+ *
+ * <p>The form authenticator redirects unauthenticated requests to a log page
+ * which should use a form to gather username/password from the user and send them
+ * to the /j_security_check URI within the context. FormAuthentication uses
+ * {@link SessionAuthentication} to wrap Authentication results so that they
+ * are associated with the session.</p>
+ */
+public class FormAuthenticator extends LoginAuthenticator
+{
+ private static final Logger LOG = Log.getLogger(FormAuthenticator.class);
+
+ public static final String __FORM_LOGIN_PAGE = "org.eclipse.jetty.security.form_login_page";
+ public static final String __FORM_ERROR_PAGE = "org.eclipse.jetty.security.form_error_page";
+ public static final String __FORM_DISPATCH = "org.eclipse.jetty.security.dispatch";
+ public static final String __J_URI = "org.eclipse.jetty.security.form_URI";
+ public static final String __J_POST = "org.eclipse.jetty.security.form_POST";
+ public static final String __J_METHOD = "org.eclipse.jetty.security.form_METHOD";
+ public static final String __J_SECURITY_CHECK = "/j_security_check";
+ public static final String __J_USERNAME = "j_username";
+ public static final String __J_PASSWORD = "j_password";
+
+ private String _formErrorPage;
+ private String _formErrorPath;
+ private String _formLoginPage;
+ private String _formLoginPath;
+ private boolean _dispatch;
+ private boolean _alwaysSaveUri;
+
+ public FormAuthenticator()
+ {
+ }
+
+ public FormAuthenticator(String login, String error, boolean dispatch)
+ {
+ this();
+ if (login != null)
+ setLoginPage(login);
+ if (error != null)
+ setErrorPage(error);
+ _dispatch = dispatch;
+ }
+
+ /**
+ * If true, uris that cause a redirect to a login page will always
+ * be remembered. If false, only the first uri that leads to a login
+ * page redirect is remembered.
+ * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=379909
+ *
+ * @param alwaysSave true to always save the uri
+ */
+ public void setAlwaysSaveUri(boolean alwaysSave)
+ {
+ _alwaysSaveUri = alwaysSave;
+ }
+
+ public boolean getAlwaysSaveUri()
+ {
+ return _alwaysSaveUri;
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.authentication.LoginAuthenticator#setConfiguration(org.eclipse.jetty.security.Authenticator.AuthConfiguration)
+ */
+ @Override
+ public void setConfiguration(AuthConfiguration configuration)
+ {
+ super.setConfiguration(configuration);
+ String login = configuration.getInitParameter(FormAuthenticator.__FORM_LOGIN_PAGE);
+ if (login != null)
+ setLoginPage(login);
+ String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE);
+ if (error != null)
+ setErrorPage(error);
+ String dispatch = configuration.getInitParameter(FormAuthenticator.__FORM_DISPATCH);
+ _dispatch = dispatch == null ? _dispatch : Boolean.parseBoolean(dispatch);
+ }
+
+ @Override
+ public String getAuthMethod()
+ {
+ return Constraint.__FORM_AUTH;
+ }
+
+ private void setLoginPage(String path)
+ {
+ if (!path.startsWith("/"))
+ {
+ LOG.warn("form-login-page must start with /");
+ path = "/" + path;
+ }
+ _formLoginPage = path;
+ _formLoginPath = path;
+ if (_formLoginPath.indexOf('?') > 0)
+ _formLoginPath = _formLoginPath.substring(0, _formLoginPath.indexOf('?'));
+ }
+
+ private void setErrorPage(String path)
+ {
+ if (path == null || path.trim().length() == 0)
+ {
+ _formErrorPath = null;
+ _formErrorPage = null;
+ }
+ else
+ {
+ if (!path.startsWith("/"))
+ {
+ LOG.warn("form-error-page must start with /");
+ path = "/" + path;
+ }
+ _formErrorPage = path;
+ _formErrorPath = path;
+
+ if (_formErrorPath.indexOf('?') > 0)
+ _formErrorPath = _formErrorPath.substring(0, _formErrorPath.indexOf('?'));
+ }
+ }
+
+ @Override
+ public UserIdentity login(String username, Object password, ServletRequest request)
+ {
+
+ UserIdentity user = super.login(username, password, request);
+ if (user != null)
+ {
+
+ HttpSession session = ((HttpServletRequest)request).getSession(true);
+ Authentication cached = new SessionAuthentication(getAuthMethod(), user, password);
+ session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached);
+ }
+ return user;
+ }
+
+ @Override
+ public void logout(ServletRequest request)
+ {
+ super.logout(request);
+ HttpServletRequest httpRequest = (HttpServletRequest)request;
+ HttpSession session = httpRequest.getSession(false);
+
+ if (session == null)
+ return;
+
+ //clean up session
+ session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
+ }
+
+ @Override
+ public void prepareRequest(ServletRequest request)
+ {
+ //if this is a request resulting from a redirect after auth is complete
+ //(ie its from a redirect to the original request uri) then due to
+ //browser handling of 302 redirects, the method may not be the same as
+ //that of the original request. Replace the method and original post
+ //params (if it was a post).
+ //
+ //See Servlet Spec 3.1 sec 13.6.3
+ HttpServletRequest httpRequest = (HttpServletRequest)request;
+ HttpSession session = httpRequest.getSession(false);
+ if (session == null || session.getAttribute(SessionAuthentication.__J_AUTHENTICATED) == null)
+ return; //not authenticated yet
+
+ String juri = (String)session.getAttribute(__J_URI);
+ if (juri == null || juri.length() == 0)
+ return; //no original uri saved
+
+ String method = (String)session.getAttribute(__J_METHOD);
+ if (method == null || method.length() == 0)
+ return; //didn't save original request method
+
+ StringBuffer buf = httpRequest.getRequestURL();
+ if (httpRequest.getQueryString() != null)
+ buf.append("?").append(httpRequest.getQueryString());
+
+ if (!juri.equals(buf.toString()))
+ return; //this request is not for the same url as the original
+
+ //restore the original request's method on this request
+ if (LOG.isDebugEnabled())
+ LOG.debug("Restoring original method {} for {} with method {}", method, juri, httpRequest.getMethod());
+ Request baseRequest = Request.getBaseRequest(request);
+ baseRequest.setMethod(method);
+ }
+
+ @Override
+ public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
+ {
+ HttpServletRequest request = (HttpServletRequest)req;
+ HttpServletResponse response = (HttpServletResponse)res;
+ Request baseRequest = Request.getBaseRequest(request);
+ Response baseResponse = baseRequest.getResponse();
+
+ String uri = request.getRequestURI();
+ if (uri == null)
+ uri = URIUtil.SLASH;
+
+ mandatory |= isJSecurityCheck(uri);
+ if (!mandatory)
+ return new DeferredAuthentication(this);
+
+ if (isLoginOrErrorPage(URIUtil.addPaths(request.getServletPath(), request.getPathInfo())) && !DeferredAuthentication.isDeferred(response))
+ return new DeferredAuthentication(this);
+
+ try
+ {
+ // Handle a request for authentication.
+ if (isJSecurityCheck(uri))
+ {
+ final String username = request.getParameter(__J_USERNAME);
+ final String password = request.getParameter(__J_PASSWORD);
+
+ UserIdentity user = login(username, password, request);
+ LOG.debug("jsecuritycheck {} {}", username, user);
+ HttpSession session = request.getSession(false);
+ if (user != null)
+ {
+ // Redirect to original request
+ String nuri;
+ FormAuthentication formAuth;
+ synchronized (session)
+ {
+ nuri = (String)session.getAttribute(__J_URI);
+
+ if (nuri == null || nuri.length() == 0)
+ {
+ nuri = request.getContextPath();
+ if (nuri.length() == 0)
+ nuri = URIUtil.SLASH;
+ }
+ formAuth = new FormAuthentication(getAuthMethod(), user);
+ }
+ LOG.debug("authenticated {}->{}", formAuth, nuri);
+
+ response.setContentLength(0);
+ baseResponse.sendRedirect(response.encodeRedirectURL(nuri), true);
+ return formAuth;
+ }
+
+ // not authenticated
+ if (LOG.isDebugEnabled())
+ LOG.debug("Form authentication FAILED for " + StringUtil.printable(username));
+ if (_formErrorPage == null)
+ {
+ LOG.debug("auth failed {}->403", username);
+ if (response != null)
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ }
+ else if (_dispatch)
+ {
+ LOG.debug("auth failed {}=={}", username, _formErrorPage);
+ RequestDispatcher dispatcher = request.getRequestDispatcher(_formErrorPage);
+ response.setHeader(HttpHeader.CACHE_CONTROL.asString(), HttpHeaderValue.NO_CACHE.asString());
+ response.setDateHeader(HttpHeader.EXPIRES.asString(), 1);
+ dispatcher.forward(new FormRequest(request), new FormResponse(response));
+ }
+ else
+ {
+ LOG.debug("auth failed {}->{}", username, _formErrorPage);
+ baseResponse.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), _formErrorPage)), true);
+ }
+
+ return Authentication.SEND_FAILURE;
+ }
+
+ // Look for cached authentication
+ HttpSession session = request.getSession(false);
+ Authentication authentication = session == null ? null : (Authentication)session.getAttribute(SessionAuthentication.__J_AUTHENTICATED);
+ if (authentication != null)
+ {
+ // Has authentication been revoked?
+ if (authentication instanceof Authentication.User &&
+ _loginService != null &&
+ !_loginService.validate(((Authentication.User)authentication).getUserIdentity()))
+ {
+ LOG.debug("auth revoked {}", authentication);
+ session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
+ }
+ else
+ {
+ synchronized (session)
+ {
+ String jUri = (String)session.getAttribute(__J_URI);
+ if (jUri != null)
+ {
+ //check if the request is for the same url as the original and restore
+ //params if it was a post
+ LOG.debug("auth retry {}->{}", authentication, jUri);
+ StringBuffer buf = request.getRequestURL();
+ if (request.getQueryString() != null)
+ buf.append("?").append(request.getQueryString());
+
+ if (jUri.equals(buf.toString()))
+ {
+ MultiMap<String> jPost = (MultiMap<String>)session.getAttribute(__J_POST);
+ if (jPost != null)
+ {
+ LOG.debug("auth rePOST {}->{}", authentication, jUri);
+ baseRequest.setContentParameters(jPost);
+ }
+ session.removeAttribute(__J_URI);
+ session.removeAttribute(__J_METHOD);
+ session.removeAttribute(__J_POST);
+ }
+ }
+ }
+ LOG.debug("auth {}", authentication);
+ return authentication;
+ }
+ }
+
+ // if we can't send challenge
+ if (DeferredAuthentication.isDeferred(response))
+ {
+ LOG.debug("auth deferred {}", session == null ? null : session.getId());
+ return Authentication.UNAUTHENTICATED;
+ }
+
+ // remember the current URI
+ session = (session != null ? session : request.getSession(true));
+ synchronized (session)
+ {
+ // But only if it is not set already, or we save every uri that leads to a login form redirect
+ if (session.getAttribute(__J_URI) == null || _alwaysSaveUri)
+ {
+ StringBuffer buf = request.getRequestURL();
+ if (request.getQueryString() != null)
+ buf.append("?").append(request.getQueryString());
+ session.setAttribute(__J_URI, buf.toString());
+ session.setAttribute(__J_METHOD, request.getMethod());
+
+ if (MimeTypes.Type.FORM_ENCODED.is(req.getContentType()) && HttpMethod.POST.is(request.getMethod()))
+ {
+ MultiMap<String> formParameters = new MultiMap<>();
+ baseRequest.extractFormParameters(formParameters);
+ session.setAttribute(__J_POST, formParameters);
+ }
+ }
+ }
+
+ // send the the challenge
+ if (_dispatch)
+ {
+ LOG.debug("challenge {}=={}", session.getId(), _formLoginPage);
+ RequestDispatcher dispatcher = request.getRequestDispatcher(_formLoginPage);
+ response.setHeader(HttpHeader.CACHE_CONTROL.asString(), HttpHeaderValue.NO_CACHE.asString());
+ response.setDateHeader(HttpHeader.EXPIRES.asString(), 1);
+ dispatcher.forward(new FormRequest(request), new FormResponse(response));
+ }
+ else
+ {
+ LOG.debug("challenge {}->{}", session.getId(), _formLoginPage);
+ baseResponse.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), _formLoginPage)), true);
+ }
+ return Authentication.SEND_CONTINUE;
+ }
+ catch (IOException | ServletException e)
+ {
+ throw new ServerAuthException(e);
+ }
+ }
+
+ public boolean isJSecurityCheck(String uri)
+ {
+ int jsc = uri.indexOf(__J_SECURITY_CHECK);
+
+ if (jsc < 0)
+ return false;
+ int e = jsc + __J_SECURITY_CHECK.length();
+ if (e == uri.length())
+ return true;
+ char c = uri.charAt(e);
+ return c == ';' || c == '#' || c == '/' || c == '?';
+ }
+
+ public boolean isLoginOrErrorPage(String pathInContext)
+ {
+ return pathInContext != null && (pathInContext.equals(_formErrorPath) || pathInContext.equals(_formLoginPath));
+ }
+
+ @Override
+ public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException
+ {
+ return true;
+ }
+
+ protected static class FormRequest extends HttpServletRequestWrapper
+ {
+ public FormRequest(HttpServletRequest request)
+ {
+ super(request);
+ }
+
+ @Override
+ public long getDateHeader(String name)
+ {
+ if (name.toLowerCase(Locale.ENGLISH).startsWith("if-"))
+ return -1;
+ return super.getDateHeader(name);
+ }
+
+ @Override
+ public String getHeader(String name)
+ {
+ if (name.toLowerCase(Locale.ENGLISH).startsWith("if-"))
+ return null;
+ return super.getHeader(name);
+ }
+
+ @Override
+ public Enumeration<String> getHeaderNames()
+ {
+ return Collections.enumeration(Collections.list(super.getHeaderNames()));
+ }
+
+ @Override
+ public Enumeration<String> getHeaders(String name)
+ {
+ if (name.toLowerCase(Locale.ENGLISH).startsWith("if-"))
+ return Collections.enumeration(Collections.emptyList());
+ return super.getHeaders(name);
+ }
+ }
+
+ protected static class FormResponse extends HttpServletResponseWrapper
+ {
+ public FormResponse(HttpServletResponse response)
+ {
+ super(response);
+ }
+
+ @Override
+ public void addDateHeader(String name, long date)
+ {
+ if (notIgnored(name))
+ super.addDateHeader(name, date);
+ }
+
+ @Override
+ public void addHeader(String name, String value)
+ {
+ if (notIgnored(name))
+ super.addHeader(name, value);
+ }
+
+ @Override
+ public void setDateHeader(String name, long date)
+ {
+ if (notIgnored(name))
+ super.setDateHeader(name, date);
+ }
+
+ @Override
+ public void setHeader(String name, String value)
+ {
+ if (notIgnored(name))
+ super.setHeader(name, value);
+ }
+
+ private boolean notIgnored(String name)
+ {
+ return !HttpHeader.CACHE_CONTROL.is(name) &&
+ !HttpHeader.PRAGMA.is(name) &&
+ !HttpHeader.ETAG.is(name) &&
+ !HttpHeader.EXPIRES.is(name) &&
+ !HttpHeader.LAST_MODIFIED.is(name) &&
+ !HttpHeader.AGE.is(name);
+ }
+ }
+
+ /**
+ * This Authentication represents a just completed Form authentication.
+ * Subsequent requests from the same user are authenticated by the presents
+ * of a {@link SessionAuthentication} instance in their session.
+ */
+ public static class FormAuthentication extends UserAuthentication implements Authentication.ResponseSent
+ {
+ public FormAuthentication(String method, UserIdentity userIdentity)
+ {
+ super(method, userIdentity);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Form" + super.toString();
+ }
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java
new file mode 100644
index 0000000..4a28e50
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java
@@ -0,0 +1,149 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security.authentication;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.eclipse.jetty.security.Authenticator;
+import org.eclipse.jetty.security.IdentityService;
+import org.eclipse.jetty.security.LoginService;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.server.session.Session;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public abstract class LoginAuthenticator implements Authenticator
+{
+ private static final Logger LOG = Log.getLogger(LoginAuthenticator.class);
+
+ protected LoginService _loginService;
+ protected IdentityService _identityService;
+ private boolean _renewSession;
+
+ protected LoginAuthenticator()
+ {
+ }
+
+ @Override
+ public void prepareRequest(ServletRequest request)
+ {
+ //empty implementation as the default
+ }
+
+ /**
+ * If the UserIdentity is not null after this method calls {@link LoginService#login(String, Object, ServletRequest)}, it
+ * is assumed that the user is fully authenticated and we need to change the session id to prevent
+ * session fixation vulnerability. If the UserIdentity is not necessarily fully
+ * authenticated, then subclasses must override this method and
+ * determine when the UserIdentity IS fully authenticated and renew the session id.
+ *
+ * @param username the username of the client to be authenticated
+ * @param password the user's credential
+ * @param servletRequest the inbound request that needs authentication
+ */
+ public UserIdentity login(String username, Object password, ServletRequest servletRequest)
+ {
+ UserIdentity user = _loginService.login(username, password, servletRequest);
+ if (user != null)
+ {
+ Request request = Request.getBaseRequest(servletRequest);
+ renewSession(request, request == null ? null : request.getResponse());
+ return user;
+ }
+ return null;
+ }
+
+ public void logout(ServletRequest request)
+ {
+ HttpServletRequest httpRequest = (HttpServletRequest)request;
+ HttpSession session = httpRequest.getSession(false);
+ if (session == null)
+ return;
+
+ session.removeAttribute(Session.SESSION_CREATED_SECURE);
+ }
+
+ @Override
+ public void setConfiguration(AuthConfiguration configuration)
+ {
+ _loginService = configuration.getLoginService();
+ if (_loginService == null)
+ throw new IllegalStateException("No LoginService for " + this + " in " + configuration);
+ _identityService = configuration.getIdentityService();
+ if (_identityService == null)
+ throw new IllegalStateException("No IdentityService for " + this + " in " + configuration);
+ _renewSession = configuration.isSessionRenewedOnAuthentication();
+ }
+
+ public LoginService getLoginService()
+ {
+ return _loginService;
+ }
+
+ /**
+ * Change the session id.
+ * The session is changed to a new instance with a new ID if and only if:<ul>
+ * <li>A session exists.
+ * <li>The {@link org.eclipse.jetty.security.Authenticator.AuthConfiguration#isSessionRenewedOnAuthentication()} returns true.
+ * <li>The session ID has been given to unauthenticated responses
+ * </ul>
+ *
+ * @param request the request
+ * @param response the response
+ * @return The new session.
+ */
+ protected HttpSession renewSession(HttpServletRequest request, HttpServletResponse response)
+ {
+ HttpSession httpSession = request.getSession(false);
+
+ if (_renewSession && httpSession != null)
+ {
+ synchronized (httpSession)
+ {
+ //if we should renew sessions, and there is an existing session that may have been seen by non-authenticated users
+ //(indicated by SESSION_SECURED not being set on the session) then we should change id
+ if (httpSession.getAttribute(Session.SESSION_CREATED_SECURE) != Boolean.TRUE)
+ {
+ if (httpSession instanceof Session)
+ {
+ Session s = (Session)httpSession;
+ String oldId = s.getId();
+ s.renewId(request);
+ s.setAttribute(Session.SESSION_CREATED_SECURE, Boolean.TRUE);
+ if (s.isIdChanged() && (response instanceof Response))
+ ((Response)response).replaceCookie(s.getSessionHandler().getSessionCookie(s, request.getContextPath(), request.isSecure()));
+ if (LOG.isDebugEnabled())
+ LOG.debug("renew {}->{}", oldId, s.getId());
+ }
+ else
+ {
+ LOG.warn("Unable to renew session " + httpSession);
+ }
+ return httpSession;
+ }
+ }
+ }
+ return httpSession;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginCallback.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginCallback.java
new file mode 100644
index 0000000..894a081
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginCallback.java
@@ -0,0 +1,51 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security.authentication;
+
+import java.security.Principal;
+import javax.security.auth.Subject;
+
+/**
+ * This is similar to the jaspi PasswordValidationCallback but includes user
+ * principal and group info as well.
+ *
+ * @version $Rev: 4792 $ $Date: 2009-03-18 22:55:52 +0100 (Wed, 18 Mar 2009) $
+ */
+public interface LoginCallback
+{
+ Subject getSubject();
+
+ String getUserName();
+
+ Object getCredential();
+
+ boolean isSuccess();
+
+ void setSuccess(boolean success);
+
+ Principal getUserPrincipal();
+
+ void setUserPrincipal(Principal userPrincipal);
+
+ String[] getRoles();
+
+ void setRoles(String[] roles);
+
+ void clearPassword();
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginCallbackImpl.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginCallbackImpl.java
new file mode 100644
index 0000000..3c39255
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginCallbackImpl.java
@@ -0,0 +1,117 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security.authentication;
+
+import java.security.Principal;
+import javax.security.auth.Subject;
+
+import org.eclipse.jetty.security.IdentityService;
+
+/**
+ * This is similar to the jaspi PasswordValidationCallback but includes user
+ * principal and group info as well.
+ *
+ * @version $Rev: 4793 $ $Date: 2009-03-19 00:00:01 +0100 (Thu, 19 Mar 2009) $
+ */
+public class LoginCallbackImpl implements LoginCallback
+{
+ // initial data
+ private final Subject subject;
+
+ private final String userName;
+
+ private Object credential;
+
+ private boolean success;
+
+ private Principal userPrincipal;
+
+ private String[] roles = IdentityService.NO_ROLES;
+
+ //TODO could use Credential instance instead of Object if Basic/Form create a Password object
+ public LoginCallbackImpl(Subject subject, String userName, Object credential)
+ {
+ this.subject = subject;
+ this.userName = userName;
+ this.credential = credential;
+ }
+
+ @Override
+ public Subject getSubject()
+ {
+ return subject;
+ }
+
+ @Override
+ public String getUserName()
+ {
+ return userName;
+ }
+
+ @Override
+ public Object getCredential()
+ {
+ return credential;
+ }
+
+ @Override
+ public boolean isSuccess()
+ {
+ return success;
+ }
+
+ @Override
+ public void setSuccess(boolean success)
+ {
+ this.success = success;
+ }
+
+ @Override
+ public Principal getUserPrincipal()
+ {
+ return userPrincipal;
+ }
+
+ @Override
+ public void setUserPrincipal(Principal userPrincipal)
+ {
+ this.userPrincipal = userPrincipal;
+ }
+
+ @Override
+ public String[] getRoles()
+ {
+ return roles;
+ }
+
+ @Override
+ public void setRoles(String[] groups)
+ {
+ this.roles = groups;
+ }
+
+ @Override
+ public void clearPassword()
+ {
+ if (credential != null)
+ {
+ credential = null;
+ }
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java
new file mode 100644
index 0000000..6329993
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java
@@ -0,0 +1,134 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security.authentication;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionActivationListener;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionBindingListener;
+import javax.servlet.http.HttpSessionEvent;
+
+import org.eclipse.jetty.security.AbstractUserAuthentication;
+import org.eclipse.jetty.security.LoginService;
+import org.eclipse.jetty.security.SecurityHandler;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * SessionAuthentication
+ *
+ * When a user has been successfully authenticated with some types
+ * of Authenticator, the Authenticator stashes a SessionAuthentication
+ * into an HttpSession to remember that the user is authenticated.
+ */
+public class SessionAuthentication extends AbstractUserAuthentication
+ implements Serializable, HttpSessionActivationListener, HttpSessionBindingListener
+{
+ private static final Logger LOG = Log.getLogger(SessionAuthentication.class);
+
+ private static final long serialVersionUID = -4643200685888258706L;
+
+ public static final String __J_AUTHENTICATED = "org.eclipse.jetty.security.UserIdentity";
+
+ private final String _name;
+ private final Object _credentials;
+ private transient HttpSession _session;
+
+ public SessionAuthentication(String method, UserIdentity userIdentity, Object credentials)
+ {
+ super(method, userIdentity);
+ _name = userIdentity.getUserPrincipal().getName();
+ _credentials = credentials;
+ }
+
+ @Override
+ public UserIdentity getUserIdentity()
+ {
+ if (_userIdentity == null)
+ throw new IllegalStateException("!UserIdentity");
+ return super.getUserIdentity();
+ }
+
+ private void readObject(ObjectInputStream stream)
+ throws IOException, ClassNotFoundException
+ {
+ stream.defaultReadObject();
+
+ SecurityHandler security = SecurityHandler.getCurrentSecurityHandler();
+ if (security == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("!SecurityHandler");
+ return;
+ }
+
+ LoginService loginService = security.getLoginService();
+ if (loginService == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("!LoginService");
+ return;
+ }
+
+ _userIdentity = loginService.login(_name, _credentials, null);
+ LOG.debug("Deserialized and relogged in {}", this);
+ }
+
+ @Override
+ @Deprecated
+ public void logout()
+ {
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{%s,%s}", this.getClass().getSimpleName(), hashCode(), _session == null ? "-" : _session.getId(), _userIdentity);
+ }
+
+ @Override
+ public void sessionWillPassivate(HttpSessionEvent se)
+ {
+ }
+
+ @Override
+ public void sessionDidActivate(HttpSessionEvent se)
+ {
+ if (_session == null)
+ {
+ _session = se.getSession();
+ }
+ }
+
+ @Override
+ @Deprecated
+ public void valueBound(HttpSessionBindingEvent event)
+ {
+ }
+
+ @Override
+ @Deprecated
+ public void valueUnbound(HttpSessionBindingEvent event)
+ {
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java
new file mode 100644
index 0000000..f43498c
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java
@@ -0,0 +1,161 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security.authentication;
+
+import java.io.IOException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.security.ServerAuthException;
+import org.eclipse.jetty.security.UserAuthentication;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Authentication.User;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.security.Constraint;
+
+/**
+ * @deprecated use {@link ConfigurableSpnegoAuthenticator} instead.
+ */
+@Deprecated
+public class SpnegoAuthenticator extends LoginAuthenticator
+{
+ private static final Logger LOG = Log.getLogger(SpnegoAuthenticator.class);
+ private String _authMethod = Constraint.__SPNEGO_AUTH;
+
+ public SpnegoAuthenticator()
+ {
+ }
+
+ /**
+ * Allow for a custom authMethod value to be set for instances where SPNEGO may not be appropriate
+ *
+ * @param authMethod the auth method
+ */
+ public SpnegoAuthenticator(String authMethod)
+ {
+ _authMethod = authMethod;
+ }
+
+ @Override
+ public String getAuthMethod()
+ {
+ return _authMethod;
+ }
+
+ @Override
+ public Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException
+ {
+ HttpServletRequest req = (HttpServletRequest)request;
+ HttpServletResponse res = (HttpServletResponse)response;
+
+ String header = req.getHeader(HttpHeader.AUTHORIZATION.asString());
+ String authScheme = getAuthSchemeFromHeader(header);
+
+ if (!mandatory)
+ {
+ return new DeferredAuthentication(this);
+ }
+
+ // The client has responded to the challenge we sent previously
+ if (header != null && isAuthSchemeNegotiate(authScheme))
+ {
+ String spnegoToken = header.substring(10);
+
+ UserIdentity user = login(null, spnegoToken, request);
+
+ if (user != null)
+ {
+ return new UserAuthentication(getAuthMethod(), user);
+ }
+ }
+
+ // A challenge should be sent if any of the following cases are true:
+ // 1. There was no Authorization header provided
+ // 2. There was an Authorization header for a type other than Negotiate
+ try
+ {
+ if (DeferredAuthentication.isDeferred(res))
+ {
+ return Authentication.UNAUTHENTICATED;
+ }
+
+ LOG.debug("Sending challenge");
+ res.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), HttpHeader.NEGOTIATE.asString());
+ res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return Authentication.SEND_CONTINUE;
+ }
+ catch (IOException ioe)
+ {
+ throw new ServerAuthException(ioe);
+ }
+ }
+
+ /**
+ * Extracts the auth_scheme from the HTTP Authorization header, {@code Authorization: <auth_scheme> <token>}.
+ *
+ * @param header The HTTP Authorization header or null.
+ * @return The parsed auth scheme from the header, or the empty string.
+ */
+ String getAuthSchemeFromHeader(String header)
+ {
+ // No header provided, return the empty string
+ if (header == null || header.isEmpty())
+ {
+ return "";
+ }
+ // Trim any leading whitespace
+ String trimmedHeader = header.trim();
+ // Find the first space, all characters prior should be the auth_scheme
+ int index = trimmedHeader.indexOf(' ');
+ if (index > 0)
+ {
+ return trimmedHeader.substring(0, index);
+ }
+ // If we don't find a space, this is likely malformed, just return the entire value
+ return trimmedHeader;
+ }
+
+ /**
+ * Determines if provided auth scheme text from the Authorization header is case-insensitively
+ * equal to {@code negotiate}.
+ *
+ * @param authScheme The auth scheme component of the Authorization header
+ * @return True if the auth scheme component is case-insensitively equal to {@code negotiate}, False otherwise.
+ */
+ boolean isAuthSchemeNegotiate(String authScheme)
+ {
+ if (authScheme == null || authScheme.length() != HttpHeader.NEGOTIATE.asString().length())
+ {
+ return false;
+ }
+ // Headers should be treated case-insensitively, so we have to jump through some extra hoops.
+ return authScheme.equalsIgnoreCase(HttpHeader.NEGOTIATE.asString());
+ }
+
+ @Override
+ public boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, User validatedUser) throws ServerAuthException
+ {
+ return true;
+ }
+}
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/package-info.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/package-info.java
new file mode 100644
index 0000000..52985e9
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Security : Authenticators and Callbacks
+ */
+package org.eclipse.jetty.security.authentication;
+
diff --git a/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/package-info.java b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/package-info.java
new file mode 100644
index 0000000..665d141
--- /dev/null
+++ b/third_party/jetty-security/src/main/java/org/eclipse/jetty/security/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Security : Modular Support for Security in Jetty
+ */
+package org.eclipse.jetty.security;
+
diff --git a/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/AliasedConstraintTest.java b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/AliasedConstraintTest.java
new file mode 100644
index 0000000..3dc2d00
--- /dev/null
+++ b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/AliasedConstraintTest.java
@@ -0,0 +1,171 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.DefaultHandler;
+import org.eclipse.jetty.server.handler.HandlerList;
+import org.eclipse.jetty.server.handler.ResourceHandler;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.security.Constraint;
+import org.eclipse.jetty.util.security.Password;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Some requests for static data that is served by ResourceHandler, but some is secured.
+ * <p>
+ * This is mainly here to test security bypass techniques using aliased names that should be caught.
+ */
+public class AliasedConstraintTest
+{
+ private static final String TEST_REALM = "TestRealm";
+ private static Server server;
+ private static LocalConnector connector;
+ private static ConstraintSecurityHandler security;
+
+ @BeforeAll
+ public static void startServer() throws Exception
+ {
+ server = new Server();
+ connector = new LocalConnector(server);
+ server.setConnectors(new Connector[]{connector});
+
+ ContextHandler context = new ContextHandler();
+ SessionHandler session = new SessionHandler();
+
+ TestLoginService loginService = new TestLoginService(TEST_REALM);
+
+ loginService.putUser("user0", new Password("password"), new String[]{});
+ loginService.putUser("user", new Password("password"), new String[]{"user"});
+ loginService.putUser("user2", new Password("password"), new String[]{"user"});
+ loginService.putUser("admin", new Password("password"), new String[]{"user", "administrator"});
+ loginService.putUser("user3", new Password("password"), new String[]{"foo"});
+
+ context.setContextPath("/ctx");
+ context.setResourceBase(MavenTestingUtils.getTestResourceDir("docroot").getAbsolutePath());
+
+ HandlerList handlers = new HandlerList();
+ handlers.setHandlers(new Handler[]{context, new DefaultHandler()});
+ server.setHandler(handlers);
+ context.setHandler(session);
+ // context.addAliasCheck(new AllowSymLinkAliasChecker());
+
+ server.addBean(loginService);
+
+ security = new ConstraintSecurityHandler();
+ session.setHandler(security);
+ ResourceHandler handler = new ResourceHandler();
+ security.setHandler(handler);
+
+ List<ConstraintMapping> constraints = new ArrayList<>();
+
+ Constraint constraint0 = new Constraint();
+ constraint0.setAuthenticate(true);
+ constraint0.setName("forbid");
+ ConstraintMapping mapping0 = new ConstraintMapping();
+ mapping0.setPathSpec("/forbid/*");
+ mapping0.setConstraint(constraint0);
+ constraints.add(mapping0);
+
+ Set<String> knownRoles = new HashSet<>();
+ knownRoles.add("user");
+ knownRoles.add("administrator");
+
+ security.setConstraintMappings(constraints, knownRoles);
+ server.start();
+ }
+
+ @AfterAll
+ public static void stopServer() throws Exception
+ {
+ server.stop();
+ }
+
+ public static Stream<Arguments> data()
+ {
+ List<Object[]> data = new ArrayList<>();
+
+ final String OPENCONTENT = "this is open content";
+
+ data.add(new Object[]{"/ctx/all/index.txt", HttpStatus.OK_200, OPENCONTENT});
+ data.add(new Object[]{"/ctx/ALL/index.txt", HttpStatus.NOT_FOUND_404, null});
+ data.add(new Object[]{"/ctx/ALL/Fred/../index.txt", HttpStatus.NOT_FOUND_404, null});
+ data.add(new Object[]{"/ctx/../bar/../ctx/all/index.txt", HttpStatus.OK_200, OPENCONTENT});
+ data.add(new Object[]{"/ctx/forbid/index.txt", HttpStatus.FORBIDDEN_403, null});
+ data.add(new Object[]{"/ctx/all/../forbid/index.txt", HttpStatus.FORBIDDEN_403, null});
+ data.add(new Object[]{"/ctx/FoRbId/index.txt", HttpStatus.NOT_FOUND_404, null});
+
+ return data.stream().map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testAccess(String uri, int expectedStatusCode, String expectedContent) throws Exception
+ {
+ StringBuilder request = new StringBuilder();
+ request.append("GET ").append(uri).append(" HTTP/1.1\r\n");
+ request.append("Host: localhost\r\n");
+ request.append("Connection: close\r\n");
+ request.append("\r\n");
+
+ String response = connector.getResponse(request.toString());
+
+ switch (expectedStatusCode)
+ {
+ case 200:
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ break;
+ case 403:
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+ break;
+ case 404:
+ assertThat(response, startsWith("HTTP/1.1 404 Not Found"));
+ break;
+ default:
+ fail("Write a handler for response status code: " + expectedStatusCode);
+ break;
+ }
+
+ if (expectedContent != null)
+ {
+ assertThat(response, containsString("this is open content"));
+ }
+ }
+}
diff --git a/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java
new file mode 100644
index 0000000..9eef996
--- /dev/null
+++ b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java
@@ -0,0 +1,2052 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import javax.servlet.HttpConstraintElement;
+import javax.servlet.HttpMethodConstraintElement;
+import javax.servlet.ServletException;
+import javax.servlet.ServletSecurityElement;
+import javax.servlet.annotation.ServletSecurity.EmptyRoleSemantic;
+import javax.servlet.annotation.ServletSecurity.TransportGuarantee;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.security.authentication.BasicAuthenticator;
+import org.eclipse.jetty.security.authentication.DigestAuthenticator;
+import org.eclipse.jetty.security.authentication.FormAuthenticator;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.HandlerWrapper;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.util.security.Constraint;
+import org.eclipse.jetty.util.security.Password;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.in;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ConstraintTest
+{
+ private static final String TEST_REALM = "TestRealm";
+ private Server _server;
+ private LocalConnector _connector;
+ private ConstraintSecurityHandler _security;
+ private HttpConfiguration _config;
+ private Constraint _forbidConstraint;
+ private Constraint _authAnyRoleConstraint;
+ private Constraint _authAdminConstraint;
+ private Constraint _relaxConstraint;
+ private Constraint _loginPageConstraint;
+ private Constraint _noAuthConstraint;
+ private Constraint _confidentialDataConstraint;
+ private Constraint _anyUserAuthConstraint;
+
+ @BeforeEach
+ public void setupServer()
+ {
+ _server = new Server();
+ _connector = new LocalConnector(_server);
+ _config = _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration();
+ _server.setConnectors(new Connector[]{_connector});
+
+ ContextHandler contextHandler = new ContextHandler();
+ SessionHandler sessionHandler = new SessionHandler();
+
+ TestLoginService loginService = new TestLoginService(TEST_REALM);
+
+ loginService.putUser("user0", new Password("password"), new String[]{});
+ loginService.putUser("user", new Password("password"), new String[]{"user"});
+ loginService.putUser("user2", new Password("password"), new String[]{"user"});
+ loginService.putUser("admin", new Password("password"), new String[]{"user", "administrator"});
+ loginService.putUser("user3", new Password("password"), new String[]{"foo"});
+
+ contextHandler.setContextPath("/ctx");
+ _server.setHandler(contextHandler);
+ contextHandler.setHandler(sessionHandler);
+
+ _server.addBean(loginService);
+
+ _security = new ConstraintSecurityHandler();
+ sessionHandler.setHandler(_security);
+ RequestHandler requestHandler = new RequestHandler();
+ _security.setHandler(requestHandler);
+
+ _security.setConstraintMappings(getConstraintMappings(), getKnownRoles());
+ }
+
+ @AfterEach
+ public void stopServer() throws Exception
+ {
+ _server.stop();
+ }
+
+ public Set<String> getKnownRoles()
+ {
+ Set<String> knownRoles = new HashSet<>();
+ knownRoles.add("user");
+ knownRoles.add("administrator");
+
+ return knownRoles;
+ }
+
+ private List<ConstraintMapping> getConstraintMappings()
+ {
+ _forbidConstraint = new Constraint();
+ _forbidConstraint.setAuthenticate(true);
+ _forbidConstraint.setName("forbid");
+ ConstraintMapping mapping0 = new ConstraintMapping();
+ mapping0.setPathSpec("/forbid/*");
+ mapping0.setConstraint(_forbidConstraint);
+
+ _authAnyRoleConstraint = new Constraint();
+ _authAnyRoleConstraint.setAuthenticate(true);
+ _authAnyRoleConstraint.setName("auth");
+ _authAnyRoleConstraint.setRoles(new String[]{Constraint.ANY_ROLE});
+ ConstraintMapping mapping1 = new ConstraintMapping();
+ mapping1.setPathSpec("/auth/*");
+ mapping1.setConstraint(_authAnyRoleConstraint);
+
+ _authAdminConstraint = new Constraint();
+ _authAdminConstraint.setAuthenticate(true);
+ _authAdminConstraint.setName("admin");
+ _authAdminConstraint.setRoles(new String[]{"administrator"});
+ ConstraintMapping mapping2 = new ConstraintMapping();
+ mapping2.setPathSpec("/admin/*");
+ mapping2.setConstraint(_authAdminConstraint);
+ mapping2.setMethod("GET");
+ ConstraintMapping mapping2o = new ConstraintMapping();
+ mapping2o.setPathSpec("/admin/*");
+ mapping2o.setConstraint(_forbidConstraint);
+ mapping2o.setMethodOmissions(new String[]{"GET"});
+
+ _relaxConstraint = new Constraint();
+ _relaxConstraint.setAuthenticate(false);
+ _relaxConstraint.setName("relax");
+ ConstraintMapping mapping3 = new ConstraintMapping();
+ mapping3.setPathSpec("/admin/relax/*");
+ mapping3.setConstraint(_relaxConstraint);
+
+ _loginPageConstraint = new Constraint();
+ _loginPageConstraint.setAuthenticate(true);
+ _loginPageConstraint.setName("loginpage");
+ _loginPageConstraint.setRoles(new String[]{"administrator"});
+ ConstraintMapping mapping4 = new ConstraintMapping();
+ mapping4.setPathSpec("/testLoginPage");
+ mapping4.setConstraint(_loginPageConstraint);
+
+ _noAuthConstraint = new Constraint();
+ _noAuthConstraint.setAuthenticate(false);
+ _noAuthConstraint.setName("allow forbidden");
+ ConstraintMapping mapping5 = new ConstraintMapping();
+ mapping5.setPathSpec("/forbid/post");
+ mapping5.setConstraint(_noAuthConstraint);
+ mapping5.setMethod("POST");
+ ConstraintMapping mapping5o = new ConstraintMapping();
+ mapping5o.setPathSpec("/forbid/post");
+ mapping5o.setConstraint(_forbidConstraint);
+ mapping5o.setMethodOmissions(new String[]{"POST"});
+
+ _confidentialDataConstraint = new Constraint();
+ _confidentialDataConstraint.setAuthenticate(false);
+ _confidentialDataConstraint.setName("data constraint");
+ _confidentialDataConstraint.setDataConstraint(Constraint.DC_CONFIDENTIAL);
+ ConstraintMapping mapping6 = new ConstraintMapping();
+ mapping6.setPathSpec("/data/*");
+ mapping6.setConstraint(_confidentialDataConstraint);
+
+ _anyUserAuthConstraint = new Constraint();
+ _anyUserAuthConstraint.setAuthenticate(true);
+ _anyUserAuthConstraint.setName("** constraint");
+ _anyUserAuthConstraint.setRoles(new String[]{
+ Constraint.ANY_AUTH, "user"
+ }); //the "user" role is superfluous once ** has been defined
+ ConstraintMapping mapping7 = new ConstraintMapping();
+ mapping7.setPathSpec("/starstar/*");
+ mapping7.setConstraint(_anyUserAuthConstraint);
+
+ return Arrays.asList(mapping0, mapping1, mapping2, mapping2o, mapping3, mapping4, mapping5, mapping5o, mapping6, mapping7);
+ }
+
+ /**
+ * Test that constraint mappings added before the context starts are
+ * retained, but those that are added after the context starts are not.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testDurableConstraints() throws Exception
+ {
+ List<ConstraintMapping> mappings = _security.getConstraintMappings();
+ assertThat("before start", getConstraintMappings().size(), Matchers.equalTo(mappings.size()));
+
+ _server.start();
+
+ mappings = _security.getConstraintMappings();
+ assertThat("after start", getConstraintMappings().size(), Matchers.equalTo(mappings.size()));
+
+ _server.stop();
+
+ //After a stop, just the durable mappings are left
+ mappings = _security.getConstraintMappings();
+ assertThat("after stop", getConstraintMappings().size(), Matchers.equalTo(mappings.size()));
+
+ _server.start();
+
+ //Verify the constraints are just the durables
+ mappings = _security.getConstraintMappings();
+ assertThat("after restart", getConstraintMappings().size(), Matchers.equalTo(mappings.size()));
+
+ //Add a non-durable constraint
+ ConstraintMapping mapping = new ConstraintMapping();
+ mapping.setPathSpec("/xxxx/*");
+ Constraint constraint = new Constraint();
+ constraint.setAuthenticate(false);
+ constraint.setName("transient");
+ mapping.setConstraint(constraint);
+
+ _security.addConstraintMapping(mapping);
+
+ mappings = _security.getConstraintMappings();
+ assertThat("after addition", getConstraintMappings().size() + 1, Matchers.equalTo(mappings.size()));
+
+ _server.stop();
+ _server.start();
+
+ //After a stop, only the durable mappings remain
+ mappings = _security.getConstraintMappings();
+ assertThat("after addition", getConstraintMappings().size(), Matchers.equalTo(mappings.size()));
+
+ //test that setConstraintMappings replaces all existing mappings whether durable or not
+
+ //test setConstraintMappings in durable state
+ _server.stop();
+ _security.setConstraintMappings(Collections.singletonList(mapping));
+ mappings = _security.getConstraintMappings();
+ assertThat("after set during stop", 1, Matchers.equalTo(mappings.size()));
+ _server.start();
+ mappings = _security.getConstraintMappings();
+ assertThat("after set after start", 1, Matchers.equalTo(mappings.size()));
+
+ //test setConstraintMappings not in durable state
+ _server.stop();
+ _server.start();
+ assertThat("no change after start", 1, Matchers.equalTo(mappings.size()));
+ _security.setConstraintMappings(getConstraintMappings());
+ mappings = _security.getConstraintMappings();
+ assertThat("durables lost", getConstraintMappings().size(), Matchers.equalTo(mappings.size()));
+ _server.stop();
+ mappings = _security.getConstraintMappings();
+ assertThat("no mappings", 0, Matchers.equalTo(mappings.size()));
+ }
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-1
+ * @ServletSecurity
+ *
+ * @throws Exception if test fails
+ */
+ @Test
+ public void testSecurityElementExample131() throws Exception
+ {
+ ServletSecurityElement element = new ServletSecurityElement();
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(mappings.isEmpty());
+ }
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-2
+ * @ServletSecurity(@HttpConstraint(transportGuarantee = TransportGuarantee.CONFIDENTIAL))
+ *
+ * @throws Exception if test fails
+ */
+ @Test
+ public void testSecurityElementExample132() throws Exception
+ {
+ HttpConstraintElement httpConstraintElement = new HttpConstraintElement(TransportGuarantee.CONFIDENTIAL);
+ ServletSecurityElement element = new ServletSecurityElement(httpConstraintElement);
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(!mappings.isEmpty());
+ assertEquals(1, mappings.size());
+ ConstraintMapping mapping = mappings.get(0);
+ assertEquals(2, mapping.getConstraint().getDataConstraint());
+ }
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-3
+ *
+ * @throws Exception if test fails
+ * @ServletSecurity(@HttpConstraint(EmptyRoleSemantic.DENY))
+ */
+ @Test
+ public void testSecurityElementExample133() throws Exception
+ {
+ HttpConstraintElement httpConstraintElement = new HttpConstraintElement(EmptyRoleSemantic.DENY);
+ ServletSecurityElement element = new ServletSecurityElement(httpConstraintElement);
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(!mappings.isEmpty());
+ assertEquals(1, mappings.size());
+ ConstraintMapping mapping = mappings.get(0);
+ assertTrue(mapping.getConstraint().isForbidden());
+ }
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-4
+ *
+ * @throws Exception if test fails
+ * @ServletSecurity(@HttpConstraint(rolesAllowed = "R1"))
+ */
+ @Test
+ public void testSecurityElementExample134() throws Exception
+ {
+ HttpConstraintElement httpConstraintElement = new HttpConstraintElement(TransportGuarantee.NONE, "R1");
+ ServletSecurityElement element = new ServletSecurityElement(httpConstraintElement);
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(!mappings.isEmpty());
+ assertEquals(1, mappings.size());
+ ConstraintMapping mapping = mappings.get(0);
+ assertTrue(mapping.getConstraint().getAuthenticate());
+ assertTrue(mapping.getConstraint().getRoles() != null);
+ assertEquals(1, mapping.getConstraint().getRoles().length);
+ assertEquals("R1", mapping.getConstraint().getRoles()[0]);
+ assertEquals(0, mapping.getConstraint().getDataConstraint());
+ }
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-5
+ *
+ * @throws Exception if test fails
+ * @ServletSecurity((httpMethodConstraints = {
+ * @HttpMethodConstraint(value = "GET", rolesAllowed = "R1"),
+ * @HttpMethodConstraint(value = "POST", rolesAllowed = "R1",
+ * transportGuarantee = TransportGuarantee.CONFIDENTIAL)})
+ */
+ @Test
+ public void testSecurityElementExample135() throws Exception
+ {
+ List<HttpMethodConstraintElement> methodElements = new ArrayList<HttpMethodConstraintElement>();
+ methodElements.add(new HttpMethodConstraintElement("GET", new HttpConstraintElement(TransportGuarantee.NONE, "R1")));
+ methodElements.add(new HttpMethodConstraintElement("POST", new HttpConstraintElement(TransportGuarantee.CONFIDENTIAL, "R1")));
+ ServletSecurityElement element = new ServletSecurityElement(methodElements);
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(!mappings.isEmpty());
+ assertEquals(2, mappings.size());
+ assertEquals("GET", mappings.get(0).getMethod());
+ assertEquals("R1", mappings.get(0).getConstraint().getRoles()[0]);
+ assertTrue(mappings.get(0).getMethodOmissions() == null);
+ assertEquals(0, mappings.get(0).getConstraint().getDataConstraint());
+ assertEquals("POST", mappings.get(1).getMethod());
+ assertEquals("R1", mappings.get(1).getConstraint().getRoles()[0]);
+ assertEquals(2, mappings.get(1).getConstraint().getDataConstraint());
+ assertTrue(mappings.get(1).getMethodOmissions() == null);
+ }
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-6
+ *
+ * @throws Exception if test fails
+ * @ServletSecurity(value = @HttpConstraint(rolesAllowed = "R1"), httpMethodConstraints = @HttpMethodConstraint("GET"))
+ */
+ @Test
+ public void testSecurityElementExample136() throws Exception
+ {
+ List<HttpMethodConstraintElement> methodElements = new ArrayList<HttpMethodConstraintElement>();
+ methodElements.add(new HttpMethodConstraintElement("GET"));
+ ServletSecurityElement element = new ServletSecurityElement(new HttpConstraintElement(TransportGuarantee.NONE, "R1"), methodElements);
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(!mappings.isEmpty());
+ assertEquals(2, mappings.size());
+ assertTrue(mappings.get(0).getMethodOmissions() != null);
+ assertEquals("GET", mappings.get(0).getMethodOmissions()[0]);
+ assertTrue(mappings.get(0).getConstraint().getAuthenticate());
+ assertEquals("R1", mappings.get(0).getConstraint().getRoles()[0]);
+ assertEquals("GET", mappings.get(1).getMethod());
+ assertTrue(mappings.get(1).getMethodOmissions() == null);
+ assertEquals(0, mappings.get(1).getConstraint().getDataConstraint());
+ assertFalse(mappings.get(1).getConstraint().getAuthenticate());
+ }
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-7
+ *
+ * @throws Exception if test fails
+ * @ServletSecurity(value = @HttpConstraint(rolesAllowed = "R1"),
+ * httpMethodConstraints = @HttpMethodConstraint(value="TRACE",
+ * emptyRoleSemantic = EmptyRoleSemantic.DENY))
+ */
+ @Test
+ public void testSecurityElementExample137() throws Exception
+ {
+ List<HttpMethodConstraintElement> methodElements = new ArrayList<HttpMethodConstraintElement>();
+ methodElements.add(new HttpMethodConstraintElement("TRACE", new HttpConstraintElement(EmptyRoleSemantic.DENY)));
+ ServletSecurityElement element = new ServletSecurityElement(new HttpConstraintElement(TransportGuarantee.NONE, "R1"), methodElements);
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(!mappings.isEmpty());
+ assertEquals(2, mappings.size());
+ assertTrue(mappings.get(0).getMethodOmissions() != null);
+ assertEquals("TRACE", mappings.get(0).getMethodOmissions()[0]);
+ assertTrue(mappings.get(0).getConstraint().getAuthenticate());
+ assertEquals("R1", mappings.get(0).getConstraint().getRoles()[0]);
+ assertEquals("TRACE", mappings.get(1).getMethod());
+ assertTrue(mappings.get(1).getMethodOmissions() == null);
+ assertEquals(0, mappings.get(1).getConstraint().getDataConstraint());
+ assertTrue(mappings.get(1).getConstraint().isForbidden());
+ }
+
+ @Test
+ public void testUncoveredHttpMethodDetection() throws Exception
+ {
+ //Test no methods named
+ Constraint constraint1 = new Constraint();
+ constraint1.setAuthenticate(true);
+ constraint1.setName("** constraint");
+ constraint1.setRoles(new String[]{Constraint.ANY_AUTH, "user"}); //No methods named, no uncovered methods
+ ConstraintMapping mapping1 = new ConstraintMapping();
+ mapping1.setPathSpec("/starstar/*");
+ mapping1.setConstraint(constraint1);
+
+ _security.setConstraintMappings(Collections.singletonList(mapping1));
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ Set<String> uncoveredPaths = _security.getPathsWithUncoveredHttpMethods();
+ assertTrue(uncoveredPaths.isEmpty()); //no uncovered methods
+
+ //Test only an explicitly named method, no omissions to cover other methods
+ Constraint constraint2 = new Constraint();
+ constraint2.setAuthenticate(true);
+ constraint2.setName("user constraint");
+ constraint2.setRoles(new String[]{"user"});
+ ConstraintMapping mapping2 = new ConstraintMapping();
+ mapping2.setPathSpec("/user/*");
+ mapping2.setMethod("GET");
+ mapping2.setConstraint(constraint2);
+
+ _security.addConstraintMapping(mapping2);
+ uncoveredPaths = _security.getPathsWithUncoveredHttpMethods();
+ assertNotNull(uncoveredPaths);
+ assertEquals(1, uncoveredPaths.size());
+ assertThat("/user/*", is(in(uncoveredPaths)));
+
+ //Test an explicitly named method with an http-method-omission to cover all other methods
+ Constraint constraint2a = new Constraint();
+ constraint2a.setAuthenticate(true);
+ constraint2a.setName("forbid constraint");
+ ConstraintMapping mapping2a = new ConstraintMapping();
+ mapping2a.setPathSpec("/user/*");
+ mapping2a.setMethodOmissions(new String[]{"GET"});
+ mapping2a.setConstraint(constraint2a);
+
+ _security.addConstraintMapping(mapping2a);
+ uncoveredPaths = _security.getPathsWithUncoveredHttpMethods();
+ assertNotNull(uncoveredPaths);
+ assertEquals(0, uncoveredPaths.size());
+
+ //Test an http-method-omission only
+ Constraint constraint3 = new Constraint();
+ constraint3.setAuthenticate(true);
+ constraint3.setName("omit constraint");
+ ConstraintMapping mapping3 = new ConstraintMapping();
+ mapping3.setPathSpec("/omit/*");
+ mapping3.setMethodOmissions(new String[]{"GET", "POST"});
+ mapping3.setConstraint(constraint3);
+
+ _security.addConstraintMapping(mapping3);
+ uncoveredPaths = _security.getPathsWithUncoveredHttpMethods();
+ assertNotNull(uncoveredPaths);
+ assertThat("/omit/*", is(in(uncoveredPaths)));
+
+ _security.setDenyUncoveredHttpMethods(true);
+ uncoveredPaths = _security.getPathsWithUncoveredHttpMethods();
+ assertNotNull(uncoveredPaths);
+ assertEquals(0, uncoveredPaths.size());
+ }
+
+ public static Stream<Arguments> basicScenarios()
+ {
+ List<Arguments> scenarios = new ArrayList<>();
+
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "GET /ctx/noauth/info HTTP/1.0\r\n\r\n",
+ HttpStatus.OK_200
+ )
+ ));
+
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "GET /ctx/forbid/info HTTP/1.0\r\n\r\n",
+ HttpStatus.FORBIDDEN_403
+ )
+ ));
+
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "GET /ctx/auth/info HTTP/1.0\r\n\r\n",
+ HttpStatus.UNAUTHORIZED_401,
+ (response) ->
+ {
+ String authHeader = response.get(HttpHeader.WWW_AUTHENTICATE);
+ assertThat(response.toString(), authHeader, containsString("basic realm=\"TestRealm\""));
+ }
+ )
+ ));
+
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "POST /ctx/auth/info HTTP/1.1\r\n" +
+ "Host: test\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "0123456789",
+ HttpStatus.UNAUTHORIZED_401,
+ (response) ->
+ {
+ String authHeader = response.get(HttpHeader.WWW_AUTHENTICATE);
+ assertThat(response.toString(), authHeader, containsString("basic realm=\"TestRealm\""));
+ assertThat(response.get(HttpHeader.CONNECTION), nullValue());
+ }
+ )
+ ));
+
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "POST /ctx/auth/info HTTP/1.1\r\n" +
+ "Host: test\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "012345",
+ HttpStatus.UNAUTHORIZED_401,
+ (response) ->
+ {
+ String authHeader = response.get(HttpHeader.WWW_AUTHENTICATE);
+ assertThat(response.toString(), authHeader, containsString("basic realm=\"TestRealm\""));
+ assertThat(response.get(HttpHeader.CONNECTION), is("close"));
+ }
+ )
+ ));
+
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("user:wrong") + "\r\n" +
+ "\r\n",
+ HttpStatus.UNAUTHORIZED_401,
+ (response) ->
+ {
+ String authHeader = response.get(HttpHeader.WWW_AUTHENTICATE);
+ assertThat(response.toString(), authHeader, containsString("basic realm=\"TestRealm\""));
+ }
+ )
+ ));
+
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("user:password") + "\r\n" +
+ "\r\n",
+ HttpStatus.OK_200
+ )
+ ));
+
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "POST /ctx/auth/info HTTP/1.0\r\n" +
+ "Content-Length: 10\r\n" +
+ "Authorization: Basic " + authBase64("user:password") + "\r\n" +
+ "\r\n" +
+ "0123456789",
+ HttpStatus.OK_200
+ )
+ ));
+
+ // == test admin
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "GET /ctx/admin/info HTTP/1.0\r\n\r\n",
+ HttpStatus.UNAUTHORIZED_401,
+ (response) ->
+ {
+ String authHeader = response.get(HttpHeader.WWW_AUTHENTICATE);
+ assertThat(response.toString(), authHeader, containsString("basic realm=\"TestRealm\""));
+ }
+ )
+ ));
+
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("admin:wrong") + "\r\n" +
+ "\r\n",
+ HttpStatus.UNAUTHORIZED_401,
+ (response) ->
+ {
+ String authHeader = response.get(HttpHeader.WWW_AUTHENTICATE);
+ assertThat(response.toString(), authHeader, containsString("basic realm=\"TestRealm\""));
+ }
+ )
+ ));
+
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("user:password") + "\r\n" +
+ "\r\n",
+ HttpStatus.FORBIDDEN_403,
+ (response) ->
+ {
+ assertThat(response.getContent(), containsString("!role"));
+ }
+ )
+ ));
+
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("admin:password") + "\r\n" +
+ "\r\n",
+ HttpStatus.OK_200)
+ ));
+
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "GET /ctx/admin/relax/info HTTP/1.0\r\n\r\n",
+ HttpStatus.OK_200
+ )
+ ));
+
+ // == check GET is in role administrator
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "GET /ctx/omit/x HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("admin:password") + "\r\n" +
+ "\r\n",
+ HttpStatus.OK_200
+
+ )
+ ));
+
+ // == check POST is in role user
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "POST /ctx/omit/x HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("user2:password") + "\r\n" +
+ "\r\n", HttpStatus.OK_200)
+ ));
+
+ // == check POST can be in role foo too
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "POST /ctx/omit/x HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("user3:password") + "\r\n" +
+ "\r\n",
+ HttpStatus.OK_200)
+ ));
+
+ // == check HEAD cannot be in role user
+ scenarios.add(Arguments.of(
+ new Scenario(
+ "HEAD /ctx/omit/x HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("user2:password") + "\r\n" +
+ "\r\n",
+ HttpStatus.FORBIDDEN_403)
+ ));
+
+ return scenarios.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("basicScenarios")
+ public void testBasic(Scenario scenario) throws Exception
+ {
+ List<ConstraintMapping> list = new ArrayList<>(getConstraintMappings());
+
+ Constraint constraint6 = new Constraint();
+ constraint6.setAuthenticate(true);
+ constraint6.setName("omit HEAD and GET");
+ constraint6.setRoles(new String[]{"user"});
+ ConstraintMapping mapping6 = new ConstraintMapping();
+ mapping6.setPathSpec("/omit/*");
+ mapping6.setConstraint(constraint6);
+ mapping6.setMethodOmissions(new String[]{
+ "GET", "HEAD"
+ }); //requests for every method except GET and HEAD must be in role "user"
+ list.add(mapping6);
+
+ Constraint constraint7 = new Constraint();
+ constraint7.setAuthenticate(true);
+ constraint7.setName("non-omitted GET");
+ constraint7.setRoles(new String[]{"administrator"});
+ ConstraintMapping mapping7 = new ConstraintMapping();
+ mapping7.setPathSpec("/omit/*");
+ mapping7.setConstraint(constraint7);
+ mapping7.setMethod("GET"); //requests for GET must be in role "admin"
+ list.add(mapping7);
+
+ Constraint constraint8 = new Constraint();
+ constraint8.setAuthenticate(true);
+ constraint8.setName("non specific");
+ constraint8.setRoles(new String[]{"foo"});
+ ConstraintMapping mapping8 = new ConstraintMapping();
+ mapping8.setPathSpec("/omit/*");
+ mapping8.setConstraint(constraint8); //requests for all methods must be in role "foo"
+ list.add(mapping8);
+
+ Set<String> knownRoles = new HashSet<>();
+ knownRoles.add("user");
+ knownRoles.add("administrator");
+ knownRoles.add("foo");
+
+ _security.setConstraintMappings(list, knownRoles);
+
+ _security.setAuthenticator(new BasicAuthenticator());
+ try
+ {
+ _server.start();
+ String rawResponse = _connector.getResponse(scenario.rawRequest);
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(scenario.expectedStatus));
+ if (scenario.extraAsserts != null)
+ scenario.extraAsserts.accept(response);
+ }
+ finally
+ {
+ _server.stop();
+ }
+ }
+
+ private static String CNONCE = "1234567890";
+
+ private String digest(String nonce, String username, String password, String uri, String nc) throws Exception
+ {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] ha1;
+ // calc A1 digest
+ md.update(username.getBytes(ISO_8859_1));
+ md.update((byte)':');
+ md.update("TestRealm".getBytes(ISO_8859_1));
+ md.update((byte)':');
+ md.update(password.getBytes(ISO_8859_1));
+ ha1 = md.digest();
+ // calc A2 digest
+ md.reset();
+ md.update("GET".getBytes(ISO_8859_1));
+ md.update((byte)':');
+ md.update(uri.getBytes(ISO_8859_1));
+ byte[] ha2 = md.digest();
+
+ // calc digest
+ // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":"
+ // nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) )
+ // <">
+ // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2)
+ // ) > <">
+
+ md.update(TypeUtil.toString(ha1, 16).getBytes(ISO_8859_1));
+ md.update((byte)':');
+ md.update(nonce.getBytes(ISO_8859_1));
+ md.update((byte)':');
+ md.update(nc.getBytes(ISO_8859_1));
+ md.update((byte)':');
+ md.update(CNONCE.getBytes(ISO_8859_1));
+ md.update((byte)':');
+ md.update("auth".getBytes(ISO_8859_1));
+ md.update((byte)':');
+ md.update(TypeUtil.toString(ha2, 16).getBytes(ISO_8859_1));
+ byte[] digest = md.digest();
+
+ // check digest
+ return TypeUtil.toString(digest, 16);
+ }
+
+ @Test
+ public void testDigest() throws Exception
+ {
+ DigestAuthenticator authenticator = new DigestAuthenticator();
+ authenticator.setMaxNonceCount(5);
+ _security.setAuthenticator(authenticator);
+ _server.start();
+
+ String response;
+ response = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ response = _connector.getResponse("GET /ctx/forbid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 401 Unauthorized"));
+ assertThat(response, containsString("WWW-Authenticate: Digest realm=\"TestRealm\""));
+
+ Pattern nonceP = Pattern.compile("nonce=\"([^\"]*)\",");
+ Matcher matcher = nonceP.matcher(response);
+ assertTrue(matcher.find());
+ String nonce = matcher.group(1);
+
+ //wrong password
+ String digest = digest(nonce, "user", "WRONG", "/ctx/auth/info", "1");
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Authorization: Digest username=\"user\", qop=auth, cnonce=\"1234567890\", uri=\"/ctx/auth/info\", realm=\"TestRealm\", " +
+ "nc=1, " +
+ "nonce=\"" + nonce + "\", " +
+ "response=\"" + digest + "\"\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 401 Unauthorized"));
+
+ // right password
+ digest = digest(nonce, "user", "password", "/ctx/auth/info", "2");
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Authorization: Digest username=\"user\", qop=auth, cnonce=\"1234567890\", uri=\"/ctx/auth/info\", realm=\"TestRealm\", " +
+ "nc=2, " +
+ "nonce=\"" + nonce + "\", " +
+ "response=\"" + digest + "\"\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ // once only
+ digest = digest(nonce, "user", "password", "/ctx/auth/info", "2");
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Authorization: Digest username=\"user\", qop=auth, cnonce=\"1234567890\", uri=\"/ctx/auth/info\", realm=\"TestRealm\", " +
+ "nc=2, " +
+ "nonce=\"" + nonce + "\", " +
+ "response=\"" + digest + "\"\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 401 Unauthorized"));
+
+ // increasing
+ digest = digest(nonce, "user", "password", "/ctx/auth/info", "4");
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Authorization: Digest username=\"user\", qop=auth, cnonce=\"1234567890\", uri=\"/ctx/auth/info\", realm=\"TestRealm\", " +
+ "nc=4, " +
+ "nonce=\"" + nonce + "\", " +
+ "response=\"" + digest + "\"\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ // out of order
+ digest = digest(nonce, "user", "password", "/ctx/auth/info", "3");
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Authorization: Digest username=\"user\", qop=auth, cnonce=\"1234567890\", uri=\"/ctx/auth/info\", realm=\"TestRealm\", " +
+ "nc=3, " +
+ "nonce=\"" + nonce + "\", " +
+ "response=\"" + digest + "\"\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ // stale
+ digest = digest(nonce, "user", "password", "/ctx/auth/info", "5");
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Authorization: Digest username=\"user\", qop=auth, cnonce=\"1234567890\", uri=\"/ctx/auth/info\", realm=\"TestRealm\", " +
+ "nc=5, " +
+ "nonce=\"" + nonce + "\", " +
+ "response=\"" + digest + "\"\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 401 Unauthorized"));
+ assertThat(response, containsString("stale=true"));
+ }
+
+ @Test
+ public void testFormDispatch() throws Exception
+ {
+ _security.setAuthenticator(new FormAuthenticator("/testLoginPage", "/testErrorPage", true));
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ response = _connector.getResponse("GET /ctx/forbid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, containsString("Cache-Control: no-cache"));
+ assertThat(response, containsString("Expires"));
+ assertThat(response, containsString("URI=/ctx/testLoginPage"));
+
+ String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 31\r\n" +
+ "\r\n" +
+ "j_username=user&j_password=wrong\r\n");
+ assertThat(response, containsString("testErrorPage"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 35\r\n" +
+ "\r\n" +
+ "j_username=user&j_password=password\r\n");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("/ctx/auth/info"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403"));
+ assertThat(response, containsString("!role"));
+ }
+
+ @Test
+ public void testFormRedirect() throws Exception
+ {
+ _security.setAuthenticator(new FormAuthenticator("/testLoginPage", "/testErrorPage", false));
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, not(containsString("JSESSIONID=")));
+
+ response = _connector.getResponse("GET /ctx/forbid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+ assertThat(response, not(containsString("JSESSIONID=")));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, containsString(" 302 Found"));
+ assertThat(response, containsString("/ctx/testLoginPage"));
+ assertThat(response, containsString("JSESSIONID="));
+ String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/testLoginPage HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("URI=/ctx/testLoginPage"));
+ assertThat(response, not(containsString("JSESSIONID=" + session)));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 32\r\n" +
+ "\r\n" +
+ "j_username=user&j_password=wrong");
+ assertThat(response, containsString("Location"));
+ assertThat(response, not(containsString("JSESSIONID=" + session)));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 35\r\n" +
+ "\r\n" +
+ "j_username=user&j_password=password");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("/ctx/auth/info"));
+ assertThat(response, containsString("JSESSIONID="));
+ assertThat(response, not(containsString("JSESSIONID=" + session)));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("JSESSIONID=" + session));
+
+ response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403"));
+ assertThat(response, containsString("!role"));
+ assertThat(response, not(containsString("JSESSIONID=" + session)));
+ }
+
+ @Test
+ public void testFormPostRedirect() throws Exception
+ {
+ _security.setAuthenticator(new FormAuthenticator("/testLoginPage", "/testErrorPage", false));
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ response = _connector.getResponse("GET /ctx/forbid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+
+ response = _connector.getResponse("POST /ctx/auth/info HTTP/1.0\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 27\r\n" +
+ "\r\n" +
+ "test_parameter=test_value\r\n");
+ assertThat(response, containsString(" 302 Found"));
+ assertThat(response, containsString("/ctx/testLoginPage"));
+ String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/testLoginPage HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("URI=/ctx/testLoginPage"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 31\r\n" +
+ "\r\n" +
+ "j_username=user&j_password=wrong\r\n");
+
+ assertThat(response, containsString("Location"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 35\r\n" +
+ "\r\n" +
+ "j_username=user&j_password=password\r\n");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("/ctx/auth/info"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ // sneak in other request
+ response = _connector.getResponse("GET /ctx/auth/other HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, not(containsString("test_value")));
+
+ // retry post as GET
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("test_value"));
+
+ response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403"));
+ assertThat(response, containsString("!role"));
+ }
+
+ @Test
+ public void testNonFormPostRedirectHttp10() throws Exception
+ {
+ _security.setAuthenticator(new FormAuthenticator("/testLoginPage", "/testErrorPage", false));
+ _server.start();
+
+ String response = _connector.getResponse("POST /ctx/auth/info HTTP/1.0\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "0123456789\r\n");
+ assertThat(response, containsString(" 302 Found"));
+ assertThat(response, containsString("/ctx/testLoginPage"));
+ assertThat(response, not(containsString("Connection: close")));
+ assertThat(response, containsString("Connection: keep-alive"));
+
+ response = _connector.getResponse("POST /ctx/auth/info HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "012345\r\n");
+ assertThat(response, containsString(" 302 Found"));
+ assertThat(response, containsString("/ctx/testLoginPage"));
+ assertThat(response, not(containsString("Connection: keep-alive")));
+ }
+
+ @Test
+ public void testNonFormPostRedirectHttp11() throws Exception
+ {
+ _security.setAuthenticator(new FormAuthenticator("/testLoginPage", "/testErrorPage", false));
+ _server.start();
+
+ String response = _connector.getResponse("POST /ctx/auth/info HTTP/1.1\r\n" +
+ "Host: test\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "0123456789\r\n");
+ assertThat(response, containsString(" 303 See Other"));
+ assertThat(response, containsString("/ctx/testLoginPage"));
+ assertThat(response, not(containsString("Connection: close")));
+
+ response = _connector.getResponse("POST /ctx/auth/info HTTP/1.1\r\n" +
+ "Host: test\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "012345\r\n");
+ assertThat(response, containsString(" 303 See Other"));
+ assertThat(response, containsString("/ctx/testLoginPage"));
+ assertThat(response, containsString("Connection: close"));
+ }
+
+ @Test
+ public void testFormNoCookies() throws Exception
+ {
+ _security.setAuthenticator(new FormAuthenticator("/testLoginPage", "/testErrorPage", false));
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ response = _connector.getResponse("GET /ctx/forbid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, containsString(" 302 Found"));
+ assertThat(response, containsString("/ctx/testLoginPage"));
+ int jsession = response.indexOf(";jsessionid=");
+ String session = response.substring(jsession + 12, response.indexOf("\r\n", jsession));
+
+ response = _connector.getResponse("GET /ctx/testLoginPage;jsessionid=" + session + ";other HTTP/1.0\r\n" +
+ "\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("URI=/ctx/testLoginPage"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check;jsessionid=" + session + ";other HTTP/1.0\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 31\r\n" +
+ "\r\n" +
+ "j_username=user&j_password=wrong\r\n");
+ assertThat(response, containsString("Location"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check;jsessionid=" + session + ";other HTTP/1.0\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 35\r\n" +
+ "\r\n" +
+ "j_username=user&j_password=password\r\n");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("/ctx/auth/info"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/auth/info;jsessionid=" + session + ";other HTTP/1.0\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ response = _connector.getResponse("GET /ctx/admin/info;jsessionid=" + session + ";other HTTP/1.0\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403"));
+ assertThat(response, containsString("!role"));
+ }
+
+ /**
+ * Test Request.login() Request.logout() with FORM authenticator
+ */
+ @Test
+ public void testFormProgrammaticLoginLogout() throws Exception
+ {
+ //Test programmatic login/logout within same request:
+ // login - perform programmatic login that should succeed, next request should be also logged in
+ // loginfail - perform programmatic login that should fail, next request should not be logged in
+ // loginfaillogin - perform programmatic login that should fail then another that succeeds, next request should be logged in
+ // loginlogin - perform successful login then try another that should fail, next request should be logged in
+ // loginlogout - perform successful login then logout, next request should not be logged in
+ // loginlogoutlogin - perform successful login then logout then login successfully again, next request should be logged in
+ _security.setHandler(new ProgrammaticLoginRequestHandler());
+ _security.setAuthenticator(new FormAuthenticator("/testLoginPage", "/testErrorPage", false));
+ _server.start();
+
+ String response;
+
+ //login
+ response = _connector.getResponse("GET /ctx/prog?action=login HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+ response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("user=admin"));
+ _server.stop();
+
+ //loginfail
+ _server.start();
+ response = _connector.getResponse("GET /ctx/prog?action=loginfail HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 500 Server Error"));
+ if (response.contains("JSESSIONID"))
+ {
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+ response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ }
+ else
+ response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n\r\n");
+
+ assertThat(response, not(containsString("user=admin")));
+ _server.stop();
+
+ //loginfaillogin
+ _server.start();
+ response = _connector.getResponse("GET /ctx/prog?action=loginfail HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 500 Server Error"));
+ response = _connector.getResponse("GET /ctx/prog?action=login HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+ response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("user=admin"));
+ _server.stop();
+
+ //loginlogin
+ _server.start();
+ response = _connector.getResponse("GET /ctx/prog?action=loginlogin HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 500 Server Error"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+ response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("user=admin"));
+ _server.stop();
+
+ //loginlogout
+ _server.start();
+ response = _connector.getResponse("GET /ctx/prog?action=loginlogout HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+ response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("user=null"));
+ _server.stop();
+
+ //loginlogoutlogin
+ _server.start();
+ response = _connector.getResponse("GET /ctx/prog?action=loginlogoutlogin HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+ response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("user=user0"));
+ _server.stop();
+
+ //Test constraint-based login with programmatic login/logout:
+ // constraintlogin - perform constraint login, followed by programmatic login which should fail (already logged in)
+ _server.start();
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, containsString(" 302 Found"));
+ assertThat(response, containsString("/ctx/testLoginPage"));
+ assertThat(response, containsString("JSESSIONID="));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/testLoginPage HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, not(containsString("JSESSIONID=" + session)));
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 35\r\n" +
+ "\r\n" +
+ "j_username=user&j_password=password");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("/ctx/auth/info"));
+ assertThat(response, containsString("JSESSIONID="));
+ assertThat(response, not(containsString("JSESSIONID=" + session)));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+ response = _connector.getResponse("GET /ctx/prog?action=constraintlogin HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 500 Server Error"));
+ _server.stop();
+
+ // logout - perform constraint login, followed by programmatic logout, which should succeed
+ _server.start();
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, containsString(" 302 Found"));
+ assertThat(response, containsString("/ctx/testLoginPage"));
+ assertThat(response, containsString("JSESSIONID="));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/testLoginPage HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, not(containsString("JSESSIONID=" + session)));
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 35\r\n" +
+ "\r\n" +
+ "j_username=user&j_password=password");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("/ctx/auth/info"));
+ assertThat(response, containsString("JSESSIONID="));
+ assertThat(response, not(containsString("JSESSIONID=" + session)));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+ response = _connector.getResponse("GET /ctx/prog?action=logout HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("user=null"));
+ }
+
+ @Test
+ public void testStrictBasic() throws Exception
+ {
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ String response;
+ response = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ response = _connector.getResponse("GET /ctx/forbid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 401 Unauthorized"));
+ assertThat(response, containsString("WWW-Authenticate: basic realm=\"TestRealm\""));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("user:wrong") + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 401 Unauthorized"));
+ assertThat(response, containsString("WWW-Authenticate: basic realm=\"TestRealm\""));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("user3:password") + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("user2:password") + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ // test admin
+ response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 401 Unauthorized"));
+ assertThat(response, containsString("WWW-Authenticate: basic realm=\"TestRealm\""));
+
+ response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("admin:wrong") + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 401 Unauthorized"));
+ assertThat(response, containsString("WWW-Authenticate: basic realm=\"TestRealm\""));
+
+ response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("user:password") + "\r\n" +
+ "\r\n");
+
+ assertThat(response, startsWith("HTTP/1.1 403 "));
+ assertThat(response, containsString("!role"));
+
+ response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("admin:password") + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ response = _connector.getResponse("GET /ctx/admin/relax/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ }
+
+ @Test
+ public void testStrictFormDispatch()
+ throws Exception
+ {
+ _security.setAuthenticator(new FormAuthenticator("/testLoginPage", "/testErrorPage", true));
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ response = _connector.getResponse("GET /ctx/forbid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n");
+ // assertThat(response,containsString(" 302 Found"));
+ // assertThat(response,containsString("/ctx/testLoginPage"));
+ assertThat(response, containsString("Cache-Control: no-cache"));
+ assertThat(response, containsString("Expires"));
+ assertThat(response, containsString("URI=/ctx/testLoginPage"));
+
+ String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 31\r\n" +
+ "\r\n" +
+ "j_username=user&j_password=wrong\r\n");
+ // assertThat(response,containsString("Location"));
+ assertThat(response, containsString("testErrorPage"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 36\r\n" +
+ "\r\n" +
+ "j_username=user0&j_password=password\r\n");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("/ctx/auth/info"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403"));
+ assertThat(response, containsString("!role"));
+
+ response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403"));
+ assertThat(response, containsString("!role"));
+
+ // log in again as user2
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n");
+// assertThat(response,startsWith("HTTP/1.1 302 "));
+// assertThat(response,containsString("testLoginPage"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 36\r\n" +
+ "\r\n" +
+ "j_username=user2&j_password=password\r\n");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("/ctx/auth/info"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403"));
+ assertThat(response, containsString("!role"));
+
+ // log in again as admin
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n");
+// assertThat(response,startsWith("HTTP/1.1 302 "));
+// assertThat(response,containsString("testLoginPage"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 36\r\n" +
+ "\r\n" +
+ "j_username=admin&j_password=password\r\n");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("/ctx/auth/info"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ }
+
+ @Test
+ public void testStrictFormRedirect() throws Exception
+ {
+ _security.setAuthenticator(new FormAuthenticator("/testLoginPage", "/testErrorPage", false));
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ response = _connector.getResponse("GET /ctx/forbid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\nHost:wibble.com:8888\r\n\r\n");
+ assertThat(response, containsString(" 302 Found"));
+ assertThat(response, containsString("http://wibble.com:8888/ctx/testLoginPage"));
+
+ String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 31\r\n" +
+ "\r\n" +
+ "j_username=user&j_password=wrong\r\n");
+ assertThat(response, containsString("Location"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 36\r\n" +
+ "\r\n" +
+ "j_username=user3&j_password=password\r\n");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("/ctx/auth/info"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403"));
+ assertThat(response, containsString("!role"));
+
+ response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403"));
+ assertThat(response, containsString("!role"));
+
+ // log in again as user2
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("testLoginPage"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 36\r\n" +
+ "\r\n" +
+ "j_username=user2&j_password=password\r\n");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("/ctx/auth/info"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ //check user2 does not have right role to access /admin/*
+ response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403"));
+ assertThat(response, containsString("!role"));
+
+ //log in as user3, who doesn't have a valid role, but we are checking a constraint
+ //of ** which just means they have to be authenticated
+ response = _connector.getResponse("GET /ctx/starstar/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("testLoginPage"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 36\r\n" +
+ "\r\n" +
+ "j_username=user3&j_password=password\r\n");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("/ctx/starstar/info"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/starstar/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ // log in again as admin
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n");
+// assertThat(response,startsWith("HTTP/1.1 302 "));
+// assertThat(response,containsString("testLoginPage"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 36\r\n" +
+ "\r\n" +
+ "j_username=admin&j_password=password\r\n");
+ assertThat(response, startsWith("HTTP/1.1 302 "));
+ assertThat(response, containsString("Location"));
+ assertThat(response, containsString("/ctx/auth/info"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx"));
+
+ response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ }
+
+ @Test
+ public void testDataRedirection() throws Exception
+ {
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ String rawResponse;
+
+ rawResponse = _connector.getResponse("GET /ctx/data/info HTTP/1.0\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.FORBIDDEN_403));
+
+ _config.setSecurePort(8443);
+ _config.setSecureScheme("https");
+
+ rawResponse = _connector.getResponse("GET /ctx/data/info HTTP/1.0\r\n\r\n");
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.FOUND_302));
+ String location = response.get(HttpHeader.LOCATION);
+ assertThat("Location header", location, containsString(":8443/ctx/data/info"));
+ assertThat("Location header", location, not(containsString("https:///")));
+
+ _config.setSecurePort(443);
+ rawResponse = _connector.getResponse("GET /ctx/data/info HTTP/1.0\r\n\r\n");
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.FOUND_302));
+ location = response.get(HttpHeader.LOCATION);
+ assertThat("Location header", location, not(containsString(":443/ctx/data/info")));
+
+ _config.setSecurePort(8443);
+ rawResponse = _connector.getResponse("GET /ctx/data/info HTTP/1.0\r\nHost: wobble.com\r\n\r\n");
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.FOUND_302));
+ location = response.get(HttpHeader.LOCATION);
+ assertThat("Location header", location, containsString("https://wobble.com:8443/ctx/data/info"));
+
+ _config.setSecurePort(443);
+ rawResponse = _connector.getResponse("GET /ctx/data/info HTTP/1.0\r\nHost: wobble.com\r\n\r\n");
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.FOUND_302));
+ location = response.get(HttpHeader.LOCATION);
+ assertThat("Location header", location, containsString("https://wobble.com/ctx/data/info"));
+ }
+
+ @Test
+ public void testRoleRef() throws Exception
+ {
+ RoleCheckHandler check = new RoleCheckHandler();
+ _security.setHandler(check);
+ _security.setAuthenticator(new BasicAuthenticator());
+
+ _server.start();
+
+ String rawResponse;
+ rawResponse = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n\r\n", 100000, TimeUnit.MILLISECONDS);
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
+
+ rawResponse = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("user2:password") + "\r\n" +
+ "\r\n", 100000, TimeUnit.MILLISECONDS);
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.INTERNAL_SERVER_ERROR_500));
+
+ _server.stop();
+
+ RoleRefHandler roleref = new RoleRefHandler();
+ roleref.setHandler(_security.getHandler());
+ _security.setHandler(roleref);
+ roleref.setHandler(check);
+
+ _server.start();
+
+ rawResponse = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("user2:password") + "\r\n" +
+ "\r\n", 100000, TimeUnit.MILLISECONDS);
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
+ }
+
+ @Test
+ public void testDeferredBasic() throws Exception
+ {
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("user=null"));
+
+ response = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("admin:wrong") + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("user=null"));
+
+ response = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n" +
+ "Authorization: Basic " + authBase64("admin:password") + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("user=admin"));
+ }
+
+ @Test
+ public void testRelaxedMethod() throws Exception
+ {
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ String response;
+ response = _connector.getResponse("GET /ctx/forbid/somethig HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 "));
+
+ response = _connector.getResponse("POST /ctx/forbid/post HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 "));
+
+ response = _connector.getResponse("GET /ctx/forbid/post HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 "));
+ }
+
+ @Test
+ public void testUncoveredMethod() throws Exception
+ {
+ ConstraintMapping specificMethod = new ConstraintMapping();
+ specificMethod.setMethod("GET");
+ specificMethod.setPathSpec("/specific/method");
+ specificMethod.setConstraint(_forbidConstraint);
+ _security.addConstraintMapping(specificMethod);
+ _security.setAuthenticator(new BasicAuthenticator());
+ Logger.getAnonymousLogger().info("Uncovered method for /specific/method is expected");
+ _server.start();
+
+ assertThat(_security.getPathsWithUncoveredHttpMethods(), contains("/specific/method"));
+
+ String response;
+ response = _connector.getResponse("GET /ctx/specific/method HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 "));
+
+ response = _connector.getResponse("POST /ctx/specific/method HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 ")); // This is so stupid, but it is the S P E C
+ }
+
+ @Test
+ public void testForbidTraceAndOptions() throws Exception
+ {
+ ConstraintMapping forbidTrace = new ConstraintMapping();
+ forbidTrace.setMethod("TRACE");
+ forbidTrace.setPathSpec("/");
+ forbidTrace.setConstraint(_forbidConstraint);
+ ConstraintMapping allowOmitTrace = new ConstraintMapping();
+ allowOmitTrace.setMethodOmissions(new String[] {"TRACE"});
+ allowOmitTrace.setPathSpec("/");
+ allowOmitTrace.setConstraint(_relaxConstraint);
+
+ ConstraintMapping forbidOptions = new ConstraintMapping();
+ forbidOptions.setMethod("OPTIONS");
+ forbidOptions.setPathSpec("/");
+ forbidOptions.setConstraint(_forbidConstraint);
+ ConstraintMapping allowOmitOptions = new ConstraintMapping();
+ allowOmitOptions.setMethodOmissions(new String[] {"OPTIONS"});
+ allowOmitOptions.setPathSpec("/");
+ allowOmitOptions.setConstraint(_relaxConstraint);
+
+ ConstraintMapping someConstraint = new ConstraintMapping();
+ someConstraint.setPathSpec("/some/constaint/*");
+ someConstraint.setConstraint(_noAuthConstraint);
+
+ _security.setConstraintMappings(new ConstraintMapping[] {forbidTrace, allowOmitTrace, forbidOptions, allowOmitOptions, someConstraint});
+
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ assertThat(_security.getPathsWithUncoveredHttpMethods(), Matchers.empty());
+
+ String response;
+ response = _connector.getResponse("TRACE /ctx/some/path HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 "));
+
+ response = _connector.getResponse("OPTIONS /ctx/some/path HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 "));
+
+ response = _connector.getResponse("GET /ctx/some/path HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 "));
+
+ response = _connector.getResponse("GET /ctx/some/constraint/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 "));
+
+ response = _connector.getResponse("OPTIONS /ctx/some/constraint/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 "));
+ }
+
+ private static String authBase64(String authorization)
+ {
+ byte[] raw = authorization.getBytes(ISO_8859_1);
+ return Base64.getEncoder().encodeToString(raw);
+ }
+
+ private class RequestHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ if (request.getAuthType() == null || "user".equals(request.getRemoteUser()) || request.isUserInRole("user") || request.isUserInRole("foo"))
+ {
+ response.setStatus(200);
+ response.setContentType("text/plain; charset=UTF-8");
+ response.getWriter().println("URI=" + request.getRequestURI());
+ String user = request.getRemoteUser();
+ response.getWriter().println("user=" + user);
+ if (request.getParameter("test_parameter") != null)
+ response.getWriter().println(request.getParameter("test_parameter"));
+ }
+ else
+ response.sendError(500);
+ }
+ }
+
+ private class ProgrammaticLoginRequestHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+
+ String action = request.getParameter("action");
+ if (StringUtil.isBlank(action))
+ {
+ response.setStatus(200);
+ response.setContentType("text/plain; charset=UTF-8");
+ response.getWriter().println("user=" + request.getRemoteUser());
+ return;
+ }
+ else if ("login".equals(action))
+ {
+ request.login("admin", "password");
+ return;
+ }
+ else if ("loginfail".equals(action))
+ {
+ request.login("admin", "fail");
+ return;
+ }
+ else if ("loginfaillogin".equals(action))
+ {
+ try
+ {
+ request.login("admin", "fail");
+ }
+ catch (ServletException e)
+ {
+ request.login("admin", "password");
+ }
+ return;
+ }
+ else if ("loginlogin".equals(action))
+ {
+ request.login("admin", "password");
+ request.login("foo", "bar");
+ }
+ else if ("loginlogout".equals(action))
+ {
+ request.login("admin", "password");
+ request.logout();
+ }
+ else if ("loginlogoutlogin".equals(action))
+ {
+ request.login("admin", "password");
+ request.logout();
+ request.login("user0", "password");
+ }
+ else if ("constraintlogin".equals(action))
+ {
+ String user = request.getRemoteUser();
+ request.login("admin", "password");
+ }
+ else if ("logout".equals(action))
+ {
+ request.logout();
+ }
+ else
+ response.sendError(500);
+ }
+ }
+
+ private class RoleRefHandler extends HandlerWrapper
+ {
+
+ /**
+ * @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+ */
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ UserIdentity.Scope old = ((Request)request).getUserIdentityScope();
+
+ UserIdentity.Scope scope = new UserIdentity.Scope()
+ {
+ @Override
+ public ContextHandler getContextHandler()
+ {
+ return null;
+ }
+
+ @Override
+ public String getContextPath()
+ {
+ return "/";
+ }
+
+ @Override
+ public String getName()
+ {
+ return "someServlet";
+ }
+
+ @Override
+ public Map<String, String> getRoleRefMap()
+ {
+ Map<String, String> map = new HashMap<>();
+ map.put("untranslated", "user");
+ return map;
+ }
+ };
+
+ ((Request)request).setUserIdentityScope(scope);
+
+ try
+ {
+ super.handle(target, baseRequest, request, response);
+ }
+ finally
+ {
+ ((Request)request).setUserIdentityScope(old);
+ }
+ }
+ }
+
+ private class RoleCheckHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ ((Request)request).setHandled(true);
+ if (request.getAuthType() == null || "user".equals(request.getRemoteUser()) || request.isUserInRole("untranslated"))
+ response.setStatus(200);
+ else
+ response.sendError(500);
+ }
+ }
+
+ public static class Scenario
+ {
+ public final String rawRequest;
+ public final int expectedStatus;
+ public Consumer<HttpTester.Response> extraAsserts;
+
+ public Scenario(String rawRequest, int expectedStatus)
+ {
+ this.rawRequest = rawRequest;
+ this.expectedStatus = expectedStatus;
+ }
+
+ public Scenario(String rawRequest, int expectedStatus, Consumer<HttpTester.Response> extraAsserts)
+ {
+ this.rawRequest = rawRequest;
+ this.expectedStatus = expectedStatus;
+ this.extraAsserts = extraAsserts;
+ }
+
+ @Override
+ public String toString()
+ {
+ return rawRequest;
+ }
+ }
+}
diff --git a/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/DataConstraintsTest.java b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/DataConstraintsTest.java
new file mode 100644
index 0000000..101587f
--- /dev/null
+++ b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/DataConstraintsTest.java
@@ -0,0 +1,462 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.IOException;
+import java.util.Arrays;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.security.authentication.BasicAuthenticator;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.util.security.Constraint;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class DataConstraintsTest
+{
+ private Server _server;
+ private LocalConnector _connector;
+ private LocalConnector _connectorS;
+ private SessionHandler _session;
+ private ConstraintSecurityHandler _security;
+
+ @BeforeEach
+ public void startServer()
+ {
+ _server = new Server();
+
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ http.getHttpConfiguration().setSecurePort(9999);
+ http.getHttpConfiguration().setSecureScheme("BWTP");
+ _connector = new LocalConnector(_server, http);
+ _connector.setIdleTimeout(300000);
+
+ HttpConnectionFactory https = new HttpConnectionFactory();
+ https.getHttpConfiguration().addCustomizer(new HttpConfiguration.Customizer()
+ {
+ @Override
+ public void customize(Connector connector, HttpConfiguration channelConfig, Request request)
+ {
+ request.setScheme(HttpScheme.HTTPS.asString());
+ request.setSecure(true);
+ }
+ });
+
+ _connectorS = new LocalConnector(_server, https);
+ _server.setConnectors(new Connector[]{_connector, _connectorS});
+
+ ContextHandler contextHandler = new ContextHandler();
+ _session = new SessionHandler();
+
+ contextHandler.setContextPath("/ctx");
+ _server.setHandler(contextHandler);
+ contextHandler.setHandler(_session);
+
+ _security = new ConstraintSecurityHandler();
+ _session.setHandler(_security);
+
+ _security.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.sendError(404);
+ }
+ });
+ }
+
+ @AfterEach
+ public void stopServer() throws Exception
+ {
+ if (_server.isRunning())
+ {
+ _server.stop();
+ _server.join();
+ }
+ }
+
+ @Test
+ public void testIntegral() throws Exception
+ {
+ Constraint constraint0 = new Constraint();
+ constraint0.setAuthenticate(false);
+ constraint0.setName("integral");
+ constraint0.setDataConstraint(Constraint.DC_INTEGRAL);
+ ConstraintMapping mapping0 = new ConstraintMapping();
+ mapping0.setPathSpec("/integral/*");
+ mapping0.setConstraint(constraint0);
+
+ _security.setConstraintMappings(Arrays.asList(new ConstraintMapping[]
+ {
+ mapping0
+ }));
+
+ _server.start();
+
+ String response;
+ response = _connector.getResponse("GET /ctx/some/thing HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+
+ response = _connector.getResponse("GET /ctx/integral/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 302 Found"));
+ assertThat(response, Matchers.containsString("Location: BWTP://"));
+ assertThat(response, Matchers.containsString(":9999"));
+
+ response = _connectorS.getResponse("GET /ctx/integral/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+ }
+
+ @Test
+ public void testConfidential() throws Exception
+ {
+ Constraint constraint0 = new Constraint();
+ constraint0.setAuthenticate(false);
+ constraint0.setName("confid");
+ constraint0.setDataConstraint(Constraint.DC_CONFIDENTIAL);
+ ConstraintMapping mapping0 = new ConstraintMapping();
+ mapping0.setPathSpec("/confid/*");
+ mapping0.setConstraint(constraint0);
+
+ _security.setConstraintMappings(Arrays.asList(new ConstraintMapping[]
+ {
+ mapping0
+ }));
+
+ _server.start();
+
+ String response;
+ response = _connector.getResponse("GET /ctx/some/thing HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+
+ response = _connector.getResponse("GET /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 302 Found"));
+ assertThat(response, Matchers.containsString("Location: BWTP://"));
+ assertThat(response, Matchers.containsString(":9999"));
+
+ response = _connectorS.getResponse("GET /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+ }
+
+ @Test
+ public void testConfidentialWithNoRolesSetAndNoMethodRestriction() throws Exception
+ {
+ Constraint constraint0 = new Constraint();
+ constraint0.setName("confid");
+ constraint0.setDataConstraint(Constraint.DC_CONFIDENTIAL);
+ ConstraintMapping mapping0 = new ConstraintMapping();
+ mapping0.setPathSpec("/confid/*");
+ mapping0.setConstraint(constraint0);
+
+ _security.setConstraintMappings(Arrays.asList(new ConstraintMapping[]
+ {
+ mapping0
+ }));
+
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 302 Found"));
+
+ response = _connectorS.getResponse("GET /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+ }
+
+ @Test
+ public void testConfidentialWithNoRolesSetAndMethodRestriction() throws Exception
+ {
+ Constraint constraint0 = new Constraint();
+ constraint0.setName("confid");
+ constraint0.setDataConstraint(Constraint.DC_CONFIDENTIAL);
+ ConstraintMapping mapping0 = new ConstraintMapping();
+ mapping0.setPathSpec("/confid/*");
+ mapping0.setMethod(HttpMethod.POST.asString());
+ mapping0.setConstraint(constraint0);
+
+ _security.setConstraintMappings(Arrays.asList(new ConstraintMapping[]
+ {
+ mapping0
+ }));
+
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+
+ response = _connectorS.getResponse("GET /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+
+ response = _connector.getResponse("POST /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 302 Found"));
+
+ response = _connectorS.getResponse("POST /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+ }
+
+ @Test
+ public void testConfidentialWithRolesSetAndMethodRestriction() throws Exception
+ {
+ Constraint constraint0 = new Constraint();
+ constraint0.setRoles(new String[]{"admin"});
+ constraint0.setName("confid");
+ constraint0.setDataConstraint(Constraint.DC_CONFIDENTIAL);
+ ConstraintMapping mapping0 = new ConstraintMapping();
+ mapping0.setPathSpec("/confid/*");
+ mapping0.setMethod(HttpMethod.POST.asString());
+ mapping0.setConstraint(constraint0);
+
+ _security.setConstraintMappings(Arrays.asList(new ConstraintMapping[]
+ {
+ mapping0
+ }));
+
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+
+ response = _connectorS.getResponse("GET /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+
+ response = _connector.getResponse("POST /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 302 Found"));
+
+ response = _connectorS.getResponse("POST /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+ }
+
+ @Test
+ public void testConfidentialWithRolesSetAndMethodRestrictionAndAuthenticationRequired() throws Exception
+ {
+ Constraint constraint0 = new Constraint();
+ constraint0.setRoles(new String[]{"admin"});
+ constraint0.setAuthenticate(true);
+ constraint0.setName("confid");
+ constraint0.setDataConstraint(Constraint.DC_CONFIDENTIAL);
+ ConstraintMapping mapping0 = new ConstraintMapping();
+ mapping0.setPathSpec("/confid/*");
+ mapping0.setMethod(HttpMethod.POST.asString());
+ mapping0.setConstraint(constraint0);
+
+ _security.setConstraintMappings(Arrays.asList(new ConstraintMapping[]
+ {
+ mapping0
+ }));
+ DefaultIdentityService identityService = new DefaultIdentityService();
+ _security.setLoginService(new CustomLoginService(identityService));
+ _security.setIdentityService(identityService);
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+
+ response = _connectorS.getResponse("GET /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+
+ response = _connector.getResponse("POST /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 302 Found"));
+
+ response = _connectorS.getResponse("POST /ctx/confid/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 401 Unauthorized"));
+
+ response = _connector.getResponse("GET /ctx/confid/info HTTP/1.0\r\nAuthorization: Basic YWRtaW46cGFzc3dvcmQ=\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+
+ response = _connector.getResponse("POST /ctx/confid/info HTTP/1.0\r\nAuthorization: Basic YWRtaW46cGFzc3dvcmQ=\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 302 Found"));
+
+ response = _connectorS.getResponse("POST /ctx/confid/info HTTP/1.0\r\nAuthorization: Basic YWRtaW46cGFzc3dvcmQ=\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+ }
+
+ @Test
+ public void testRestrictedWithoutAuthenticator() throws Exception
+ {
+ Constraint constraint0 = new Constraint();
+ constraint0.setAuthenticate(true);
+ constraint0.setRoles(new String[]{"admin"});
+ constraint0.setName("restricted");
+ ConstraintMapping mapping0 = new ConstraintMapping();
+ mapping0.setPathSpec("/restricted/*");
+ mapping0.setConstraint(constraint0);
+
+ _security.setConstraintMappings(Arrays.asList(new ConstraintMapping[]
+ {
+ mapping0
+ }));
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/restricted/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 403 Forbidden"));
+
+ response = _connectorS.getResponse("GET /ctx/restricted/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 403 Forbidden"));
+
+ response = _connector.getResponse("GET /ctx/restricted/info HTTP/1.0\r\nAuthorization: Basic YWRtaW46cGFzc3dvcmQ=\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 403 Forbidden"));
+
+ response = _connectorS.getResponse("GET /ctx/restricted/info HTTP/1.0\r\nAuthorization: Basic YWRtaW46cGFzc3dvcmQ=\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 403 Forbidden"));
+ }
+
+ @Test
+ public void testRestrictedWithoutAuthenticatorAndMethod() throws Exception
+ {
+ Constraint constraint0 = new Constraint();
+ constraint0.setAuthenticate(true);
+ constraint0.setRoles(new String[]{"admin"});
+ constraint0.setName("restricted");
+ ConstraintMapping mapping0 = new ConstraintMapping();
+ mapping0.setPathSpec("/restricted/*");
+ mapping0.setMethod("GET");
+ mapping0.setConstraint(constraint0);
+
+ _security.setConstraintMappings(Arrays.asList(new ConstraintMapping[]
+ {
+ mapping0
+ }));
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/restricted/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 403 Forbidden"));
+
+ response = _connectorS.getResponse("GET /ctx/restricted/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 403 Forbidden"));
+
+ response = _connector.getResponse("GET /ctx/restricted/info HTTP/1.0\r\nAuthorization: Basic YWRtaW46cGFzc3dvcmQ=\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 403 Forbidden"));
+
+ response = _connectorS.getResponse("GET /ctx/restricted/info HTTP/1.0\r\nAuthorization: Basic YWRtaW46cGFzc3dvcmQ=\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 403 Forbidden"));
+ }
+
+ @Test
+ public void testRestricted() throws Exception
+ {
+ Constraint constraint0 = new Constraint();
+ constraint0.setAuthenticate(true);
+ constraint0.setRoles(new String[]{"admin"});
+ constraint0.setName("restricted");
+ ConstraintMapping mapping0 = new ConstraintMapping();
+ mapping0.setPathSpec("/restricted/*");
+ mapping0.setMethod("GET");
+ mapping0.setConstraint(constraint0);
+
+ _security.setConstraintMappings(Arrays.asList(new ConstraintMapping[]
+ {
+ mapping0
+ }));
+ DefaultIdentityService identityService = new DefaultIdentityService();
+ _security.setLoginService(new CustomLoginService(identityService));
+ _security.setIdentityService(identityService);
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ String response;
+
+ response = _connector.getResponse("GET /ctx/restricted/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 401 Unauthorized"));
+
+ response = _connectorS.getResponse("GET /ctx/restricted/info HTTP/1.0\r\n\r\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 401 Unauthorized"));
+
+ response = _connector.getResponse("GET /ctx/restricted/info HTTP/1.0\nAuthorization: Basic YWRtaW46cGFzc3dvcmQ=\n\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+
+ response = _connectorS.getResponse("GET /ctx/restricted/info HTTP/1.0\nAuthorization: Basic YWRtaW46cGFzc3dvcmQ=\n\n");
+ assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found"));
+ }
+
+ private class CustomLoginService implements LoginService
+ {
+ private IdentityService identityService;
+
+ public CustomLoginService(IdentityService identityService)
+ {
+ this.identityService = identityService;
+ }
+
+ @Override
+ public String getName()
+ {
+ return "name";
+ }
+
+ @Override
+ public UserIdentity login(String username, Object credentials, ServletRequest request)
+ {
+ if ("admin".equals(username) && "password".equals(credentials))
+ return new DefaultUserIdentity(null, null, new String[]{"admin"});
+ return null;
+ }
+
+ @Override
+ public boolean validate(UserIdentity user)
+ {
+ return false;
+ }
+
+ @Override
+ public IdentityService getIdentityService()
+ {
+ return identityService;
+ }
+
+ @Override
+ public void setIdentityService(IdentityService service)
+ {
+ }
+
+ @Override
+ public void logout(UserIdentity user)
+ {
+ }
+ }
+}
diff --git a/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/DefaultIdentityServiceTest.java b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/DefaultIdentityServiceTest.java
new file mode 100644
index 0000000..f9555c7
--- /dev/null
+++ b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/DefaultIdentityServiceTest.java
@@ -0,0 +1,94 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Server;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
+
+public class DefaultIdentityServiceTest
+{
+ @Test
+ public void testDefaultIdentityService() throws Exception
+ {
+ Server server = new Server();
+ ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
+ TestAuthenticator authenticator = new TestAuthenticator();
+ securityHandler.setAuthenticator(authenticator);
+
+ try
+ {
+ server.setHandler(securityHandler);
+ server.start();
+
+ // The DefaultIdentityService should have been created by default.
+ assertThat(securityHandler.getIdentityService(), instanceOf(DefaultIdentityService.class));
+ assertThat(authenticator.getIdentityService(), instanceOf(DefaultIdentityService.class));
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ public static class TestAuthenticator implements Authenticator
+ {
+ private IdentityService _identityService;
+
+ public IdentityService getIdentityService()
+ {
+ return _identityService;
+ }
+
+ @Override
+ public void setConfiguration(AuthConfiguration configuration)
+ {
+ _identityService = configuration.getIdentityService();
+ }
+
+ @Override
+ public String getAuthMethod()
+ {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public void prepareRequest(ServletRequest request)
+ {
+ }
+
+ @Override
+ public Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException
+ {
+ return null;
+ }
+
+ @Override
+ public boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, Authentication.User validatedUser) throws ServerAuthException
+ {
+ return false;
+ }
+ }
+}
diff --git a/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/HashLoginServiceTest.java b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/HashLoginServiceTest.java
new file mode 100644
index 0000000..c299e53
--- /dev/null
+++ b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/HashLoginServiceTest.java
@@ -0,0 +1,68 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests of the HashLoginService.
+ */
+public class HashLoginServiceTest
+{
+ @Test
+ public void testAutoCreatedUserStore() throws Exception
+ {
+ HashLoginService loginService = new HashLoginService("foo", MavenTestingUtils.getTestResourceFile("foo.properties").getAbsolutePath());
+ assertThat(loginService.getIdentityService(), is(notNullValue()));
+ loginService.start();
+ assertTrue(loginService.getUserStore().isStarted());
+ assertTrue(loginService.isUserStoreAutoCreate());
+
+ loginService.stop();
+ assertFalse(loginService.isUserStoreAutoCreate());
+ assertThat(loginService.getUserStore(), is(nullValue()));
+ }
+
+ @Test
+ public void testProvidedUserStore() throws Exception
+ {
+ HashLoginService loginService = new HashLoginService("foo");
+ assertThat(loginService.getIdentityService(), is(notNullValue()));
+ UserStore store = new UserStore();
+ loginService.setUserStore(store);
+ assertFalse(store.isStarted());
+ loginService.start();
+ assertTrue(loginService.getUserStore().isStarted());
+ assertFalse(loginService.isUserStoreAutoCreate());
+
+ loginService.stop();
+
+ assertFalse(loginService.isUserStoreAutoCreate());
+ assertFalse(store.isStarted());
+ assertThat(loginService.getUserStore(), is(notNullValue()));
+ }
+}
diff --git a/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/PropertyUserStoreTest.java b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/PropertyUserStoreTest.java
new file mode 100644
index 0000000..f762eeb
--- /dev/null
+++ b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/PropertyUserStoreTest.java
@@ -0,0 +1,310 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.security.Credential;
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.condition.OS.MAC;
+
+@ExtendWith(WorkDirExtension.class)
+public class PropertyUserStoreTest
+{
+ private final class UserCount implements PropertyUserStore.UserListener
+ {
+ private final AtomicInteger userCount = new AtomicInteger();
+ private final List<String> users = new ArrayList<String>();
+
+ private UserCount()
+ {
+ }
+
+ @Override
+ public void update(String username, Credential credential, String[] roleArray)
+ {
+ if (!users.contains(username))
+ {
+ users.add(username);
+ userCount.getAndIncrement();
+ }
+ }
+
+ @Override
+ public void remove(String username)
+ {
+ users.remove(username);
+ userCount.getAndDecrement();
+ }
+
+ public void awaitCount(int expectedCount) throws InterruptedException
+ {
+ long timeout = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + TimeUnit.SECONDS.toMillis(10);
+
+ while (userCount.get() != expectedCount && (TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) < timeout))
+ {
+ TimeUnit.MILLISECONDS.sleep(100);
+ }
+
+ assertThatCount(is(expectedCount));
+ }
+
+ public void assertThatCount(Matcher<Integer> matcher)
+ {
+ assertThat("User count", userCount.get(), matcher);
+ }
+
+ public void assertThatUsers(Matcher<Iterable<? super String>> matcher)
+ {
+ assertThat("Users list", users, matcher);
+ }
+ }
+
+ public WorkDir testdir;
+
+ private Path initUsersText() throws Exception
+ {
+ Path dir = testdir.getPath();
+ Path users = dir.resolve("users.txt");
+ Files.deleteIfExists(users);
+
+ writeUser(users);
+ return users;
+ }
+
+ private String initUsersPackedFileText()
+ throws Exception
+ {
+ Path dir = testdir.getPath();
+ File users = dir.resolve("users.txt").toFile();
+ writeUser(users);
+ File usersJar = dir.resolve("users.jar").toFile();
+ String entryPath = "mountain_goat/pale_ale.txt";
+ try (FileInputStream fileInputStream = new FileInputStream(users))
+ {
+ try (OutputStream outputStream = new FileOutputStream(usersJar))
+ {
+ try (JarOutputStream jarOutputStream = new JarOutputStream(outputStream))
+ {
+ // add fake entry
+ jarOutputStream.putNextEntry(new JarEntry("foo/wine"));
+
+ JarEntry jarEntry = new JarEntry(entryPath);
+ jarOutputStream.putNextEntry(jarEntry);
+ byte[] buffer = new byte[1024];
+ int bytesRead;
+ while ((bytesRead = fileInputStream.read(buffer)) != -1)
+ {
+ jarOutputStream.write(buffer, 0, bytesRead);
+ }
+ // add fake entry
+ jarOutputStream.putNextEntry(new JarEntry("foo/cheese"));
+ }
+ }
+ }
+ return "jar:" + usersJar.toURI().toASCIIString() + "!/" + entryPath;
+ }
+
+ private void writeUser(File usersFile) throws IOException
+ {
+ writeUser(usersFile.toPath());
+ }
+
+ private void writeUser(Path usersFile) throws IOException
+ {
+ try (Writer writer = Files.newBufferedWriter(usersFile, UTF_8))
+ {
+ writer.append("tom: tom, roleA\n");
+ writer.append("dick: dick, roleB\n");
+ writer.append("harry: harry, roleA, roleB\n");
+ }
+ }
+
+ private void addAdditionalUser(Path usersFile, String userRef) throws Exception
+ {
+ Thread.sleep(1001);
+ try (Writer writer = Files.newBufferedWriter(usersFile, UTF_8, StandardOpenOption.APPEND))
+ {
+ writer.append(userRef);
+ }
+ }
+
+ @Test
+ public void testPropertyUserStoreLoad() throws Exception
+ {
+ testdir.ensureEmpty();
+
+ final UserCount userCount = new UserCount();
+ final Path usersFile = initUsersText();
+
+ PropertyUserStore store = new PropertyUserStore();
+ store.setConfigFile(usersFile.toFile());
+
+ store.registerUserListener(userCount);
+
+ store.start();
+
+ assertThat("Failed to retrieve UserIdentity directly from PropertyUserStore", store.getUserIdentity("tom"), notNullValue());
+ assertThat("Failed to retrieve UserIdentity directly from PropertyUserStore", store.getUserIdentity("dick"), notNullValue());
+ assertThat("Failed to retrieve UserIdentity directly from PropertyUserStore", store.getUserIdentity("harry"), notNullValue());
+ userCount.assertThatCount(is(3));
+ userCount.awaitCount(3);
+ }
+
+ @Test
+ public void testPropertyUserStoreFails() throws Exception
+ {
+ assertThrows(IllegalStateException.class, () ->
+ {
+ PropertyUserStore store = new PropertyUserStore();
+ store.setConfig("file:/this/file/does/not/exist.txt");
+ store.start();
+ });
+ }
+
+ @Test
+ public void testPropertyUserStoreLoadFromJarFile() throws Exception
+ {
+ testdir.ensureEmpty();
+
+ final UserCount userCount = new UserCount();
+ final String usersFile = initUsersPackedFileText();
+
+ PropertyUserStore store = new PropertyUserStore();
+ store.setConfig(usersFile);
+
+ store.registerUserListener(userCount);
+
+ store.start();
+
+ assertThat("Failed to retrieve UserIdentity directly from PropertyUserStore", //
+ store.getUserIdentity("tom"), notNullValue());
+ assertThat("Failed to retrieve UserIdentity directly from PropertyUserStore", //
+ store.getUserIdentity("dick"), notNullValue());
+ assertThat("Failed to retrieve UserIdentity directly from PropertyUserStore", //
+ store.getUserIdentity("harry"), notNullValue());
+ userCount.assertThatCount(is(3));
+ userCount.awaitCount(3);
+ }
+
+ @Test
+ @DisabledOnOs(MAC)
+ public void testPropertyUserStoreLoadUpdateUser() throws Exception
+ {
+ testdir.ensureEmpty();
+
+ final UserCount userCount = new UserCount();
+ final Path usersFile = initUsersText();
+ final AtomicInteger loadCount = new AtomicInteger(0);
+ PropertyUserStore store = new PropertyUserStore()
+ {
+ @Override
+ protected void loadUsers() throws IOException
+ {
+ loadCount.incrementAndGet();
+ super.loadUsers();
+ }
+ };
+ store.setHotReload(true);
+ store.setConfigFile(usersFile.toFile());
+ store.registerUserListener(userCount);
+
+ store.start();
+
+ userCount.assertThatCount(is(3));
+ assertThat(loadCount.get(), is(1));
+
+ addAdditionalUser(usersFile, "skip: skip, roleA\n");
+ userCount.awaitCount(4);
+ assertThat(loadCount.get(), is(2));
+ assertThat(store.getUserIdentity("skip"), notNullValue());
+ userCount.assertThatCount(is(4));
+ userCount.assertThatUsers(hasItem("skip"));
+
+ if (OS.LINUX.isCurrentOs())
+ Files.createFile(testdir.getPath().toRealPath().resolve("unrelated.txt"),
+ PosixFilePermissions.asFileAttribute(EnumSet.noneOf(PosixFilePermission.class)));
+ else
+ Files.createFile(testdir.getPath().toRealPath().resolve("unrelated.txt"));
+
+ Thread.sleep(1100);
+ assertThat(loadCount.get(), is(2));
+
+ userCount.assertThatCount(is(4));
+ userCount.assertThatUsers(hasItem("skip"));
+ }
+
+ @Test
+ public void testPropertyUserStoreLoadRemoveUser() throws Exception
+ {
+ testdir.ensureEmpty();
+
+ final UserCount userCount = new UserCount();
+ // initial user file (3) users
+ final Path usersFile = initUsersText();
+
+ // adding 4th user
+ addAdditionalUser(usersFile, "skip: skip, roleA\n");
+
+ PropertyUserStore store = new PropertyUserStore();
+ store.setHotReload(true);
+ store.setConfigFile(usersFile.toFile());
+
+ store.registerUserListener(userCount);
+
+ store.start();
+
+ userCount.assertThatCount(is(4));
+
+ // rewrite file with original 3 users
+ initUsersText();
+
+ userCount.awaitCount(3);
+ }
+}
diff --git a/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/SessionAuthenticationTest.java b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/SessionAuthenticationTest.java
new file mode 100644
index 0000000..aca6a7a
--- /dev/null
+++ b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/SessionAuthenticationTest.java
@@ -0,0 +1,93 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.eclipse.jetty.security.authentication.SessionAuthentication;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.security.Password;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * SessionAuthenticationTest
+ *
+ */
+public class SessionAuthenticationTest
+{
+ /**
+ * Check that a SessionAuthenticator is serializable, and that
+ * the deserialized SessionAuthenticator contains the same authentication
+ * and authorization information.
+ */
+ @Test
+ public void testSessionAuthenticationSerialization()
+ throws Exception
+ {
+
+ ContextHandler contextHandler = new ContextHandler();
+ SecurityHandler securityHandler = new ConstraintSecurityHandler();
+ contextHandler.setHandler(securityHandler);
+ TestLoginService loginService = new TestLoginService("SessionAuthTest");
+ Password pwd = new Password("foo");
+ loginService.putUser("foo", pwd, new String[]{"boss", "worker"});
+ securityHandler.setLoginService(loginService);
+ securityHandler.setAuthMethod("FORM");
+ UserIdentity user = loginService.login("foo", pwd, null);
+ assertNotNull(user);
+ assertNotNull(user.getUserPrincipal());
+ assertEquals("foo", user.getUserPrincipal().getName());
+ SessionAuthentication sessionAuth = new SessionAuthentication("FORM", user, pwd);
+ assertTrue(sessionAuth.isUserInRole(null, "boss"));
+ contextHandler.handle(new Runnable()
+ {
+ public void run()
+ {
+ try
+ {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(baos);
+ oos.writeObject(sessionAuth);
+ oos.close();
+ ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
+ SessionAuthentication reactivatedSessionAuth = (SessionAuthentication)ois.readObject();
+ assertNotNull(reactivatedSessionAuth);
+ assertNotNull(reactivatedSessionAuth.getUserIdentity());
+ assertNotNull(reactivatedSessionAuth.getUserIdentity().getUserPrincipal());
+ assertEquals("foo", reactivatedSessionAuth.getUserIdentity().getUserPrincipal().getName());
+ assertNotNull(reactivatedSessionAuth.getUserIdentity().getSubject());
+ assertTrue(reactivatedSessionAuth.isUserInRole(null, "boss"));
+ }
+ catch (Exception e)
+ {
+ fail(e);
+ }
+ }
+ });
+ }
+}
diff --git a/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java
new file mode 100644
index 0000000..77d50f9
--- /dev/null
+++ b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java
@@ -0,0 +1,351 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashSet;
+import java.util.Set;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.security.authentication.BasicAuthenticator;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.util.security.Constraint;
+import org.eclipse.jetty.util.security.Password;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @version $Revision: 1441 $ $Date: 2010-04-02 12:28:17 +0200 (Fri, 02 Apr 2010) $
+ */
+public class SpecExampleConstraintTest
+{
+ private static final String TEST_REALM = "TestRealm";
+ private static Server _server;
+ private static LocalConnector _connector;
+ private static SessionHandler _session;
+ private ConstraintSecurityHandler _security;
+
+ @BeforeAll
+ public static void startServer()
+ {
+ _server = new Server();
+ _connector = new LocalConnector(_server);
+ _server.setConnectors(new Connector[]{_connector});
+
+ ContextHandler context = new ContextHandler();
+ _session = new SessionHandler();
+
+ TestLoginService loginService = new TestLoginService(TEST_REALM);
+
+ loginService.putUser("fred", new Password("password"), IdentityService.NO_ROLES);
+ loginService.putUser("harry", new Password("password"), new String[]{"HOMEOWNER"});
+ loginService.putUser("chris", new Password("password"), new String[]{"CONTRACTOR"});
+ loginService.putUser("steven", new Password("password"), new String[]{"SALESCLERK"});
+
+ context.setContextPath("/ctx");
+ _server.setHandler(context);
+ context.setHandler(_session);
+
+ _server.addBean(loginService);
+ }
+
+ @BeforeEach
+ public void setupSecurity()
+ {
+ _security = new ConstraintSecurityHandler();
+ _session.setHandler(_security);
+ RequestHandler handler = new RequestHandler();
+ _security.setHandler(handler);
+
+
+ /*
+
+ <security-constraint>
+ <web-resource-collection>
+ <web-resource-name>precluded methods</web-resource-name>
+ <url-pattern>/*</url-pattern>
+ <url-pattern>/acme/wholesale/*</url-pattern>
+ <url-pattern>/acme/retail/*</url-pattern>
+ <http-method-exception>GET</http-method-exception>
+ <http-method-exception>POST</http-method-exception>
+ </web-resource-collection>
+ <auth-constraint/>
+ </security-constraint>
+ */
+
+ Constraint constraint0 = new Constraint();
+ constraint0.setAuthenticate(true);
+ constraint0.setName("precluded methods");
+ ConstraintMapping mapping0 = new ConstraintMapping();
+ mapping0.setPathSpec("/*");
+ mapping0.setConstraint(constraint0);
+ mapping0.setMethodOmissions(new String[]{"GET", "POST"});
+
+ ConstraintMapping mapping1 = new ConstraintMapping();
+ mapping1.setPathSpec("/acme/wholesale/*");
+ mapping1.setConstraint(constraint0);
+ mapping1.setMethodOmissions(new String[]{"GET", "POST"});
+
+ ConstraintMapping mapping2 = new ConstraintMapping();
+ mapping2.setPathSpec("/acme/retail/*");
+ mapping2.setConstraint(constraint0);
+ mapping2.setMethodOmissions(new String[]{"GET", "POST"});
+
+ /*
+
+ <security-constraint>
+ <web-resource-collection>
+ <web-resource-name>wholesale</web-resource-name>
+ <url-pattern>/acme/wholesale/*</url-pattern>
+ <http-method>GET</http-method>
+ <http-method>PUT</http-method>
+ </web-resource-collection>
+ <auth-constraint>
+ <role-name>SALESCLERK</role-name>
+ </auth-constraint>
+ </security-constraint>
+ */
+ Constraint constraint1 = new Constraint();
+ constraint1.setAuthenticate(true);
+ constraint1.setName("wholesale");
+ constraint1.setRoles(new String[]{"SALESCLERK"});
+ ConstraintMapping mapping3 = new ConstraintMapping();
+ mapping3.setPathSpec("/acme/wholesale/*");
+ mapping3.setConstraint(constraint1);
+ mapping3.setMethod("GET");
+ ConstraintMapping mapping4 = new ConstraintMapping();
+ mapping4.setPathSpec("/acme/wholesale/*");
+ mapping4.setConstraint(constraint1);
+ mapping4.setMethod("PUT");
+
+ /*
+ <security-constraint>
+ <web-resource-collection>
+ <web-resource-name>wholesale 2</web-resource-name>
+ <url-pattern>/acme/wholesale/*</url-pattern>
+ <http-method>GET</http-method>
+ <http-method>POST</http-method>
+ </web-resource-collection>
+ <auth-constraint>
+ <role-name>CONTRACTOR</role-name>
+ </auth-constraint>
+ <user-data-constraint>
+ <transport-guarantee>CONFIDENTIAL</transport-guarantee>
+ </user-data-constraint>
+ </security-constraint>
+ */
+ Constraint constraint2 = new Constraint();
+ constraint2.setAuthenticate(true);
+ constraint2.setName("wholesale 2");
+ constraint2.setRoles(new String[]{"CONTRACTOR"});
+ constraint2.setDataConstraint(Constraint.DC_CONFIDENTIAL);
+ ConstraintMapping mapping5 = new ConstraintMapping();
+ mapping5.setPathSpec("/acme/wholesale/*");
+ mapping5.setMethod("GET");
+ mapping5.setConstraint(constraint2);
+ ConstraintMapping mapping6 = new ConstraintMapping();
+ mapping6.setPathSpec("/acme/wholesale/*");
+ mapping6.setMethod("POST");
+ mapping6.setConstraint(constraint2);
+
+ /*
+<security-constraint>
+<web-resource-collection>
+<web-resource-name>retail</web-resource-name>
+<url-pattern>/acme/retail/*</url-pattern>
+<http-method>GET</http-method>
+<http-method>POST</http-method>
+</web-resource-collection>
+<auth-constraint>
+<role-name>CONTRACTOR</role-name>
+<role-name>HOMEOWNER</role-name>
+</auth-constraint>
+</security-constraint>
+*/
+ Constraint constraint4 = new Constraint();
+ constraint4.setName("retail");
+ constraint4.setAuthenticate(true);
+ constraint4.setRoles(new String[]{"CONTRACTOR", "HOMEOWNER"});
+ ConstraintMapping mapping7 = new ConstraintMapping();
+ mapping7.setPathSpec("/acme/retail/*");
+ mapping7.setMethod("GET");
+ mapping7.setConstraint(constraint4);
+ ConstraintMapping mapping8 = new ConstraintMapping();
+ mapping8.setPathSpec("/acme/retail/*");
+ mapping8.setMethod("POST");
+ mapping8.setConstraint(constraint4);
+
+ Set<String> knownRoles = new HashSet<String>();
+ knownRoles.add("CONTRACTOR");
+ knownRoles.add("HOMEOWNER");
+ knownRoles.add("SALESCLERK");
+
+ _security.setConstraintMappings(Arrays.asList(new ConstraintMapping[]
+ {
+ mapping0, mapping1, mapping2, mapping3, mapping4, mapping5, mapping6, mapping7, mapping8
+ }), knownRoles);
+ }
+
+ @AfterEach
+ public void stopServer() throws Exception
+ {
+ if (_server.isRunning())
+ {
+ _server.stop();
+ _server.join();
+ }
+ }
+
+ @Test
+ public void testUncoveredHttpMethodDetection() throws Exception
+ {
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ Set<String> paths = _security.getPathsWithUncoveredHttpMethods();
+ assertEquals(1, paths.size());
+ assertEquals("/*", paths.iterator().next());
+ }
+
+ @Test
+ public void testUncoveredHttpMethodsDenied() throws Exception
+ {
+ try
+ {
+ _security.setDenyUncoveredHttpMethods(false);
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ //There are uncovered methods for GET/POST at url /*
+ //without deny-uncovered-http-methods they should be accessible
+ String response;
+ response = _connector.getResponse("GET /ctx/index.html HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ //set deny-uncovered-http-methods true
+ _security.setDenyUncoveredHttpMethods(true);
+
+ //check they cannot be accessed
+ response = _connector.getResponse("GET /ctx/index.html HTTP/1.0\r\n\r\n");
+ assertTrue(response.startsWith("HTTP/1.1 403 Forbidden"));
+ }
+ finally
+ {
+ _security.setDenyUncoveredHttpMethods(false);
+ }
+ }
+
+ @Test
+ public void testBasic() throws Exception
+ {
+
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ String response;
+ /*
+ /star all methods except GET/POST forbidden
+ /acme/wholesale/star all methods except GET/POST forbidden
+ /acme/retail/star all methods except GET/POST forbidden
+ /acme/wholesale/star GET must be in role CONTRACTOR or SALESCLERK
+ /acme/wholesale/star POST must be in role CONTRACTOR and confidential transport
+ /acme/retail/star GET must be in role CONTRACTOR or HOMEOWNER
+ /acme/retail/star POST must be in role CONTRACTOR or HOMEOWNER
+ */
+
+ //a user in role HOMEOWNER is forbidden HEAD request
+ response = _connector.getResponse("HEAD /ctx/index.html HTTP/1.0\r\n\r\n");
+ assertTrue(response.startsWith("HTTP/1.1 403 Forbidden"));
+
+ Base64.Encoder authEncoder = Base64.getEncoder();
+ String encodedHarry = authEncoder.encodeToString("harry:password".getBytes(ISO_8859_1));
+ String encodedChris = authEncoder.encodeToString("chris:password".getBytes(ISO_8859_1));
+
+ response = _connector.getResponse("HEAD /ctx/index.html HTTP/1.0\r\n" +
+ "Authorization: Basic " + encodedHarry + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+
+ response = _connector.getResponse("HEAD /ctx/acme/wholesale/index.html HTTP/1.0\r\n" +
+ "Authorization: Basic " + encodedHarry + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+
+ response = _connector.getResponse("HEAD /ctx/acme/retail/index.html HTTP/1.0\r\n" +
+ "Authorization: Basic " + encodedHarry + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+
+ //a user in role CONTRACTOR can do a GET
+ response = _connector.getResponse("GET /ctx/acme/wholesale/index.html HTTP/1.0\r\n" +
+ "Authorization: Basic " + encodedChris + "\r\n" +
+ "\r\n");
+
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+
+ //a user in role CONTRACTOR can only do a post if confidential
+ response = _connector.getResponse("POST /ctx/acme/wholesale/index.html HTTP/1.0\r\n" +
+ "Authorization: Basic " + encodedChris + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+ assertThat(response, containsString("!Secure"));
+
+ //a user in role HOMEOWNER can do a GET
+ response = _connector.getResponse("GET /ctx/acme/retail/index.html HTTP/1.0\r\n" +
+ "Authorization: Basic " + encodedHarry + "\r\n" +
+ "\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ }
+
+ private class RequestHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+
+ response.setStatus(200);
+ response.setContentType("text/plain; charset=UTF-8");
+ response.getWriter().println("URI=" + request.getRequestURI());
+ String user = request.getRemoteUser();
+ response.getWriter().println("user=" + user);
+ if (request.getParameter("test_parameter") != null)
+ response.getWriter().println(request.getParameter("test_parameter"));
+ }
+ }
+}
diff --git a/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/TestLoginService.java b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/TestLoginService.java
new file mode 100644
index 0000000..34f91c5
--- /dev/null
+++ b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/TestLoginService.java
@@ -0,0 +1,75 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.security.Credential;
+
+/**
+ * TestLoginService
+ */
+public class TestLoginService extends AbstractLoginService
+{
+
+ UserStore userStore = new UserStore();
+
+ public TestLoginService(String name)
+ {
+ setName(name);
+ }
+
+ public void putUser(String username, Credential credential, String[] roles)
+ {
+ userStore.addUser(username, credential, roles);
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.AbstractLoginService#loadRoleInfo(org.eclipse.jetty.security.AbstractLoginService.UserPrincipal)
+ */
+ @Override
+ protected String[] loadRoleInfo(UserPrincipal user)
+ {
+ UserIdentity userIdentity = userStore.getUserIdentity(user.getName());
+ Set<RolePrincipal> roles = userIdentity.getSubject().getPrincipals(RolePrincipal.class);
+ if (roles == null)
+ return null;
+
+ List<String> list = new ArrayList<>();
+ for (RolePrincipal r : roles)
+ {
+ list.add(r.getName());
+ }
+
+ return list.toArray(new String[roles.size()]);
+ }
+
+ /**
+ * @see org.eclipse.jetty.security.AbstractLoginService#loadUserInfo(java.lang.String)
+ */
+ @Override
+ protected UserPrincipal loadUserInfo(String username)
+ {
+ UserIdentity userIdentity = userStore.getUserIdentity(username);
+ return userIdentity == null ? null : (UserPrincipal)userIdentity.getUserPrincipal();
+ }
+}
diff --git a/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/UnauthenticatedTest.java b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/UnauthenticatedTest.java
new file mode 100644
index 0000000..f317339
--- /dev/null
+++ b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/UnauthenticatedTest.java
@@ -0,0 +1,150 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.security.authentication.DeferredAuthentication;
+import org.eclipse.jetty.security.authentication.LoginAuthenticator;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.security.Constraint;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+
+public class UnauthenticatedTest
+{
+ private LocalConnector connector;
+ private TestAuthenticator authenticator;
+
+ @BeforeEach
+ public void beforeEach() throws Exception
+ {
+ Server server = new Server();
+ connector = new LocalConnector(server);
+ server.addConnector(connector);
+
+ // Authenticator that always returns UNAUTHENTICATED.
+ authenticator = new TestAuthenticator();
+
+ // Add a security handler which requires paths under /requireAuth to be authenticated.
+ ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
+ Constraint requireAuthentication = new Constraint();
+ requireAuthentication.setRoles(new String[]{"**"});
+ requireAuthentication.setAuthenticate(true);
+ ConstraintMapping constraintMapping = new ConstraintMapping();
+ constraintMapping.setPathSpec("/requireAuth/*");
+ constraintMapping.setConstraint(requireAuthentication);
+ securityHandler.addConstraintMapping(constraintMapping);
+
+ securityHandler.setAuthenticator(authenticator);
+ securityHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(HttpStatus.OK_200);
+ response.getWriter().println("authentication: " + baseRequest.getAuthentication());
+ }
+ });
+
+ server.setHandler(securityHandler);
+ server.start();
+ }
+
+ @Test
+ public void testUnauthenticated() throws Exception
+ {
+ TestAuthenticator.AUTHENTICATION.set(Authentication.UNAUTHENTICATED);
+
+ // Request to URI which doesn't require authentication can get through even though auth is UNAUTHENTICATED.
+ String response = connector.getResponse("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("authentication: UNAUTHENTICATED"));
+
+ // This URI requires just that the request is authenticated.
+ response = connector.getResponse("GET /requireAuth/test HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString("HTTP/1.1 401 Unauthorized"));
+ }
+
+ @Test
+ public void testDeferredAuth() throws Exception
+ {
+ TestAuthenticator.AUTHENTICATION.set(new DeferredAuthentication(authenticator));
+
+ // Request to URI which doesn't require authentication can get through even though auth is UNAUTHENTICATED.
+ String response = connector.getResponse("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("DeferredAuthentication"));
+
+ // This URI requires just that the request is authenticated. But DeferredAuthentication can bypass this.
+ response = connector.getResponse("GET /requireAuth/test HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("DeferredAuthentication"));
+ }
+
+ public static class TestAuthenticator extends LoginAuthenticator
+ {
+ static AtomicReference<Authentication> AUTHENTICATION = new AtomicReference<>();
+
+ @Override
+ public void setConfiguration(AuthConfiguration configuration)
+ {
+ // Do nothing.
+ }
+
+ @Override
+ public String getAuthMethod()
+ {
+ return this.getClass().getSimpleName();
+ }
+
+ @Override
+ public void prepareRequest(ServletRequest request)
+ {
+ // Do nothing.
+ }
+
+ @Override
+ public Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException
+ {
+ return AUTHENTICATION.get();
+ }
+
+ @Override
+ public boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, Authentication.User validatedUser) throws ServerAuthException
+ {
+ return true;
+ }
+ }
+}
diff --git a/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/UserStoreTest.java b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/UserStoreTest.java
new file mode 100644
index 0000000..5f67a6a
--- /dev/null
+++ b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/UserStoreTest.java
@@ -0,0 +1,73 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.security.Credential;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class UserStoreTest
+{
+ UserStore userStore;
+
+ @BeforeEach
+ public void setup()
+ {
+ userStore = new UserStore();
+ }
+
+ @Test
+ public void addUser()
+ {
+ this.userStore.addUser("foo", Credential.getCredential("beer"), new String[]{"pub"});
+ assertEquals(1, this.userStore.getKnownUserIdentities().size());
+ UserIdentity userIdentity = this.userStore.getUserIdentity("foo");
+ assertNotNull(userIdentity);
+ assertEquals("foo", userIdentity.getUserPrincipal().getName());
+ Set<AbstractLoginService.RolePrincipal>
+ roles = userIdentity.getSubject().getPrincipals(AbstractLoginService.RolePrincipal.class);
+ List<String> list = roles.stream()
+ .map(rolePrincipal -> rolePrincipal.getName())
+ .collect(Collectors.toList());
+ assertEquals(1, list.size());
+ assertEquals("pub", list.get(0));
+ }
+
+ @Test
+ public void removeUser()
+ {
+ this.userStore.addUser("foo", Credential.getCredential("beer"), new String[]{"pub"});
+ assertEquals(1, this.userStore.getKnownUserIdentities().size());
+ UserIdentity userIdentity = this.userStore.getUserIdentity("foo");
+ assertNotNull(userIdentity);
+ assertEquals("foo", userIdentity.getUserPrincipal().getName());
+ userStore.removeUser("foo");
+ userIdentity = this.userStore.getUserIdentity("foo");
+ assertNull(userIdentity);
+ }
+}
diff --git a/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java
new file mode 100644
index 0000000..7ca7893
--- /dev/null
+++ b/third_party/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java
@@ -0,0 +1,191 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security.authentication;
+
+import java.io.IOException;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.MetaData;
+import org.eclipse.jetty.server.AbstractConnector;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpChannelState;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpOutput;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Server;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test class for {@link SpnegoAuthenticator}.
+ */
+public class SpnegoAuthenticatorTest
+{
+ private SpnegoAuthenticator _authenticator;
+
+ @BeforeEach
+ public void setup() throws Exception
+ {
+ _authenticator = new SpnegoAuthenticator();
+ }
+
+ @Test
+ public void testChallengeSentWithNoAuthorization() throws Exception
+ {
+ HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null)
+ {
+ @Override
+ public Server getServer()
+ {
+ return null;
+ }
+
+ @Override
+ protected HttpOutput newHttpOutput()
+ {
+ return new HttpOutput(this)
+ {
+ @Override
+ public void close() {}
+
+ @Override
+ public void flush() throws IOException {}
+ };
+ }
+ };
+ Request req = channel.getRequest();
+ Response res = channel.getResponse();
+ MetaData.Request metadata = new MetaData.Request(new HttpFields());
+ metadata.setURI(new HttpURI("http://localhost"));
+ req.setMetaData(metadata);
+
+ assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH));
+ assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true));
+ assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString()));
+ assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus());
+ }
+
+ @Test
+ public void testChallengeSentWithUnhandledAuthorization() throws Exception
+ {
+ HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null)
+ {
+ @Override
+ public Server getServer()
+ {
+ return null;
+ }
+
+ @Override
+ protected HttpOutput newHttpOutput()
+ {
+ return new HttpOutput(this)
+ {
+ @Override
+ public void close() {}
+
+ @Override
+ public void flush() throws IOException {}
+ };
+ }
+ };
+ Request req = channel.getRequest();
+ Response res = channel.getResponse();
+ HttpFields httpFields = new HttpFields();
+ // Create a bogus Authorization header. We don't care about the actual credentials.
+ httpFields.add(HttpHeader.AUTHORIZATION, "Basic asdf");
+ MetaData.Request metadata = new MetaData.Request(httpFields);
+ metadata.setURI(new HttpURI("http://localhost"));
+ req.setMetaData(metadata);
+
+ assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH));
+ assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true));
+ assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString()));
+ assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus());
+ }
+
+ @Test
+ public void testCaseInsensitiveHeaderParsing()
+ {
+ assertFalse(_authenticator.isAuthSchemeNegotiate(null));
+ assertFalse(_authenticator.isAuthSchemeNegotiate(""));
+ assertFalse(_authenticator.isAuthSchemeNegotiate("Basic"));
+ assertFalse(_authenticator.isAuthSchemeNegotiate("basic"));
+ assertFalse(_authenticator.isAuthSchemeNegotiate("Digest"));
+ assertFalse(_authenticator.isAuthSchemeNegotiate("LotsandLotsandLots of nonsense"));
+ assertFalse(_authenticator.isAuthSchemeNegotiate("Negotiat asdfasdf"));
+ assertFalse(_authenticator.isAuthSchemeNegotiate("Negotiated"));
+ assertFalse(_authenticator.isAuthSchemeNegotiate("Negotiate-and-more"));
+
+ assertTrue(_authenticator.isAuthSchemeNegotiate("Negotiate"));
+ assertTrue(_authenticator.isAuthSchemeNegotiate("negotiate"));
+ assertTrue(_authenticator.isAuthSchemeNegotiate("negOtiAte"));
+ }
+
+ @Test
+ public void testExtractAuthScheme()
+ {
+ assertEquals("", _authenticator.getAuthSchemeFromHeader(null));
+ assertEquals("", _authenticator.getAuthSchemeFromHeader(""));
+ assertEquals("", _authenticator.getAuthSchemeFromHeader(" "));
+ assertEquals("Basic", _authenticator.getAuthSchemeFromHeader(" Basic asdfasdf"));
+ assertEquals("Basicasdf", _authenticator.getAuthSchemeFromHeader("Basicasdf asdfasdf"));
+ assertEquals("basic", _authenticator.getAuthSchemeFromHeader(" basic asdfasdf "));
+ assertEquals("Negotiate", _authenticator.getAuthSchemeFromHeader("Negotiate asdfasdf"));
+ assertEquals("negotiate", _authenticator.getAuthSchemeFromHeader("negotiate asdfasdf"));
+ assertEquals("negotiate", _authenticator.getAuthSchemeFromHeader(" negotiate asdfasdf"));
+ assertEquals("negotiated", _authenticator.getAuthSchemeFromHeader(" negotiated asdfasdf"));
+ }
+
+ class MockConnector extends AbstractConnector
+ {
+ public MockConnector()
+ {
+ super(new Server(), null, null, null, 0);
+ }
+
+ @Override
+ protected void accept(int acceptorID) throws IOException, InterruptedException
+ {
+ }
+
+ @Override
+ public Object getTransport()
+ {
+ return null;
+ }
+
+ @Override
+ public String dumpSelf()
+ {
+ return null;
+ }
+ }
+}
diff --git a/third_party/jetty-security/src/test/resources/docroot/all/index.txt b/third_party/jetty-security/src/test/resources/docroot/all/index.txt
new file mode 100644
index 0000000..290f702
--- /dev/null
+++ b/third_party/jetty-security/src/test/resources/docroot/all/index.txt
@@ -0,0 +1 @@
+this is open content.
\ No newline at end of file
diff --git a/third_party/jetty-security/src/test/resources/docroot/forbid/index.txt b/third_party/jetty-security/src/test/resources/docroot/forbid/index.txt
new file mode 100644
index 0000000..aed1cf3
--- /dev/null
+++ b/third_party/jetty-security/src/test/resources/docroot/forbid/index.txt
@@ -0,0 +1 @@
+this is forbidden content.
\ No newline at end of file
diff --git a/third_party/jetty-security/src/test/resources/foo.properties b/third_party/jetty-security/src/test/resources/foo.properties
new file mode 100644
index 0000000..aee03ec
--- /dev/null
+++ b/third_party/jetty-security/src/test/resources/foo.properties
@@ -0,0 +1,2 @@
+ken=123
+amy=456
diff --git a/third_party/jetty-security/src/test/resources/jetty-logging.properties b/third_party/jetty-security/src/test/resources/jetty-logging.properties
new file mode 100755
index 0000000..24d5e3a
--- /dev/null
+++ b/third_party/jetty-security/src/test/resources/jetty-logging.properties
@@ -0,0 +1,7 @@
+# Setup default logging implementation for during testing
+org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
+
+#org.eclipse.jetty.LEVEL=DEBUG
+
+#org.eclipse.jetty.util.PathWatcher.LEVEL=DEBUG
+#org.eclipse.jetty.util.PathWatcher.Noisy.LEVEL=OFF
diff --git a/third_party/jetty-server/pom.xml b/third_party/jetty-server/pom.xml
new file mode 100644
index 0000000..f73fe47
--- /dev/null
+++ b/third_party/jetty-server/pom.xml
@@ -0,0 +1,102 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-project</artifactId>
+ <version>9.4.44.v20210927</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>jetty-server</artifactId>
+ <name>Jetty :: Server Core</name>
+ <description>The core jetty server artifact.</description>
+ <properties>
+ <bundle-symbolic-name>${project.groupId}.server</bundle-symbolic-name>
+ <spotbugs.onlyAnalyze>org.eclipse.jetty.server.*</spotbugs.onlyAnalyze>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>test-jar</id>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-resources-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>copy-resources-keystore</id>
+ <phase>process-resources</phase>
+ <goals>
+ <goal>copy-resources</goal>
+ </goals>
+ <configuration>
+ <encoding>UTF-8</encoding>
+ <outputDirectory>${project.build.directory}/jetty-config-files</outputDirectory>
+ <resources>
+ <resource>
+ <directory>src/main/config</directory>
+ <includes>
+ <include>**/**keystore**</include>
+ </includes>
+ </resource>
+ </resources>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ <dependencies>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-http</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-io</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-xml</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-jmx</artifactId>
+ <version>${project.version}</version>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.toolchain</groupId>
+ <artifactId>jetty-test-helper</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-http</artifactId>
+ <version>${project.version}</version>
+ <classifier>tests</classifier>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-util-ajax</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/third_party/jetty-server/src/main/assembly/site-component.xml b/third_party/jetty-server/src/main/assembly/site-component.xml
new file mode 100644
index 0000000..575269c
--- /dev/null
+++ b/third_party/jetty-server/src/main/assembly/site-component.xml
@@ -0,0 +1,15 @@
+<assembly>
+ <id>site-component</id>
+ <formats>
+ <format>jar</format>
+ </formats>
+ <fileSets>
+ <fileSet>
+ <directory>${basedir}</directory>
+ <outputDirectory>jetty</outputDirectory>
+ <includes>
+ <include>src/main/resources/org/eclipse/**</include>
+ </includes>
+ </fileSet>
+ </fileSets>
+</assembly>
diff --git a/third_party/jetty-server/src/main/config/etc/home-base-warning.xml b/third_party/jetty-server/src/main/config/etc/home-base-warning.xml
new file mode 100644
index 0000000..64a02e9
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/home-base-warning.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- ============================================================= -->
+<!-- Display a Warning Message if {jetty.home} == {jetty.base} -->
+<!-- ============================================================= -->
+<Configure id="homeBaseWarning" class="org.eclipse.jetty.server.HomeBaseWarning" />
diff --git a/third_party/jetty-server/src/main/config/etc/jdbcRealm.properties b/third_party/jetty-server/src/main/config/etc/jdbcRealm.properties
new file mode 100644
index 0000000..48104d8
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jdbcRealm.properties
@@ -0,0 +1,72 @@
+#
+# This is a sample properties file for the org.eclipse.jetty.security.JDBCLoginService
+# implemtation of the UserRealm interface. This allows Jetty users authentication
+# to work from a database.
+#
+# +-------+ +------------+ +-------+
+# | users | | user_roles | | roles |
+# +-------+ +------------+ +-------+
+# | id | /| user_id |\ | id |
+# | user -------| role_id |------- role |
+# | pwd | \| |/ | |
+# +-------+ +------------+ +-------+
+#
+#
+# 'cachetime' is a time in seconds to cache positive database
+# lookups in internal hash table. Set to 0 to disable caching.
+#
+#
+# For MySQL:
+# create a MYSQL user called "jetty" with password "jetty"
+#
+# Create the tables:
+# create table users
+# (
+# id integer primary key,
+# username varchar(100) not null unique key,
+# pwd varchar(20) not null
+# );
+#
+# create table roles
+# (
+# id integer primary key,
+# role varchar(100) not null unique key
+# );
+#
+# create table user_roles
+# (
+# user_id integer not null,
+# role_id integer not null,
+# unique key (user_id, role_id),
+# index(user_id)
+# );
+#
+# I'm not sure unique key with a first component of user_id will be
+# user by MySQL in query, so additional index wouldn't hurt.
+#
+# To test JDBC implementation:
+#
+# mysql> insert into users values (1, 'admin', 'password');
+# mysql> insert into roles values (1, 'server-administrator');
+# mysql> insert into roles values (2, 'content-administrator');
+# mysql> insert into user_roles values (1, 1);
+# mysql> insert into user_roles values (1, 2);
+#
+# Replace HashUserRealm in etc/admin.xml with JDBCUserRealm and
+# set path to properties file.
+#
+jdbcdriver = org.gjt.mm.mysql.Driver
+url = jdbc:mysql://localhost/jetty
+username = jetty
+password = jetty
+usertable = users
+usertablekey = id
+usertableuserfield = username
+usertablepasswordfield = pwd
+roletable = roles
+roletablekey = id
+roletablerolefield = role
+userroletable = user_roles
+userroletableuserkey = user_id
+userroletablerolekey = role_id
+cachetime = 300
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-acceptratelimit.xml b/third_party/jetty-server/src/main/config/etc/jetty-acceptratelimit.xml
new file mode 100644
index 0000000..80230c9
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-acceptratelimit.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <Call name="addBean">
+ <Arg>
+ <New class="org.eclipse.jetty.server.AcceptRateLimit">
+ <Arg name="maxRate" type="int"><Property name="jetty.acceptratelimit.acceptRateLimit" default="1000" /></Arg>
+ <Arg name="period" type="long"><Property name="jetty.acceptratelimit.period" default="1000" /></Arg>
+ <Arg name="units"><Call class="java.util.concurrent.TimeUnit" name="valueOf"><Arg>
+ <Property name="jetty.acceptratelimit.units" default="MILLISECONDS" />
+ </Arg></Call></Arg>
+ <Arg name="server"><Ref refid="Server" /></Arg>
+ </New>
+ </Arg>
+ </Call>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-bytebufferpool.xml b/third_party/jetty-server/src/main/config/etc/jetty-bytebufferpool.xml
new file mode 100644
index 0000000..ab8f4a3
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-bytebufferpool.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+<Configure>
+ <New id="byteBufferPool" class="org.eclipse.jetty.io.ArrayByteBufferPool">
+ <Arg type="int"><Property name="jetty.byteBufferPool.minCapacity" default="0"/></Arg>
+ <Arg type="int"><Property name="jetty.byteBufferPool.factor" default="1024"/></Arg>
+ <Arg type="int"><Property name="jetty.byteBufferPool.maxCapacity" default="65536"/></Arg>
+ <Arg type="int"><Property name="jetty.byteBufferPool.maxQueueLength" default="-1"/></Arg>
+ <Arg type="long"><Property name="jetty.byteBufferPool.maxHeapMemory" default="-1"/></Arg>
+ <Arg type="long"><Property name="jetty.byteBufferPool.maxDirectMemory" default="-1"/></Arg>
+ </New>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-connectionlimit.xml b/third_party/jetty-server/src/main/config/etc/jetty-connectionlimit.xml
new file mode 100644
index 0000000..933e5ac
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-connectionlimit.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <Call name="addBean">
+ <Arg>
+ <New class="org.eclipse.jetty.server.ConnectionLimit">
+ <Arg name= "maxConnections" type="int">
+ <Property name="jetty.connectionlimit.maxConnections" deprecated="jetty.connection.limit" default="1000" />
+ </Arg>
+ <Arg name="server">
+ <Ref refid="Server" />
+ </Arg>
+ <Set name="idleTimeout"><Property name="jetty.connectionlimit.idleTimeout" default="1000" /></Set>
+ </New>
+ </Arg>
+ </Call>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-customrequestlog.xml b/third_party/jetty-server/src/main/config/etc/jetty-customrequestlog.xml
new file mode 100644
index 0000000..a01217d
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-customrequestlog.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- =============================================================== -->
+<!-- Configure the Jetty Request Log -->
+<!-- =============================================================== -->
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+
+ <!-- =========================================================== -->
+ <!-- Configure Request Log for Server -->
+ <!-- (Use RequestLogHandler for a context specific RequestLog -->
+ <!-- =========================================================== -->
+ <Set name="RequestLog">
+ <New id="RequestLog" class="org.eclipse.jetty.server.CustomRequestLog">
+ <!-- Writer -->
+ <Arg>
+ <New class="org.eclipse.jetty.server.AsyncRequestLogWriter">
+ <Arg><Property name="jetty.base" default="." />/<Property>
+ <Name>jetty.requestlog.filePath</Name>
+ <Default><Property name="jetty.requestlog.dir" default="logs"/>/yyyy_mm_dd.request.log</Default>
+ </Property></Arg>
+ <Arg/>
+
+ <Set name="filenameDateFormat"><Property name="jetty.requestlog.filenameDateFormat" default="yyyy_MM_dd"/></Set>
+ <Set name="retainDays"><Property name="jetty.requestlog.retainDays" default="90"/></Set>
+ <Set name="append"><Property name="jetty.requestlog.append" default="false"/></Set>
+ <Set name="timeZone"><Property name="jetty.requestlog.timezone" default="GMT"/></Set>
+ </New>
+ </Arg>
+
+ <!-- Format String -->
+ <Arg>
+ <Property name="jetty.customrequestlog.formatString">
+ <Default>
+ <Get class="org.eclipse.jetty.server.CustomRequestLog" name="EXTENDED_NCSA_FORMAT"/>
+ </Default>
+ </Property>
+ </Arg>
+ </New>
+ </Set>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-debug.xml b/third_party/jetty-server/src/main/config/etc/jetty-debug.xml
new file mode 100644
index 0000000..aba9781
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-debug.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- =============================================================== -->
+<!-- The DebugListener -->
+<!-- =============================================================== -->
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <New id="DebugListener" class="org.eclipse.jetty.server.DebugListener">
+ <Arg name="outputStream">
+ <New class="org.eclipse.jetty.util.RolloverFileOutputStream">
+ <Arg type="String"><Property name="jetty.logs" default="./logs"/>/yyyy_mm_dd.debug.log</Arg>
+ <Arg type="boolean"><Property name="jetty.debug.append" default="true"/></Arg>
+ <Arg type="int"><Property name="jetty.debug.retainDays" default="14"/></Arg>
+ <Arg>
+ <Call class="java.util.TimeZone" name="getTimeZone"><Arg><Property name="jetty.debug.timezone" default="GMT"/></Arg></Call>
+ </Arg>
+ </New>
+ </Arg>
+ <Arg name="showHeaders" type="boolean"><Property name="jetty.debug.showHeaders" default="true"/></Arg>
+ <Arg name="renameThread" type="boolean"><Property name="jetty.debug.renameThread" default="false"/></Arg>
+ <Arg name="dumpContext" type="boolean"><Property name="jetty.debug.dumpContext" default="true"/></Arg>
+ </New>
+
+ <Call name="addBean"><Arg><Ref refid="DebugListener"/></Arg></Call>
+
+ <Ref refid="DeploymentManager">
+ <Call name="addLifeCycleBinding">
+ <Arg>
+ <New class="org.eclipse.jetty.deploy.bindings.DebugListenerBinding">
+ <Arg><Ref refid="DebugListener"/></Arg>
+ </New>
+ </Arg>
+ </Call>
+ </Ref>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-debuglog.xml b/third_party/jetty-server/src/main/config/etc/jetty-debuglog.xml
new file mode 100644
index 0000000..646d544
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-debuglog.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- =============================================================== -->
+<!-- The DebugHandler -->
+<!-- =============================================================== -->
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <Call name="insertHandler">
+ <Arg>
+ <New id="DebugHandler" class="org.eclipse.jetty.server.handler.DebugHandler">
+ <Set name="outputStream">
+ <New class="org.eclipse.jetty.util.RolloverFileOutputStream">
+ <Arg type="String"><Property name="jetty.debuglog.dir" deprecated="jetty.logs" default="./logs"/>/yyyy_mm_dd.debug.log</Arg>
+ <Arg type="boolean"><Property name="jetty.debuglog.append" default="true"/></Arg>
+ <Arg type="int"><Property name="jetty.debuglog.retainDays" default="90"/></Arg>
+ <Arg>
+ <Call class="java.util.TimeZone" name="getTimeZone"><Arg><Property name="jetty.debuglog.timezone" default="GMT"/></Arg></Call>
+ </Arg>
+ </New>
+ </Set>
+ </New>
+ </Arg>
+ </Call>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-gzip.xml b/third_party/jetty-server/src/main/config/etc/jetty-gzip.xml
new file mode 100644
index 0000000..c7efdac
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-gzip.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- =============================================================== -->
+<!-- Mixin the GZIP Handler -->
+<!-- This applies the GZIP Handler to the entire server -->
+<!-- If a GZIP handler is required for an individual context, then -->
+<!-- use a context XML (see test.xml example in distribution) -->
+<!-- =============================================================== -->
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <Call name="insertHandler">
+ <Arg>
+ <New id="GzipHandler" class="org.eclipse.jetty.server.handler.gzip.GzipHandler">
+ <Set name="minGzipSize"><Property name="jetty.gzip.minGzipSize" deprecated="gzip.minGzipSize" default="2048"/></Set>
+ <Set name="checkGzExists"><Property name="jetty.gzip.checkGzExists" deprecated="gzip.checkGzExists" default="false"/></Set>
+ <Set name="compressionLevel"><Property name="jetty.gzip.compressionLevel" deprecated="gzip.compressionLevel" default="-1"/></Set>
+ <Set name="inflateBufferSize"><Property name="jetty.gzip.inflateBufferSize" default="0"/></Set>
+ <Set name="deflaterPoolCapacity"><Property name="jetty.gzip.deflaterPoolCapacity" default="-1"/></Set>
+ <Set name="syncFlush"><Property name="jetty.gzip.syncFlush" default="false" /></Set>
+
+ <Set name="excludedAgentPatterns">
+ <Array type="String">
+ <Item><Property name="jetty.gzip.excludedUserAgent" deprecated="gzip.excludedUserAgent" default=".*MSIE.6\.0.*"/></Item>
+ </Array>
+ </Set>
+
+ <Set name="includedMethodList"><Property name="jetty.gzip.includedMethodList" default="GET" /></Set>
+ <Set name="excludedMethodList"><Property name="jetty.gzip.excludedMethodList" default="" /></Set>
+
+<!--
+ <Set name="includedMethods">
+ <Array type="String">
+ <Item>GET</Item>
+ </Array>
+ </Set>
+
+ <Set name="includedPaths">
+ <Array type="String">
+ <Item>/*</Item>
+ </Array>
+ </Set>
+
+ <Set name="excludedPaths">
+ <Array type="String">
+ <Item>*.gz</Item>
+ </Array>
+ </Set>
+
+ <Call name="addIncludedMimeTypes">
+ <Arg><Array type="String">
+ <Item>some/type</Item>
+ </Array></Arg>
+ </Call>
+
+ <Call name="addExcludedMimeTypes">
+ <Arg><Array type="String">
+ <Item>some/type</Item>
+ </Array></Arg>
+ </Call>
+-->
+
+ </New>
+ </Arg>
+ </Call>
+</Configure>
+
+
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-http-forwarded.xml b/third_party/jetty-server/src/main/config/etc/jetty-http-forwarded.xml
new file mode 100644
index 0000000..460ed57
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-http-forwarded.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+<Configure id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
+ <Call name="addCustomizer">
+ <Arg>
+ <New class="org.eclipse.jetty.server.ForwardedRequestCustomizer">
+ <Set name="forwardedOnly"><Property name="jetty.httpConfig.forwardedOnly" default="false"/></Set>
+ <Set name="proxyAsAuthority"><Property name="jetty.httpConfig.forwardedProxyAsAuthority" default="false"/></Set>
+ <Set name="forwardedPortAsAuthority"><Property name="jetty.httpConfig.forwardedPortAsAuthority" default="true"/></Set>
+ <Set name="forwardedHeader"><Property name="jetty.httpConfig.forwardedHeader" default="Forwarded"/></Set>
+ <Set name="forwardedHostHeader"><Property name="jetty.httpConfig.forwardedHostHeader" default="X-Forwarded-Host"/></Set>
+ <Set name="forwardedServerHeader"><Property name="jetty.httpConfig.forwardedServerHeader" default="X-Forwarded-Server"/></Set>
+ <Set name="forwardedProtoHeader"><Property name="jetty.httpConfig.forwardedProtoHeader" default="X-Forwarded-Proto"/></Set>
+ <Set name="forwardedForHeader"><Property name="jetty.httpConfig.forwardedForHeader" default="X-Forwarded-For"/></Set>
+ <Set name="forwardedPortHeader"><Property name="jetty.httpConfig.forwardedPortHeader" default="X-Forwarded-Port"/></Set>
+ <Set name="forwardedHttpsHeader"><Property name="jetty.httpConfig.forwardedHttpsHeader" default="X-Proxied-Https"/></Set>
+ <Set name="forwardedSslSessionIdHeader"><Property name="jetty.httpConfig.forwardedSslSessionIdHeader" default="Proxy-ssl-id" /></Set>
+ <Set name="forwardedCipherSuiteHeader"><Property name="jetty.httpConfig.forwardedCipherSuiteHeader" default="Proxy-auth-cert"/></Set>
+ </New>
+ </Arg>
+ </Call>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-http.xml b/third_party/jetty-server/src/main/config/etc/jetty-http.xml
new file mode 100644
index 0000000..2dd3fd4
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-http.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- ============================================================= -->
+<!-- Configure the Jetty Server instance with an ID "Server" -->
+<!-- by adding an HTTP connector. -->
+<!-- This configuration must be used in conjunction with jetty.xml -->
+<!-- ============================================================= -->
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+
+ <!-- =========================================================== -->
+ <!-- Add an HTTP Connector. -->
+ <!-- Configure an o.e.j.server.ServerConnector with a single -->
+ <!-- HttpConnectionFactory instance using the common httpConfig -->
+ <!-- instance defined in jetty.xml -->
+ <!-- -->
+ <!-- Consult the javadoc of o.e.j.server.ServerConnector and -->
+ <!-- o.e.j.server.HttpConnectionFactory for all configuration -->
+ <!-- that may be set here. -->
+ <!-- =========================================================== -->
+ <Call name="addConnector">
+ <Arg>
+ <New id="httpConnector" class="org.eclipse.jetty.server.ServerConnector">
+ <Arg name="server"><Ref refid="Server" /></Arg>
+ <Arg name="acceptors" type="int"><Property name="jetty.http.acceptors" deprecated="http.acceptors" default="-1"/></Arg>
+ <Arg name="selectors" type="int"><Property name="jetty.http.selectors" deprecated="http.selectors" default="-1"/></Arg>
+ <Arg name="factories">
+ <Array type="org.eclipse.jetty.server.ConnectionFactory">
+ <Item>
+ <New class="org.eclipse.jetty.server.HttpConnectionFactory">
+ <Arg name="config"><Ref refid="httpConfig" /></Arg>
+ <Arg name="compliance"><Call class="org.eclipse.jetty.http.HttpCompliance" name="valueOf"><Arg><Property name="jetty.http.compliance" default="RFC7230_LEGACY"/></Arg></Call></Arg>
+ </New>
+ </Item>
+ </Array>
+ </Arg>
+ <Set name="host"><Property name="jetty.http.host" deprecated="jetty.host" /></Set>
+ <Set name="port"><Property name="jetty.http.port" deprecated="jetty.port" default="8080" /></Set>
+ <Set name="idleTimeout"><Property name="jetty.http.idleTimeout" deprecated="http.timeout" default="30000"/></Set>
+ <Set name="acceptorPriorityDelta"><Property name="jetty.http.acceptorPriorityDelta" deprecated="http.acceptorPriorityDelta" default="0"/></Set>
+ <Set name="acceptQueueSize"><Property name="jetty.http.acceptQueueSize" deprecated="http.acceptQueueSize" default="0"/></Set>
+ <Set name="reuseAddress"><Property name="jetty.http.reuseAddress" default="true"/></Set>
+ <Set name="acceptedTcpNoDelay"><Property name="jetty.http.acceptedTcpNoDelay" default="true"/></Set>
+ <Set name="acceptedReceiveBufferSize"><Property name="jetty.http.acceptedReceiveBufferSize" default="-1"/></Set>
+ <Set name="acceptedSendBufferSize"><Property name="jetty.http.acceptedSendBufferSize" default="-1"/></Set>
+ <Get name="SelectorManager">
+ <Set name="connectTimeout"><Property name="jetty.http.connectTimeout" default="15000"/></Set>
+ </Get>
+ </New>
+ </Arg>
+ </Call>
+
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-https.xml b/third_party/jetty-server/src/main/config/etc/jetty-https.xml
new file mode 100644
index 0000000..529b6d0
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-https.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- ============================================================= -->
+<!-- Configure an HTTPS connector. -->
+<!-- This configuration must be used in conjunction with jetty.xml -->
+<!-- and jetty-ssl.xml. -->
+<!-- ============================================================= -->
+<Configure id="sslConnector" class="org.eclipse.jetty.server.ServerConnector">
+
+ <Call name="addIfAbsentConnectionFactory">
+ <Arg>
+ <New class="org.eclipse.jetty.server.SslConnectionFactory">
+ <Arg name="next">http/1.1</Arg>
+ <Arg name="sslContextFactory"><Ref refid="sslContextFactory"/></Arg>
+ </New>
+ </Arg>
+ </Call>
+
+ <Call name="addConnectionFactory">
+ <Arg>
+ <New class="org.eclipse.jetty.server.HttpConnectionFactory">
+ <Arg name="config"><Ref refid="sslHttpConfig" /></Arg>
+ <Arg name="compliance"><Call class="org.eclipse.jetty.http.HttpCompliance" name="valueOf"><Arg><Property name="jetty.http.compliance" default="RFC7230"/></Arg></Call></Arg>
+ </New>
+ </Arg>
+ </Call>
+
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-ipaccess.xml b/third_party/jetty-server/src/main/config/etc/jetty-ipaccess.xml
new file mode 100644
index 0000000..e9607a9
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-ipaccess.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- =============================================================== -->
+<!-- The IP Access Handler -->
+<!-- =============================================================== -->
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <Call name="insertHandler">
+ <Arg>
+ <New id="IPAccessHandler" class="org.eclipse.jetty.server.handler.IPAccessHandler">
+ <Set name="white">
+ <Array type="String">
+ <Item>127.0.0.1</Item>
+ <Item>127.0.0.2/*.html</Item>
+ </Array>
+ </Set>
+ <Set name="black">
+ <Array type="String">
+ <Item>127.0.0.1/blacklisted</Item>
+ <Item>127.0.0.2/black.html</Item>
+ </Array>
+ </Set>
+ <Set name="whiteListByPath">false</Set>
+ </New>
+ </Arg>
+ </Call>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-lowresources.xml b/third_party/jetty-server/src/main/config/etc/jetty-lowresources.xml
new file mode 100644
index 0000000..215b7b9
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-lowresources.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- =============================================================== -->
+<!-- Mixin the Low Resources Monitor -->
+<!-- =============================================================== -->
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <Call name="addBean">
+ <Arg>
+ <New id="lowResourceMonitor" class="org.eclipse.jetty.server.LowResourceMonitor">
+ <Arg name="server"><Ref refid='Server'/></Arg>
+ <Set name="period"><Property name="jetty.lowresources.period" deprecated="lowresources.period" default="1000"/></Set>
+ <Set name="lowResourcesIdleTimeout"><Property name="jetty.lowresources.idleTimeout" deprecated="lowresources.lowResourcesIdleTimeout" default="1000"/></Set>
+ <Set name="monitorThreads"><Property name="jetty.lowresources.monitorThreads" deprecated="lowresources.monitorThreads" default="true"/></Set>
+ <Set name="maxConnections"><Property name="jetty.lowresources.maxConnections" deprecated="lowresources.maxConnections" default="0"/></Set>
+ <Set name="maxMemory"><Property name="jetty.lowresources.maxMemory" deprecated="lowresources.maxMemory" default="0"/></Set>
+ <Set name="maxLowResourcesTime"><Property name="jetty.lowresources.maxLowResourcesTime" deprecated="lowresources.maxLowResourcesTime" default="5000"/></Set>
+ <Set name="acceptingInLowResources"><Property name="jetty.lowresources.accepting" default="true"/></Set>
+ </New>
+ </Arg>
+ </Call>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-proxy-protocol-ssl.xml b/third_party/jetty-server/src/main/config/etc/jetty-proxy-protocol-ssl.xml
new file mode 100644
index 0000000..d4f0286
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-proxy-protocol-ssl.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure id="sslConnector" class="org.eclipse.jetty.server.ServerConnector">
+ <Call name="addFirstConnectionFactory">
+ <Arg>
+ <New class="org.eclipse.jetty.server.ProxyConnectionFactory"/>
+ </Arg>
+ </Call>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-proxy-protocol.xml b/third_party/jetty-server/src/main/config/etc/jetty-proxy-protocol.xml
new file mode 100644
index 0000000..f1ba3a7
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-proxy-protocol.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure id="httpConnector" class="org.eclipse.jetty.server.ServerConnector">
+ <Call name="addFirstConnectionFactory">
+ <Arg>
+ <New class="org.eclipse.jetty.server.ProxyConnectionFactory"/>
+ </Arg>
+ </Call>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-requestlog.xml b/third_party/jetty-server/src/main/config/etc/jetty-requestlog.xml
new file mode 100644
index 0000000..569e362
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-requestlog.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- =============================================================== -->
+<!-- Configure the Jetty Request Log -->
+<!-- =============================================================== -->
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+
+ <!-- =========================================================== -->
+ <!-- Configure Request Log for Server -->
+ <!-- (Use RequestLogHandler for a context specific RequestLog -->
+ <!-- =========================================================== -->
+ <Set name="RequestLog">
+ <New id="RequestLog" class="org.eclipse.jetty.server.AsyncNCSARequestLog">
+ <Set name="filename"><Property name="jetty.base" default="." />/<Property>
+ <Name>jetty.requestlog.filePath</Name>
+ <Deprecated>requestlog.filename</Deprecated>
+ <Default><Property name="jetty.requestlog.dir" default="logs"/>/yyyy_mm_dd.request.log</Default>
+ </Property>
+ </Set>
+ <Set name="filenameDateFormat"><Property name="jetty.requestlog.filenameDateFormat" deprecated="requestlog.filenameDateFormat" default="yyyy_MM_dd"/></Set>
+ <Set name="retainDays"><Property name="jetty.requestlog.retainDays" deprecated="requestlog.retain" default="90"/></Set>
+ <Set name="append"><Property name="jetty.requestlog.append" deprecated="requestlog.append" default="false"/></Set>
+ <Set name="extended"><Property name="jetty.requestlog.extended" deprecated="requestlog.extended" default="false"/></Set>
+ <Set name="logCookies"><Property name="jetty.requestlog.cookies" deprecated="requestlog.cookies" default="false"/></Set>
+ <Set name="LogTimeZone"><Property name="jetty.requestlog.timezone" deprecated="requestlog.timezone" default="GMT"/></Set>
+ <Set name="LogLatency"><Property name="jetty.requestlog.loglatency" default="false"/></Set>
+ </New>
+ </Set>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-ssl-context-reload.xml b/third_party/jetty-server/src/main/config/etc/jetty-ssl-context-reload.xml
new file mode 100644
index 0000000..4634635
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-ssl-context-reload.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <Call name="addBean">
+ <Arg>
+ <New id="keyStoreScanner" class="org.eclipse.jetty.util.ssl.KeyStoreScanner">
+ <Arg><Ref refid="sslContextFactory"/></Arg>
+ <Set name="scanInterval"><Property name="jetty.sslContext.reload.scanInterval" default="1"/></Set>
+ </New>
+ </Arg>
+ </Call>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-ssl-context.xml b/third_party/jetty-server/src/main/config/etc/jetty-ssl-context.xml
new file mode 100644
index 0000000..5af7f98
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-ssl-context.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- ============================================================= -->
+<!-- SSL ContextFactory configuration -->
+<!-- ============================================================= -->
+
+<!--
+ To configure Includes / Excludes for Cipher Suites or Protocols see tweak-ssl.xml example at
+ https://www.eclipse.org/jetty/documentation/current/configuring-ssl.html#configuring-sslcontextfactory-cipherSuites
+-->
+
+<Configure id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory$Server">
+ <Set name="Provider"><Property name="jetty.sslContext.provider"/></Set>
+ <Set name="KeyStorePath">
+ <Property name="jetty.sslContext.keyStoreAbsolutePath">
+ <Default>
+ <Property name="jetty.base" default="." />/<Property name="jetty.sslContext.keyStorePath" deprecated="jetty.keystore" default="etc/keystore"/>
+ </Default>
+ </Property>
+ </Set>
+ <Set name="KeyStorePassword"><Property name="jetty.sslContext.keyStorePassword" deprecated="jetty.keystore.password" default="OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"/></Set>
+ <Set name="KeyStoreType"><Property name="jetty.sslContext.keyStoreType" default="JKS"/></Set>
+ <Set name="KeyStoreProvider"><Property name="jetty.sslContext.keyStoreProvider"/></Set>
+ <Set name="KeyManagerPassword"><Property name="jetty.sslContext.keyManagerPassword" deprecated="jetty.keymanager.password" default="OBF:1u2u1wml1z7s1z7a1wnl1u2g"/></Set>
+ <Set name="TrustStorePath">
+ <Property name="jetty.sslContext.trustStoreAbsolutePath">
+ <Default>
+ <Property name="jetty.base" default="." />/<Property name="jetty.sslContext.trustStorePath" deprecated="jetty.truststore" default="etc/keystore"/>
+ </Default>
+ </Property>
+ </Set>
+ <Set name="TrustStorePassword"><Property name="jetty.sslContext.trustStorePassword" deprecated="jetty.truststore.password"/></Set>
+ <Set name="TrustStoreType"><Property name="jetty.sslContext.trustStoreType"/></Set>
+ <Set name="TrustStoreProvider"><Property name="jetty.sslContext.trustStoreProvider"/></Set>
+ <Set name="EndpointIdentificationAlgorithm"><Property name="jetty.sslContext.endpointIdentificationAlgorithm"/></Set>
+ <Set name="NeedClientAuth"><Property name="jetty.sslContext.needClientAuth" deprecated="jetty.ssl.needClientAuth" default="false"/></Set>
+ <Set name="WantClientAuth"><Property name="jetty.sslContext.wantClientAuth" deprecated="jetty.ssl.wantClientAuth" default="false"/></Set>
+ <Set name="useCipherSuitesOrder"><Property name="jetty.sslContext.useCipherSuitesOrder" default="true"/></Set>
+ <Set name="sslSessionCacheSize"><Property name="jetty.sslContext.sslSessionCacheSize" default="-1"/></Set>
+ <Set name="sslSessionTimeout"><Property name="jetty.sslContext.sslSessionTimeout" default="-1"/></Set>
+ <Set name="RenegotiationAllowed"><Property name="jetty.sslContext.renegotiationAllowed" default="true"/></Set>
+ <Set name="RenegotiationLimit"><Property name="jetty.sslContext.renegotiationLimit" default="5"/></Set>
+ <Set name="SniRequired"><Property name="jetty.sslContext.sniRequired" default="false"/></Set>
+
+ <!-- Example of how to configure a PKIX Certificate Path revocation Checker
+ <Call id="pkixPreferCrls" class="java.security.cert.PKIXRevocationChecker$Option" name="valueOf"><Arg>PREFER_CRLS</Arg></Call>
+ <Call id="pkixSoftFail" class="java.security.cert.PKIXRevocationChecker$Option" name="valueOf"><Arg>SOFT_FAIL</Arg></Call>
+ <Call id="pkixNoFallback" class="java.security.cert.PKIXRevocationChecker$Option" name="valueOf"><Arg>NO_FALLBACK</Arg></Call>
+ <Call class="java.security.cert.CertPathBuilder" name="getInstance">
+ <Arg>PKIX</Arg>
+ <Call id="pkixRevocationChecker" name="getRevocationChecker">
+ <Call name="setOptions">
+ <Arg>
+ <Call class="java.util.EnumSet" name="of">
+ <Arg><Ref refid="pkixPreferCrls"/></Arg>
+ <Arg><Ref refid="pkixSoftFail"/></Arg>
+ <Arg><Ref refid="pkixNoFallback"/></Arg>
+ </Call>
+ </Arg>
+ </Call>
+ </Call>
+ </Call>
+ <Set name="PkixCertPathChecker"><Ref refid="pkixRevocationChecker"/></Set>
+ -->
+
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-ssl.xml b/third_party/jetty-server/src/main/config/etc/jetty-ssl.xml
new file mode 100644
index 0000000..acc06da
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-ssl.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- ============================================================= -->
+<!-- Base SSL configuration -->
+<!-- This configuration needs to be used together with 1 or more -->
+<!-- of jetty-https.xml or jetty-http2.xml -->
+<!-- ============================================================= -->
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+
+ <!-- =========================================================== -->
+ <!-- Add an SSL Connector with no protocol factories -->
+ <!-- =========================================================== -->
+ <Call name="addConnector">
+ <Arg>
+ <New id="sslConnector" class="org.eclipse.jetty.server.ServerConnector">
+ <Arg name="server"><Ref refid="Server" /></Arg>
+ <Arg name="acceptors" type="int"><Property name="jetty.ssl.acceptors" deprecated="ssl.acceptors" default="-1"/></Arg>
+ <Arg name="selectors" type="int"><Property name="jetty.ssl.selectors" deprecated="ssl.selectors" default="-1"/></Arg>
+ <Arg name="factories">
+ <Array type="org.eclipse.jetty.server.ConnectionFactory">
+ <!-- uncomment to support proxy protocol
+ <Item>
+ <New class="org.eclipse.jetty.server.ProxyConnectionFactory"/>
+ </Item>-->
+ </Array>
+ </Arg>
+
+ <Set name="host"><Property name="jetty.ssl.host" deprecated="jetty.host" /></Set>
+ <Set name="port"><Property name="jetty.ssl.port" deprecated="ssl.port" default="8443" /></Set>
+ <Set name="idleTimeout"><Property name="jetty.ssl.idleTimeout" deprecated="ssl.timeout" default="30000"/></Set>
+ <Set name="acceptorPriorityDelta"><Property name="jetty.ssl.acceptorPriorityDelta" deprecated="ssl.acceptorPriorityDelta" default="0"/></Set>
+ <Set name="acceptQueueSize"><Property name="jetty.ssl.acceptQueueSize" deprecated="ssl.acceptQueueSize" default="0"/></Set>
+ <Set name="reuseAddress"><Property name="jetty.ssl.reuseAddress" default="true"/></Set>
+ <Set name="acceptedTcpNoDelay"><Property name="jetty.ssl.acceptedTcpNoDelay" default="true"/></Set>
+ <Set name="acceptedReceiveBufferSize"><Property name="jetty.ssl.acceptedReceiveBufferSize" default="-1"/></Set>
+ <Set name="acceptedSendBufferSize"><Property name="jetty.ssl.acceptedSendBufferSize" default="-1"/></Set>
+ <Get name="SelectorManager">
+ <Set name="connectTimeout"><Property name="jetty.ssl.connectTimeout" default="15000"/></Set>
+ </Get>
+ </New>
+ </Arg>
+ </Call>
+
+ <!-- =========================================================== -->
+ <!-- Create a TLS specific HttpConfiguration based on the -->
+ <!-- common HttpConfiguration defined in jetty.xml -->
+ <!-- Add a SecureRequestCustomizer to extract certificate and -->
+ <!-- session information -->
+ <!-- =========================================================== -->
+ <New id="sslHttpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
+ <Arg><Ref refid="httpConfig"/></Arg>
+ <Call name="addCustomizer">
+ <Arg>
+ <New class="org.eclipse.jetty.server.SecureRequestCustomizer">
+ <Arg name="sniRequired" type="boolean"><Property name="jetty.ssl.sniRequired" default="false"/></Arg>
+ <Arg name="sniHostCheck" type="boolean"><Property name="jetty.ssl.sniHostCheck" default="true"/></Arg>
+ <Arg name="stsMaxAgeSeconds" type="int"><Property name="jetty.ssl.stsMaxAgeSeconds" default="-1"/></Arg>
+ <Arg name="stsIncludeSubdomains" type="boolean"><Property name="jetty.ssl.stsIncludeSubdomains" default="false"/></Arg>
+ </New>
+ </Arg>
+ </Call>
+ </New>
+
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-stats.xml b/third_party/jetty-server/src/main/config/etc/jetty-stats.xml
new file mode 100644
index 0000000..8f695f3
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-stats.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- =============================================================== -->
+<!-- Mixin the Statistics Handler -->
+<!-- =============================================================== -->
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <Call name="insertHandler">
+ <Arg>
+ <New id="StatsHandler" class="org.eclipse.jetty.server.handler.StatisticsHandler">
+ <Set name="gracefulShutdownWaitsForRequests"><Property name="jetty.statistics.gracefulShutdownWaitsForRequests" default="true"/></Set>
+ </New>
+ </Arg>
+ </Call>
+ <Call name="addBeanToAllConnectors">
+ <Arg>
+ <New class="org.eclipse.jetty.io.ConnectionStatistics"/>
+ </Arg>
+ </Call>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-threadlimit.xml b/third_party/jetty-server/src/main/config/etc/jetty-threadlimit.xml
new file mode 100644
index 0000000..a16b37c
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-threadlimit.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- =============================================================== -->
+<!-- Mixin the Thread Limit Handler to the entire server -->
+<!-- =============================================================== -->
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <Call name="insertHandler">
+ <Arg>
+ <New id="ThreadLimitHandler" class="org.eclipse.jetty.server.handler.ThreadLimitHandler">
+ <Arg name="forwardedHeader"><Property name="jetty.threadlimit.forwardedHeader"/></Arg>
+ <Set name="enabled"><Property name="jetty.threadlimit.enabled" default="true"/></Set>
+ <Set name="threadLimit"><Property name="jetty.threadlimit.threadLimit" default="10"/></Set>
+ </New>
+ </Arg>
+ </Call>
+</Configure>
+
+
diff --git a/third_party/jetty-server/src/main/config/etc/jetty-threadpool.xml b/third_party/jetty-server/src/main/config/etc/jetty-threadpool.xml
new file mode 100644
index 0000000..6f69faa
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty-threadpool.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure>
+ <!-- =========================================================== -->
+ <!-- Configure the Server Thread Pool. -->
+ <!-- The server holds a common thread pool which is used by -->
+ <!-- default as the executor used by all connectors and servlet -->
+ <!-- dispatches. -->
+ <!-- -->
+ <!-- Configuring a fixed thread pool is vital to controlling the -->
+ <!-- maximal memory footprint of the server and is a key tuning -->
+ <!-- parameter for tuning. In an application that rarely blocks -->
+ <!-- then maximal threads may be close to the number of 5*CPUs. -->
+ <!-- In an application that frequently blocks, then maximal -->
+ <!-- threads should be set as high as possible given the memory -->
+ <!-- available. -->
+ <!-- -->
+ <!-- Consult the javadoc of o.e.j.util.thread.QueuedThreadPool -->
+ <!-- for all configuration that may be set here. -->
+ <!-- =========================================================== -->
+ <New id="threadPool" class="org.eclipse.jetty.util.thread.QueuedThreadPool">
+ <Set name="minThreads" type="int"><Property name="jetty.threadPool.minThreads" deprecated="threads.min" default="10"/></Set>
+ <Set name="maxThreads" type="int"><Property name="jetty.threadPool.maxThreads" deprecated="threads.max" default="200"/></Set>
+ <Set name="reservedThreads" type="int"><Property name="jetty.threadPool.reservedThreads" default="-1"/></Set>
+ <Set name="idleTimeout" type="int"><Property name="jetty.threadPool.idleTimeout" deprecated="threads.timeout" default="60000"/></Set>
+ <Set name="detailedDump" type="boolean"><Property name="jetty.threadPool.detailedDump" default="false"/></Set>
+ </New>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/jetty.xml b/third_party/jetty-server/src/main/config/etc/jetty.xml
new file mode 100644
index 0000000..c42b35c
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/jetty.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- =============================================================== -->
+<!-- Documentation of this file format can be found at: -->
+<!-- https://www.eclipse.org/jetty/documentation/current/ -->
+<!-- -->
+<!-- Additional configuration files are available in $JETTY_HOME/etc -->
+<!-- and can be mixed in. See start.ini file for the default -->
+<!-- configuration files. -->
+<!-- -->
+<!-- For a description of the configuration mechanism, see the -->
+<!-- output of: -->
+<!-- java -jar start.jar -? -->
+<!-- =============================================================== -->
+
+<!-- =============================================================== -->
+<!-- Configure a Jetty Server instance with an ID "Server" -->
+<!-- Other configuration files may also configure the "Server" -->
+<!-- ID, in which case they are adding configuration to the same -->
+<!-- instance. If other configuration have a different ID, they -->
+<!-- will create and configure another instance of Jetty. -->
+<!-- Consult the javadoc of o.e.j.server.Server for all -->
+<!-- configuration that may be set here. -->
+<!-- =============================================================== -->
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <Arg name="threadpool"><Ref refid="threadPool"/></Arg>
+
+ <Call name="addBean">
+ <Arg><Ref refid="byteBufferPool"/></Arg>
+ </Call>
+
+ <!-- =========================================================== -->
+ <!-- Add shared Scheduler instance -->
+ <!-- =========================================================== -->
+ <Call name="addBean">
+ <Arg>
+ <New class="org.eclipse.jetty.util.thread.ScheduledExecutorScheduler">
+ <Arg name="name"><Property name="jetty.scheduler.name"/></Arg>
+ <Arg name="daemon" type="boolean"><Property name="jetty.scheduler.daemon" default="false" /></Arg>
+ <Arg name="threads" type="int"><Property name="jetty.scheduler.threads" default="-1" /></Arg>
+ </New>
+ </Arg>
+ </Call>
+
+ <!-- =========================================================== -->
+ <!-- Http Configuration. -->
+ <!-- This is a common configuration instance used by all -->
+ <!-- connectors that can carry HTTP semantics (HTTP, HTTPS, etc.)-->
+ <!-- It configures the non wire protocol aspects of the HTTP -->
+ <!-- semantic. -->
+ <!-- -->
+ <!-- This configuration is only defined here and is used by -->
+ <!-- reference from other XML files such as jetty-http.xml, -->
+ <!-- jetty-https.xml and other configuration files which -->
+ <!-- instantiate the connectors. -->
+ <!-- -->
+ <!-- Consult the javadoc of o.e.j.server.HttpConfiguration -->
+ <!-- for all configuration that may be set here. -->
+ <!-- =========================================================== -->
+ <New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
+ <Set name="secureScheme"><Property name="jetty.httpConfig.secureScheme" default="https" /></Set>
+ <Set name="securePort"><Property name="jetty.httpConfig.securePort" deprecated="jetty.secure.port" default="8443" /></Set>
+ <Set name="outputBufferSize"><Property name="jetty.httpConfig.outputBufferSize" deprecated="jetty.output.buffer.size" default="32768" /></Set>
+ <Set name="outputAggregationSize"><Property name="jetty.httpConfig.outputAggregationSize" deprecated="jetty.output.aggregation.size" default="8192" /></Set>
+ <Set name="requestHeaderSize"><Property name="jetty.httpConfig.requestHeaderSize" deprecated="jetty.request.header.size" default="8192" /></Set>
+ <Set name="responseHeaderSize"><Property name="jetty.httpConfig.responseHeaderSize" deprecated="jetty.response.header.size" default="8192" /></Set>
+ <Set name="sendServerVersion"><Property name="jetty.httpConfig.sendServerVersion" deprecated="jetty.send.server.version" default="true" /></Set>
+ <Set name="sendDateHeader"><Property name="jetty.httpConfig.sendDateHeader" deprecated="jetty.send.date.header" default="false" /></Set>
+ <Set name="headerCacheSize"><Property name="jetty.httpConfig.headerCacheSize" default="1024" /></Set>
+ <Set name="delayDispatchUntilContent"><Property name="jetty.httpConfig.delayDispatchUntilContent" deprecated="jetty.delayDispatchUntilContent" default="true"/></Set>
+ <Set name="maxErrorDispatches"><Property name="jetty.httpConfig.maxErrorDispatches" default="10"/></Set>
+ <Set name="blockingTimeout"><Property deprecated="jetty.httpConfig.blockingTimeout" name="jetty.httpConfig.blockingTimeout.DEPRECATED" default="-1"/></Set>
+ <Set name="persistentConnectionsEnabled"><Property name="jetty.httpConfig.persistentConnectionsEnabled" default="true"/></Set>
+ <Set name="requestCookieCompliance"><Call class="org.eclipse.jetty.http.CookieCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.requestCookieCompliance" deprecated="jetty.httpConfig.cookieCompliance" default="RFC6265"/></Arg></Call></Set>
+ <Set name="responseCookieCompliance"><Call class="org.eclipse.jetty.http.CookieCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.responseCookieCompliance" default="RFC6265"/></Arg></Call></Set>
+ <Set name="multiPartFormDataCompliance"><Call class="org.eclipse.jetty.server.MultiPartFormDataCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.multiPartFormDataCompliance" default="RFC7578"/></Arg></Call></Set>
+ <Set name="relativeRedirectAllowed"><Property name="jetty.httpConfig.relativeRedirectAllowed" default="false"/></Set>
+ </New>
+
+ <!-- =========================================================== -->
+ <!-- Set the default handler structure for the Server -->
+ <!-- A handler collection is used to pass received requests to -->
+ <!-- both the ContextHandlerCollection, which selects the next -->
+ <!-- handler by context path and virtual host, and the -->
+ <!-- DefaultHandler, which handles any requests not handled by -->
+ <!-- the context handlers. -->
+ <!-- Other handlers may be added to the "Handlers" collection, -->
+ <!-- for example the jetty-requestlog.xml file adds the -->
+ <!-- RequestLogHandler after the default handler -->
+ <!-- =========================================================== -->
+ <Set name="handler">
+ <New id="Handlers" class="org.eclipse.jetty.server.handler.HandlerCollection">
+ <Set name="handlers">
+ <Array type="org.eclipse.jetty.server.Handler">
+ <Item>
+ <New id="Contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection"/>
+ </Item>
+ <Item>
+ <New id="DefaultHandler" class="org.eclipse.jetty.server.handler.DefaultHandler"/>
+ </Item>
+ </Array>
+ </Set>
+ </New>
+ </Set>
+
+ <!-- =========================================================== -->
+ <!-- extra server options -->
+ <!-- =========================================================== -->
+ <Set name="stopAtShutdown"><Property name="jetty.server.stopAtShutdown" default="true"/></Set>
+ <Set name="stopTimeout"><Property name="jetty.server.stopTimeout" default="5000"/></Set>
+ <Set name="dumpAfterStart"><Property name="jetty.server.dumpAfterStart" deprecated="jetty.dump.start" default="false"/></Set>
+ <Set name="dumpBeforeStop"><Property name="jetty.server.dumpBeforeStop" deprecated="jetty.dump.stop" default="false"/></Set>
+
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/sessions/file/session-store.xml b/third_party/jetty-server/src/main/config/etc/sessions/file/session-store.xml
new file mode 100644
index 0000000..238e3b6
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/sessions/file/session-store.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+
+ <!-- ===================================================================== -->
+ <!-- Configure a factory for FileSessionStores -->
+ <!-- ===================================================================== -->
+ <Call name="addBean">
+ <Arg>
+ <New id="sessionDataStoreFactory" class="org.eclipse.jetty.server.session.FileSessionDataStoreFactory">
+ <Set name="deleteUnrestorableFiles"><Property name="jetty.session.file.deleteUnrestorableFiles" default="false" /></Set>
+ <Set name="storeDir"><Property name="jetty.session.file.storeDir"/></Set>
+ <Set name="savePeriodSec"><Property name="jetty.session.savePeriod.seconds" default="0" /></Set>
+ </New>
+ </Arg>
+ </Call>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/sessions/id-manager.xml b/third_party/jetty-server/src/main/config/etc/sessions/id-manager.xml
new file mode 100644
index 0000000..d64dad8
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/sessions/id-manager.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+
+ <!-- ===================================================================== -->
+ <!-- Configure a SessionIdManager -->
+ <!-- ===================================================================== -->
+ <Set name="sessionIdManager">
+ <New id="idMgr" class="org.eclipse.jetty.server.session.DefaultSessionIdManager">
+ <Arg>
+ <Ref refid="Server"/>
+ </Arg>
+ <Set name="workerName">
+ <Property name="jetty.sessionIdManager.workerName">
+ <Default>node<Env name="JETTY_WORKER_INSTANCE">
+ <Default>
+ <Env name="GAE_MODULE_INSTANCE">
+ <Default>0</Default>
+ </Env>
+ </Default>
+ </Env>
+ </Default>
+ </Property>
+ </Set>
+
+ <!-- ===================================================================== -->
+ <!-- Configure a session housekeeper to help with scavenging -->
+ <!-- ===================================================================== -->
+ <Set name="sessionHouseKeeper">
+ <New class="org.eclipse.jetty.server.session.HouseKeeper">
+ <Set name="intervalSec"><Property name="jetty.sessionScavengeInterval.seconds" default="600"/></Set>
+ </New>
+ </Set>
+ </New>
+ </Set>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/sessions/jdbc/datasource.xml b/third_party/jetty-server/src/main/config/etc/sessions/jdbc/datasource.xml
new file mode 100644
index 0000000..0fed215
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/sessions/jdbc/datasource.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+
+ <New id="databaseAdaptor" class="org.eclipse.jetty.server.session.DatabaseAdaptor">
+ <Set name="DatasourceName"><Property name="jetty.session.jdbc.datasourceName" default="/jdbc/sessions" /></Set>
+ <Set name="blobType"><Property name="jetty.session.jdbc.blobType"/></Set>
+ <Set name="longType"><Property name="jetty.session.jdbc.longType"/></Set>
+ <Set name="stringType"><Property name="jetty.session.jdbc.stringType"/></Set>
+ </New>
+
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/sessions/jdbc/driver.xml b/third_party/jetty-server/src/main/config/etc/sessions/jdbc/driver.xml
new file mode 100644
index 0000000..b6b66f3
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/sessions/jdbc/driver.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+
+ <New id="databaseAdaptor" class="org.eclipse.jetty.server.session.DatabaseAdaptor">
+ <Call name="setDriverInfo">
+ <Arg><Property name="jetty.session.jdbc.driverClass"/></Arg>
+ <Arg><Property name="jetty.session.jdbc.driverUrl"/></Arg>
+ </Call>
+ <Set name="blobType"><Property name="jetty.session.jdbc.blobType"/></Set>
+ <Set name="longType"><Property name="jetty.session.jdbc.longType"/></Set>
+ <Set name="stringType"><Property name="jetty.session.jdbc.stringType"/></Set>
+ </New>
+
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/sessions/jdbc/session-store.xml b/third_party/jetty-server/src/main/config/etc/sessions/jdbc/session-store.xml
new file mode 100644
index 0000000..438c9b3
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/sessions/jdbc/session-store.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0"?>
+<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+
+ <!-- ===================================================================== -->
+ <!-- Configure a factory for JDBCSessionStores -->
+ <!-- ===================================================================== -->
+ <Call name="addBean">
+ <Arg>
+ <New id="sessionDataStoreFactory" class="org.eclipse.jetty.server.session.JDBCSessionDataStoreFactory">
+ <Set name="gracePeriodSec"><Property name="jetty.session.gracePeriod.seconds" default="3600" /></Set>
+ <Set name="savePeriodSec"><Property name="jetty.session.savePeriod.seconds" default="0" /></Set>
+ <Set name="databaseAdaptor">
+ <Ref refid="databaseAdaptor"/>
+ </Set>
+ <Set name="sessionTableSchema">
+ <New
+ class="org.eclipse.jetty.server.session.JDBCSessionDataStore$SessionTableSchema">
+ <Set name="accessTimeColumn">
+ <Property name="jetty.session.jdbc.schema.accessTimeColumn" default="accessTime" />
+ </Set>
+ <Set name="contextPathColumn">
+ <Property name="jetty.session.jdbc.schema.contextPathColumn" default="contextPath" />
+ </Set>
+ <Set name="cookieTimeColumn">
+ <Property name="jetty.session.jdbc.schema.cookieTimeColumn" default="cookieTime" />
+ </Set>
+ <Set name="createTimeColumn">
+ <Property name="jetty.session.jdbc.schema.createTimeColumn" default="createTime" />
+ </Set>
+ <Set name="expiryTimeColumn">
+ <Property name="jetty.session.jdbc.schema.expiryTimeColumn" default="expiryTime" />
+ </Set>
+ <Set name="lastAccessTimeColumn">
+ <Property name="jetty.session.jdbc.schema.lastAccessTimeColumn" default="lastAccessTime" />
+ </Set>
+ <Set name="lastSavedTimeColumn">
+ <Property name="jetty.session.jdbc.schema.lastSavedTimeColumn" default="lastSavedTime" />
+ </Set>
+ <Set name="idColumn">
+ <Property name="jetty.session.jdbc.schema.idColumn" default="sessionId" />
+ </Set>
+ <Set name="lastNodeColumn">
+ <Property name="jetty.session.jdbc.schema.lastNodeColumn" default="lastNode" />
+ </Set>
+ <Set name="virtualHostColumn">
+ <Property name="jetty.session.jdbc.schema.virtualHostColumn" default="virtualHost" />
+ </Set>
+ <Set name="maxIntervalColumn">
+ <Property name="jetty.session.jdbc.schema.maxIntervalColumn" default="maxInterval" />
+ </Set>
+ <Set name="mapColumn">
+ <Property name="jetty.session.jdbc.schema.mapColumn" default="map" />
+ </Set>
+ <Set name="schemaName">
+ <Property name="jetty.session.jdbc.schema.schemaName" />
+ </Set>
+ <Set name="catalogName">
+ <Property name="jetty.session.jdbc.schema.catalogName" />
+ </Set>
+ <Set name="tableName">
+ <Property name="jetty.session.jdbc.schema.table" default="JettySessions" />
+ </Set>
+ </New>
+ </Set>
+ </New>
+ </Arg>
+ </Call>
+
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/sessions/session-cache-hash.xml b/third_party/jetty-server/src/main/config/etc/sessions/session-cache-hash.xml
new file mode 100644
index 0000000..941ea1e
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/sessions/session-cache-hash.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+
+ <!-- ===================================================================== -->
+ <!-- Configure a factory for DefaultSessionCache -->
+ <!-- ===================================================================== -->
+ <Call name="addBean">
+ <Arg>
+ <New class="org.eclipse.jetty.server.session.DefaultSessionCacheFactory">
+ <Set name="evictionPolicy"><Property name="jetty.session.evictionPolicy" default="-1" /></Set>
+ <Set name="saveOnInactiveEvict"><Property name="jetty.session.saveOnInactiveEvict" default="false" /></Set>
+ <Set name="saveOnCreate"><Property name="jetty.session.saveOnCreate" default="false" /></Set>
+ <Set name="removeUnloadableSessions"><Property name="jetty.session.removeUnloadableSessions" default="false"/></Set>
+ <Set name="flushOnResponseCommit"><Property name="jetty.session.flushOnResponseCommit" default="false"/></Set>
+ <Set name="invalidateOnShutdown"><Property name="jetty.session.invalidateOnShutdown" default="false"/></Set>
+ </New>
+ </Arg>
+ </Call>
+
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/sessions/session-cache-null.xml b/third_party/jetty-server/src/main/config/etc/sessions/session-cache-null.xml
new file mode 100644
index 0000000..34a4343
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/sessions/session-cache-null.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+
+ <!-- ===================================================================== -->
+ <!-- Configure a factory for NullSessionCache -->
+ <!-- ===================================================================== -->
+ <Call name="addBean">
+ <Arg>
+ <New class="org.eclipse.jetty.server.session.NullSessionCacheFactory">
+ <Set name="saveOnCreate"><Property name="jetty.session.saveOnCreate" default="false" /></Set>
+ <Set name="removeUnloadableSessions"><Property name="jetty.session.removeUnloadableSessions" default="false" /></Set>
+ <Set name="flushOnResponseCommit"><Property name="jetty.session.flushOnResponseCommit" default="false" /></Set>
+ </New>
+ </Arg>
+ </Call>
+
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/etc/sessions/session-data-cache/session-caching-store.xml b/third_party/jetty-server/src/main/config/etc/sessions/session-data-cache/session-caching-store.xml
new file mode 100644
index 0000000..dd5231e
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/etc/sessions/session-data-cache/session-caching-store.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+
+ <!-- ===================================================================== -->
+ <!-- Configure a factory for CachingSessionDataStores -->
+ <!-- ===================================================================== -->
+
+ <Call name="removeBean">
+ <Arg>
+ <Ref refid="sessionDataStoreFactory"/>
+ </Arg>
+ </Call>
+
+ <Call name="addBean">
+ <Arg>
+ <New class="org.eclipse.jetty.server.session.CachingSessionDataStoreFactory">
+ <Set name="sessionStoreFactory"><Ref refid="sessionDataStoreFactory"/></Set>
+ <Set name="sessionDataMapFactory"><Ref refid="sessionDataMapFactory"/></Set>
+ </New>
+ </Arg>
+ </Call>
+
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/modules/acceptratelimit.mod b/third_party/jetty-server/src/main/config/modules/acceptratelimit.mod
new file mode 100644
index 0000000..dae63e5
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/acceptratelimit.mod
@@ -0,0 +1,23 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enable a server wide accept rate limit
+
+[tags]
+connector
+
+[depend]
+server
+
+[xml]
+etc/jetty-acceptratelimit.xml
+
+[ini-template]
+## The limit of accepted connections
+#jetty.acceptratelimit.acceptRateLimit=1000
+
+## The period over which the rate applies
+#jetty.acceptratelimit.period=1000
+
+# The unit of time for the period
+#jetty.acceptratelimit.units=MILLISECONDS
diff --git a/third_party/jetty-server/src/main/config/modules/bytebufferpool.mod b/third_party/jetty-server/src/main/config/modules/bytebufferpool.mod
new file mode 100644
index 0000000..8d31ece
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/bytebufferpool.mod
@@ -0,0 +1,27 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Configures the ByteBufferPool used by ServerConnectors.
+
+[xml]
+etc/jetty-bytebufferpool.xml
+
+[ini-template]
+### Server ByteBufferPool Configuration
+## Minimum capacity to pool ByteBuffers
+#jetty.byteBufferPool.minCapacity=0
+
+## Maximum capacity to pool ByteBuffers
+#jetty.byteBufferPool.maxCapacity=65536
+
+## Capacity factor
+#jetty.byteBufferPool.factor=1024
+
+## Maximum queue length for each bucket (-1 for unbounded)
+#jetty.byteBufferPool.maxQueueLength=-1
+
+## Maximum heap memory retainable by the pool (-1 for unlimited)
+#jetty.byteBufferPool.maxHeapMemory=-1
+
+## Maximum direct memory retainable by the pool (-1 for unlimited)
+#jetty.byteBufferPool.maxDirectMemory=-1
diff --git a/third_party/jetty-server/src/main/config/modules/connectionlimit.mod b/third_party/jetty-server/src/main/config/modules/connectionlimit.mod
new file mode 100644
index 0000000..a0113a6
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/connectionlimit.mod
@@ -0,0 +1,21 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enable a server wide connection limit
+
+[tags]
+connector
+
+[depend]
+server
+
+[xml]
+etc/jetty-connectionlimit.xml
+
+[ini-template]
+
+## The limit of connections to apply
+#jetty.connectionlimit.maxConnections=1000
+
+## The idle timeout to apply (in milliseconds) when connections are limited
+#jetty.connectionlimit.idleTimeout=1000
diff --git a/third_party/jetty-server/src/main/config/modules/continuation.mod b/third_party/jetty-server/src/main/config/modules/continuation.mod
new file mode 100644
index 0000000..f900ec4
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/continuation.mod
@@ -0,0 +1,9 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables support for Continuation style asynchronous
+Servlets. Now deprecated in favour of Servlet 3.1
+API
+
+[lib]
+lib/jetty-continuation-${jetty.version}.jar
diff --git a/third_party/jetty-server/src/main/config/modules/customrequestlog.mod b/third_party/jetty-server/src/main/config/modules/customrequestlog.mod
new file mode 100644
index 0000000..56794b6
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/customrequestlog.mod
@@ -0,0 +1,41 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables a format string style request log.
+
+[provides]
+requestlog
+
+[tags]
+requestlog
+
+[depend]
+server
+
+[xml]
+etc/jetty-customrequestlog.xml
+
+[files]
+logs/
+
+[ini-template]
+## Logging directory (relative to $jetty.base)
+# jetty.requestlog.dir=logs
+
+## File path
+# jetty.requestlog.filePath=${jetty.requestlog.dir}/yyyy_mm_dd.request.log
+
+## Date format for rollovered files (uses SimpleDateFormat syntax)
+# jetty.requestlog.filenameDateFormat=yyyy_MM_dd
+
+## How many days to retain old log files
+# jetty.requestlog.retainDays=90
+
+## Whether to append to existing file
+# jetty.requestlog.append=false
+
+## Timezone of the log file rollover
+# jetty.requestlog.timezone=GMT
+
+## Format string
+# jetty.customrequestlog.formatString=%a - %u %{dd/MMM/yyyy:HH:mm:ss ZZZ|GMT}t "%r" %s %B "%{Referer}i" "%{User-Agent}i" "%C"
diff --git a/third_party/jetty-server/src/main/config/modules/debug.mod b/third_party/jetty-server/src/main/config/modules/debug.mod
new file mode 100644
index 0000000..177da05
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/debug.mod
@@ -0,0 +1,35 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables the DebugListener to generate additional
+logging regarding detailed request handling events.
+Renames threads to include request URI.
+
+[tags]
+debug
+
+[depend]
+deploy
+
+[files]
+logs/
+
+[xml]
+etc/jetty-debug.xml
+
+[ini-template]
+
+## How many days to retain old log files
+# jetty.debug.retainDays=14
+
+## Timezone of the log entries
+# jetty.debug.timezone=GMT
+
+## Show Request/Response headers
+# jetty.debug.showHeaders=true
+
+## Rename threads while in context scope
+# jetty.debug.renameThread=false
+
+## Dump context as deployed
+# jetty.debug.dumpContext=true
diff --git a/third_party/jetty-server/src/main/config/modules/debuglog.mod b/third_party/jetty-server/src/main/config/modules/debuglog.mod
new file mode 100644
index 0000000..91a2d61
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/debuglog.mod
@@ -0,0 +1,30 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Deprecated Debug Log using the DebugHandle.
+Replaced with the debug module.
+
+[tags]
+debug
+
+[depend]
+server
+
+[files]
+logs/
+
+[xml]
+etc/jetty-debuglog.xml
+
+[ini-template]
+## Logging directory (relative to $jetty.base)
+# jetty.debuglog.dir=logs
+
+## Whether to append to existing file
+# jetty.debuglog.append=false
+
+## How many days to retain old log files
+# jetty.debuglog.retainDays=90
+
+## Timezone of the log entries
+# jetty.debuglog.timezone=GMT
diff --git a/third_party/jetty-server/src/main/config/modules/ext.mod b/third_party/jetty-server/src/main/config/modules/ext.mod
new file mode 100644
index 0000000..6808de4
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/ext.mod
@@ -0,0 +1,16 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Adds all jar files discovered in $JETTY_HOME/lib/ext
+and $JETTY_BASE/lib/ext to the servers classpath.
+
+[tags]
+classpath
+
+[lib]
+lib/ext/**.jar
+
+[files]
+lib/
+lib/ext/
+
diff --git a/third_party/jetty-server/src/main/config/modules/flight-recorder.mod b/third_party/jetty-server/src/main/config/modules/flight-recorder.mod
new file mode 100644
index 0000000..219173d
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/flight-recorder.mod
@@ -0,0 +1,13 @@
+# Enables Java Mission Control's Flight Recorder for low overhead profiling.
+
+[depend]
+server
+
+[exec]
+-XX:+UnlockCommercialFeatures
+-XX:+FlightRecorder
+
+[license]
+Java Flight Recorder requires a commercial license for use in production.
+To learn more about commercial features and how to enable them please visit
+http://www.oracle.com/technetwork/java/javaseproducts/
diff --git a/third_party/jetty-server/src/main/config/modules/gzip.mod b/third_party/jetty-server/src/main/config/modules/gzip.mod
new file mode 100644
index 0000000..261615e
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/gzip.mod
@@ -0,0 +1,39 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enable GzipHandler for dynamic gzip compression
+for the entire server.
+
+[tags]
+handler
+
+[depend]
+server
+
+[xml]
+etc/jetty-gzip.xml
+
+[ini-template]
+## Minimum content length after which gzip is enabled
+# jetty.gzip.minGzipSize=32
+
+## Check whether a file with *.gz extension exists
+# jetty.gzip.checkGzExists=false
+
+## Gzip compression level (-1 for default)
+# jetty.gzip.compressionLevel=-1
+
+## User agents for which gzip is disabled
+# jetty.gzip.excludedUserAgent=.*MSIE.6\.0.*
+
+## Inflate request buffer size, or 0 for no request inflation
+# jetty.gzip.inflateBufferSize=0
+
+## Deflater pool max size (-1 for unlimited, 0 for no pool)
+# jetty.gzip.deflaterPoolCapacity=-1
+
+## Comma separated list of included methods
+# jetty.gzip.includedMethodList=GET
+
+## Comma separated list of excluded methods
+# jetty.gzip.excludedMethodList=
diff --git a/third_party/jetty-server/src/main/config/modules/home-base-warning.mod b/third_party/jetty-server/src/main/config/modules/home-base-warning.mod
new file mode 100644
index 0000000..8430066
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/home-base-warning.mod
@@ -0,0 +1,9 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Generates a warning that server has been run from $JETTY_HOME
+rather than from a $JETTY_BASE.
+
+[xml]
+etc/home-base-warning.xml
+
diff --git a/third_party/jetty-server/src/main/config/modules/http-forwarded.mod b/third_party/jetty-server/src/main/config/modules/http-forwarded.mod
new file mode 100644
index 0000000..8591df5
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/http-forwarded.mod
@@ -0,0 +1,37 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Adds a forwarded request customizer to the HTTP Connector
+to process forwarded-for style headers from a proxy.
+
+[tags]
+connector
+
+[depend]
+http
+
+[xml]
+etc/jetty-http-forwarded.xml
+
+[ini-template]
+### ForwardedRequestCustomizer Configuration
+
+## If true, only the RFC7239 Forwarded header is accepted
+# jetty.httpConfig.forwardedOnly=false
+
+## if true, the proxy address obtained from X-Forwarded-Server or RFC7239 is used as the request authority.
+# jetty.httpConfig.forwardedProxyAsAuthority=false
+
+## if true, the X-Forwarded-Port header applies to the authority, else it applies to the remote client address
+# jetty.httpConfig.forwardedPortAsAuthority=true
+
+# jetty.httpConfig.forwardedHeader=Forwarded
+# jetty.httpConfig.forwardedHostHeader=X-Forwarded-Host
+# jetty.httpConfig.forwardedServerHeader=X-Forwarded-Server
+# jetty.httpConfig.forwardedProtoHeader=X-Forwarded-Proto
+# jetty.httpConfig.forwardedForHeader=X-Forwarded-For
+# jetty.httpConfig.forwardedPortHeader=X-Forwarded-Port
+# jetty.httpConfig.forwardedHttpsHeader=X-Proxied-Https
+# jetty.httpConfig.forwardedSslSessionIdHeader=Proxy-ssl-id
+# jetty.httpConfig.forwardedCipherSuiteHeader=Proxy-auth-cert
+
diff --git a/third_party/jetty-server/src/main/config/modules/http.mod b/third_party/jetty-server/src/main/config/modules/http.mod
new file mode 100644
index 0000000..f36c741
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/http.mod
@@ -0,0 +1,61 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables an HTTP connector on the server.
+By default HTTP/1 is support, but HTTP2C can
+be added to the connector with the http2c module.
+
+[tags]
+connector
+http
+
+[depend]
+server
+
+[xml]
+etc/jetty-http.xml
+
+[ini-template]
+### HTTP Connector Configuration
+
+## Connector host/address to bind to
+# jetty.http.host=0.0.0.0
+
+## Connector port to listen on
+# jetty.http.port=8080
+
+## Connector idle timeout in milliseconds
+# jetty.http.idleTimeout=30000
+
+## Number of acceptors (-1 picks default based on number of cores)
+# jetty.http.acceptors=-1
+
+## Number of selectors (-1 picks default based on number of cores)
+# jetty.http.selectors=-1
+
+## ServerSocketChannel backlog (0 picks platform default)
+# jetty.http.acceptQueueSize=0
+
+## Thread priority delta to give to acceptor threads
+# jetty.http.acceptorPriorityDelta=0
+
+## The requested maximum length of the queue of incoming connections.
+# jetty.http.acceptQueueSize=0
+
+## Enable/disable the SO_REUSEADDR socket option.
+# jetty.http.reuseAddress=true
+
+## Enable/disable TCP_NODELAY on accepted sockets.
+# jetty.http.acceptedTcpNoDelay=true
+
+## The SO_RCVBUF option to set on accepted sockets. A value of -1 indicates that it is left to its default value.
+# jetty.http.acceptedReceiveBufferSize=-1
+
+## The SO_SNDBUF option to set on accepted sockets. A value of -1 indicates that it is left to its default value.
+# jetty.http.acceptedSendBufferSize=-1
+
+## Connect Timeout in milliseconds
+# jetty.http.connectTimeout=15000
+
+## HTTP Compliance: RFC7230, RFC7230_LEGACY, RFC2616, RFC2616_LEGACY, LEGACY or CUSTOMn
+# jetty.http.compliance=RFC7230_LEGACY
diff --git a/third_party/jetty-server/src/main/config/modules/https.mod b/third_party/jetty-server/src/main/config/modules/https.mod
new file mode 100644
index 0000000..b316a2e
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/https.mod
@@ -0,0 +1,21 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Adds HTTPS protocol support to the TLS(SSL) Connector
+
+[tags]
+connector
+https
+http
+ssl
+
+[depend]
+ssl
+
+[optional]
+http2
+http-forwarded
+
+[xml]
+etc/jetty-https.xml
+
diff --git a/third_party/jetty-server/src/main/config/modules/inetaccess.mod b/third_party/jetty-server/src/main/config/modules/inetaccess.mod
new file mode 100644
index 0000000..509be53
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/inetaccess.mod
@@ -0,0 +1,32 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enable the InetAccessHandler to apply a include/exclude
+control of the remote IP of requests.
+
+[tags]
+handler
+
+[depend]
+server
+
+[files]
+basehome:modules/inetaccess/jetty-inetaccess.xml|etc/jetty-inetaccess.xml
+
+[xml]
+etc/jetty-inetaccess.xml
+
+[ini-template]
+
+## List of InetAddress patterns to include
+#jetty.inetaccess.include=127.0.0.1,127.0.0.2
+
+## List of InetAddress patterns to exclude
+#jetty.inetaccess.exclude=127.0.0.1,127.0.0.2
+
+## List of Connector names to include
+#jetty.inetaccess.includeConnectors=http
+
+## List of Connector names to exclude
+#jetty.inetaccess.excludeConnectors=tls
+
diff --git a/third_party/jetty-server/src/main/config/modules/inetaccess/jetty-inetaccess.xml b/third_party/jetty-server/src/main/config/modules/inetaccess/jetty-inetaccess.xml
new file mode 100644
index 0000000..6c538ea
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/inetaccess/jetty-inetaccess.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <Call name="insertHandler">
+ <Arg>
+ <New id="InetAccessHandler" class="org.eclipse.jetty.server.handler.InetAccessHandler">
+ <Call name="include">
+ <Arg>
+ <Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
+ <Arg><Property name="jetty.inetaccess.include" default="" /></Arg>
+ </Call>
+ </Arg>
+ </Call>
+ <Call name="exclude">
+ <Arg>
+ <Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
+ <Arg><Property name="jetty.inetaccess.exclude" default="" /></Arg>
+ </Call>
+ </Arg>
+ </Call>
+ <Call name="includeConnectors">
+ <Arg>
+ <Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
+ <Arg><Property name="jetty.inetaccess.includeConnectors" default="" /></Arg>
+ </Call>
+ </Arg>
+ </Call>
+ <Call name="excludeConnectors">
+ <Arg>
+ <Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
+ <Arg><Property name="jetty.inetaccess.excludeConnectors" default="" /></Arg>
+ </Call>
+ </Arg>
+ </Call>
+ </New>
+ </Arg>
+ </Call>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/modules/ipaccess.mod b/third_party/jetty-server/src/main/config/modules/ipaccess.mod
new file mode 100644
index 0000000..ac46801
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/ipaccess.mod
@@ -0,0 +1,14 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enable the ipaccess handler to apply a white/black list
+control of the remote IP of requests.
+
+[tags]
+handler
+
+[depend]
+server
+
+[xml]
+etc/jetty-ipaccess.xml
diff --git a/third_party/jetty-server/src/main/config/modules/jdbc.mod b/third_party/jetty-server/src/main/config/modules/jdbc.mod
new file mode 100644
index 0000000..98e5754
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/jdbc.mod
@@ -0,0 +1,4 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[jpms]
+add-modules: java.sql
diff --git a/third_party/jetty-server/src/main/config/modules/jvm.mod b/third_party/jetty-server/src/main/config/modules/jvm.mod
new file mode 100644
index 0000000..04a5285
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/jvm.mod
@@ -0,0 +1,28 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+A noop module that creates an ini template useful for
+setting JVM arguments (eg -Xmx )
+
+[ini-template]
+## JVM Configuration
+## If JVM args are include in an ini file then --exec is needed
+## to start a new JVM from start.jar with the extra args.
+##
+## If you wish to avoid an extra JVM running, place JVM args
+## on the normal command line and do not use --exec
+# --exec
+# -Xmx2000m
+# -Xmn512m
+# -XX:+UseConcMarkSweepGC
+# -XX:ParallelCMSThreads=2
+# -XX:+CMSClassUnloadingEnabled
+# -XX:+UseCMSCompactAtFullCollection
+# -XX:CMSInitiatingOccupancyFraction=80
+# -internal:gc
+# -XX:+PrintGCDateStamps
+# -XX:+PrintGCTimeStamps
+# -XX:+PrintGCDetails
+# -XX:+PrintTenuringDistribution
+# -XX:+PrintCommandLineFlags
+# -XX:+DisableExplicitGC
diff --git a/third_party/jetty-server/src/main/config/modules/logback-access.mod b/third_party/jetty-server/src/main/config/modules/logback-access.mod
new file mode 100644
index 0000000..6c823a6
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/logback-access.mod
@@ -0,0 +1,30 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables logback request log.
+
+[tags]
+requestlog
+logging
+logback
+
+[depend]
+server
+logback-impl
+resources
+
+[provide]
+requestlog
+
+[xml]
+etc/jetty-logback-access.xml
+
+[files]
+logs/
+basehome:modules/logback-access/jetty-logback-access.xml|etc/jetty-logback-access.xml
+basehome:modules/logback-access/logback-access.xml|resources/logback-access.xml
+maven://ch.qos.logback/logback-access/${logback.version}|lib/logback/logback-access-${logback.version}.jar
+
+[lib]
+lib/logback/logback-access-${logback.version}.jar
+
diff --git a/third_party/jetty-server/src/main/config/modules/logback-access/jetty-logback-access.xml b/third_party/jetty-server/src/main/config/modules/logback-access/jetty-logback-access.xml
new file mode 100644
index 0000000..cec331b
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/logback-access/jetty-logback-access.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<!-- =============================================================== -->
+<!-- Configure the Logback Request Log -->
+<!-- =============================================================== -->
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <Set name="RequestLog">
+ <New id="RequestLog" class="ch.qos.logback.access.jetty.RequestLogImpl">
+ <Set name="name">logback-access</Set>
+ <Set name="resource">/logback-access.xml</Set>
+ <Call name="start"/>
+ </New>
+ </Set>
+</Configure>
diff --git a/third_party/jetty-server/src/main/config/modules/logback-access/logback-access.xml b/third_party/jetty-server/src/main/config/modules/logback-access/logback-access.xml
new file mode 100644
index 0000000..72e73b2
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/logback-access/logback-access.xml
@@ -0,0 +1,17 @@
+<configuration>
+ <!-- always a good activate OnConsoleStatusListener -->
+ <statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
+
+ <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <file>logs/access.log</file>
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <fileNamePattern>logs/access.%d{yyyy-MM-dd}.log.zip</fileNamePattern>
+ </rollingPolicy>
+ <encoder>
+ <pattern>combined</pattern>
+ </encoder>
+ </appender>
+
+ <appender-ref ref="FILE" />
+</configuration>
+
diff --git a/third_party/jetty-server/src/main/config/modules/lowresources.mod b/third_party/jetty-server/src/main/config/modules/lowresources.mod
new file mode 100644
index 0000000..5767760
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/lowresources.mod
@@ -0,0 +1,34 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables a low resource monitor on the server
+that can take actions if threads and/or connections
+cross configured threshholds.
+
+[depend]
+server
+
+[xml]
+etc/jetty-lowresources.xml
+
+[ini-template]
+## Scan period to look for low resources (in milliseconds)
+# jetty.lowresources.period=1000
+
+## The idle timeout to apply to low resources (in milliseconds)
+# jetty.lowresources.idleTimeout=1000
+
+## Whether to monitor ThreadPool threads for low resources
+# jetty.lowresources.monitorThreads=true
+
+## Max number of connections allowed before being in low resources mode
+# jetty.lowresources.maxConnections=0
+
+## Max memory allowed before being in low resources mode (in bytes)
+# jetty.lowresources.maxMemory=0
+
+## Max time a resource may stay in low resource mode before actions are taken (in milliseconds)
+# jetty.lowresources.maxLowResourcesTime=5000
+
+## Accept new connections while in low resources
+# jetty.lowresources.accepting=true
diff --git a/third_party/jetty-server/src/main/config/modules/proxy-protocol-ssl.mod b/third_party/jetty-server/src/main/config/modules/proxy-protocol-ssl.mod
new file mode 100644
index 0000000..1919f83
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/proxy-protocol-ssl.mod
@@ -0,0 +1,18 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables the Proxy Protocol on the TLS(SSL) Connector.
+http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt
+This allows a Proxy operating in TCP mode to transport
+details of the proxied connection to the server.
+Both V1 and V2 versions of the protocol are supported.
+
+[tags]
+connector
+ssl
+
+[depend]
+ssl
+
+[xml]
+etc/jetty-proxy-protocol-ssl.xml
diff --git a/third_party/jetty-server/src/main/config/modules/proxy-protocol.mod b/third_party/jetty-server/src/main/config/modules/proxy-protocol.mod
new file mode 100644
index 0000000..7ee3a63
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/proxy-protocol.mod
@@ -0,0 +1,15 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables the Proxy Protocol on the HTTP Connector.
+http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt
+This allows a proxy operating in TCP mode to
+transport details of the proxied connection to
+the server.
+Both V1 and V2 versions of the protocol are supported.
+
+[depend]
+http
+
+[xml]
+etc/jetty-proxy-protocol.xml
diff --git a/third_party/jetty-server/src/main/config/modules/requestlog.mod b/third_party/jetty-server/src/main/config/modules/requestlog.mod
new file mode 100644
index 0000000..cc13773
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/requestlog.mod
@@ -0,0 +1,44 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables a NCSA style request log.
+
+[tags]
+requestlog
+
+[depend]
+server
+
+[xml]
+etc/jetty-requestlog.xml
+
+[files]
+logs/
+
+[ini-template]
+## Logging directory (relative to $jetty.base)
+# jetty.requestlog.dir=logs
+
+## File path
+# jetty.requestlog.filePath=${jetty.requestlog.dir}/yyyy_mm_dd.request.log
+
+## Date format for rollovered files (uses SimpleDateFormat syntax)
+# jetty.requestlog.filenameDateFormat=yyyy_MM_dd
+
+## How many days to retain old log files
+# jetty.requestlog.retainDays=90
+
+## Whether to append to existing file
+# jetty.requestlog.append=false
+
+## Whether to use the extended log output
+# jetty.requestlog.extended=true
+
+## Whether to log http cookie information
+# jetty.requestlog.cookies=true
+
+## Timezone of the log entries
+# jetty.requestlog.timezone=GMT
+
+## Whether to log LogLatency
+# jetty.requestlog.loglatency=false
diff --git a/third_party/jetty-server/src/main/config/modules/resources.mod b/third_party/jetty-server/src/main/config/modules/resources.mod
new file mode 100644
index 0000000..0a762a1
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/resources.mod
@@ -0,0 +1,16 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Adds the $JETTY_HOME/resources and/or $JETTY_BASE/resources
+directory to the server classpath. Useful for configuration
+property files (eg jetty-logging.properties)
+
+[tags]
+classpath
+
+[lib]
+resources/
+
+[files]
+resources/
+
diff --git a/third_party/jetty-server/src/main/config/modules/server.mod b/third_party/jetty-server/src/main/config/modules/server.mod
new file mode 100644
index 0000000..11fa3ba
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/server.mod
@@ -0,0 +1,94 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables the core Jetty server on the classpath.
+
+[optional]
+jvm
+ext
+resources
+logging
+
+[depend]
+threadpool
+bytebufferpool
+
+[lib]
+lib/servlet-api-3.1.jar
+lib/jetty-schemas-3.1.jar
+lib/jetty-http-${jetty.version}.jar
+lib/jetty-server-${jetty.version}.jar
+lib/jetty-xml-${jetty.version}.jar
+lib/jetty-util-${jetty.version}.jar
+lib/jetty-io-${jetty.version}.jar
+
+[xml]
+etc/jetty.xml
+
+[jpms]
+patch-module: servlet.api=lib/jetty-schemas-3.1.jar
+
+[ini-template]
+### Common HTTP configuration
+## Scheme to use to build URIs for secure redirects
+# jetty.httpConfig.secureScheme=https
+
+## Port to use to build URIs for secure redirects
+# jetty.httpConfig.securePort=8443
+
+## Response content buffer size (in bytes)
+# jetty.httpConfig.outputBufferSize=32768
+
+## Max response content write length that is buffered (in bytes)
+# jetty.httpConfig.outputAggregationSize=8192
+
+## Max request headers size (in bytes)
+# jetty.httpConfig.requestHeaderSize=8192
+
+## Max response headers size (in bytes)
+# jetty.httpConfig.responseHeaderSize=8192
+
+## Whether to send the Server: header
+# jetty.httpConfig.sendServerVersion=true
+
+## Whether to send the Date: header
+# jetty.httpConfig.sendDateHeader=false
+
+## Max per-connection header cache size (in nodes)
+# jetty.httpConfig.headerCacheSize=1024
+
+## Whether, for requests with content, delay dispatch until some content has arrived
+# jetty.httpConfig.delayDispatchUntilContent=true
+
+## Maximum number of error dispatches to prevent looping
+# jetty.httpConfig.maxErrorDispatches=10
+
+## Cookie compliance mode for parsing request Cookie headers: RFC2965, RFC6265
+# jetty.httpConfig.requestCookieCompliance=RFC6265
+
+## Cookie compliance mode for generating response Set-Cookie: RFC2965, RFC6265
+# jetty.httpConfig.responseCookieCompliance=RFC6265
+
+## multipart/form-data compliance mode of: LEGACY(slow), RFC7578(fast)
+# jetty.httpConfig.multiPartFormDataCompliance=LEGACY
+
+## Relative Redirect Locations allowed
+# jetty.httpConfig.relativeRedirectAllowed=false
+
+### Server configuration
+## Whether ctrl+c on the console gracefully stops the Jetty server
+# jetty.server.stopAtShutdown=true
+
+## Timeout in ms to apply when stopping the server gracefully
+# jetty.server.stopTimeout=5000
+
+## Dump the state of the Jetty server, components, and webapps after startup
+# jetty.server.dumpAfterStart=false
+
+## Dump the state of the Jetty server, components, and webapps before shutdown
+# jetty.server.dumpBeforeStop=false
+
+## Scheduler Configuration
+# jetty.scheduler.name=
+# jetty.scheduler.deamon=false
+# jetty.scheduler.threads=-1
diff --git a/third_party/jetty-server/src/main/config/modules/session-cache-hash.mod b/third_party/jetty-server/src/main/config/modules/session-cache-hash.mod
new file mode 100644
index 0000000..a420d69
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/session-cache-hash.mod
@@ -0,0 +1,26 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enable first level session cache. If this module is not enabled, sessions will
+use the DefaultSessionCache by default, so enabling via this module is only needed
+if the configuration properties need to be changed from their defaults.
+
+[tags]
+session
+
+[provides]
+session-cache
+
+[depends]
+sessions
+
+[xml]
+etc/sessions/session-cache-hash.xml
+
+[ini-template]
+#jetty.session.evictionPolicy=-1
+#jetty.session.saveOnInactiveEvict=false
+#jetty.session.saveOnCreate=false
+#jetty.session.removeUnloadableSessions=false
+#jetty.session.flushOnResponseCommit=false
+#jetty.session.invalidateOnShutdown=false
diff --git a/third_party/jetty-server/src/main/config/modules/session-cache-null.mod b/third_party/jetty-server/src/main/config/modules/session-cache-null.mod
new file mode 100644
index 0000000..c556dbf
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/session-cache-null.mod
@@ -0,0 +1,21 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+A trivial SessionCache that does not actually cache sessions.
+
+[tags]
+session
+
+[provides]
+session-cache
+
+[depends]
+sessions
+
+[xml]
+etc/sessions/session-cache-null.xml
+
+[ini-template]
+#jetty.session.saveOnCreate=false
+#jetty.session.removeUnloadableSessions=false
+#jetty.session.flushOnResponseCommit=false
\ No newline at end of file
diff --git a/third_party/jetty-server/src/main/config/modules/session-store-cache.mod b/third_party/jetty-server/src/main/config/modules/session-store-cache.mod
new file mode 100644
index 0000000..8baf625
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/session-store-cache.mod
@@ -0,0 +1,29 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables caching of SessionData in front of a SessionDataStore.
+
+[tags]
+session
+
+
+[depend]
+session-store
+sessions/session-data-cache/${session-data-cache}
+
+
+[xml]
+etc/sessions/session-data-cache/session-caching-store.xml
+
+
+[ini]
+session-data-cache?=xmemcached
+
+[ini-template]
+
+## Session Data Cache type: xmemcached
+session-data-cache=xmemcached
+#jetty.session.memcached.host=localhost
+#jetty.session.memcached.port=11211
+#jetty.session.memcached.expirySec=
+#jetty.session.memcached.heartbeats=true
diff --git a/third_party/jetty-server/src/main/config/modules/session-store-file.mod b/third_party/jetty-server/src/main/config/modules/session-store-file.mod
new file mode 100644
index 0000000..0be0571
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/session-store-file.mod
@@ -0,0 +1,24 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables session persistent storage in files.
+
+[tags]
+session
+
+[provides]
+session-store
+
+[depends]
+sessions
+
+[xml]
+etc/sessions/file/session-store.xml
+
+[files]
+sessions/
+
+[ini-template]
+jetty.session.file.storeDir=${jetty.base}/sessions
+#jetty.session.file.deleteUnrestorableFiles=false
+#jetty.session.savePeriod.seconds=0
\ No newline at end of file
diff --git a/third_party/jetty-server/src/main/config/modules/session-store-jdbc.mod b/third_party/jetty-server/src/main/config/modules/session-store-jdbc.mod
new file mode 100644
index 0000000..2982b7a
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/session-store-jdbc.mod
@@ -0,0 +1,68 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables JDBC persistent/distributed session storage.
+
+[tags]
+session
+
+[provides]
+session-store
+
+[depend]
+jdbc
+sessions
+sessions/jdbc/${db-connection-type}
+
+[xml]
+etc/sessions/jdbc/session-store.xml
+
+[ini]
+db-connection-type?=datasource
+
+[ini-template]
+##
+##JDBC Session properties
+##
+
+#jetty.session.gracePeriod.seconds=3600
+#jetty.session.savePeriod.seconds=0
+
+#jetty.session.jdbc.blobType=
+#jetty.session.jdbc.longType=
+#jetty.session.jdbc.stringType=
+
+## Connection type:Datasource
+db-connection-type=datasource
+#jetty.session.jdbc.datasourceName=/jdbc/sessions
+
+## Connection type:driver
+#db-connection-type=driver
+#jetty.session.jdbc.driverClass=
+#jetty.session.jdbc.driverUrl=
+
+## Session table schema
+#jetty.session.jdbc.schema.accessTimeColumn=accessTime
+#jetty.session.jdbc.schema.contextPathColumn=contextPath
+#jetty.session.jdbc.schema.cookieTimeColumn=cookieTime
+#jetty.session.jdbc.schema.createTimeColumn=createTime
+#jetty.session.jdbc.schema.expiryTimeColumn=expiryTime
+#jetty.session.jdbc.schema.lastAccessTimeColumn=lastAccessTime
+#jetty.session.jdbc.schema.lastSavedTimeColumn=lastSavedTime
+#jetty.session.jdbc.schema.idColumn=sessionId
+#jetty.session.jdbc.schema.lastNodeColumn=lastNode
+#jetty.session.jdbc.schema.virtualHostColumn=virtualHost
+#jetty.session.jdbc.schema.maxIntervalColumn=maxInterval
+#jetty.session.jdbc.schema.mapColumn=map
+#jetty.session.jdbc.schema.table=JettySessions
+# Optional name of the schema used to identify where the session table is defined in the database:
+# "" - empty string, no schema name
+# "INFERRED" - special string meaning infer from the current db connection
+# name - a string defined by the user
+#jetty.session.jdbc.schema.schemaName
+# Optional name of the catalog used to identify where the session table is defined in the database:
+# "" - empty string, no catalog name
+# "INFERRED" - special string meaning infer from the current db connection
+# name - a string defined by the user
+#jetty.session.jdbc.schema.catalogName
+
diff --git a/third_party/jetty-server/src/main/config/modules/sessions.mod b/third_party/jetty-server/src/main/config/modules/sessions.mod
new file mode 100644
index 0000000..ef7b38d
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/sessions.mod
@@ -0,0 +1,24 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+The session management. By enabling this module, it allows
+session management to be configured via the ini templates
+created or by enabling other session-cache or session-store
+modules. Without this module enabled, the server may still
+use sessions, but their management cannot be configured.
+
+[tags]
+session
+
+[depends]
+server
+
+[xml]
+etc/sessions/id-manager.xml
+
+[ini-template]
+## The name to uniquely identify this server instance
+#jetty.sessionIdManager.workerName=node1
+
+## Period between runs of the session scavenger (in seconds)
+#jetty.sessionScavengeInterval.seconds=600
diff --git a/third_party/jetty-server/src/main/config/modules/sessions/jdbc/datasource.mod b/third_party/jetty-server/src/main/config/modules/sessions/jdbc/datasource.mod
new file mode 100644
index 0000000..c5edc43
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/sessions/jdbc/datasource.mod
@@ -0,0 +1,10 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+JDBC Datasource connections for session storage
+
+[depends]
+jdbc
+
+[xml]
+etc/sessions/jdbc/datasource.xml
diff --git a/third_party/jetty-server/src/main/config/modules/sessions/jdbc/driver.mod b/third_party/jetty-server/src/main/config/modules/sessions/jdbc/driver.mod
new file mode 100644
index 0000000..6c42abf
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/sessions/jdbc/driver.mod
@@ -0,0 +1,10 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+JDBC Driver connections for session storage
+
+[depend]
+jdbc
+
+[xml]
+etc/sessions/jdbc/driver.xml
diff --git a/third_party/jetty-server/src/main/config/modules/ssl-reload.mod b/third_party/jetty-server/src/main/config/modules/ssl-reload.mod
new file mode 100644
index 0000000..acddb16
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/ssl-reload.mod
@@ -0,0 +1,18 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables the SSL keystore to be reloaded after any changes are detected on the file system.
+
+[tags]
+connector
+ssl
+
+[depend]
+ssl
+
+[xml]
+etc/jetty-ssl-context-reload.xml
+
+[ini-template]
+# Monitored directory scan period (seconds)
+# jetty.sslContext.reload.scanInterval=1
\ No newline at end of file
diff --git a/third_party/jetty-server/src/main/config/modules/ssl.mod b/third_party/jetty-server/src/main/config/modules/ssl.mod
new file mode 100644
index 0000000..87c9723
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/ssl.mod
@@ -0,0 +1,137 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables a TLS(SSL) Connector on the server.
+This may be used for HTTPS and/or HTTP2 by enabling
+the associated support modules.
+
+[tags]
+connector
+ssl
+
+[depend]
+server
+
+[xml]
+etc/jetty-ssl.xml
+etc/jetty-ssl-context.xml
+
+[files]
+basehome:modules/ssl/keystore|etc/keystore
+
+[ini-template]
+### TLS(SSL) Connector Configuration
+
+## Connector host/address to bind to
+# jetty.ssl.host=0.0.0.0
+
+## Connector port to listen on
+# jetty.ssl.port=8443
+
+## Connector idle timeout in milliseconds
+# jetty.ssl.idleTimeout=30000
+
+## Number of acceptors (-1 picks default based on number of cores)
+# jetty.ssl.acceptors=-1
+
+## Number of selectors (-1 picks default based on number of cores)
+# jetty.ssl.selectors=-1
+
+## ServerSocketChannel backlog (0 picks platform default)
+# jetty.ssl.acceptQueueSize=0
+
+## Thread priority delta to give to acceptor threads
+# jetty.ssl.acceptorPriorityDelta=0
+
+## The requested maximum length of the queue of incoming connections.
+# jetty.ssl.acceptQueueSize=0
+
+## Enable/disable the SO_REUSEADDR socket option.
+# jetty.ssl.reuseAddress=true
+
+## Enable/disable TCP_NODELAY on accepted sockets.
+# jetty.ssl.acceptedTcpNoDelay=true
+
+## The SO_RCVBUF option to set on accepted sockets. A value of -1 indicates that it is left to its default value.
+# jetty.ssl.acceptedReceiveBufferSize=-1
+
+## The SO_SNDBUF option to set on accepted sockets. A value of -1 indicates that it is left to its default value.
+# jetty.ssl.acceptedSendBufferSize=-1
+
+## Connect Timeout in milliseconds
+# jetty.ssl.connectTimeout=15000
+
+## Whether SNI is required for all secure connections. Rejections are in TLS handshakes.
+# jetty.sslContext.sniRequired=false
+
+## Whether SNI is required for all secure connections. Rejections are in HTTP 400 response.
+# jetty.ssl.sniRequired=false
+
+## Whether request host names are checked to match any SNI names
+# jetty.ssl.sniHostCheck=true
+
+## max age in seconds for a Strict-Transport-Security response header (default -1)
+# jetty.ssl.stsMaxAgeSeconds=31536000
+
+## include subdomain property in any Strict-Transport-Security header (default false)
+# jetty.ssl.stsIncludeSubdomains=true
+
+### SslContextFactory Configuration
+## Note that OBF passwords are not secure, just protected from casual observation
+## See https://eclipse.org/jetty/documentation/current/configuring-security-secure-passwords.html
+
+## The Endpoint Identification Algorithm
+## Same as javax.net.ssl.SSLParameters#setEndpointIdentificationAlgorithm(String)
+#jetty.sslContext.endpointIdentificationAlgorithm=
+
+## SSL JSSE Provider
+# jetty.sslContext.provider=
+
+## KeyStore file path (relative to $jetty.base)
+# jetty.sslContext.keyStorePath=etc/keystore
+## KeyStore absolute file path
+# jetty.sslContext.keyStoreAbsolutePath=${jetty.base}/etc/keystore
+
+## TrustStore file path (relative to $jetty.base)
+# jetty.sslContext.trustStorePath=etc/keystore
+## TrustStore absolute file path
+# jetty.sslContext.trustStoreAbsolutePath=${jetty.base}/etc/keystore
+
+## KeyStore password
+# jetty.sslContext.keyStorePassword=OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4
+
+## KeyStore type and provider
+# jetty.sslContext.keyStoreType=JKS
+# jetty.sslContext.keyStoreProvider=
+
+## KeyManager password
+# jetty.sslContext.keyManagerPassword=OBF:1u2u1wml1z7s1z7a1wnl1u2g
+
+## TrustStore password
+# jetty.sslContext.trustStorePassword=OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4
+
+## TrustStore type and provider
+# jetty.sslContext.trustStoreType=JKS
+# jetty.sslContext.trustStoreProvider=
+
+## whether client certificate authentication is required
+# jetty.sslContext.needClientAuth=false
+
+## Whether client certificate authentication is desired
+# jetty.sslContext.wantClientAuth=false
+
+## Whether cipher order is significant (since java 8 only)
+# jetty.sslContext.useCipherSuitesOrder=true
+
+## To configure Includes / Excludes for Cipher Suites or Protocols see tweak-ssl.xml example at
+## https://www.eclipse.org/jetty/documentation/current/configuring-ssl.html#configuring-sslcontextfactory-cipherSuites
+
+## Set the size of the SslSession cache
+# jetty.sslContext.sslSessionCacheSize=-1
+
+## Set the timeout (in seconds) of the SslSession cache timeout
+# jetty.sslContext.sslSessionTimeout=-1
+
+## Allow SSL renegotiation
+# jetty.sslContext.renegotiationAllowed=true
+# jetty.sslContext.renegotiationLimit=5
diff --git a/third_party/jetty-server/src/main/config/modules/ssl/keystore b/third_party/jetty-server/src/main/config/modules/ssl/keystore
new file mode 100644
index 0000000..d6592f9
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/ssl/keystore
Binary files differ
diff --git a/third_party/jetty-server/src/main/config/modules/stats.mod b/third_party/jetty-server/src/main/config/modules/stats.mod
new file mode 100644
index 0000000..7ecdb25
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/stats.mod
@@ -0,0 +1,26 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enable detailed statistics collection for the server,
+available via JMX.
+
+[tags]
+handler
+
+[depend]
+server
+servlet
+
+[lib]
+lib/jetty-util-ajax-${jetty.version}.jar
+
+[xml]
+etc/jetty-stats.xml
+
+[ini]
+jetty.webapp.addServerClasses+=,-org.eclipse.jetty.servlet.StatisticsServlet
+
+[ini-template]
+
+## If the Graceful shutdown should wait for async requests as well as the currently dispatched ones.
+# jetty.statistics.gracefulShutdownWaitsForRequests=true
diff --git a/third_party/jetty-server/src/main/config/modules/threadlimit.mod b/third_party/jetty-server/src/main/config/modules/threadlimit.mod
new file mode 100644
index 0000000..247d834
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/threadlimit.mod
@@ -0,0 +1,25 @@
+#
+# Thread Limit module
+# Applies ThreadLimiteHandler to entire server
+#
+
+[tags]
+handler
+
+[depend]
+server
+
+[xml]
+etc/jetty-threadlimit.xml
+
+[ini-template]
+## Select style of proxy forwarded header
+#jetty.threadlimit.forwardedHeader=X-Forwarded-For
+#jetty.threadlimit.forwardedHeader=Forwarded
+
+## Enabled by default?
+#jetty.threadlimit.enabled=true
+
+## Thread limit per remote IP
+#jetty.threadlimit.threadLimit=10
+
diff --git a/third_party/jetty-server/src/main/config/modules/threadpool.mod b/third_party/jetty-server/src/main/config/modules/threadpool.mod
new file mode 100644
index 0000000..c8be7d6
--- /dev/null
+++ b/third_party/jetty-server/src/main/config/modules/threadpool.mod
@@ -0,0 +1,25 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Enables the Server thread pool.
+
+[xml]
+etc/jetty-threadpool.xml
+
+[ini-template]
+
+### Server Thread Pool Configuration
+## Minimum Number of Threads
+#jetty.threadPool.minThreads=10
+
+## Maximum Number of Threads
+#jetty.threadPool.maxThreads=200
+
+## Number of reserved threads (-1 for heuristic)
+#jetty.threadPool.reservedThreads=-1
+
+## Thread Idle Timeout (in milliseconds)
+#jetty.threadPool.idleTimeout=60000
+
+## Whether to Output a Detailed Dump
+#jetty.threadPool.detailedDump=false
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnectionFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnectionFactory.java
new file mode 100644
index 0000000..d5cf3b7
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnectionFactory.java
@@ -0,0 +1,157 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.jetty.io.AbstractConnection;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.ArrayUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+/**
+ * <p>Provides the common handling for {@link ConnectionFactory} implementations including:</p>
+ * <ul>
+ * <li>Protocol identification</li>
+ * <li>Configuration of new Connections:
+ * <ul>
+ * <li>Setting inputbuffer size</li>
+ * <li>Calling {@link Connection#addListener(Connection.Listener)} for all
+ * Connection.Listener instances found as beans on the {@link Connector}
+ * and this {@link ConnectionFactory}</li>
+ * </ul>
+ * </ul>
+ */
+@ManagedObject
+public abstract class AbstractConnectionFactory extends ContainerLifeCycle implements ConnectionFactory
+{
+ private final String _protocol;
+ private final List<String> _protocols;
+ private int _inputbufferSize = 8192;
+
+ protected AbstractConnectionFactory(String protocol)
+ {
+ _protocol = protocol;
+ _protocols = Collections.unmodifiableList(Arrays.asList(protocol));
+ }
+
+ protected AbstractConnectionFactory(String... protocols)
+ {
+ _protocol = protocols[0];
+ _protocols = Collections.unmodifiableList(Arrays.asList(protocols));
+ }
+
+ @Override
+ @ManagedAttribute(value = "The protocol name", readonly = true)
+ public String getProtocol()
+ {
+ return _protocol;
+ }
+
+ @Override
+ public List<String> getProtocols()
+ {
+ return _protocols;
+ }
+
+ @ManagedAttribute("The buffer size used to read from the network")
+ public int getInputBufferSize()
+ {
+ return _inputbufferSize;
+ }
+
+ public void setInputBufferSize(int size)
+ {
+ _inputbufferSize = size;
+ }
+
+ protected String findNextProtocol(Connector connector)
+ {
+ return findNextProtocol(connector, getProtocol());
+ }
+
+ protected static String findNextProtocol(Connector connector, String currentProtocol)
+ {
+ String nextProtocol = null;
+ for (Iterator<String> it = connector.getProtocols().iterator(); it.hasNext(); )
+ {
+ String protocol = it.next();
+ if (currentProtocol.equalsIgnoreCase(protocol))
+ {
+ nextProtocol = it.hasNext() ? it.next() : null;
+ break;
+ }
+ }
+ return nextProtocol;
+ }
+
+ protected AbstractConnection configure(AbstractConnection connection, Connector connector, EndPoint endPoint)
+ {
+ connection.setInputBufferSize(getInputBufferSize());
+
+ // Add Connection.Listeners from Connector
+ if (connector instanceof ContainerLifeCycle)
+ {
+ ContainerLifeCycle aggregate = (ContainerLifeCycle)connector;
+ for (Connection.Listener listener : aggregate.getBeans(Connection.Listener.class))
+ {
+ connection.addListener(listener);
+ }
+ }
+ // Add Connection.Listeners from this factory
+ for (Connection.Listener listener : getBeans(Connection.Listener.class))
+ {
+ connection.addListener(listener);
+ }
+
+ return connection;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x%s", this.getClass().getSimpleName(), hashCode(), getProtocols());
+ }
+
+ public static ConnectionFactory[] getFactories(SslContextFactory sslContextFactory, ConnectionFactory... factories)
+ {
+ factories = ArrayUtil.removeNulls(factories);
+
+ if (sslContextFactory == null)
+ return factories;
+
+ for (ConnectionFactory factory : factories)
+ {
+ if (factory instanceof HttpConfiguration.ConnectionFactory)
+ {
+ HttpConfiguration config = ((HttpConfiguration.ConnectionFactory)factory).getHttpConfiguration();
+ if (config.getCustomizer(SecureRequestCustomizer.class) == null)
+ config.addCustomizer(new SecureRequestCustomizer());
+ }
+ }
+ return ArrayUtil.prependToArray(new SslConnectionFactory(sslContextFactory, factories[0].getProtocol()), factories, ConnectionFactory.class);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java
new file mode 100644
index 0000000..28353a0
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java
@@ -0,0 +1,785 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedByInterruptException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.stream.Collectors;
+
+import org.eclipse.jetty.io.ArrayByteBufferPool;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.ssl.SslConnection;
+import org.eclipse.jetty.util.ProcessorUtils;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.Container;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.component.Graceful;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.Locker;
+import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.eclipse.jetty.util.thread.ThreadPoolBudget;
+
+/**
+ * <p>An abstract implementation of {@link Connector} that provides a {@link ConnectionFactory} mechanism
+ * for creating {@link org.eclipse.jetty.io.Connection} instances for various protocols (HTTP, SSL, etc).</p>
+ *
+ * <h2>Connector Services</h2>
+ * The abstract connector manages the dependent services needed by all specific connector instances:
+ * <ul>
+ * <li>The {@link Executor} service is used to run all active tasks needed by this connector such as accepting connections
+ * or handle HTTP requests. The default is to use the {@link Server#getThreadPool()} as an executor.
+ * </li>
+ * <li>The {@link Scheduler} service is used to monitor the idle timeouts of all connections and is also made available
+ * to the connections to time such things as asynchronous request timeouts. The default is to use a new
+ * {@link ScheduledExecutorScheduler} instance.
+ * </li>
+ * <li>The {@link ByteBufferPool} service is made available to all connections to be used to acquire and release
+ * {@link ByteBuffer} instances from a pool. The default is to use a new {@link ArrayByteBufferPool} instance.
+ * </li>
+ * </ul>
+ * These services are managed as aggregate beans by the {@link ContainerLifeCycle} super class and
+ * may either be managed or unmanaged beans.
+ *
+ * <h2>Connection Factories</h2>
+ * The connector keeps a collection of {@link ConnectionFactory} instances, each of which are known by their
+ * protocol name. The protocol name may be a real protocol (e.g. "http/1.1" or "h2") or it may be a private name
+ * that represents a special connection factory. For example, the name "SSL-http/1.1" is used for
+ * an {@link SslConnectionFactory} that has been instantiated with the {@link HttpConnectionFactory} as it's
+ * next protocol.
+ *
+ * <h2>Configuring Connection Factories</h2>
+ * The collection of available {@link ConnectionFactory} may be constructor injected or modified with the
+ * methods {@link #addConnectionFactory(ConnectionFactory)}, {@link #removeConnectionFactory(String)} and
+ * {@link #setConnectionFactories(Collection)}. Only a single {@link ConnectionFactory} instance may be configured
+ * per protocol name, so if two factories with the same {@link ConnectionFactory#getProtocol()} are set, then
+ * the second will replace the first.
+ * <p>
+ * The protocol factory used for newly accepted connections is specified by
+ * the method {@link #setDefaultProtocol(String)} or defaults to the protocol of the first configured factory.
+ * <p>
+ * Each Connection factory type is responsible for the configuration of the protocols that it accepts. Thus to
+ * configure the HTTP protocol, you pass a {@link HttpConfiguration} instance to the {@link HttpConnectionFactory}
+ * (or other factories that can also provide HTTP Semantics). Similarly the {@link SslConnectionFactory} is
+ * configured by passing it a {@link SslContextFactory} and a next protocol name.
+ *
+ * <h2>Connection Factory Operation</h2>
+ * {@link ConnectionFactory}s may simply create a {@link org.eclipse.jetty.io.Connection} instance to support a specific
+ * protocol. For example, the {@link HttpConnectionFactory} will create a {@link HttpConnection} instance
+ * that can handle http/1.1, http/1.0 and http/0.9.
+ * <p>
+ * {@link ConnectionFactory}s may also create a chain of {@link org.eclipse.jetty.io.Connection} instances, using other {@link ConnectionFactory} instances.
+ * For example, the {@link SslConnectionFactory} is configured with a next protocol name, so that once it has accepted
+ * a connection and created an {@link SslConnection}, it then used the next {@link ConnectionFactory} from the
+ * connector using the {@link #getConnectionFactory(String)} method, to create a {@link org.eclipse.jetty.io.Connection} instance that
+ * will handle the unencrypted bytes from the {@link SslConnection}. If the next protocol is "http/1.1", then the
+ * {@link SslConnectionFactory} will have a protocol name of "SSL-http/1.1" and lookup "http/1.1" for the protocol
+ * to run over the SSL connection.
+ * <p>
+ * {@link ConnectionFactory}s may also create temporary {@link org.eclipse.jetty.io.Connection} instances that will exchange bytes
+ * over the connection to determine what is the next protocol to use. For example the ALPN protocol is an extension
+ * of SSL to allow a protocol to be specified during the SSL handshake. ALPN is used by the HTTP/2 protocol to
+ * negotiate the protocol that the client and server will speak. Thus to accept an HTTP/2 connection, the
+ * connector will be configured with {@link ConnectionFactory}s for "SSL-ALPN", "h2", "http/1.1"
+ * with the default protocol being "SSL-ALPN". Thus a newly accepted connection uses "SSL-ALPN", which specifies a
+ * SSLConnectionFactory with "ALPN" as the next protocol. Thus an SSL connection instance is created chained to an ALPN
+ * connection instance. The ALPN connection then negotiates with the client to determined the next protocol, which
+ * could be "h2" or the default of "http/1.1". Once the next protocol is determined, the ALPN connection
+ * calls {@link #getConnectionFactory(String)} to create a connection instance that will replace the ALPN connection as
+ * the connection chained to the SSL connection.
+ * <h2>Acceptors</h2>
+ * The connector will execute a number of acceptor tasks to the {@link Exception} service passed to the constructor.
+ * The acceptor tasks run in a loop while the connector is running and repeatedly call the abstract {@link #accept(int)} method.
+ * The implementation of the accept method must:
+ * <ol>
+ * <li>block waiting for new connections</li>
+ * <li>accept the connection (eg socket accept)</li>
+ * <li>perform any configuration of the connection (eg. socket configuration)</li>
+ * <li>call the {@link #getDefaultConnectionFactory()} {@link ConnectionFactory#newConnection(Connector, org.eclipse.jetty.io.EndPoint)}
+ * method to create a new Connection instance.</li>
+ * </ol>
+ * The default number of acceptor tasks is the minimum of 1 and the number of available CPUs divided by 8. Having more acceptors may reduce
+ * the latency for servers that see a high rate of new connections (eg HTTP/1.0 without keep-alive). Typically the default is
+ * sufficient for modern persistent protocols (HTTP/1.1, HTTP/2 etc.)
+ */
+@ManagedObject("Abstract implementation of the Connector Interface")
+public abstract class AbstractConnector extends ContainerLifeCycle implements Connector, Dumpable
+{
+ protected static final Logger LOG = Log.getLogger(AbstractConnector.class);
+
+ private final Locker _locker = new Locker();
+ private final Condition _setAccepting = _locker.newCondition();
+ private final Map<String, ConnectionFactory> _factories = new LinkedHashMap<>(); // Order is important on server side, so we use a LinkedHashMap
+ private final Server _server;
+ private final Executor _executor;
+ private final Scheduler _scheduler;
+ private final ByteBufferPool _byteBufferPool;
+ private final Thread[] _acceptors;
+ private final Set<EndPoint> _endpoints = Collections.newSetFromMap(new ConcurrentHashMap<>());
+ private final Set<EndPoint> _immutableEndPoints = Collections.unmodifiableSet(_endpoints);
+ private final Graceful.Shutdown _shutdown = new Graceful.Shutdown();
+ private HttpChannel.Listener _httpChannelListeners = HttpChannel.NOOP_LISTENER;
+ private CountDownLatch _stopping;
+ private long _idleTimeout = 30000;
+ private String _defaultProtocol;
+ private ConnectionFactory _defaultConnectionFactory;
+ private String _name;
+ private int _acceptorPriorityDelta = -2;
+ private boolean _accepting = true;
+ private ThreadPoolBudget.Lease _lease;
+
+ /**
+ * @param server The server this connector will be added to. Must not be null.
+ * @param executor An executor for this connector or null to use the servers executor
+ * @param scheduler A scheduler for this connector or null to either a {@link Scheduler} set as a server bean or if none set, then a new {@link ScheduledExecutorScheduler} instance.
+ * @param pool A buffer pool for this connector or null to either a {@link ByteBufferPool} set as a server bean or none set, the new {@link ArrayByteBufferPool} instance.
+ * @param acceptors the number of acceptor threads to use, or -1 for a default value. If 0, then no acceptor threads will be launched and some other mechanism will need to be used to accept new connections.
+ * @param factories The Connection Factories to use.
+ */
+ public AbstractConnector(
+ Server server,
+ Executor executor,
+ Scheduler scheduler,
+ ByteBufferPool pool,
+ int acceptors,
+ ConnectionFactory... factories)
+ {
+ _server = server;
+ _executor = executor != null ? executor : _server.getThreadPool();
+ if (scheduler == null)
+ scheduler = _server.getBean(Scheduler.class);
+ _scheduler = scheduler != null ? scheduler : new ScheduledExecutorScheduler(String.format("Connector-Scheduler-%x", hashCode()), false);
+ if (pool == null)
+ pool = _server.getBean(ByteBufferPool.class);
+ _byteBufferPool = pool != null ? pool : new ArrayByteBufferPool();
+
+ addEventListener(new Container.Listener()
+ {
+ @Override
+ public void beanAdded(Container parent, Object bean)
+ {
+ if (bean instanceof HttpChannel.Listener)
+ _httpChannelListeners = new HttpChannelListeners(getBeans(HttpChannel.Listener.class));
+ }
+
+ @Override
+ public void beanRemoved(Container parent, Object bean)
+ {
+ if (bean instanceof HttpChannel.Listener)
+ _httpChannelListeners = new HttpChannelListeners(getBeans(HttpChannel.Listener.class));
+ }
+ });
+
+ addBean(_server, false);
+ addBean(_executor);
+ if (executor == null)
+ unmanage(_executor); // inherited from server
+ addBean(_scheduler);
+ addBean(_byteBufferPool);
+
+ for (ConnectionFactory factory : factories)
+ {
+ addConnectionFactory(factory);
+ }
+
+ int cores = ProcessorUtils.availableProcessors();
+ if (acceptors < 0)
+ acceptors = Math.max(1, Math.min(4, cores / 8));
+ if (acceptors > cores)
+ LOG.warn("Acceptors should be <= availableProcessors: " + this);
+ _acceptors = new Thread[acceptors];
+ }
+
+ /**
+ * Get the {@link HttpChannel.Listener}s added to the connector
+ * as a single combined Listener.
+ * This is equivalent to a listener that iterates over the individual
+ * listeners returned from <code>getBeans(HttpChannel.Listener.class);</code>,
+ * except that: <ul>
+ * <li>The result is precomputed, so it is more efficient</li>
+ * <li>The result is ordered by the order added.</li>
+ * <li>The result is immutable.</li>
+ * </ul>
+ * @see #getBeans(Class)
+ * @return An unmodifiable list of EventListener beans
+ */
+ public HttpChannel.Listener getHttpChannelListeners()
+ {
+ return _httpChannelListeners;
+ }
+
+ @Override
+ public Server getServer()
+ {
+ return _server;
+ }
+
+ @Override
+ public Executor getExecutor()
+ {
+ return _executor;
+ }
+
+ @Override
+ public ByteBufferPool getByteBufferPool()
+ {
+ return _byteBufferPool;
+ }
+
+ @Override
+ @ManagedAttribute("The connection idle timeout in milliseconds")
+ public long getIdleTimeout()
+ {
+ return _idleTimeout;
+ }
+
+ /**
+ * <p>Sets the maximum Idle time for a connection, which roughly translates to the {@link Socket#setSoTimeout(int)}
+ * call, although with NIO implementations other mechanisms may be used to implement the timeout.</p>
+ * <p>The max idle time is applied:</p>
+ * <ul>
+ * <li>When waiting for a new message to be received on a connection</li>
+ * <li>When waiting for a new message to be sent on a connection</li>
+ * </ul>
+ * <p>This value is interpreted as the maximum time between some progress being made on the connection.
+ * So if a single byte is read or written, then the timeout is reset.</p>
+ *
+ * @param idleTimeout the idle timeout
+ */
+ public void setIdleTimeout(long idleTimeout)
+ {
+ _idleTimeout = idleTimeout;
+ }
+
+ /**
+ * @return Returns the number of acceptor threads.
+ */
+ @ManagedAttribute("number of acceptor threads")
+ public int getAcceptors()
+ {
+ return _acceptors.length;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ _shutdown.cancel();
+
+ if (_defaultProtocol == null)
+ throw new IllegalStateException("No default protocol for " + this);
+ _defaultConnectionFactory = getConnectionFactory(_defaultProtocol);
+ if (_defaultConnectionFactory == null)
+ throw new IllegalStateException("No protocol factory for default protocol '" + _defaultProtocol + "' in " + this);
+ SslConnectionFactory ssl = getConnectionFactory(SslConnectionFactory.class);
+ if (ssl != null)
+ {
+ String next = ssl.getNextProtocol();
+ ConnectionFactory cf = getConnectionFactory(next);
+ if (cf == null)
+ throw new IllegalStateException("No protocol factory for SSL next protocol: '" + next + "' in " + this);
+ }
+
+ _lease = ThreadPoolBudget.leaseFrom(getExecutor(), this, _acceptors.length);
+ super.doStart();
+
+ _stopping = new CountDownLatch(_acceptors.length);
+ for (int i = 0; i < _acceptors.length; i++)
+ {
+ Acceptor a = new Acceptor(i);
+ addBean(a);
+ getExecutor().execute(a);
+ }
+
+ LOG.info("Started {}", this);
+ }
+
+ protected void interruptAcceptors()
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ for (Thread thread : _acceptors)
+ {
+ if (thread != null)
+ thread.interrupt();
+ }
+ }
+ }
+
+ @Override
+ public Future<Void> shutdown()
+ {
+ return _shutdown.shutdown();
+ }
+
+ @Override
+ public boolean isShutdown()
+ {
+ return _shutdown.isShutdown();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ if (_lease != null)
+ _lease.close();
+
+ // Tell the acceptors we are stopping
+ interruptAcceptors();
+
+ // If we have a stop timeout
+ long stopTimeout = getStopTimeout();
+ CountDownLatch stopping = _stopping;
+ if (stopTimeout > 0 && stopping != null && getAcceptors() > 0)
+ stopping.await(stopTimeout, TimeUnit.MILLISECONDS);
+ _stopping = null;
+
+ super.doStop();
+
+ for (Acceptor a : getBeans(Acceptor.class))
+ {
+ removeBean(a);
+ }
+
+ LOG.info("Stopped {}", this);
+ }
+
+ public void join() throws InterruptedException
+ {
+ join(0);
+ }
+
+ public void join(long timeout) throws InterruptedException
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ for (Thread thread : _acceptors)
+ {
+ if (thread != null)
+ thread.join(timeout);
+ }
+ }
+ }
+
+ protected abstract void accept(int acceptorID) throws IOException, InterruptedException;
+
+ /**
+ * @return Is the connector accepting new connections
+ */
+ public boolean isAccepting()
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ return _accepting;
+ }
+ }
+
+ public void setAccepting(boolean accepting)
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ _accepting = accepting;
+ _setAccepting.signalAll();
+ }
+ }
+
+ @Override
+ public ConnectionFactory getConnectionFactory(String protocol)
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ return _factories.get(StringUtil.asciiToLowerCase(protocol));
+ }
+ }
+
+ @Override
+ public <T> T getConnectionFactory(Class<T> factoryType)
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ for (ConnectionFactory f : _factories.values())
+ {
+ if (factoryType.isAssignableFrom(f.getClass()))
+ return (T)f;
+ }
+ return null;
+ }
+ }
+
+ public void addConnectionFactory(ConnectionFactory factory)
+ {
+ if (isRunning())
+ throw new IllegalStateException(getState());
+
+ Set<ConnectionFactory> toRemove = new HashSet<>();
+ for (String key : factory.getProtocols())
+ {
+ key = StringUtil.asciiToLowerCase(key);
+ ConnectionFactory old = _factories.remove(key);
+ if (old != null)
+ {
+ if (old.getProtocol().equals(_defaultProtocol))
+ _defaultProtocol = null;
+ toRemove.add(old);
+ }
+ _factories.put(key, factory);
+ }
+
+ // keep factories still referenced
+ for (ConnectionFactory f : _factories.values())
+ {
+ toRemove.remove(f);
+ }
+
+ // remove old factories
+ for (ConnectionFactory old : toRemove)
+ {
+ removeBean(old);
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} removed {}", this, old);
+ }
+
+ // add new Bean
+ addBean(factory);
+ if (_defaultProtocol == null)
+ _defaultProtocol = factory.getProtocol();
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} added {}", this, factory);
+ }
+
+ public void addFirstConnectionFactory(ConnectionFactory factory)
+ {
+ if (isRunning())
+ throw new IllegalStateException(getState());
+
+ List<ConnectionFactory> existings = new ArrayList<>(_factories.values());
+ _factories.clear();
+ addConnectionFactory(factory);
+ for (ConnectionFactory existing : existings)
+ {
+ addConnectionFactory(existing);
+ }
+ _defaultProtocol = factory.getProtocol();
+ }
+
+ public void addIfAbsentConnectionFactory(ConnectionFactory factory)
+ {
+ if (isRunning())
+ throw new IllegalStateException(getState());
+
+ String key = StringUtil.asciiToLowerCase(factory.getProtocol());
+ if (_factories.containsKey(key))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} addIfAbsent ignored {}", this, factory);
+ }
+ else
+ {
+ _factories.put(key, factory);
+ addBean(factory);
+ if (_defaultProtocol == null)
+ _defaultProtocol = factory.getProtocol();
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} addIfAbsent added {}", this, factory);
+ }
+ }
+
+ public ConnectionFactory removeConnectionFactory(String protocol)
+ {
+ if (isRunning())
+ throw new IllegalStateException(getState());
+
+ ConnectionFactory factory = _factories.remove(StringUtil.asciiToLowerCase(protocol));
+ removeBean(factory);
+ return factory;
+ }
+
+ @Override
+ public Collection<ConnectionFactory> getConnectionFactories()
+ {
+ return _factories.values();
+ }
+
+ public void setConnectionFactories(Collection<ConnectionFactory> factories)
+ {
+ if (isRunning())
+ throw new IllegalStateException(getState());
+
+ List<ConnectionFactory> existing = new ArrayList<>(_factories.values());
+ for (ConnectionFactory factory : existing)
+ {
+ removeConnectionFactory(factory.getProtocol());
+ }
+ for (ConnectionFactory factory : factories)
+ {
+ if (factory != null)
+ addConnectionFactory(factory);
+ }
+ }
+
+ @ManagedAttribute("The priority delta to apply to acceptor threads")
+ public int getAcceptorPriorityDelta()
+ {
+ return _acceptorPriorityDelta;
+ }
+
+ /**
+ * Set the acceptor thread priority delta.
+ * <p>This allows the acceptor thread to run at a different priority.
+ * Typically this would be used to lower the priority to give preference
+ * to handling previously accepted connections rather than accepting
+ * new connections</p>
+ *
+ * @param acceptorPriorityDelta the acceptor priority delta
+ */
+ public void setAcceptorPriorityDelta(int acceptorPriorityDelta)
+ {
+ int old = _acceptorPriorityDelta;
+ _acceptorPriorityDelta = acceptorPriorityDelta;
+ if (old != acceptorPriorityDelta && isStarted())
+ {
+ for (Thread thread : _acceptors)
+ {
+ thread.setPriority(Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY, thread.getPriority() - old + acceptorPriorityDelta)));
+ }
+ }
+ }
+
+ @Override
+ @ManagedAttribute("Protocols supported by this connector")
+ public List<String> getProtocols()
+ {
+ return new ArrayList<>(_factories.keySet());
+ }
+
+ public void clearConnectionFactories()
+ {
+ if (isRunning())
+ throw new IllegalStateException(getState());
+
+ _factories.clear();
+ }
+
+ @ManagedAttribute("This connector's default protocol")
+ public String getDefaultProtocol()
+ {
+ return _defaultProtocol;
+ }
+
+ public void setDefaultProtocol(String defaultProtocol)
+ {
+ _defaultProtocol = StringUtil.asciiToLowerCase(defaultProtocol);
+ if (isRunning())
+ _defaultConnectionFactory = getConnectionFactory(_defaultProtocol);
+ }
+
+ @Override
+ public ConnectionFactory getDefaultConnectionFactory()
+ {
+ if (isStarted())
+ return _defaultConnectionFactory;
+ return getConnectionFactory(_defaultProtocol);
+ }
+
+ protected boolean handleAcceptFailure(Throwable ex)
+ {
+ if (isRunning())
+ {
+ if (ex instanceof InterruptedException)
+ {
+ LOG.debug(ex);
+ return true;
+ }
+
+ if (ex instanceof ClosedByInterruptException)
+ {
+ LOG.debug(ex);
+ return false;
+ }
+
+ LOG.warn(ex);
+ try
+ {
+ // Arbitrary sleep to avoid spin looping.
+ // Subclasses may decide for a different
+ // sleep policy or closing the connector.
+ Thread.sleep(1000);
+ return true;
+ }
+ catch (Throwable x)
+ {
+ LOG.ignore(x);
+ }
+ return false;
+ }
+ else
+ {
+ LOG.ignore(ex);
+ return false;
+ }
+ }
+
+ private class Acceptor implements Runnable
+ {
+ private final int _id;
+ private String _name;
+
+ private Acceptor(int id)
+ {
+ _id = id;
+ }
+
+ @Override
+ public void run()
+ {
+ final Thread thread = Thread.currentThread();
+ String name = thread.getName();
+ _name = String.format("%s-acceptor-%d@%x-%s", name, _id, hashCode(), AbstractConnector.this.toString());
+ thread.setName(_name);
+
+ int priority = thread.getPriority();
+ if (_acceptorPriorityDelta != 0)
+ thread.setPriority(Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY, priority + _acceptorPriorityDelta)));
+
+ _acceptors[_id] = thread;
+
+ try
+ {
+ while (isRunning())
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ if (!_accepting && isRunning())
+ {
+ _setAccepting.await();
+ continue;
+ }
+ }
+ catch (InterruptedException e)
+ {
+ continue;
+ }
+
+ try
+ {
+ accept(_id);
+ }
+ catch (Throwable x)
+ {
+ if (!handleAcceptFailure(x))
+ break;
+ }
+ }
+ }
+ finally
+ {
+ thread.setName(name);
+ if (_acceptorPriorityDelta != 0)
+ thread.setPriority(priority);
+
+ synchronized (AbstractConnector.this)
+ {
+ _acceptors[_id] = null;
+ }
+ CountDownLatch stopping = _stopping;
+ if (stopping != null)
+ stopping.countDown();
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ String name = _name;
+ if (name == null)
+ return String.format("acceptor-%d@%x", _id, hashCode());
+ return name;
+ }
+ }
+
+ @Override
+ public Collection<EndPoint> getConnectedEndPoints()
+ {
+ return _immutableEndPoints;
+ }
+
+ protected void onEndPointOpened(EndPoint endp)
+ {
+ _endpoints.add(endp);
+ }
+
+ protected void onEndPointClosed(EndPoint endp)
+ {
+ _endpoints.remove(endp);
+ }
+
+ @Override
+ public Scheduler getScheduler()
+ {
+ return _scheduler;
+ }
+
+ @Override
+ public String getName()
+ {
+ return _name;
+ }
+
+ /**
+ * Set a connector name. A context may be configured with
+ * virtual hosts in the form "@contextname" and will only serve
+ * requests from the named connector,
+ *
+ * @param name A connector name.
+ */
+ public void setName(String name)
+ {
+ _name = name;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{%s, %s}",
+ _name == null ? getClass().getSimpleName() : _name,
+ hashCode(),
+ getDefaultProtocol(), getProtocols().stream().collect(Collectors.joining(", ", "(", ")")));
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractNCSARequestLog.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractNCSARequestLog.java
new file mode 100644
index 0000000..49ff305
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractNCSARequestLog.java
@@ -0,0 +1,516 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.util.Locale;
+import javax.servlet.http.Cookie;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.pathmap.PathMappings;
+import org.eclipse.jetty.server.handler.StatisticsHandler;
+import org.eclipse.jetty.util.DateCache;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Base implementation of the {@link RequestLog} outputs logs in the pseudo-standard NCSA common log format.
+ * Configuration options allow a choice between the standard Common Log Format (as used in the 3 log format) and the
+ * Combined Log Format (single log format). This log format can be output by most web servers, and almost all web log
+ * analysis software can understand these formats.
+ *
+ * @deprecated use {@link CustomRequestLog} given format string {@link CustomRequestLog#EXTENDED_NCSA_FORMAT} with a {@link RequestLog.Writer}
+ */
+@Deprecated
+public class AbstractNCSARequestLog extends ContainerLifeCycle implements RequestLog
+{
+ protected static final Logger LOG = Log.getLogger(AbstractNCSARequestLog.class);
+
+ private static ThreadLocal<StringBuilder> _buffers = ThreadLocal.withInitial(() -> new StringBuilder(256));
+
+ protected final RequestLog.Writer _requestLogWriter;
+
+ private String[] _ignorePaths;
+ private boolean _extended;
+ private transient PathMappings<String> _ignorePathMap;
+ private boolean _logLatency = false;
+ private boolean _logCookies = false;
+ private boolean _logServer = false;
+ private boolean _preferProxiedForAddress;
+ private transient DateCache _logDateCache;
+ private String _logDateFormat = "dd/MMM/yyyy:HH:mm:ss Z";
+ private Locale _logLocale = Locale.getDefault();
+ private String _logTimeZone = "GMT";
+
+ public AbstractNCSARequestLog(RequestLog.Writer requestLogWriter)
+ {
+ this._requestLogWriter = requestLogWriter;
+ addBean(_requestLogWriter);
+ }
+
+ /**
+ * Is logging enabled
+ *
+ * @return true if logging is enabled
+ */
+ protected boolean isEnabled()
+ {
+ return true;
+ }
+
+ /**
+ * Write requestEntry out. (to disk or slf4j log)
+ *
+ * @param requestEntry the request entry
+ * @throws IOException if unable to write the entry
+ */
+ public void write(String requestEntry) throws IOException
+ {
+ _requestLogWriter.write(requestEntry);
+ }
+
+ private void append(StringBuilder buf, String s)
+ {
+ if (s == null || s.length() == 0)
+ buf.append('-');
+ else
+ buf.append(s);
+ }
+
+ /**
+ * Writes the request and response information to the output stream.
+ *
+ * @see org.eclipse.jetty.server.RequestLog#log(Request, Response)
+ */
+ @Override
+ public void log(Request request, Response response)
+ {
+ try
+ {
+ if (_ignorePathMap != null && _ignorePathMap.getMatch(request.getRequestURI()) != null)
+ return;
+
+ if (!isEnabled())
+ return;
+
+ StringBuilder buf = _buffers.get();
+ buf.setLength(0);
+
+ if (_logServer)
+ {
+ append(buf, request.getServerName());
+ buf.append(' ');
+ }
+
+ String addr = null;
+ if (_preferProxiedForAddress)
+ {
+ addr = request.getHeader(HttpHeader.X_FORWARDED_FOR.toString());
+ }
+
+ if (addr == null)
+ addr = request.getRemoteAddr();
+
+ buf.append(addr);
+ buf.append(" - ");
+
+ String auth = getAuthentication(request);
+ append(buf, auth == null ? "-" : auth);
+
+ buf.append(" [");
+ if (_logDateCache != null)
+ buf.append(_logDateCache.format(request.getTimeStamp()));
+ else
+ buf.append(request.getTimeStamp());
+
+ buf.append("] \"");
+ append(buf, request.getMethod());
+ buf.append(' ');
+ append(buf, request.getOriginalURI());
+ buf.append(' ');
+ append(buf, request.getProtocol());
+ buf.append("\" ");
+
+ int status = response.getCommittedMetaData().getStatus();
+ if (status >= 0)
+ {
+ buf.append((char)('0' + ((status / 100) % 10)));
+ buf.append((char)('0' + ((status / 10) % 10)));
+ buf.append((char)('0' + (status % 10)));
+ }
+ else
+ buf.append(status);
+
+ long written = response.getHttpChannel().getBytesWritten();
+ if (written >= 0)
+ {
+ buf.append(' ');
+ if (written > 99999)
+ buf.append(written);
+ else
+ {
+ if (written > 9999)
+ buf.append((char)('0' + ((written / 10000) % 10)));
+ if (written > 999)
+ buf.append((char)('0' + ((written / 1000) % 10)));
+ if (written > 99)
+ buf.append((char)('0' + ((written / 100) % 10)));
+ if (written > 9)
+ buf.append((char)('0' + ((written / 10) % 10)));
+ buf.append((char)('0' + (written) % 10));
+ }
+ buf.append(' ');
+ }
+ else
+ buf.append(" - ");
+
+ if (_extended)
+ logExtended(buf, request, response);
+
+ if (_logCookies)
+ {
+ Cookie[] cookies = request.getCookies();
+ if (cookies == null || cookies.length == 0)
+ buf.append(" -");
+ else
+ {
+ buf.append(" \"");
+ for (int i = 0; i < cookies.length; i++)
+ {
+ if (i != 0)
+ buf.append(';');
+ buf.append(cookies[i].getName());
+ buf.append('=');
+ buf.append(cookies[i].getValue());
+ }
+ buf.append('\"');
+ }
+ }
+
+ if (_logLatency)
+ {
+ long now = System.currentTimeMillis();
+
+ if (_logLatency)
+ {
+ buf.append(' ');
+ buf.append(now - request.getTimeStamp());
+ }
+ }
+
+ String log = buf.toString();
+ write(log);
+ }
+ catch (IOException e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ /**
+ * Extract the user authentication
+ *
+ * @param request The request to extract from
+ * @return The string to log for authenticated user.
+ */
+ protected String getAuthentication(Request request)
+ {
+ Authentication authentication = request.getAuthentication();
+
+ if (authentication instanceof Authentication.User)
+ return ((Authentication.User)authentication).getUserIdentity().getUserPrincipal().getName();
+
+ // TODO extract the user name if it is Authentication.Deferred and return as '?username'
+
+ return null;
+ }
+
+ /**
+ * Writes extended request and response information to the output stream.
+ *
+ * @param b StringBuilder to write to
+ * @param request request object
+ * @param response response object
+ * @throws IOException if unable to log the extended information
+ */
+ protected void logExtended(StringBuilder b, Request request, Response response) throws IOException
+ {
+ String referer = request.getHeader(HttpHeader.REFERER.toString());
+ if (referer == null)
+ b.append("\"-\" ");
+ else
+ {
+ b.append('"');
+ b.append(referer);
+ b.append("\" ");
+ }
+
+ String agent = request.getHeader(HttpHeader.USER_AGENT.toString());
+ if (agent == null)
+ b.append("\"-\"");
+ else
+ {
+ b.append('"');
+ b.append(agent);
+ b.append('"');
+ }
+ }
+
+ /**
+ * Set request paths that will not be logged.
+ *
+ * @param ignorePaths array of request paths
+ */
+ public void setIgnorePaths(String[] ignorePaths)
+ {
+ _ignorePaths = ignorePaths;
+ }
+
+ /**
+ * Retrieve the request paths that will not be logged.
+ *
+ * @return array of request paths
+ */
+ public String[] getIgnorePaths()
+ {
+ return _ignorePaths;
+ }
+
+ /**
+ * Controls logging of the request cookies.
+ *
+ * @param logCookies true - values of request cookies will be logged, false - values of request cookies will not be
+ * logged
+ */
+ public void setLogCookies(boolean logCookies)
+ {
+ _logCookies = logCookies;
+ }
+
+ /**
+ * Retrieve log cookies flag
+ *
+ * @return value of the flag
+ */
+ public boolean getLogCookies()
+ {
+ return _logCookies;
+ }
+
+ /**
+ * Controls logging of the request hostname.
+ *
+ * @param logServer true - request hostname will be logged, false - request hostname will not be logged
+ */
+ public void setLogServer(boolean logServer)
+ {
+ _logServer = logServer;
+ }
+
+ /**
+ * Retrieve log hostname flag.
+ *
+ * @return value of the flag
+ */
+ public boolean getLogServer()
+ {
+ return _logServer;
+ }
+
+ /**
+ * Controls logging of request processing time.
+ *
+ * @param logLatency true - request processing time will be logged false - request processing time will not be
+ * logged
+ */
+ public void setLogLatency(boolean logLatency)
+ {
+ _logLatency = logLatency;
+ }
+
+ /**
+ * Retrieve log request processing time flag.
+ *
+ * @return value of the flag
+ */
+ public boolean getLogLatency()
+ {
+ return _logLatency;
+ }
+
+ /**
+ * @param value true to log dispatch
+ * @deprecated use {@link StatisticsHandler}
+ */
+ @Deprecated
+ public void setLogDispatch(boolean value)
+ {
+ }
+
+ /**
+ * @return true if logging dispatches
+ * @deprecated use {@link StatisticsHandler}
+ */
+ @Deprecated
+ public boolean isLogDispatch()
+ {
+ return false;
+ }
+
+ /**
+ * Controls whether the actual IP address of the connection or the IP address from the X-Forwarded-For header will
+ * be logged.
+ *
+ * @param preferProxiedForAddress true - IP address from header will be logged, false - IP address from the
+ * connection will be logged
+ */
+ public void setPreferProxiedForAddress(boolean preferProxiedForAddress)
+ {
+ _preferProxiedForAddress = preferProxiedForAddress;
+ }
+
+ /**
+ * Retrieved log X-Forwarded-For IP address flag.
+ *
+ * @return value of the flag
+ */
+ public boolean getPreferProxiedForAddress()
+ {
+ return _preferProxiedForAddress;
+ }
+
+ /**
+ * Set the extended request log format flag.
+ *
+ * @param extended true - log the extended request information, false - do not log the extended request information
+ */
+ public void setExtended(boolean extended)
+ {
+ _extended = extended;
+ }
+
+ /**
+ * Retrieve the extended request log format flag.
+ *
+ * @return value of the flag
+ */
+ @ManagedAttribute("use extended NCSA format")
+ public boolean isExtended()
+ {
+ return _extended;
+ }
+
+ /**
+ * Set up request logging and open log file.
+ *
+ * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
+ */
+ @Override
+ protected synchronized void doStart() throws Exception
+ {
+ if (_logDateFormat != null)
+ {
+ _logDateCache = new DateCache(_logDateFormat, _logLocale, _logTimeZone);
+ }
+
+ if (_ignorePaths != null && _ignorePaths.length > 0)
+ {
+ _ignorePathMap = new PathMappings<>();
+ for (int i = 0; i < _ignorePaths.length; i++)
+ {
+ _ignorePathMap.put(_ignorePaths[i], _ignorePaths[i]);
+ }
+ }
+ else
+ _ignorePathMap = null;
+
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ _logDateCache = null;
+ super.doStop();
+ }
+
+ /**
+ * Set the timestamp format for request log entries in the file. If this is not set, the pre-formated request
+ * timestamp is used.
+ *
+ * @param format timestamp format string
+ */
+ public void setLogDateFormat(String format)
+ {
+ _logDateFormat = format;
+ }
+
+ /**
+ * Retrieve the timestamp format string for request log entries.
+ *
+ * @return timestamp format string.
+ */
+ public String getLogDateFormat()
+ {
+ return _logDateFormat;
+ }
+
+ /**
+ * Set the locale of the request log.
+ *
+ * @param logLocale locale object
+ */
+ public void setLogLocale(Locale logLocale)
+ {
+ _logLocale = logLocale;
+ }
+
+ /**
+ * Retrieve the locale of the request log.
+ *
+ * @return locale object
+ */
+ public Locale getLogLocale()
+ {
+ return _logLocale;
+ }
+
+ /**
+ * Set the timezone of the request log.
+ *
+ * @param tz timezone string
+ */
+ public void setLogTimeZone(String tz)
+ {
+ _logTimeZone = tz;
+ }
+
+ /**
+ * Retrieve the timezone of the request log.
+ *
+ * @return timezone string
+ */
+ @ManagedAttribute("the timezone")
+ public String getLogTimeZone()
+ {
+ return _logTimeZone;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractNetworkConnector.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractNetworkConnector.java
new file mode 100644
index 0000000..7134d85
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractNetworkConnector.java
@@ -0,0 +1,125 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * An abstract Network Connector.
+ * <p>
+ * Extends the {@link AbstractConnector} support for the {@link NetworkConnector} interface.
+ */
+@ManagedObject("AbstractNetworkConnector")
+public abstract class AbstractNetworkConnector extends AbstractConnector implements NetworkConnector
+{
+
+ private volatile String _host;
+ private volatile int _port = 0;
+
+ public AbstractNetworkConnector(Server server, Executor executor, Scheduler scheduler, ByteBufferPool pool, int acceptors, ConnectionFactory... factories)
+ {
+ super(server, executor, scheduler, pool, acceptors, factories);
+ }
+
+ public void setHost(String host)
+ {
+ _host = host;
+ }
+
+ @Override
+ @ManagedAttribute("The network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces.")
+ public String getHost()
+ {
+ return _host;
+ }
+
+ public void setPort(int port)
+ {
+ _port = port;
+ }
+
+ @Override
+ @ManagedAttribute("Port this connector listens on. If set the 0 a random port is assigned which may be obtained with getLocalPort()")
+ public int getPort()
+ {
+ return _port;
+ }
+
+ @Override
+ public int getLocalPort()
+ {
+ return -1;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ open();
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ close();
+ super.doStop();
+ }
+
+ @Override
+ public void open() throws IOException
+ {
+ }
+
+ @Override
+ public void close()
+ {
+ }
+
+ @Override
+ public Future<Void> shutdown()
+ {
+ close();
+ return super.shutdown();
+ }
+
+ @Override
+ protected boolean handleAcceptFailure(Throwable ex)
+ {
+ if (isOpen())
+ return super.handleAcceptFailure(ex);
+ LOG.ignore(ex);
+ return false;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s{%s:%d}",
+ super.toString(),
+ getHost() == null ? "0.0.0.0" : getHost(),
+ getLocalPort() <= 0 ? getPort() : getLocalPort());
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AcceptRateLimit.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AcceptRateLimit.java
new file mode 100644
index 0000000..bda5045
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AcceptRateLimit.java
@@ -0,0 +1,274 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.nio.channels.SelectableChannel;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.io.SelectorManager;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.annotation.Name;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.Container;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.statistic.RateStatistic;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * <p>A Listener that limits the rate at which new connections are accepted</p>
+ * <p>
+ * If the limits are exceeded, accepting is suspended until the rate is again below
+ * the limit, so incoming connections are held in the operating system accept
+ * queue (no syn ack sent), where they may either timeout or wait for the server
+ * to resume accepting.
+ * </p>
+ * <p>
+ * It can be applied to an entire server or to a specific connector by adding it
+ * via {@link Container#addBean(Object)}
+ * </p>
+ * <p>
+ * <b>Usage:</b>
+ * </p>
+ * <pre>
+ * Server server = new Server();
+ * server.addBean(new AcceptLimit(100,5,TimeUnit.SECONDS,server));
+ * ...
+ * server.start();
+ * </pre>
+ *
+ * @see SelectorManager.AcceptListener
+ */
+@ManagedObject
+public class AcceptRateLimit extends AbstractLifeCycle implements SelectorManager.AcceptListener, Runnable
+{
+ private static final Logger LOG = Log.getLogger(AcceptRateLimit.class);
+
+ private final Server _server;
+ private final List<AbstractConnector> _connectors = new ArrayList<>();
+ private final Rate _rate;
+ private final int _acceptRateLimit;
+ private boolean _limiting;
+ private Scheduler.Task _task;
+
+ public AcceptRateLimit(@Name("acceptRateLimit") int acceptRateLimit, @Name("period") long period, @Name("units") TimeUnit units, @Name("server") Server server)
+ {
+ _server = server;
+ _acceptRateLimit = acceptRateLimit;
+ _rate = new Rate(period, units);
+ }
+
+ public AcceptRateLimit(@Name("limit") int limit, @Name("period") long period, @Name("units") TimeUnit units, @Name("connectors") Connector... connectors)
+ {
+ this(limit, period, units, (Server)null);
+ for (Connector c : connectors)
+ {
+ if (c instanceof AbstractConnector)
+ _connectors.add((AbstractConnector)c);
+ else
+ LOG.warn("Connector {} is not an AbstractConnector. Connections not limited", c);
+ }
+ }
+
+ @ManagedAttribute("The accept rate limit")
+ public int getAcceptRateLimit()
+ {
+ return _acceptRateLimit;
+ }
+
+ @ManagedAttribute("The accept rate period")
+ public long getPeriod()
+ {
+ return _rate.getPeriod();
+ }
+
+ @ManagedAttribute("The accept rate period units")
+ public TimeUnit getUnits()
+ {
+ return _rate.getUnits();
+ }
+
+ @ManagedAttribute("The current accept rate")
+ public int getRate()
+ {
+ return _rate.getRate();
+ }
+
+ @ManagedAttribute("The maximum accept rate achieved")
+ public long getMaxRate()
+ {
+ return _rate.getMax();
+ }
+
+ @ManagedOperation(value = "Resets the accept rate", impact = "ACTION")
+ public void reset()
+ {
+ synchronized (_rate)
+ {
+ _rate.reset();
+ if (_limiting)
+ {
+ _limiting = false;
+ unlimit();
+ }
+ }
+ }
+
+ protected void age(long period, TimeUnit units)
+ {
+ _rate.age(period, units);
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ synchronized (_rate)
+ {
+ if (_server != null)
+ {
+ for (Connector c : _server.getConnectors())
+ {
+ if (c instanceof AbstractConnector)
+ _connectors.add((AbstractConnector)c);
+ else
+ LOG.warn("Connector {} is not an AbstractConnector. Connections not limited", c);
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("AcceptLimit accept<{} rate<{} in {} for {}", _acceptRateLimit, _rate, _connectors);
+
+ for (AbstractConnector c : _connectors)
+ {
+ c.addBean(this);
+ }
+ }
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ synchronized (_rate)
+ {
+ if (_task != null)
+ _task.cancel();
+ _task = null;
+ for (AbstractConnector c : _connectors)
+ {
+ c.removeBean(this);
+ }
+ if (_server != null)
+ _connectors.clear();
+ _limiting = false;
+ }
+ }
+
+ protected void limit()
+ {
+ for (AbstractConnector c : _connectors)
+ {
+ c.setAccepting(false);
+ }
+ schedule();
+ }
+
+ protected void unlimit()
+ {
+ for (AbstractConnector c : _connectors)
+ {
+ c.setAccepting(true);
+ }
+ }
+
+ @Override
+ public void onAccepting(SelectableChannel channel)
+ {
+ synchronized (_rate)
+ {
+ int rate = _rate.record();
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onAccepting rate {}/{} for {} {}", rate, _acceptRateLimit, _rate, channel);
+ }
+ if (rate > _acceptRateLimit)
+ {
+ if (!_limiting)
+ {
+ _limiting = true;
+
+ LOG.warn("AcceptLimit rate exceeded {}>{} on {}", rate, _acceptRateLimit, _connectors);
+ limit();
+ }
+ }
+ }
+ }
+
+ private void schedule()
+ {
+ long oldest = _rate.getOldest(TimeUnit.MILLISECONDS);
+ long period = TimeUnit.MILLISECONDS.convert(_rate.getPeriod(), _rate.getUnits());
+ long delay = period - (oldest > 0 ? oldest : 0);
+ if (delay < 0)
+ delay = 0;
+ if (LOG.isDebugEnabled())
+ LOG.debug("schedule {} {}", delay, TimeUnit.MILLISECONDS);
+ _task = _connectors.get(0).getScheduler().schedule(this, delay, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void run()
+ {
+ synchronized (_rate)
+ {
+ _task = null;
+ if (!isRunning())
+ return;
+ int rate = _rate.getRate();
+ if (rate > _acceptRateLimit)
+ {
+ schedule();
+ return;
+ }
+ if (_limiting)
+ {
+ _limiting = false;
+ LOG.warn("AcceptLimit rate OK {}<={} on {}", rate, _acceptRateLimit, _connectors);
+ unlimit();
+ }
+ }
+ }
+
+ private final class Rate extends RateStatistic
+ {
+ private Rate(long period, TimeUnit units)
+ {
+ super(period, units);
+ }
+
+ @Override
+ protected void age(long period, TimeUnit units)
+ {
+ super.age(period, units);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncAttributes.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncAttributes.java
new file mode 100644
index 0000000..b98b10f
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncAttributes.java
@@ -0,0 +1,140 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.HashSet;
+import java.util.Set;
+import javax.servlet.AsyncContext;
+
+import org.eclipse.jetty.util.Attributes;
+
+class AsyncAttributes extends Attributes.Wrapper
+{
+ public static final String __ASYNC_PREFIX = "javax.servlet.async.";
+
+ private String _requestURI;
+ private String _contextPath;
+ private String _servletPath;
+ private String _pathInfo;
+ private String _queryString;
+
+ public AsyncAttributes(Attributes attributes, String requestUri, String contextPath, String servletPath, String pathInfo, String queryString)
+ {
+ super(attributes);
+
+ // TODO: make fields final in jetty-10 and NOOP when one of these attributes is set.
+ _requestURI = requestUri;
+ _contextPath = contextPath;
+ _servletPath = servletPath;
+ _pathInfo = pathInfo;
+ _queryString = queryString;
+ }
+
+ @Override
+ public Object getAttribute(String key)
+ {
+ switch (key)
+ {
+ case AsyncContext.ASYNC_REQUEST_URI:
+ return _requestURI;
+ case AsyncContext.ASYNC_CONTEXT_PATH:
+ return _contextPath;
+ case AsyncContext.ASYNC_SERVLET_PATH:
+ return _servletPath;
+ case AsyncContext.ASYNC_PATH_INFO:
+ return _pathInfo;
+ case AsyncContext.ASYNC_QUERY_STRING:
+ return _queryString;
+ default:
+ return super.getAttribute(key);
+ }
+ }
+
+ @Override
+ public Set<String> getAttributeNameSet()
+ {
+ Set<String> set = new HashSet<>();
+ super.getAttributeNameSet().stream()
+ .filter(name -> !name.startsWith(__ASYNC_PREFIX))
+ .forEach(set::add);
+
+ if (_requestURI != null)
+ set.add(AsyncContext.ASYNC_REQUEST_URI);
+ if (_contextPath != null)
+ set.add(AsyncContext.ASYNC_CONTEXT_PATH);
+ if (_servletPath != null)
+ set.add(AsyncContext.ASYNC_SERVLET_PATH);
+ if (_pathInfo != null)
+ set.add(AsyncContext.ASYNC_PATH_INFO);
+ if (_queryString != null)
+ set.add(AsyncContext.ASYNC_QUERY_STRING);
+ return set;
+ }
+
+ @Override
+ public void setAttribute(String key, Object value)
+ {
+ switch (key)
+ {
+ case AsyncContext.ASYNC_REQUEST_URI:
+ _requestURI = (String)value;
+ break;
+ case AsyncContext.ASYNC_CONTEXT_PATH:
+ _contextPath = (String)value;
+ break;
+ case AsyncContext.ASYNC_SERVLET_PATH:
+ _servletPath = (String)value;
+ break;
+ case AsyncContext.ASYNC_PATH_INFO:
+ _pathInfo = (String)value;
+ break;
+ case AsyncContext.ASYNC_QUERY_STRING:
+ _queryString = (String)value;
+ break;
+ default:
+ super.setAttribute(key, value);
+ break;
+ }
+ }
+
+ @Override
+ public void clearAttributes()
+ {
+ _requestURI = null;
+ _contextPath = null;
+ _servletPath = null;
+ _pathInfo = null;
+ _queryString = null;
+ super.clearAttributes();
+ }
+
+ public static void applyAsyncAttributes(Attributes attributes, String requestURI, String contextPath, String servletPath, String pathInfo, String queryString)
+ {
+ if (requestURI != null)
+ attributes.setAttribute(AsyncContext.ASYNC_REQUEST_URI, requestURI);
+ if (contextPath != null)
+ attributes.setAttribute(AsyncContext.ASYNC_CONTEXT_PATH, contextPath);
+ if (servletPath != null)
+ attributes.setAttribute(AsyncContext.ASYNC_SERVLET_PATH, servletPath);
+ if (pathInfo != null)
+ attributes.setAttribute(AsyncContext.ASYNC_PATH_INFO, pathInfo);
+ if (queryString != null)
+ attributes.setAttribute(AsyncContext.ASYNC_QUERY_STRING, queryString);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java
new file mode 100644
index 0000000..457de6c
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java
@@ -0,0 +1,150 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.AsyncEvent;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.eclipse.jetty.server.handler.ContextHandler.Context;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+public class AsyncContextEvent extends AsyncEvent implements Runnable
+{
+ private final Context _context;
+ private final AsyncContextState _asyncContext;
+ private final HttpChannelState _state;
+ private ServletContext _dispatchContext;
+ private String _dispatchPath;
+ private volatile Scheduler.Task _timeoutTask;
+ private Throwable _throwable;
+
+ public AsyncContextEvent(Context context, AsyncContextState asyncContext, HttpChannelState state, Request baseRequest, ServletRequest request, ServletResponse response)
+ {
+ super(null, request, response, null);
+ _context = context;
+ _asyncContext = asyncContext;
+ _state = state;
+
+ // We are setting these attributes during startAsync, when the spec implies that
+ // they are only available after a call to AsyncContext.dispatch(...);
+ baseRequest.setAsyncAttributes();
+ }
+
+ public ServletContext getSuspendedContext()
+ {
+ return _context;
+ }
+
+ public Context getContext()
+ {
+ return _context;
+ }
+
+ public ServletContext getDispatchContext()
+ {
+ return _dispatchContext;
+ }
+
+ public ServletContext getServletContext()
+ {
+ return _dispatchContext == null ? _context : _dispatchContext;
+ }
+
+ /**
+ * @return The path in the context (encoded with possible query string)
+ */
+ public String getPath()
+ {
+ return _dispatchPath;
+ }
+
+ public void setTimeoutTask(Scheduler.Task task)
+ {
+ _timeoutTask = task;
+ }
+
+ public boolean hasTimeoutTask()
+ {
+ return _timeoutTask != null;
+ }
+
+ public void cancelTimeoutTask()
+ {
+ Scheduler.Task task = _timeoutTask;
+ _timeoutTask = null;
+ if (task != null)
+ task.cancel();
+ }
+
+ @Override
+ public AsyncContext getAsyncContext()
+ {
+ return _asyncContext;
+ }
+
+ @Override
+ public Throwable getThrowable()
+ {
+ return _throwable;
+ }
+
+ public void setDispatchContext(ServletContext context)
+ {
+ _dispatchContext = context;
+ }
+
+ /**
+ * @param path encoded URI
+ */
+ public void setDispatchPath(String path)
+ {
+ _dispatchPath = path;
+ }
+
+ public void completed()
+ {
+ _timeoutTask = null;
+ _asyncContext.reset();
+ }
+
+ public HttpChannelState getHttpChannelState()
+ {
+ return _state;
+ }
+
+ @Override
+ public void run()
+ {
+ Scheduler.Task task = _timeoutTask;
+ _timeoutTask = null;
+ if (task != null)
+ _state.timeout();
+ }
+
+ public void addThrowable(Throwable e)
+ {
+ if (_throwable == null)
+ _throwable = e;
+ else if (e != _throwable)
+ _throwable.addSuppressed(e);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextState.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextState.java
new file mode 100644
index 0000000..4cb4386
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextState.java
@@ -0,0 +1,210 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import javax.servlet.AsyncContext;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.eclipse.jetty.server.handler.ContextHandler;
+
+public class AsyncContextState implements AsyncContext
+{
+ private final HttpChannel _channel;
+ volatile HttpChannelState _state;
+
+ public AsyncContextState(HttpChannelState state)
+ {
+ _state = state;
+ _channel = _state.getHttpChannel();
+ }
+
+ public HttpChannel getHttpChannel()
+ {
+ return _channel;
+ }
+
+ HttpChannelState state()
+ {
+ HttpChannelState state = _state;
+ if (state == null)
+ throw new IllegalStateException("AsyncContext completed and/or Request lifecycle recycled");
+ return state;
+ }
+
+ @Override
+ public void addListener(final AsyncListener listener, final ServletRequest request, final ServletResponse response)
+ {
+ AsyncListener wrap = new WrappedAsyncListener(listener, request, response);
+ state().addListener(wrap);
+ }
+
+ @Override
+ public void addListener(AsyncListener listener)
+ {
+ state().addListener(listener);
+ }
+
+ @Override
+ public void complete()
+ {
+ state().complete();
+ }
+
+ @Override
+ public <T extends AsyncListener> T createListener(Class<T> clazz) throws ServletException
+ {
+ ContextHandler contextHandler = state().getContextHandler();
+ if (contextHandler != null)
+ return contextHandler.getServletContext().createListener(clazz);
+ try
+ {
+ return clazz.getDeclaredConstructor().newInstance();
+ }
+ catch (Exception e)
+ {
+ throw new ServletException(e);
+ }
+ }
+
+ @Override
+ public void dispatch()
+ {
+ state().dispatch(null, null);
+ }
+
+ @Override
+ public void dispatch(String path)
+ {
+ state().dispatch(null, path);
+ }
+
+ @Override
+ public void dispatch(ServletContext context, String path)
+ {
+ state().dispatch(context, path);
+ }
+
+ @Override
+ public ServletRequest getRequest()
+ {
+ return state().getAsyncContextEvent().getSuppliedRequest();
+ }
+
+ @Override
+ public ServletResponse getResponse()
+ {
+ return state().getAsyncContextEvent().getSuppliedResponse();
+ }
+
+ @Override
+ public long getTimeout()
+ {
+ return state().getTimeout();
+ }
+
+ @Override
+ public boolean hasOriginalRequestAndResponse()
+ {
+ HttpChannel channel = state().getHttpChannel();
+ return channel.getRequest() == getRequest() && channel.getResponse() == getResponse();
+ }
+
+ @Override
+ public void setTimeout(long arg0)
+ {
+ state().setTimeout(arg0);
+ }
+
+ @Override
+ public void start(final Runnable task)
+ {
+ final HttpChannel channel = state().getHttpChannel();
+ channel.execute(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ ContextHandler.Context context = state().getAsyncContextEvent().getContext();
+ if (context == null)
+ task.run();
+ else
+ context.getContextHandler().handle(channel.getRequest(), task);
+ }
+ });
+ }
+
+ public void reset()
+ {
+ _state = null;
+ }
+
+ public HttpChannelState getHttpChannelState()
+ {
+ return state();
+ }
+
+ public static class WrappedAsyncListener implements AsyncListener
+ {
+ private final AsyncListener _listener;
+ private final ServletRequest _request;
+ private final ServletResponse _response;
+
+ public WrappedAsyncListener(AsyncListener listener, ServletRequest request, ServletResponse response)
+ {
+ _listener = listener;
+ _request = request;
+ _response = response;
+ }
+
+ public AsyncListener getListener()
+ {
+ return _listener;
+ }
+
+ @Override
+ public void onTimeout(AsyncEvent event) throws IOException
+ {
+ _listener.onTimeout(new AsyncEvent(event.getAsyncContext(), _request, _response, event.getThrowable()));
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event) throws IOException
+ {
+ _listener.onStartAsync(new AsyncEvent(event.getAsyncContext(), _request, _response, event.getThrowable()));
+ }
+
+ @Override
+ public void onError(AsyncEvent event) throws IOException
+ {
+ _listener.onError(new AsyncEvent(event.getAsyncContext(), _request, _response, event.getThrowable()));
+ }
+
+ @Override
+ public void onComplete(AsyncEvent event) throws IOException
+ {
+ _listener.onComplete(new AsyncEvent(event.getAsyncContext(), _request, _response, event.getThrowable()));
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncNCSARequestLog.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncNCSARequestLog.java
new file mode 100644
index 0000000..e8c7b50
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncNCSARequestLog.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * An asynchronously writing NCSA Request Log
+ *
+ * @deprecated use {@link CustomRequestLog} given format string {@link CustomRequestLog#EXTENDED_NCSA_FORMAT} with an {@link AsyncRequestLogWriter}
+ */
+@Deprecated
+public class AsyncNCSARequestLog extends NCSARequestLog
+{
+ public AsyncNCSARequestLog()
+ {
+ this(null, null);
+ }
+
+ public AsyncNCSARequestLog(String filename, BlockingQueue<String> queue)
+ {
+ super(new AsyncRequestLogWriter(filename, queue));
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncRequestLogWriter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncRequestLogWriter.java
new file mode 100644
index 0000000..165a69f
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncRequestLogWriter.java
@@ -0,0 +1,121 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * An asynchronously writing RequestLogWriter
+ */
+public class AsyncRequestLogWriter extends RequestLogWriter
+{
+ private static final Logger LOG = Log.getLogger(AsyncRequestLogWriter.class);
+ private final BlockingQueue<String> _queue;
+ private transient AsyncRequestLogWriter.WriterThread _thread;
+ private boolean _warnedFull;
+
+ public AsyncRequestLogWriter()
+ {
+ this(null, null);
+ }
+
+ public AsyncRequestLogWriter(String filename)
+ {
+ this(filename, null);
+ }
+
+ public AsyncRequestLogWriter(String filename, BlockingQueue<String> queue)
+ {
+ super(filename);
+ if (queue == null)
+ queue = new BlockingArrayQueue<>(1024);
+ _queue = queue;
+ }
+
+ private class WriterThread extends Thread
+ {
+ WriterThread()
+ {
+ setName("AsyncRequestLogWriter@" + Integer.toString(AsyncRequestLogWriter.this.hashCode(), 16));
+ }
+
+ @Override
+ public void run()
+ {
+ while (isRunning())
+ {
+ try
+ {
+ String log = _queue.poll(10, TimeUnit.SECONDS);
+ if (log != null)
+ AsyncRequestLogWriter.super.write(log);
+
+ while (!_queue.isEmpty())
+ {
+ log = _queue.poll();
+ if (log != null)
+ AsyncRequestLogWriter.super.write(log);
+ }
+ }
+ catch (InterruptedException e)
+ {
+ LOG.ignore(e);
+ }
+ catch (Throwable t)
+ {
+ LOG.warn(t);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected synchronized void doStart() throws Exception
+ {
+ super.doStart();
+ _thread = new AsyncRequestLogWriter.WriterThread();
+ _thread.start();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ _thread.interrupt();
+ _thread.join();
+ super.doStop();
+ _thread = null;
+ }
+
+ @Override
+ public void write(String log) throws IOException
+ {
+ if (!_queue.offer(log))
+ {
+ if (_warnedFull)
+ LOG.warn("Log Queue overflow");
+ _warnedFull = true;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Authentication.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Authentication.java
new file mode 100644
index 0000000..87c8e5d
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Authentication.java
@@ -0,0 +1,237 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * The Authentication state of a request.
+ * <p>
+ * The Authentication state can be one of several sub-types that
+ * reflects where the request is in the many different authentication
+ * cycles. Authentication might not yet be checked or it might be checked
+ * and failed, checked and deferred or succeeded.
+ */
+public interface Authentication
+{
+
+ class Failed extends QuietServletException
+ {
+ public Failed(String message)
+ {
+ super(message);
+ }
+ }
+
+ /**
+ * A successful Authentication with User information.
+ */
+ interface User extends LogoutAuthentication
+ {
+ String getAuthMethod();
+
+ UserIdentity getUserIdentity();
+
+ boolean isUserInRole(UserIdentity.Scope scope, String role);
+
+ @Deprecated
+ void logout();
+ }
+
+ /**
+ * A wrapped authentication with methods provide the
+ * wrapped request/response for use by the application
+ */
+ interface Wrapped extends Authentication
+ {
+ HttpServletRequest getHttpServletRequest();
+
+ HttpServletResponse getHttpServletResponse();
+ }
+
+ /**
+ * An authentication that is capable of performing a programmatic login
+ * operation.
+ */
+ interface LoginAuthentication extends Authentication
+ {
+
+ /**
+ * Login with the LOGIN authenticator
+ *
+ * @param username the username
+ * @param password the password
+ * @param request the request
+ * @return The new Authentication state
+ */
+ Authentication login(String username, Object password, ServletRequest request);
+ }
+
+ /**
+ * An authentication that is capable of performing a programmatic
+ * logout operation.
+ */
+ interface LogoutAuthentication extends Authentication
+ {
+
+ /**
+ * Remove any user information that may be present in the request
+ * such that a call to getUserPrincipal/getRemoteUser will return null.
+ *
+ * @param request the request
+ * @return NoAuthentication if we successfully logged out
+ */
+ Authentication logout(ServletRequest request);
+ }
+
+ /**
+ * A deferred authentication with methods to progress
+ * the authentication process.
+ */
+ interface Deferred extends LoginAuthentication, LogoutAuthentication
+ {
+
+ /**
+ * Authenticate if possible without sending a challenge.
+ * This is used to check credentials that have been sent for
+ * non-mandatory authentication.
+ *
+ * @param request the request
+ * @return The new Authentication state.
+ */
+ Authentication authenticate(ServletRequest request);
+
+ /**
+ * Authenticate and possibly send a challenge.
+ * This is used to initiate authentication for previously
+ * non-mandatory authentication.
+ *
+ * @param request the request
+ * @param response the response
+ * @return The new Authentication state.
+ */
+ Authentication authenticate(ServletRequest request, ServletResponse response);
+ }
+
+ /**
+ * Authentication Response sent state.
+ * Responses are sent by authenticators either to issue an
+ * authentication challenge or on successful authentication in
+ * order to redirect the user to the original URL.
+ */
+ interface ResponseSent extends Authentication
+ {
+ }
+
+ /**
+ * An Authentication Challenge has been sent.
+ */
+ interface Challenge extends ResponseSent
+ {
+ }
+
+ /**
+ * An Authentication Failure has been sent.
+ */
+ interface Failure extends ResponseSent
+ {
+ }
+
+ interface SendSuccess extends ResponseSent
+ {
+ }
+
+ /**
+ * After a logout, the authentication reverts to a state
+ * where it is possible to programmatically log in again.
+ */
+ interface NonAuthenticated extends LoginAuthentication
+ {
+ }
+
+ /**
+ * Unauthenticated state.
+ * <p>
+ * This convenience instance is for non mandatory authentication where credentials
+ * have been presented and checked, but failed authentication.
+ */
+ Authentication UNAUTHENTICATED = new Authentication()
+ {
+ @Override
+ public String toString()
+ {
+ return "UNAUTHENTICATED";
+ }
+ };
+
+ /**
+ * Authentication not checked
+ * <p>
+ * This convenience instance us for non mandatory authentication when no
+ * credentials are present to be checked.
+ */
+ Authentication NOT_CHECKED = new Authentication()
+ {
+ @Override
+ public String toString()
+ {
+ return "NOT CHECKED";
+ }
+ };
+
+ /**
+ * Authentication challenge sent.
+ * <p>
+ * This convenience instance is for when an authentication challenge has been sent.
+ */
+ Authentication SEND_CONTINUE = new Authentication.Challenge()
+ {
+ @Override
+ public String toString()
+ {
+ return "CHALLENGE";
+ }
+ };
+
+ /**
+ * Authentication failure sent.
+ * <p>
+ * This convenience instance is for when an authentication failure has been sent.
+ */
+ Authentication SEND_FAILURE = new Authentication.Failure()
+ {
+ @Override
+ public String toString()
+ {
+ return "FAILURE";
+ }
+ };
+
+ Authentication SEND_SUCCESS = new SendSuccess()
+ {
+ @Override
+ public String toString()
+ {
+ return "SEND_SUCCESS";
+ }
+ };
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/CachedContentFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/CachedContentFactory.java
new file mode 100644
index 0000000..c82edba
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/CachedContentFactory.java
@@ -0,0 +1,726 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.http.CompressedContentFormat;
+import org.eclipse.jetty.http.DateGenerator;
+import org.eclipse.jetty.http.HttpContent;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.http.MimeTypes.Type;
+import org.eclipse.jetty.http.PreEncodedHttpField;
+import org.eclipse.jetty.http.PrecompressedHttpContent;
+import org.eclipse.jetty.http.ResourceHttpContent;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+
+public class CachedContentFactory implements HttpContent.ContentFactory
+{
+ private static final Logger LOG = Log.getLogger(CachedContentFactory.class);
+ private static final Map<CompressedContentFormat, CachedPrecompressedHttpContent> NO_PRECOMPRESSED = Collections.unmodifiableMap(Collections.emptyMap());
+
+ private final ConcurrentMap<String, CachedHttpContent> _cache;
+ private final AtomicInteger _cachedSize;
+ private final AtomicInteger _cachedFiles;
+ private final ResourceFactory _factory;
+ private final CachedContentFactory _parent;
+ private final MimeTypes _mimeTypes;
+ private final boolean _etags;
+ private final CompressedContentFormat[] _precompressedFormats;
+ private final boolean _useFileMappedBuffer;
+
+ private int _maxCachedFileSize = 128 * 1024 * 1024;
+ private int _maxCachedFiles = 2048;
+ private int _maxCacheSize = 256 * 1024 * 1024;
+
+ /**
+ * Constructor.
+ *
+ * @param parent the parent resource cache
+ * @param factory the resource factory
+ * @param mimeTypes Mimetype to use for meta data
+ * @param useFileMappedBuffer true to file memory mapped buffers
+ * @param etags true to support etags
+ * @param precompressedFormats array of precompression formats to support
+ */
+ public CachedContentFactory(CachedContentFactory parent, ResourceFactory factory, MimeTypes mimeTypes, boolean useFileMappedBuffer, boolean etags, CompressedContentFormat[] precompressedFormats)
+ {
+ _factory = factory;
+ _cache = new ConcurrentHashMap<>();
+ _cachedSize = new AtomicInteger();
+ _cachedFiles = new AtomicInteger();
+ _mimeTypes = mimeTypes;
+ _parent = parent;
+ _useFileMappedBuffer = useFileMappedBuffer;
+ _etags = etags;
+ _precompressedFormats = precompressedFormats;
+ }
+
+ public int getCachedSize()
+ {
+ return _cachedSize.get();
+ }
+
+ public int getCachedFiles()
+ {
+ return _cachedFiles.get();
+ }
+
+ public int getMaxCachedFileSize()
+ {
+ return _maxCachedFileSize;
+ }
+
+ public void setMaxCachedFileSize(int maxCachedFileSize)
+ {
+ _maxCachedFileSize = maxCachedFileSize;
+ shrinkCache();
+ }
+
+ public int getMaxCacheSize()
+ {
+ return _maxCacheSize;
+ }
+
+ public void setMaxCacheSize(int maxCacheSize)
+ {
+ _maxCacheSize = maxCacheSize;
+ shrinkCache();
+ }
+
+ /**
+ * @return the max number of cached files.
+ */
+ public int getMaxCachedFiles()
+ {
+ return _maxCachedFiles;
+ }
+
+ /**
+ * @param maxCachedFiles the max number of cached files.
+ */
+ public void setMaxCachedFiles(int maxCachedFiles)
+ {
+ _maxCachedFiles = maxCachedFiles;
+ shrinkCache();
+ }
+
+ public boolean isUseFileMappedBuffer()
+ {
+ return _useFileMappedBuffer;
+ }
+
+ public void flushCache()
+ {
+ while (_cache.size() > 0)
+ {
+ for (String path : _cache.keySet())
+ {
+ CachedHttpContent content = _cache.remove(path);
+ if (content != null)
+ content.invalidate();
+ }
+ }
+ }
+
+ @Deprecated
+ public HttpContent lookup(String pathInContext) throws IOException
+ {
+ return getContent(pathInContext, _maxCachedFileSize);
+ }
+
+ /**
+ * <p>Returns an entry from the cache, or creates a new one.</p>
+ *
+ * @param pathInContext The key into the cache
+ * @param maxBufferSize The maximum buffer size allocated for this request. For cached content, a larger buffer may have
+ * previously been allocated and returned by the {@link HttpContent#getDirectBuffer()} or {@link HttpContent#getIndirectBuffer()} calls.
+ * @return The entry matching {@code pathInContext}, or a new entry
+ * if no matching entry was found. If the content exists but is not cacheable,
+ * then a {@link ResourceHttpContent} instance is returned. If
+ * the resource does not exist, then null is returned.
+ * @throws IOException if the resource cannot be retrieved
+ */
+ @Override
+ public HttpContent getContent(String pathInContext, int maxBufferSize) throws IOException
+ {
+ // Is the content in this cache?
+ CachedHttpContent content = _cache.get(pathInContext);
+ if (content != null && (content).isValid())
+ return content;
+
+ // try loading the content from our factory.
+ Resource resource = _factory.getResource(pathInContext);
+ HttpContent loaded = load(pathInContext, resource, maxBufferSize);
+ if (loaded != null)
+ return loaded;
+
+ // Is the content in the parent cache?
+ if (_parent != null)
+ {
+ HttpContent httpContent = _parent.getContent(pathInContext, maxBufferSize);
+ if (httpContent != null)
+ return httpContent;
+ }
+
+ return null;
+ }
+
+ /**
+ * @param resource the resource to test
+ * @return whether the resource is cacheable. The default implementation tests the cache sizes.
+ */
+ protected boolean isCacheable(Resource resource)
+ {
+ if (_maxCachedFiles <= 0)
+ return false;
+
+ long len = resource.length();
+
+ // Will it fit in the cache?
+ return (len > 0 && (_useFileMappedBuffer || (len < _maxCachedFileSize && len < _maxCacheSize)));
+ }
+
+ private HttpContent load(String pathInContext, Resource resource, int maxBufferSize)
+ {
+ if (resource == null || !resource.exists())
+ return null;
+
+ if (resource.isDirectory())
+ return new ResourceHttpContent(resource, _mimeTypes.getMimeByExtension(resource.toString()), getMaxCachedFileSize());
+
+ // Will it fit in the cache?
+ if (isCacheable(resource))
+ {
+ CachedHttpContent content;
+
+ // Look for precompressed resources
+ if (_precompressedFormats.length > 0)
+ {
+ Map<CompressedContentFormat, CachedHttpContent> precompresssedContents = new HashMap<>(_precompressedFormats.length);
+ for (CompressedContentFormat format : _precompressedFormats)
+ {
+ String compressedPathInContext = pathInContext + format.getExtension();
+ CachedHttpContent compressedContent = _cache.get(compressedPathInContext);
+ if (compressedContent == null || compressedContent.isValid())
+ {
+ compressedContent = null;
+ Resource compressedResource = _factory.getResource(compressedPathInContext);
+ if (compressedResource.exists() && compressedResource.lastModified() >= resource.lastModified() &&
+ compressedResource.length() < resource.length())
+ {
+ compressedContent = new CachedHttpContent(compressedPathInContext, compressedResource, null);
+ CachedHttpContent added = _cache.putIfAbsent(compressedPathInContext, compressedContent);
+ if (added != null)
+ {
+ compressedContent.invalidate();
+ compressedContent = added;
+ }
+ }
+ }
+ if (compressedContent != null)
+ precompresssedContents.put(format, compressedContent);
+ }
+ content = new CachedHttpContent(pathInContext, resource, precompresssedContents);
+ }
+ else
+ content = new CachedHttpContent(pathInContext, resource, null);
+
+ // Add it to the cache.
+ CachedHttpContent added = _cache.putIfAbsent(pathInContext, content);
+ if (added != null)
+ {
+ content.invalidate();
+ content = added;
+ }
+
+ return content;
+ }
+
+ // Look for non Cacheable precompressed resource or content
+ String mt = _mimeTypes.getMimeByExtension(pathInContext);
+ if (_precompressedFormats.length > 0)
+ {
+ // Is the precompressed content cached?
+ Map<CompressedContentFormat, HttpContent> compressedContents = new HashMap<>();
+ for (CompressedContentFormat format : _precompressedFormats)
+ {
+ String compressedPathInContext = pathInContext + format.getExtension();
+ CachedHttpContent compressedContent = _cache.get(compressedPathInContext);
+ if (compressedContent != null && compressedContent.isValid() && compressedContent.getResource().lastModified() >= resource.lastModified())
+ compressedContents.put(format, compressedContent);
+
+ // Is there a precompressed resource?
+ Resource compressedResource = _factory.getResource(compressedPathInContext);
+ if (compressedResource.exists() && compressedResource.lastModified() >= resource.lastModified() &&
+ compressedResource.length() < resource.length())
+ compressedContents.put(format,
+ new ResourceHttpContent(compressedResource, _mimeTypes.getMimeByExtension(compressedPathInContext), maxBufferSize));
+ }
+ if (!compressedContents.isEmpty())
+ return new ResourceHttpContent(resource, mt, maxBufferSize, compressedContents);
+ }
+
+ return new ResourceHttpContent(resource, mt, maxBufferSize);
+ }
+
+ private void shrinkCache()
+ {
+ // While we need to shrink
+ while (_cache.size() > 0 && (_cachedFiles.get() > _maxCachedFiles || _cachedSize.get() > _maxCacheSize))
+ {
+ // Scan the entire cache and generate an ordered list by last accessed time.
+ SortedSet<CachedHttpContent> sorted = new TreeSet<>((c1, c2) ->
+ {
+ if (c1._lastAccessed < c2._lastAccessed)
+ return -1;
+
+ if (c1._lastAccessed > c2._lastAccessed)
+ return 1;
+
+ if (c1._contentLengthValue < c2._contentLengthValue)
+ return -1;
+
+ return c1._key.compareTo(c2._key);
+ });
+ sorted.addAll(_cache.values());
+
+ // Invalidate least recently used first
+ for (CachedHttpContent content : sorted)
+ {
+ if (_cachedFiles.get() <= _maxCachedFiles && _cachedSize.get() <= _maxCacheSize)
+ break;
+ if (content == _cache.remove(content.getKey()))
+ content.invalidate();
+ }
+ }
+ }
+
+ protected ByteBuffer getIndirectBuffer(Resource resource)
+ {
+ try
+ {
+ return BufferUtil.toBuffer(resource, false);
+ }
+ catch (IOException | IllegalArgumentException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(e);
+ }
+ return null;
+ }
+
+ protected ByteBuffer getMappedBuffer(Resource resource)
+ {
+ // Only use file mapped buffers for cached resources, otherwise too much virtual memory commitment for
+ // a non shared resource. Also ignore max buffer size
+ try
+ {
+ if (_useFileMappedBuffer && resource.getFile() != null && resource.length() < Integer.MAX_VALUE)
+ return BufferUtil.toMappedBuffer(resource.getFile());
+ }
+ catch (IOException | IllegalArgumentException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(e);
+ }
+ return null;
+ }
+
+ protected ByteBuffer getDirectBuffer(Resource resource)
+ {
+ try
+ {
+ return BufferUtil.toBuffer(resource, true);
+ }
+ catch (IOException | IllegalArgumentException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(e);
+ }
+ return null;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ResourceCache[" + _parent + "," + _factory + "]@" + hashCode();
+ }
+
+ /**
+ * MetaData associated with a context Resource.
+ */
+ public class CachedHttpContent implements HttpContent
+ {
+ private final String _key;
+ private final Resource _resource;
+ private final long _contentLengthValue;
+ private final HttpField _contentType;
+ private final String _characterEncoding;
+ private final MimeTypes.Type _mimeType;
+ private final HttpField _contentLength;
+ private final HttpField _lastModified;
+ private final long _lastModifiedValue;
+ private final HttpField _etag;
+ private final Map<CompressedContentFormat, CachedPrecompressedHttpContent> _precompressed;
+ private final AtomicReference<ByteBuffer> _indirectBuffer = new AtomicReference<>();
+ private final AtomicReference<ByteBuffer> _directBuffer = new AtomicReference<>();
+ private final AtomicReference<ByteBuffer> _mappedBuffer = new AtomicReference<>();
+ private volatile long _lastAccessed;
+
+ CachedHttpContent(String pathInContext, Resource resource, Map<CompressedContentFormat, CachedHttpContent> precompressedResources)
+ {
+ _key = pathInContext;
+ _resource = resource;
+
+ String contentType = _mimeTypes.getMimeByExtension(_resource.toString());
+ _contentType = contentType == null ? null : new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, contentType);
+ _characterEncoding = _contentType == null ? null : MimeTypes.getCharsetFromContentType(contentType);
+ _mimeType = _contentType == null ? null : MimeTypes.CACHE.get(MimeTypes.getContentTypeWithoutCharset(contentType));
+
+ boolean exists = resource.exists();
+ _lastModifiedValue = exists ? resource.lastModified() : -1L;
+ _lastModified = _lastModifiedValue == -1 ? null
+ : new PreEncodedHttpField(HttpHeader.LAST_MODIFIED, DateGenerator.formatDate(_lastModifiedValue));
+
+ _contentLengthValue = exists ? resource.length() : 0;
+ _contentLength = new PreEncodedHttpField(HttpHeader.CONTENT_LENGTH, Long.toString(_contentLengthValue));
+
+ if (_cachedFiles.incrementAndGet() > _maxCachedFiles)
+ shrinkCache();
+
+ _lastAccessed = System.currentTimeMillis();
+
+ _etag = CachedContentFactory.this._etags ? new PreEncodedHttpField(HttpHeader.ETAG, resource.getWeakETag()) : null;
+
+ if (precompressedResources != null)
+ {
+ _precompressed = new HashMap<>(precompressedResources.size());
+ for (Map.Entry<CompressedContentFormat, CachedHttpContent> entry : precompressedResources.entrySet())
+ {
+ _precompressed.put(entry.getKey(), new CachedPrecompressedHttpContent(this, entry.getValue(), entry.getKey()));
+ }
+ }
+ else
+ {
+ _precompressed = NO_PRECOMPRESSED;
+ }
+ }
+
+ public String getKey()
+ {
+ return _key;
+ }
+
+ public boolean isCached()
+ {
+ return _key != null;
+ }
+
+ @Override
+ public Resource getResource()
+ {
+ return _resource;
+ }
+
+ @Override
+ public HttpField getETag()
+ {
+ return _etag;
+ }
+
+ @Override
+ public String getETagValue()
+ {
+ return _etag.getValue();
+ }
+
+ boolean isValid()
+ {
+ if (_lastModifiedValue == _resource.lastModified() && _contentLengthValue == _resource.length())
+ {
+ _lastAccessed = System.currentTimeMillis();
+ return true;
+ }
+
+ if (this == _cache.remove(_key))
+ invalidate();
+ return false;
+ }
+
+ protected void invalidate()
+ {
+ ByteBuffer indirect = _indirectBuffer.getAndSet(null);
+ if (indirect != null)
+ _cachedSize.addAndGet(-BufferUtil.length(indirect));
+
+ ByteBuffer direct = _directBuffer.getAndSet(null);
+ if (direct != null)
+ _cachedSize.addAndGet(-BufferUtil.length(direct));
+
+ _mappedBuffer.getAndSet(null);
+
+ _cachedFiles.decrementAndGet();
+ _resource.close();
+ }
+
+ @Override
+ public HttpField getLastModified()
+ {
+ return _lastModified;
+ }
+
+ @Override
+ public String getLastModifiedValue()
+ {
+ return _lastModified == null ? null : _lastModified.getValue();
+ }
+
+ @Override
+ public HttpField getContentType()
+ {
+ return _contentType;
+ }
+
+ @Override
+ public String getContentTypeValue()
+ {
+ return _contentType == null ? null : _contentType.getValue();
+ }
+
+ @Override
+ public HttpField getContentEncoding()
+ {
+ return null;
+ }
+
+ @Override
+ public String getContentEncodingValue()
+ {
+ return null;
+ }
+
+ @Override
+ public String getCharacterEncoding()
+ {
+ return _characterEncoding;
+ }
+
+ @Override
+ public Type getMimeType()
+ {
+ return _mimeType;
+ }
+
+ @Override
+ public void release()
+ {
+ }
+
+ @Override
+ public ByteBuffer getIndirectBuffer()
+ {
+ if (_resource.length() > _maxCachedFileSize)
+ {
+ return null;
+ }
+
+ ByteBuffer buffer = _indirectBuffer.get();
+ if (buffer == null)
+ {
+ ByteBuffer buffer2 = CachedContentFactory.this.getIndirectBuffer(_resource);
+ if (buffer2 == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Could not load indirect buffer from " + this);
+ return null;
+ }
+
+ if (_indirectBuffer.compareAndSet(null, buffer2))
+ {
+ buffer = buffer2;
+ if (_cachedSize.addAndGet(BufferUtil.length(buffer)) > _maxCacheSize)
+ shrinkCache();
+ }
+ else
+ {
+ buffer = _indirectBuffer.get();
+ }
+ }
+ return buffer == null ? null : buffer.asReadOnlyBuffer();
+ }
+
+ @Override
+ public ByteBuffer getDirectBuffer()
+ {
+ ByteBuffer buffer = _mappedBuffer.get();
+ if (buffer == null)
+ buffer = _directBuffer.get();
+ if (buffer == null)
+ {
+ ByteBuffer mapped = CachedContentFactory.this.getMappedBuffer(_resource);
+ if (mapped != null)
+ {
+ if (_mappedBuffer.compareAndSet(null, mapped))
+ buffer = mapped;
+ else
+ buffer = _mappedBuffer.get();
+ }
+ // Since MappedBuffers don't use heap, we don't care about the resource.length
+ else if (_resource.length() < _maxCachedFileSize)
+ {
+ ByteBuffer direct = CachedContentFactory.this.getDirectBuffer(_resource);
+ if (direct != null)
+ {
+ if (_directBuffer.compareAndSet(null, direct))
+ {
+ buffer = direct;
+ if (_cachedSize.addAndGet(BufferUtil.length(buffer)) > _maxCacheSize)
+ shrinkCache();
+ }
+ else
+ {
+ buffer = _directBuffer.get();
+ }
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Could not load " + this);
+ }
+ }
+ }
+ return buffer == null ? null : buffer.asReadOnlyBuffer();
+ }
+
+ @Override
+ public HttpField getContentLength()
+ {
+ return _contentLength;
+ }
+
+ @Override
+ public long getContentLengthValue()
+ {
+ return _contentLengthValue;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException
+ {
+ ByteBuffer indirect = getIndirectBuffer();
+ if (indirect != null && indirect.hasArray())
+ return new ByteArrayInputStream(indirect.array(), indirect.arrayOffset() + indirect.position(), indirect.remaining());
+
+ return _resource.getInputStream();
+ }
+
+ @Override
+ public ReadableByteChannel getReadableByteChannel() throws IOException
+ {
+ return _resource.getReadableByteChannel();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("CachedContent@%x{r=%s,e=%b,lm=%s,ct=%s,c=%d}", hashCode(), _resource, _resource.exists(), _lastModified, _contentType, _precompressed.size());
+ }
+
+ @Override
+ public Map<CompressedContentFormat, ? extends HttpContent> getPrecompressedContents()
+ {
+ if (_precompressed.size() == 0)
+ return null;
+ Map<CompressedContentFormat, CachedPrecompressedHttpContent> ret = _precompressed;
+ for (Map.Entry<CompressedContentFormat, CachedPrecompressedHttpContent> entry : _precompressed.entrySet())
+ {
+ if (!entry.getValue().isValid())
+ {
+ if (ret == _precompressed)
+ ret = new HashMap<>(_precompressed);
+ ret.remove(entry.getKey());
+ }
+ }
+ return ret;
+ }
+ }
+
+ public class CachedPrecompressedHttpContent extends PrecompressedHttpContent
+ {
+ private final CachedHttpContent _content;
+ private final CachedHttpContent _precompressedContent;
+ private final HttpField _etag;
+
+ CachedPrecompressedHttpContent(CachedHttpContent content, CachedHttpContent precompressedContent, CompressedContentFormat format)
+ {
+ super(content, precompressedContent, format);
+ _content = content;
+ _precompressedContent = precompressedContent;
+
+ _etag = (CachedContentFactory.this._etags) ? new PreEncodedHttpField(HttpHeader.ETAG, _content.getResource().getWeakETag(format.getEtagSuffix())) : null;
+ }
+
+ public boolean isValid()
+ {
+ return _precompressedContent.isValid() && _content.isValid() && _content.getResource().lastModified() <= _precompressedContent.getResource().lastModified();
+ }
+
+ @Override
+ public HttpField getETag()
+ {
+ if (_etag != null)
+ return _etag;
+ return super.getETag();
+ }
+
+ @Override
+ public String getETagValue()
+ {
+ if (_etag != null)
+ return _etag.getValue();
+ return super.getETagValue();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Cached" + super.toString();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ClassLoaderDump.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ClassLoaderDump.java
new file mode 100644
index 0000000..71cae41
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ClassLoaderDump.java
@@ -0,0 +1,79 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.net.URLClassLoader;
+
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.component.DumpableCollection;
+
+public class ClassLoaderDump implements Dumpable
+{
+ final ClassLoader _loader;
+
+ public ClassLoaderDump(ClassLoader loader)
+ {
+ _loader = loader;
+ }
+
+ @Override
+ public String dump()
+ {
+ return Dumpable.dump(this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ if (_loader == null)
+ out.append("No ClassLoader\n");
+ else if (_loader instanceof Dumpable)
+ {
+ ((Dumpable)_loader).dump(out, indent);
+ }
+ else if (_loader instanceof URLClassLoader)
+ {
+ String loader = _loader.toString();
+ DumpableCollection urls = DumpableCollection.fromArray("URLs", ((URLClassLoader)_loader).getURLs());
+ ClassLoader parent = _loader.getParent();
+ if (parent == null)
+ Dumpable.dumpObjects(out, indent, loader, urls);
+ else if (parent == Server.class.getClassLoader())
+ Dumpable.dumpObjects(out, indent, loader, urls, parent.toString());
+ else if (parent instanceof Dumpable)
+ Dumpable.dumpObjects(out, indent, loader, urls, parent);
+ else
+ Dumpable.dumpObjects(out, indent, loader, urls, new ClassLoaderDump(parent));
+ }
+ else
+ {
+ String loader = _loader.toString();
+ ClassLoader parent = _loader.getParent();
+ if (parent == null)
+ Dumpable.dumpObject(out, loader);
+ if (parent == Server.class.getClassLoader())
+ Dumpable.dumpObjects(out, indent, loader, parent.toString());
+ else if (parent instanceof Dumpable)
+ Dumpable.dumpObjects(out, indent, loader, parent);
+ else if (parent != null)
+ Dumpable.dumpObjects(out, indent, loader, new ClassLoaderDump(parent));
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java
new file mode 100644
index 0000000..c078576
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java
@@ -0,0 +1,128 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.MetaData;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+
+/**
+ * A Factory to create {@link Connection} instances for {@link Connector}s.
+ * <p>
+ * A Connection factory is responsible for instantiating and configuring a {@link Connection} instance
+ * to handle an {@link EndPoint} accepted by a {@link Connector}.
+ * <p>
+ * A ConnectionFactory has a protocol name that represents the protocol of the Connections
+ * created. Example of protocol names include:
+ * <dl>
+ * <dt>http</dt><dd>Creates an HTTP connection that can handle multiple versions of HTTP from 0.9 to 1.1</dd>
+ * <dt>h2</dt><dd>Creates an HTTP/2 connection that handles the HTTP/2 protocol</dd>
+ * <dt>SSL-XYZ</dt><dd>Create an SSL connection chained to a connection obtained from a connection factory
+ * with a protocol "XYZ".</dd>
+ * <dt>SSL-http</dt><dd>Create an SSL connection chained to an HTTP connection (aka https)</dd>
+ * <dt>SSL-ALPN</dt><dd>Create an SSL connection chained to a ALPN connection, that uses a negotiation with
+ * the client to determine the next protocol.</dd>
+ * </dl>
+ */
+public interface ConnectionFactory
+{
+
+ /**
+ * @return A string representing the primary protocol name.
+ */
+ String getProtocol();
+
+ /**
+ * @return A list of alternative protocol names/versions including the primary protocol.
+ */
+ List<String> getProtocols();
+
+ /**
+ * <p>Creates a new {@link Connection} with the given parameters</p>
+ *
+ * @param connector The {@link Connector} creating this connection
+ * @param endPoint the {@link EndPoint} associated with the connection
+ * @return a new {@link Connection}
+ */
+ Connection newConnection(Connector connector, EndPoint endPoint);
+
+ interface Upgrading extends ConnectionFactory
+ {
+
+ /**
+ * Create a connection for an upgrade request.
+ * <p>This is a variation of {@link #newConnection(Connector, EndPoint)} that can create (and/or customise)
+ * a connection for an upgrade request. Implementations may call {@link #newConnection(Connector, EndPoint)} or
+ * may construct the connection instance themselves.</p>
+ *
+ * @param connector The connector to upgrade for.
+ * @param endPoint The endpoint of the connection.
+ * @param upgradeRequest The meta data of the upgrade request.
+ * @param responseFields The fields to be sent with the 101 response
+ * @return Null to indicate that request processing should continue normally without upgrading. A new connection instance to
+ * indicate that the upgrade should proceed.
+ * @throws BadMessageException Thrown to indicate the upgrade attempt was illegal and that a bad message response should be sent.
+ */
+ Connection upgradeConnection(Connector connector, EndPoint endPoint, MetaData.Request upgradeRequest, HttpFields responseFields) throws BadMessageException;
+ }
+
+ /**
+ * <p>Connections created by this factory MUST implement {@link Connection.UpgradeTo}.</p>
+ */
+ interface Detecting extends ConnectionFactory
+ {
+ /**
+ * The possible outcomes of the {@link #detect(ByteBuffer)} method.
+ */
+ enum Detection
+ {
+ /**
+ * A {@link Detecting} can work with the given bytes.
+ */
+ RECOGNIZED,
+ /**
+ * A {@link Detecting} cannot work with the given bytes.
+ */
+ NOT_RECOGNIZED,
+ /**
+ * A {@link Detecting} requires more bytes to make a decision.
+ */
+ NEED_MORE_BYTES
+ }
+
+ /**
+ * <p>Check the bytes in the given {@code buffer} to figure out if this {@link Detecting} instance
+ * can work with them or not.</p>
+ * <p>The {@code buffer} MUST be left untouched by this method: bytes MUST NOT be consumed and MUST NOT be modified.</p>
+ * @param buffer the buffer.
+ * @return One of:
+ * <ul>
+ * <li>{@link Detection#RECOGNIZED} if this {@link Detecting} instance can work with the bytes in the buffer</li>
+ * <li>{@link Detection#NOT_RECOGNIZED} if this {@link Detecting} instance cannot work with the bytes in the buffer</li>
+ * <li>{@link Detection#NEED_MORE_BYTES} if this {@link Detecting} instance requires more bytes to make a decision</li>
+ * </ul>
+ */
+ Detection detect(ByteBuffer buffer);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionLimit.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionLimit.java
new file mode 100644
index 0000000..3f33cd7
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionLimit.java
@@ -0,0 +1,282 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.nio.channels.SelectableChannel;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.Connection.Listener;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.SelectorManager;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.Name;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.Container;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * <p>A Listener that limits the number of Connections.</p>
+ * <p>This listener applies a limit to the number of connections, which when
+ * exceeded results in a call to {@link AbstractConnector#setAccepting(boolean)}
+ * to prevent further connections being received. It can be applied to an
+ * entire server or to a specific connector by adding it via {@link Container#addBean(Object)}
+ * </p>
+ * <p>
+ * <b>Usage:</b>
+ * </p>
+ * <pre>
+ * Server server = new Server();
+ * server.addBean(new ConnectionLimit(5000,server));
+ * ...
+ * server.start();
+ * </pre>
+ *
+ * @see LowResourceMonitor
+ * @see Connection.Listener
+ * @see SelectorManager.AcceptListener
+ */
+@ManagedObject
+public class ConnectionLimit extends AbstractLifeCycle implements Listener, SelectorManager.AcceptListener
+{
+ private static final Logger LOG = Log.getLogger(ConnectionLimit.class);
+
+ private final Server _server;
+ private final List<AbstractConnector> _connectors = new ArrayList<>();
+ private final Set<SelectableChannel> _accepting = new HashSet<>();
+ private int _connections;
+ private int _maxConnections;
+ private long _idleTimeout;
+ private boolean _limiting = false;
+
+ public ConnectionLimit(@Name("maxConnections") int maxConnections, @Name("server") Server server)
+ {
+ _maxConnections = maxConnections;
+ _server = server;
+ }
+
+ public ConnectionLimit(@Name("maxConnections") int maxConnections, @Name("connectors") Connector... connectors)
+ {
+ this(maxConnections, (Server)null);
+ for (Connector c : connectors)
+ {
+ if (c instanceof AbstractConnector)
+ _connectors.add((AbstractConnector)c);
+ else
+ LOG.warn("Connector {} is not an AbstractConnection. Connections not limited", c);
+ }
+ }
+
+ /**
+ * @return If >= 0, the endpoint idle timeout in ms to apply when the connection limit is reached
+ */
+ @ManagedAttribute("The endpoint idle timeout in ms to apply when the connection limit is reached")
+ public long getIdleTimeout()
+ {
+ return _idleTimeout;
+ }
+
+ /**
+ * @param idleTimeout If >= 0 the endpoint idle timeout in ms to apply when the connection limit is reached
+ */
+ public void setIdleTimeout(long idleTimeout)
+ {
+ _idleTimeout = idleTimeout;
+ }
+
+ @ManagedAttribute("The maximum number of connections allowed")
+ public int getMaxConnections()
+ {
+ synchronized (this)
+ {
+ return _maxConnections;
+ }
+ }
+
+ public void setMaxConnections(int max)
+ {
+ synchronized (this)
+ {
+ _maxConnections = max;
+ }
+ }
+
+ @ManagedAttribute("The current number of connections ")
+ public int getConnections()
+ {
+ synchronized (this)
+ {
+ return _connections;
+ }
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ synchronized (this)
+ {
+ if (_server != null)
+ {
+ for (Connector c : _server.getConnectors())
+ {
+ if (c instanceof AbstractConnector)
+ _connectors.add((AbstractConnector)c);
+ else
+ LOG.warn("Connector {} is not an AbstractConnector. Connections not limited", c);
+ }
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("ConnectionLimit {} for {}", _maxConnections, _connectors);
+ _connections = 0;
+ _limiting = false;
+ for (AbstractConnector c : _connectors)
+ {
+ c.addBean(this);
+ }
+ }
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ synchronized (this)
+ {
+ for (AbstractConnector c : _connectors)
+ {
+ c.removeBean(this);
+ }
+ _connections = 0;
+ if (_server != null)
+ _connectors.clear();
+ }
+ }
+
+ protected void check()
+ {
+ if ((_accepting.size() + _connections) >= _maxConnections)
+ {
+ if (!_limiting)
+ {
+ _limiting = true;
+ LOG.info("Connection Limit({}) reached for {}", _maxConnections, _connectors);
+ limit();
+ }
+ }
+ else
+ {
+ if (_limiting)
+ {
+ _limiting = false;
+ LOG.info("Connection Limit({}) cleared for {}", _maxConnections, _connectors);
+ unlimit();
+ }
+ }
+ }
+
+ protected void limit()
+ {
+ for (AbstractConnector c : _connectors)
+ {
+ c.setAccepting(false);
+
+ if (_idleTimeout > 0)
+ {
+ for (EndPoint endPoint : c.getConnectedEndPoints())
+ {
+ endPoint.setIdleTimeout(_idleTimeout);
+ }
+ }
+ }
+ }
+
+ protected void unlimit()
+ {
+ for (AbstractConnector c : _connectors)
+ {
+ c.setAccepting(true);
+
+ if (_idleTimeout > 0)
+ {
+ for (EndPoint endPoint : c.getConnectedEndPoints())
+ {
+ endPoint.setIdleTimeout(c.getIdleTimeout());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onAccepting(SelectableChannel channel)
+ {
+ synchronized (this)
+ {
+ _accepting.add(channel);
+ if (LOG.isDebugEnabled())
+ LOG.debug("onAccepting ({}+{}) < {} {}", _accepting.size(), _connections, _maxConnections, channel);
+ check();
+ }
+ }
+
+ @Override
+ public void onAcceptFailed(SelectableChannel channel, Throwable cause)
+ {
+ synchronized (this)
+ {
+ _accepting.remove(channel);
+ if (LOG.isDebugEnabled())
+ LOG.debug("onAcceptFailed ({}+{}) < {} {} {}", _accepting.size(), _connections, _maxConnections, channel, cause);
+ check();
+ }
+ }
+
+ @Override
+ public void onAccepted(SelectableChannel channel)
+ {
+ }
+
+ @Override
+ public void onOpened(Connection connection)
+ {
+ synchronized (this)
+ {
+ _accepting.remove(connection.getEndPoint().getTransport());
+ _connections++;
+ if (LOG.isDebugEnabled())
+ LOG.debug("onOpened ({}+{}) < {} {}", _accepting.size(), _connections, _maxConnections, connection);
+ check();
+ }
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ synchronized (this)
+ {
+ _connections--;
+ if (LOG.isDebugEnabled())
+ LOG.debug("onClosed ({}+{}) < {} {}", _accepting.size(), _connections, _maxConnections, connection);
+ check();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Connector.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Connector.java
new file mode 100644
index 0000000..a6e9e6a
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Connector.java
@@ -0,0 +1,105 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.Container;
+import org.eclipse.jetty.util.component.Graceful;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * <p>A {@link Connector} accept connections and data from remote peers,
+ * and allows applications to send data to remote peers, by setting up
+ * the machinery needed to handle such tasks.</p>
+ */
+@ManagedObject("Connector Interface")
+public interface Connector extends LifeCycle, Container, Graceful
+{
+ /**
+ * @return the {@link Server} instance associated with this {@link Connector}
+ */
+ Server getServer();
+
+ /**
+ * @return the {@link Executor} used to submit tasks
+ */
+ Executor getExecutor();
+
+ /**
+ * @return the {@link Scheduler} used to schedule tasks
+ */
+ Scheduler getScheduler();
+
+ /**
+ * @return the {@link ByteBufferPool} to acquire buffers from and release buffers to
+ */
+ ByteBufferPool getByteBufferPool();
+
+ /**
+ * @param nextProtocol the next protocol
+ * @return the {@link ConnectionFactory} associated with the protocol name
+ */
+ ConnectionFactory getConnectionFactory(String nextProtocol);
+
+ <T> T getConnectionFactory(Class<T> factoryType);
+
+ /**
+ * @return the default {@link ConnectionFactory} associated with the default protocol name
+ */
+ ConnectionFactory getDefaultConnectionFactory();
+
+ Collection<ConnectionFactory> getConnectionFactories();
+
+ List<String> getProtocols();
+
+ /**
+ * @return the max idle timeout for connections in milliseconds
+ */
+ @ManagedAttribute("maximum time a connection can be idle before being closed (in ms)")
+ long getIdleTimeout();
+
+ /**
+ * @return the underlying socket, channel, buffer etc. for the connector.
+ */
+ Object getTransport();
+
+ /**
+ * @return immutable collection of connected endpoints
+ */
+ Collection<EndPoint> getConnectedEndPoints();
+
+ /**
+ * Get the connector name if set.
+ * <p>A {@link ContextHandler} may be configured with
+ * virtual hosts in the form "@connectorName" and will only serve
+ * requests from the named connector.
+ *
+ * @return The connector name or null.
+ */
+ String getName();
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectorStatistics.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectorStatistics.java
new file mode 100644
index 0000000..fd82a0b
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectorStatistics.java
@@ -0,0 +1,312 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.LongAdder;
+
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.Container;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.statistic.CounterStatistic;
+import org.eclipse.jetty.util.statistic.SampleStatistic;
+
+/**
+ * A Connector.Listener that gathers Connector and Connections Statistics.
+ * Adding an instance of this class as with {@link AbstractConnector#addBean(Object)}
+ * will register the listener with all connections accepted by that connector.
+ *
+ * @deprecated use {@link org.eclipse.jetty.io.ConnectionStatistics} instead.
+ */
+@Deprecated
+@ManagedObject("Connector Statistics")
+public class ConnectorStatistics extends AbstractLifeCycle implements Dumpable, Connection.Listener
+{
+ private static final Sample ZERO = new Sample();
+ private final AtomicLong _startMillis = new AtomicLong(-1L);
+ private final CounterStatistic _connectionStats = new CounterStatistic();
+ private final SampleStatistic _messagesIn = new SampleStatistic();
+ private final SampleStatistic _messagesOut = new SampleStatistic();
+ private final SampleStatistic _connectionDurationStats = new SampleStatistic();
+ private final ConcurrentMap<Connection, Sample> _samples = new ConcurrentHashMap<>();
+ private final LongAdder _closedIn = new LongAdder();
+ private final LongAdder _closedOut = new LongAdder();
+ private AtomicLong _nanoStamp = new AtomicLong();
+ private volatile int _messagesInPerSecond;
+ private volatile int _messagesOutPerSecond;
+
+ @Override
+ public void onOpened(Connection connection)
+ {
+ if (isStarted())
+ {
+ _connectionStats.increment();
+ _samples.put(connection, ZERO);
+ }
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ if (isStarted())
+ {
+ long msgsIn = connection.getMessagesIn();
+ long msgsOut = connection.getMessagesOut();
+ _messagesIn.record(msgsIn);
+ _messagesOut.record(msgsOut);
+ _connectionStats.decrement();
+ _connectionDurationStats.record(System.currentTimeMillis() - connection.getCreatedTimeStamp());
+
+ Sample sample = _samples.remove(connection);
+ if (sample != null)
+ {
+ _closedIn.add(msgsIn - sample._messagesIn);
+ _closedOut.add(msgsOut - sample._messagesOut);
+ }
+ }
+ }
+
+ @ManagedAttribute("Total number of bytes received by this connector")
+ public int getBytesIn()
+ {
+ // TODO
+ return -1;
+ }
+
+ @ManagedAttribute("Total number of bytes sent by this connector")
+ public int getBytesOut()
+ {
+ // TODO
+ return -1;
+ }
+
+ @ManagedAttribute("Total number of connections seen by this connector")
+ public int getConnections()
+ {
+ return (int)_connectionStats.getTotal();
+ }
+
+ @ManagedAttribute("Connection duration maximum in ms")
+ public long getConnectionDurationMax()
+ {
+ return _connectionDurationStats.getMax();
+ }
+
+ @ManagedAttribute("Connection duration mean in ms")
+ public double getConnectionDurationMean()
+ {
+ return _connectionDurationStats.getMean();
+ }
+
+ @ManagedAttribute("Connection duration standard deviation")
+ public double getConnectionDurationStdDev()
+ {
+ return _connectionDurationStats.getStdDev();
+ }
+
+ @ManagedAttribute("Messages In for all connections")
+ public int getMessagesIn()
+ {
+ return (int)_messagesIn.getTotal();
+ }
+
+ @ManagedAttribute("Messages In per connection maximum")
+ public int getMessagesInPerConnectionMax()
+ {
+ return (int)_messagesIn.getMax();
+ }
+
+ @ManagedAttribute("Messages In per connection mean")
+ public double getMessagesInPerConnectionMean()
+ {
+ return _messagesIn.getMean();
+ }
+
+ @ManagedAttribute("Messages In per connection standard deviation")
+ public double getMessagesInPerConnectionStdDev()
+ {
+ return _messagesIn.getStdDev();
+ }
+
+ @ManagedAttribute("Connections open")
+ public int getConnectionsOpen()
+ {
+ return (int)_connectionStats.getCurrent();
+ }
+
+ @ManagedAttribute("Connections open maximum")
+ public int getConnectionsOpenMax()
+ {
+ return (int)_connectionStats.getMax();
+ }
+
+ @ManagedAttribute("Messages Out for all connections")
+ public int getMessagesOut()
+ {
+ return (int)_messagesIn.getTotal();
+ }
+
+ @ManagedAttribute("Messages In per connection maximum")
+ public int getMessagesOutPerConnectionMax()
+ {
+ return (int)_messagesIn.getMax();
+ }
+
+ @ManagedAttribute("Messages In per connection mean")
+ public double getMessagesOutPerConnectionMean()
+ {
+ return _messagesIn.getMean();
+ }
+
+ @ManagedAttribute("Messages In per connection standard deviation")
+ public double getMessagesOutPerConnectionStdDev()
+ {
+ return _messagesIn.getStdDev();
+ }
+
+ @ManagedAttribute("Connection statistics started ms since epoch")
+ public long getStartedMillis()
+ {
+ long start = _startMillis.get();
+ return start < 0 ? 0 : System.currentTimeMillis() - start;
+ }
+
+ @ManagedAttribute("Messages in per second calculated over period since last called")
+ public int getMessagesInPerSecond()
+ {
+ update();
+ return _messagesInPerSecond;
+ }
+
+ @ManagedAttribute("Messages out per second calculated over period since last called")
+ public int getMessagesOutPerSecond()
+ {
+ update();
+ return _messagesOutPerSecond;
+ }
+
+ @Override
+ public void doStart()
+ {
+ reset();
+ }
+
+ @Override
+ public void doStop()
+ {
+ _samples.clear();
+ }
+
+ @ManagedOperation("Reset the statistics")
+ public void reset()
+ {
+ _startMillis.set(System.currentTimeMillis());
+ _messagesIn.reset();
+ _messagesOut.reset();
+ _connectionStats.reset();
+ _connectionDurationStats.reset();
+ _samples.clear();
+ }
+
+ @Override
+ @ManagedOperation("dump thread state")
+ public String dump()
+ {
+ return Dumpable.dump(this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ Dumpable.dumpObjects(out, indent, this,
+ "connections=" + _connectionStats,
+ "duration=" + _connectionDurationStats,
+ "in=" + _messagesIn,
+ "out=" + _messagesOut);
+ }
+
+ public static void addToAllConnectors(Server server)
+ {
+ for (Connector connector : server.getConnectors())
+ {
+ if (connector instanceof Container)
+ connector.addBean(new ConnectorStatistics());
+ }
+ }
+
+ private static final long SECOND_NANOS = TimeUnit.SECONDS.toNanos(1);
+
+ private synchronized void update()
+ {
+ long now = System.nanoTime();
+ long then = _nanoStamp.get();
+ long duration = now - then;
+
+ if (duration > SECOND_NANOS / 2)
+ {
+ if (_nanoStamp.compareAndSet(then, now))
+ {
+ long msgsIn = _closedIn.sumThenReset();
+ long msgsOut = _closedOut.sumThenReset();
+
+ for (Map.Entry<Connection, Sample> entry : _samples.entrySet())
+ {
+ Connection connection = entry.getKey();
+ Sample sample = entry.getValue();
+ Sample next = new Sample(connection);
+ if (_samples.replace(connection, sample, next))
+ {
+ msgsIn += next._messagesIn - sample._messagesIn;
+ msgsOut += next._messagesOut - sample._messagesOut;
+ }
+ }
+
+ _messagesInPerSecond = (int)(msgsIn * SECOND_NANOS / duration);
+ _messagesOutPerSecond = (int)(msgsOut * SECOND_NANOS / duration);
+ }
+ }
+ }
+
+ private static class Sample
+ {
+ Sample()
+ {
+ _messagesIn = 0;
+ _messagesOut = 0;
+ }
+
+ Sample(Connection connection)
+ {
+ _messagesIn = connection.getMessagesIn();
+ _messagesOut = connection.getMessagesOut();
+ }
+
+ final long _messagesIn;
+ final long _messagesOut;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java
new file mode 100644
index 0000000..6d72c3e
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java
@@ -0,0 +1,417 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import javax.servlet.http.Cookie;
+
+import org.eclipse.jetty.http.CookieCompliance;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Cookie parser
+ * <p>
+ * Optimized stateful {@code Cookie} header parser.
+ * Does not support {@code Set-Cookie} header parsing.
+ * </p>
+ * <p>
+ * Cookies fields are added with the {@link #addCookieField(String)} method and
+ * parsed on the next subsequent call to {@link #getCookies()}.
+ * </p>
+ * <p>
+ * If the added fields are identical to those last added (as strings), then the
+ * cookies are not re parsed.
+ * </p>
+ */
+public class CookieCutter
+{
+ private static final Logger LOG = Log.getLogger(CookieCutter.class);
+
+ private final CookieCompliance _compliance;
+ private Cookie[] _cookies;
+ private Cookie[] _lastCookies;
+ private final List<String> _fieldList = new ArrayList<>();
+ int _fields;
+
+ public CookieCutter()
+ {
+ this(CookieCompliance.RFC6265);
+ }
+
+ public CookieCutter(CookieCompliance compliance)
+ {
+ _compliance = compliance;
+ }
+
+ public Cookie[] getCookies()
+ {
+ if (_cookies != null)
+ return _cookies;
+
+ if (_lastCookies != null && _fields == _fieldList.size())
+ _cookies = _lastCookies;
+ else
+ parseFields();
+ _lastCookies = _cookies;
+ return _cookies;
+ }
+
+ public void setCookies(Cookie[] cookies)
+ {
+ _cookies = cookies;
+ _lastCookies = null;
+ _fieldList.clear();
+ _fields = 0;
+ }
+
+ public void reset()
+ {
+ _cookies = null;
+ _fields = 0;
+ }
+
+ public void addCookieField(String f)
+ {
+ if (f == null)
+ return;
+ f = f.trim();
+ if (f.length() == 0)
+ return;
+
+ if (_fieldList.size() > _fields)
+ {
+ if (f.equals(_fieldList.get(_fields)))
+ {
+ _fields++;
+ return;
+ }
+
+ while (_fieldList.size() > _fields)
+ {
+ _fieldList.remove(_fields);
+ }
+ }
+ _cookies = null;
+ _lastCookies = null;
+ _fieldList.add(_fields++, f);
+ }
+
+ protected void parseFields()
+ {
+ _lastCookies = null;
+ _cookies = null;
+
+ List<Cookie> cookies = new ArrayList<>();
+
+ int version = 0;
+
+ // delete excess fields
+ while (_fieldList.size() > _fields)
+ {
+ _fieldList.remove(_fields);
+ }
+
+ StringBuilder unquoted = null;
+
+ // For each cookie field
+ for (String hdr : _fieldList)
+ {
+ // Parse the header
+ String name = null;
+
+ Cookie cookie = null;
+
+ boolean invalue = false;
+ boolean inQuoted = false;
+ boolean quoted = false;
+ boolean escaped = false;
+ boolean reject = false;
+ int tokenstart = -1;
+ int tokenend = -1;
+ for (int i = 0, length = hdr.length(); i <= length; i++)
+ {
+ char c = i == length ? 0 : hdr.charAt(i);
+
+ // Handle quoted values for name or value
+ if (inQuoted)
+ {
+ if (escaped)
+ {
+ escaped = false;
+ if (c > 0)
+ unquoted.append(c);
+ else
+ {
+ unquoted.setLength(0);
+ inQuoted = false;
+ i--;
+ }
+ continue;
+ }
+
+ switch (c)
+ {
+ case '"':
+ inQuoted = false;
+ quoted = true;
+ tokenstart = i;
+ tokenend = -1;
+ break;
+
+ case '\\':
+ escaped = true;
+ continue;
+
+ case 0:
+ // unterminated quote, let's ignore quotes
+ unquoted.setLength(0);
+ inQuoted = false;
+ i--;
+ continue;
+
+ default:
+ unquoted.append(c);
+ continue;
+ }
+ }
+ else
+ {
+ // Handle name and value state machines
+ if (invalue)
+ {
+ // parse the cookie-value
+ switch (c)
+ {
+ case ' ':
+ case '\t':
+ break;
+
+ case ',':
+ if (_compliance != CookieCompliance.RFC2965)
+ {
+ if (quoted)
+ {
+ // must have been a bad internal quote. let's fix as best we can
+ unquoted.append(hdr, tokenstart, i--);
+ inQuoted = true;
+ quoted = false;
+ continue;
+ }
+ if (tokenstart < 0)
+ tokenstart = i;
+ tokenend = i;
+ continue;
+ }
+ // fall through
+ case 0:
+ case ';':
+ {
+ String value;
+
+ if (quoted)
+ {
+ value = unquoted.toString();
+ unquoted.setLength(0);
+ quoted = false;
+ }
+ else if (tokenstart >= 0)
+ value = tokenend >= tokenstart ? hdr.substring(tokenstart, tokenend + 1) : hdr.substring(tokenstart);
+ else
+ value = "";
+
+ try
+ {
+ if (name.startsWith("$"))
+ {
+ if (_compliance == CookieCompliance.RFC2965)
+ {
+ String lowercaseName = name.toLowerCase(Locale.ENGLISH);
+ switch (lowercaseName)
+ {
+ case "$path":
+ if (cookie != null)
+ cookie.setPath(value);
+ break;
+ case "$domain":
+ if (cookie != null)
+ cookie.setDomain(value);
+ break;
+ case "$port":
+ if (cookie != null)
+ cookie.setComment("$port=" + value);
+ break;
+ case "$version":
+ version = Integer.parseInt(value);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ else
+ {
+ cookie = new Cookie(name, value);
+ if (version > 0)
+ cookie.setVersion(version);
+ if (!reject)
+ {
+ cookies.add(cookie);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.debug(e);
+ }
+
+ name = null;
+ tokenstart = -1;
+ invalue = false;
+ reject = false;
+
+ break;
+ }
+
+ case '"':
+ if (tokenstart < 0)
+ {
+ tokenstart = i;
+ inQuoted = true;
+ if (unquoted == null)
+ unquoted = new StringBuilder();
+ break;
+ }
+ // fall through to default case
+
+ default:
+ if (quoted)
+ {
+ // must have been a bad internal quote. let's fix as best we can
+ unquoted.append(hdr, tokenstart, i--);
+ inQuoted = true;
+ quoted = false;
+ continue;
+ }
+
+ if (_compliance == CookieCompliance.RFC6265)
+ {
+ if (isRFC6265RejectedCharacter(inQuoted, c))
+ {
+ reject = true;
+ }
+ }
+
+ if (tokenstart < 0)
+ tokenstart = i;
+ tokenend = i;
+ continue;
+ }
+ }
+ else
+ {
+ // parse the cookie-name
+ switch (c)
+ {
+ case ' ':
+ case '\t':
+ continue;
+
+ case ';':
+ // a cookie terminated with no '=' sign.
+ tokenstart = -1;
+ invalue = false;
+ reject = false;
+ continue;
+
+ case '=':
+ if (quoted)
+ {
+ name = unquoted.toString();
+ unquoted.setLength(0);
+ quoted = false;
+ }
+ else if (tokenstart >= 0)
+ name = tokenend >= tokenstart ? hdr.substring(tokenstart, tokenend + 1) : hdr.substring(tokenstart);
+
+ tokenstart = -1;
+ invalue = true;
+ break;
+
+ default:
+ if (quoted)
+ {
+ // must have been a bad internal quote. let's fix as best we can
+ unquoted.append(hdr, tokenstart, i--);
+ inQuoted = true;
+ quoted = false;
+ continue;
+ }
+
+ if (_compliance == CookieCompliance.RFC6265)
+ {
+ if (isRFC6265RejectedCharacter(inQuoted, c))
+ {
+ reject = true;
+ }
+ }
+
+ if (tokenstart < 0)
+ tokenstart = i;
+ tokenend = i;
+ continue;
+ }
+ }
+ }
+ }
+ }
+
+ _cookies = cookies.toArray(new Cookie[0]);
+ _lastCookies = _cookies;
+ }
+
+ protected boolean isRFC6265RejectedCharacter(boolean inQuoted, char c)
+ {
+ if (inQuoted)
+ {
+ // We only reject if a Control Character is encountered
+ if (Character.isISOControl(c))
+ {
+ return true;
+ }
+ }
+ else
+ {
+ /* From RFC6265 - Section 4.1.1 - Syntax
+ * cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
+ * ; US-ASCII characters excluding CTLs,
+ * ; whitespace DQUOTE, comma, semicolon,
+ * ; and backslash
+ */
+ return Character.isISOControl(c) || // control characters
+ c > 127 || // 8-bit characters
+ c == ',' || // comma
+ c == ';'; // semicolon
+ }
+
+ return false;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java
new file mode 100644
index 0000000..f1df36a
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java
@@ -0,0 +1,1246 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.QuotedCSV;
+import org.eclipse.jetty.http.pathmap.PathMappings;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.DateCache;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+import static java.lang.invoke.MethodHandles.dropArguments;
+import static java.lang.invoke.MethodHandles.foldArguments;
+import static java.lang.invoke.MethodType.methodType;
+
+/**
+ * A flexible RequestLog, which produces log strings in a customizable format.
+ * The Logger takes a format string where request characteristics can be added using "%" format codes which are
+ * replaced by the corresponding value in the log output.
+ * <p>
+ * The terms server, client, local and remote are used to refer to the different addresses and ports
+ * which can be logged. Server and client refer to the logical addresses which can be modified in the request
+ * headers. Where local and remote refer to the physical addresses which may be a proxy between the
+ * end-user and the server.
+ *
+ *
+ * <br><br>Percent codes are specified in the format %MODIFIERS{PARAM}CODE
+ * <pre>
+ * MODIFIERS:
+ * Optional list of comma separated HTTP status codes which may be preceded by a single "!" to indicate
+ * negation. If the status code is not in the list the literal string "-" will be logged instead of
+ * the resulting value from the percent code.
+ * {PARAM}:
+ * Parameter string which may be optional depending on the percent code used.
+ * CODE:
+ * A one or two character code specified by the {@link CustomRequestLog} table of format codes.
+ * </pre>
+ *
+ * <table>
+ * <caption>Format Codes</caption>
+ * <tr>
+ * <td><b>Format String</b></td>
+ * <td><b>Description</b></td>
+ * </tr>
+ *
+ * <tr>
+ * <td>%%</td>
+ * <td>The percent sign.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%{format}a</td>
+ * <td>
+ * Address or Hostname. Valid formats are {server, client, local, remote}
+ * Optional format parameter which will be server by default.
+ * <br>
+ * Where server and client are the logical addresses which can be modified in the request headers, while local and
+ * remote are the physical addresses so may be a proxy between the end-user and the server.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%{format}p</td>
+ * <td>
+ * Port. Valid formats are {server, client, local, remote}
+ * Optional format parameter which will be server by default.
+ * <br>
+ * Where server and client are the logical ports which can be modified in the request headers, while local and
+ * remote are the physical ports so may be to a proxy between the end-user and the server.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%{CLF}I</td>
+ * <td>
+ * Size of request in bytes, excluding HTTP headers.
+ * Optional parameter with value of "CLF" to use CLF format, i.e. a '-' rather than a 0 when no bytes are sent.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%{CLF}O</td>
+ * <td>
+ * Size of response in bytes, excluding HTTP headers.
+ * Optional parameter with value of "CLF" to use CLF format, i.e. a '-' rather than a 0 when no bytes are sent.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%{CLF}S</td>
+ * <td>
+ * Bytes transferred (received and sent). This is the combination of %I and %O.
+ * Optional parameter with value of "CLF" to use CLF format, i.e. a '-' rather than a 0 when no bytes are sent.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%{VARNAME}C</td>
+ * <td>
+ * The contents of cookie VARNAME in the request sent to the server. Only version 0 cookies are fully supported.
+ * Optional VARNAME parameter, without this parameter %C will log all cookies from the request.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%D</td>
+ * <td>The time taken to serve the request, in microseconds.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%{VARNAME}e</td>
+ * <td>The contents of the environment variable VARNAME.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%f</td>
+ * <td>Filename.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%H</td>
+ * <td>The name and version of the request protocol, such as "HTTP/1.1".</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%{VARNAME}i</td>
+ * <td>The contents of VARNAME: header line(s) in the request sent to the server.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%k</td>
+ * <td>Number of keepalive requests handled on this connection.
+ * Interesting if KeepAlive is being used, so that, for example, a '1' means the first keepalive request
+ * after the initial one, '2' the second, etc...; otherwise this is always 0 (indicating the initial request).</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%m</td>
+ * <td>The request method.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%{VARNAME}o</td>
+ * <td>The contents of VARNAME: header line(s) in the response.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%q</td>
+ * <td>The query string (prepended with a ? if a query string exists, otherwise an empty string).</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%r</td>
+ * <td>First line of request.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%R</td>
+ * <td>The handler generating the response (if any).</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%s</td>
+ * <td>Response status.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%{format|timeZone|locale}t</td>
+ * <td>
+ * The time that the request was received.
+ * Optional parameter in one of the following formats {format}, {format|timeZone} or {format|timeZone|locale}.<br><br>
+ *
+ * <pre>
+ * Format Parameter: (default format [18/Sep/2011:19:18:28 -0400] where the last number indicates the timezone offset from GMT.)
+ * Must be in a format supported by {@link DateCache}
+ *
+ * TimeZone Parameter:
+ * Default timeZone GMT
+ * Must be in a format supported by {@link TimeZone#getTimeZone(String)}
+ *
+ * Locale Parameter:
+ * Default locale {@link Locale#getDefault()}
+ * Must be in a format supported by {@link Locale#forLanguageTag(String)}</pre>
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%T</td>
+ * <td>The time taken to serve the request, in seconds.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%{UNIT}T</td>
+ * <td>The time taken to serve the request, in a time unit given by UNIT.
+ * Valid units are ms for milliseconds, us for microseconds, and s for seconds.
+ * Using s gives the same result as %T without any format; using us gives the same result as %D.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%{d}u</td>
+ * <td>
+ * Remote user if the request was authenticated with servlet authentication. May be bogus if return status (%s) is 401 (unauthorized).
+ * Optional parameter d, with this parameter deferred authentication will also be checked,
+ * this is equivalent to {@link HttpServletRequest#getRemoteUser()}.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%U</td>
+ * <td>The URL path requested, not including any query string.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%X</td>
+ * <td>
+ * Connection status when response is completed:
+ * <pre>
+ * X = Connection aborted before the response completed.
+ * + = Connection may be kept alive after the response is sent.
+ * - = Connection will be closed after the response is sent.</pre>
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td valign="top">%{VARNAME}^ti</td>
+ * <td>The contents of VARNAME: trailer line(s) in the request sent to the server.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td>%{VARNAME}^to</td>
+ * <td>The contents of VARNAME: trailer line(s) in the response sent from the server.</td>
+ * </tr>
+ * </table>
+ */
+@ManagedObject("Custom format request log")
+public class CustomRequestLog extends ContainerLifeCycle implements RequestLog
+{
+ protected static final Logger LOG = Log.getLogger(CustomRequestLog.class);
+
+ public static final String DEFAULT_DATE_FORMAT = "dd/MMM/yyyy:HH:mm:ss ZZZ";
+ public static final String NCSA_FORMAT = "%{client}a - %u %t \"%r\" %s %O";
+ public static final String EXTENDED_NCSA_FORMAT = NCSA_FORMAT + " \"%{Referer}i\" \"%{User-Agent}i\"";
+ private static final ThreadLocal<StringBuilder> _buffers = ThreadLocal.withInitial(() -> new StringBuilder(256));
+
+ private final RequestLog.Writer _requestLogWriter;
+ private final MethodHandle _logHandle;
+ private final String _formatString;
+ private transient PathMappings<String> _ignorePathMap;
+ private String[] _ignorePaths;
+
+ public CustomRequestLog(RequestLog.Writer writer, String formatString)
+ {
+ _formatString = formatString;
+ _requestLogWriter = writer;
+ addBean(_requestLogWriter);
+
+ try
+ {
+ _logHandle = getLogHandle(formatString);
+ }
+ catch (NoSuchMethodException | IllegalAccessException e)
+ {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public CustomRequestLog(String file)
+ {
+ this(file, EXTENDED_NCSA_FORMAT);
+ }
+
+ public CustomRequestLog(String file, String format)
+ {
+ this(new RequestLogWriter(file), format);
+ }
+
+ @ManagedAttribute("The RequestLogWriter")
+ public RequestLog.Writer getWriter()
+ {
+ return _requestLogWriter;
+ }
+
+ /**
+ * Writes the request and response information to the output stream.
+ *
+ * @see org.eclipse.jetty.server.RequestLog#log(Request, Response)
+ */
+ @Override
+ public void log(Request request, Response response)
+ {
+ try
+ {
+ if (_ignorePathMap != null && _ignorePathMap.getMatch(request.getRequestURI()) != null)
+ return;
+
+ StringBuilder sb = _buffers.get();
+ sb.setLength(0);
+
+ _logHandle.invoke(sb, request, response);
+
+ String log = sb.toString();
+ _requestLogWriter.write(log);
+ }
+ catch (Throwable e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ /**
+ * Extract the user authentication
+ *
+ * @param request The request to extract from
+ * @param checkDeferred Whether to check for deferred authentication
+ * @return The string to log for authenticated user.
+ */
+ protected static String getAuthentication(Request request, boolean checkDeferred)
+ {
+ Authentication authentication = request.getAuthentication();
+ if (checkDeferred && authentication instanceof Authentication.Deferred)
+ authentication = ((Authentication.Deferred)authentication).authenticate(request);
+
+ String name = null;
+ if (authentication instanceof Authentication.User)
+ name = ((Authentication.User)authentication).getUserIdentity().getUserPrincipal().getName();
+
+ return name;
+ }
+
+ /**
+ * Set request paths that will not be logged.
+ *
+ * @param ignorePaths array of request paths
+ */
+ public void setIgnorePaths(String[] ignorePaths)
+ {
+ _ignorePaths = ignorePaths;
+ }
+
+ /**
+ * Retrieve the request paths that will not be logged.
+ *
+ * @return array of request paths
+ */
+ public String[] getIgnorePaths()
+ {
+ return _ignorePaths;
+ }
+
+ /**
+ * Retrieve the format string.
+ *
+ * @return the format string
+ */
+ @ManagedAttribute("format string")
+ public String getFormatString()
+ {
+ return _formatString;
+ }
+
+ /**
+ * Set up request logging and open log file.
+ *
+ * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
+ */
+ @Override
+ protected synchronized void doStart() throws Exception
+ {
+ if (_ignorePaths != null && _ignorePaths.length > 0)
+ {
+ _ignorePathMap = new PathMappings<>();
+ for (String ignorePath : _ignorePaths)
+ {
+ _ignorePathMap.put(ignorePath, ignorePath);
+ }
+ }
+ else
+ _ignorePathMap = null;
+
+ super.doStart();
+ }
+
+ private static void append(StringBuilder buf, String s)
+ {
+ if (s == null || s.length() == 0)
+ buf.append('-');
+ else
+ buf.append(s);
+ }
+
+ private static void append(String s, StringBuilder buf)
+ {
+ append(buf, s);
+ }
+
+ private MethodHandle getLogHandle(String formatString) throws NoSuchMethodException, IllegalAccessException
+ {
+ MethodHandles.Lookup lookup = MethodHandles.lookup();
+ MethodHandle append = lookup.findStatic(CustomRequestLog.class, "append", methodType(void.class, String.class, StringBuilder.class));
+ MethodHandle logHandle = lookup.findStatic(CustomRequestLog.class, "logNothing", methodType(void.class, StringBuilder.class, Request.class, Response.class));
+
+ List<Token> tokens = getTokens(formatString);
+ Collections.reverse(tokens);
+
+ for (Token t : tokens)
+ {
+ if (t.isLiteralString())
+ logHandle = updateLogHandle(logHandle, append, t.literal);
+ else if (t.isPercentCode())
+ logHandle = updateLogHandle(logHandle, append, lookup, t.code, t.arg, t.modifiers, t.negated);
+ else
+ throw new IllegalStateException("bad token " + t);
+ }
+
+ return logHandle;
+ }
+
+ private static List<Token> getTokens(String formatString)
+ {
+ /*
+ Extracts literal strings and percent codes out of the format string.
+ We will either match a percent code of the format %MODIFIERS{PARAM}CODE, or a literal string
+ until the next percent code or the end of the formatString is reached.
+
+ where
+ MODIFIERS is an optional comma separated list of numbers.
+ {PARAM} is an optional string parameter to the percent code.
+ CODE is a 1 to 2 character string corresponding to a format code.
+ */
+ final Pattern PATTERN = Pattern.compile("^(?:%(?<MOD>!?[0-9,]+)?(?:\\{(?<ARG>[^}]+)})?(?<CODE>(?:(?:ti)|(?:to)|[a-zA-Z%]))|(?<LITERAL>[^%]+))(?<REMAINING>.*)", Pattern.DOTALL | Pattern.MULTILINE);
+
+ List<Token> tokens = new ArrayList<>();
+ String remaining = formatString;
+ while (remaining.length() > 0)
+ {
+ Matcher m = PATTERN.matcher(remaining);
+ if (m.matches())
+ {
+ if (m.group("CODE") != null)
+ {
+ String code = m.group("CODE");
+ String arg = m.group("ARG");
+ String modifierString = m.group("MOD");
+
+ List<Integer> modifiers = null;
+ boolean negated = false;
+ if (modifierString != null)
+ {
+ if (modifierString.startsWith("!"))
+ {
+ modifierString = modifierString.substring(1);
+ negated = true;
+ }
+
+ modifiers = new QuotedCSV(modifierString)
+ .getValues()
+ .stream()
+ .map(Integer::parseInt)
+ .collect(Collectors.toList());
+ }
+
+ tokens.add(new Token(code, arg, modifiers, negated));
+ }
+ else if (m.group("LITERAL") != null)
+ {
+ String literal = m.group("LITERAL");
+ tokens.add(new Token(literal));
+ }
+ else
+ {
+ throw new IllegalStateException("formatString parsing error");
+ }
+
+ remaining = m.group("REMAINING");
+ }
+ else
+ {
+ throw new IllegalArgumentException("Invalid format string");
+ }
+ }
+
+ return tokens;
+ }
+
+ private static class Token
+ {
+ public final String code;
+ public final String arg;
+ public final List<Integer> modifiers;
+ public final boolean negated;
+
+ public final String literal;
+
+ public Token(String code, String arg, List<Integer> modifiers, boolean negated)
+ {
+ this.code = code;
+ this.arg = arg;
+ this.modifiers = modifiers;
+ this.negated = negated;
+ this.literal = null;
+ }
+
+ public Token(String literal)
+ {
+ this.code = null;
+ this.arg = null;
+ this.modifiers = null;
+ this.negated = false;
+ this.literal = literal;
+ }
+
+ public boolean isLiteralString()
+ {
+ return (literal != null);
+ }
+
+ public boolean isPercentCode()
+ {
+ return (code != null);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static boolean modify(List<Integer> modifiers, Boolean negated, StringBuilder b, Request request, Response response)
+ {
+ if (negated)
+ return !modifiers.contains(response.getStatus());
+ else
+ return modifiers.contains(response.getStatus());
+ }
+
+ private MethodHandle updateLogHandle(MethodHandle logHandle, MethodHandle append, String literal)
+ {
+ return foldArguments(logHandle, dropArguments(dropArguments(append.bindTo(literal), 1, Request.class), 2, Response.class));
+ }
+
+ private MethodHandle updateLogHandle(MethodHandle logHandle, MethodHandle append, MethodHandles.Lookup lookup, String code, String arg, List<Integer> modifiers, boolean negated) throws NoSuchMethodException, IllegalAccessException
+ {
+ MethodType logType = methodType(void.class, StringBuilder.class, Request.class, Response.class);
+ MethodType logTypeArg = methodType(void.class, String.class, StringBuilder.class, Request.class, Response.class);
+
+ //TODO should we throw IllegalArgumentExceptions when given arguments for codes which do not take them
+ MethodHandle specificHandle;
+ switch (code)
+ {
+ case "%":
+ {
+ specificHandle = dropArguments(dropArguments(append.bindTo("%"), 1, Request.class), 2, Response.class);
+ break;
+ }
+
+ case "a":
+ {
+ if (StringUtil.isEmpty(arg))
+ arg = "server";
+
+ String method;
+ switch (arg)
+ {
+ case "server":
+ method = "logServerHost";
+ break;
+
+ case "client":
+ method = "logClientHost";
+ break;
+
+ case "local":
+ method = "logLocalHost";
+ break;
+
+ case "remote":
+ method = "logRemoteHost";
+ break;
+
+ default:
+ throw new IllegalArgumentException("Invalid arg for %a");
+ }
+
+ specificHandle = lookup.findStatic(CustomRequestLog.class, method, logType);
+ break;
+ }
+
+ case "p":
+ {
+ if (StringUtil.isEmpty(arg))
+ arg = "server";
+
+ String method;
+ switch (arg)
+ {
+
+ case "server":
+ method = "logServerPort";
+ break;
+
+ case "client":
+ method = "logClientPort";
+ break;
+
+ case "local":
+ method = "logLocalPort";
+ break;
+
+ case "remote":
+ method = "logRemotePort";
+ break;
+
+ default:
+ throw new IllegalArgumentException("Invalid arg for %p");
+ }
+
+ specificHandle = lookup.findStatic(CustomRequestLog.class, method, logType);
+ break;
+ }
+
+ case "I":
+ {
+ String method;
+ if (StringUtil.isEmpty(arg))
+ method = "logBytesReceived";
+ else if (arg.equalsIgnoreCase("clf"))
+ method = "logBytesReceivedCLF";
+ else
+ throw new IllegalArgumentException("Invalid argument for %I");
+
+ specificHandle = lookup.findStatic(CustomRequestLog.class, method, logType);
+ break;
+ }
+
+ case "O":
+ {
+ String method;
+ if (StringUtil.isEmpty(arg))
+ method = "logBytesSent";
+ else if (arg.equalsIgnoreCase("clf"))
+ method = "logBytesSentCLF";
+ else
+ throw new IllegalArgumentException("Invalid argument for %O");
+
+ specificHandle = lookup.findStatic(CustomRequestLog.class, method, logType);
+ break;
+ }
+
+ case "S":
+ {
+ String method;
+ if (StringUtil.isEmpty(arg))
+ method = "logBytesTransferred";
+ else if (arg.equalsIgnoreCase("clf"))
+ method = "logBytesTransferredCLF";
+ else
+ throw new IllegalArgumentException("Invalid argument for %S");
+
+ specificHandle = lookup.findStatic(CustomRequestLog.class, method, logType);
+ break;
+ }
+
+ case "C":
+ {
+ if (StringUtil.isEmpty(arg))
+ {
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logRequestCookies", logType);
+ }
+ else
+ {
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logRequestCookie", logTypeArg);
+ specificHandle = specificHandle.bindTo(arg);
+ }
+ break;
+ }
+
+ case "D":
+ {
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logLatencyMicroseconds", logType);
+ break;
+ }
+
+ case "e":
+ {
+ if (StringUtil.isEmpty(arg))
+ throw new IllegalArgumentException("No arg for %e");
+
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logEnvironmentVar", logTypeArg);
+ specificHandle = specificHandle.bindTo(arg);
+ break;
+ }
+
+ case "f":
+ {
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logFilename", logType);
+ break;
+ }
+
+ case "H":
+ {
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logRequestProtocol", logType);
+ break;
+ }
+
+ case "i":
+ {
+ if (StringUtil.isEmpty(arg))
+ throw new IllegalArgumentException("No arg for %i");
+
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logRequestHeader", logTypeArg);
+ specificHandle = specificHandle.bindTo(arg);
+ break;
+ }
+
+ case "k":
+ {
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logKeepAliveRequests", logType);
+ break;
+ }
+
+ case "m":
+ {
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logRequestMethod", logType);
+ break;
+ }
+
+ case "o":
+ {
+ if (StringUtil.isEmpty(arg))
+ throw new IllegalArgumentException("No arg for %o");
+
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logResponseHeader", logTypeArg);
+ specificHandle = specificHandle.bindTo(arg);
+ break;
+ }
+
+ case "q":
+ {
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logQueryString", logType);
+ break;
+ }
+
+ case "r":
+ {
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logRequestFirstLine", logType);
+ break;
+ }
+
+ case "R":
+ {
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logRequestHandler", logType);
+ break;
+ }
+
+ case "s":
+ {
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logResponseStatus", logType);
+ break;
+ }
+
+ case "t":
+ {
+ String format = DEFAULT_DATE_FORMAT;
+ TimeZone timeZone = TimeZone.getTimeZone("GMT");
+ Locale locale = Locale.getDefault();
+
+ if (arg != null && !arg.isEmpty())
+ {
+ String[] args = arg.split("\\|");
+ switch (args.length)
+ {
+ case 1:
+ format = args[0];
+ break;
+
+ case 2:
+ format = args[0];
+ timeZone = TimeZone.getTimeZone(args[1]);
+ break;
+
+ case 3:
+ format = args[0];
+ timeZone = TimeZone.getTimeZone(args[1]);
+ locale = Locale.forLanguageTag(args[2]);
+ break;
+
+ default:
+ throw new IllegalArgumentException("Too many \"|\" characters in %t");
+ }
+ }
+
+ DateCache logDateCache = new DateCache(format, locale, timeZone);
+
+ MethodType logTypeDateCache = methodType(void.class, DateCache.class, StringBuilder.class, Request.class, Response.class);
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logRequestTime", logTypeDateCache);
+ specificHandle = specificHandle.bindTo(logDateCache);
+ break;
+ }
+
+ case "T":
+ {
+ if (arg == null)
+ arg = "s";
+
+ String method;
+ switch (arg)
+ {
+ case "s":
+ method = "logLatencySeconds";
+ break;
+ case "us":
+ method = "logLatencyMicroseconds";
+ break;
+ case "ms":
+ method = "logLatencyMilliseconds";
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid arg for %T");
+ }
+
+ specificHandle = lookup.findStatic(CustomRequestLog.class, method, logType);
+ break;
+ }
+
+ case "u":
+ {
+ String method;
+ if (StringUtil.isEmpty(arg))
+ method = "logRequestAuthentication";
+ else if ("d".equals(arg))
+ method = "logRequestAuthenticationWithDeferred";
+ else
+ throw new IllegalArgumentException("Invalid arg for %u: " + arg);
+
+ specificHandle = lookup.findStatic(CustomRequestLog.class, method, logType);
+ break;
+ }
+
+ case "U":
+ {
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logUrlRequestPath", logType);
+ break;
+ }
+
+ case "X":
+ {
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logConnectionStatus", logType);
+ break;
+ }
+
+ case "ti":
+ {
+ if (StringUtil.isEmpty(arg))
+ throw new IllegalArgumentException("No arg for %ti");
+
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logRequestTrailer", logTypeArg);
+ specificHandle = specificHandle.bindTo(arg);
+ break;
+ }
+
+ case "to":
+ {
+ if (StringUtil.isEmpty(arg))
+ throw new IllegalArgumentException("No arg for %to");
+
+ specificHandle = lookup.findStatic(CustomRequestLog.class, "logResponseTrailer", logTypeArg);
+ specificHandle = specificHandle.bindTo(arg);
+ break;
+ }
+
+ default:
+ throw new IllegalArgumentException("Unsupported code %" + code);
+ }
+
+ if (modifiers != null && !modifiers.isEmpty())
+ {
+ MethodHandle dash = updateLogHandle(logHandle, append, "-");
+ MethodHandle log = foldArguments(logHandle, specificHandle);
+
+ MethodHandle modifierTest = lookup.findStatic(CustomRequestLog.class, "modify",
+ methodType(Boolean.TYPE, List.class, Boolean.class, StringBuilder.class, Request.class, Response.class));
+ modifierTest = modifierTest.bindTo(modifiers).bindTo(negated);
+ return MethodHandles.guardWithTest(modifierTest, log, dash);
+ }
+
+ return foldArguments(logHandle, specificHandle);
+ }
+
+ //-----------------------------------------------------------------------------------//
+ @SuppressWarnings("unused")
+ private static void logNothing(StringBuilder b, Request request, Response response)
+ {
+ }
+
+ @SuppressWarnings("unused")
+ private static void logServerHost(StringBuilder b, Request request, Response response)
+ {
+ append(b, request.getServerName());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logClientHost(StringBuilder b, Request request, Response response)
+ {
+ append(b, request.getRemoteHost());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logLocalHost(StringBuilder b, Request request, Response response)
+ {
+ append(b, request.getHttpChannel().getEndPoint().getLocalAddress().getAddress().getHostAddress());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logRemoteHost(StringBuilder b, Request request, Response response)
+ {
+ append(b, request.getHttpChannel().getEndPoint().getRemoteAddress().getAddress().getHostAddress());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logServerPort(StringBuilder b, Request request, Response response)
+ {
+ b.append(request.getServerPort());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logClientPort(StringBuilder b, Request request, Response response)
+ {
+ b.append(request.getRemotePort());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logLocalPort(StringBuilder b, Request request, Response response)
+ {
+ b.append(request.getHttpChannel().getEndPoint().getLocalAddress().getPort());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logRemotePort(StringBuilder b, Request request, Response response)
+ {
+ b.append(request.getHttpChannel().getEndPoint().getRemoteAddress().getPort());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logResponseSize(StringBuilder b, Request request, Response response)
+ {
+ long written = response.getHttpChannel().getBytesWritten();
+ b.append(written);
+ }
+
+ @SuppressWarnings("unused")
+ private static void logResponseSizeCLF(StringBuilder b, Request request, Response response)
+ {
+ long written = response.getHttpChannel().getBytesWritten();
+ if (written == 0)
+ b.append('-');
+ else
+ b.append(written);
+ }
+
+ @SuppressWarnings("unused")
+ private static void logBytesSent(StringBuilder b, Request request, Response response)
+ {
+ b.append(response.getHttpChannel().getBytesWritten());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logBytesSentCLF(StringBuilder b, Request request, Response response)
+ {
+ long sent = response.getHttpChannel().getBytesWritten();
+ if (sent == 0)
+ b.append('-');
+ else
+ b.append(sent);
+ }
+
+ @SuppressWarnings("unused")
+ private static void logBytesReceived(StringBuilder b, Request request, Response response)
+ {
+ b.append(request.getHttpInput().getContentReceived());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logBytesReceivedCLF(StringBuilder b, Request request, Response response)
+ {
+ long received = request.getHttpInput().getContentReceived();
+ if (received == 0)
+ b.append('-');
+ else
+ b.append(received);
+ }
+
+ @SuppressWarnings("unused")
+ private static void logBytesTransferred(StringBuilder b, Request request, Response response)
+ {
+ b.append(request.getHttpInput().getContentReceived() + response.getHttpOutput().getWritten());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logBytesTransferredCLF(StringBuilder b, Request request, Response response)
+ {
+ long transferred = request.getHttpInput().getContentReceived() + response.getHttpOutput().getWritten();
+ if (transferred == 0)
+ b.append('-');
+ else
+ b.append(transferred);
+ }
+
+ @SuppressWarnings("unused")
+ private static void logRequestCookie(String arg, StringBuilder b, Request request, Response response)
+ {
+ Cookie[] cookies = request.getCookies();
+ if (cookies != null)
+ {
+ for (Cookie c : cookies)
+ {
+ if (arg.equals(c.getName()))
+ {
+ b.append(c.getValue());
+ return;
+ }
+ }
+ }
+
+ b.append('-');
+ }
+
+ @SuppressWarnings("unused")
+ private static void logRequestCookies(StringBuilder b, Request request, Response response)
+ {
+ Cookie[] cookies = request.getCookies();
+ if (cookies == null || cookies.length == 0)
+ b.append("-");
+ else
+ {
+ for (int i = 0; i < cookies.length; i++)
+ {
+ if (i != 0)
+ b.append(';');
+ b.append(cookies[i].getName());
+ b.append('=');
+ b.append(cookies[i].getValue());
+ }
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static void logEnvironmentVar(String arg, StringBuilder b, Request request, Response response)
+ {
+ append(b, System.getenv(arg));
+ }
+
+ @SuppressWarnings("unused")
+ private static void logFilename(StringBuilder b, Request request, Response response)
+ {
+ UserIdentity.Scope scope = request.getUserIdentityScope();
+ if (scope == null || scope.getContextHandler() == null)
+ b.append('-');
+ else
+ {
+ ContextHandler context = scope.getContextHandler();
+ int lengthToStrip = scope.getContextPath().length() > 1 ? scope.getContextPath().length() : 0;
+ String filename = context.getServletContext().getRealPath(request.getPathInfo().substring(lengthToStrip));
+ append(b, filename);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static void logRequestProtocol(StringBuilder b, Request request, Response response)
+ {
+ append(b, request.getProtocol());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logRequestHeader(String arg, StringBuilder b, Request request, Response response)
+ {
+ append(b, request.getHeader(arg));
+ }
+
+ @SuppressWarnings("unused")
+ private static void logKeepAliveRequests(StringBuilder b, Request request, Response response)
+ {
+ long requests = request.getHttpChannel().getConnection().getMessagesIn();
+ if (requests >= 0)
+ b.append(requests);
+ else
+ b.append('-');
+ }
+
+ @SuppressWarnings("unused")
+ private static void logRequestMethod(StringBuilder b, Request request, Response response)
+ {
+ append(b, request.getMethod());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logResponseHeader(String arg, StringBuilder b, Request request, Response response)
+ {
+ append(b, response.getHeader(arg));
+ }
+
+ @SuppressWarnings("unused")
+ private static void logQueryString(StringBuilder b, Request request, Response response)
+ {
+ append(b, "?" + request.getQueryString());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logRequestFirstLine(StringBuilder b, Request request, Response response)
+ {
+ append(b, request.getMethod());
+ b.append(" ");
+ append(b, request.getOriginalURI());
+ b.append(" ");
+ append(b, request.getProtocol());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logRequestHandler(StringBuilder b, Request request, Response response)
+ {
+ append(b, request.getServletName());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logResponseStatus(StringBuilder b, Request request, Response response)
+ {
+ b.append(response.getCommittedMetaData().getStatus());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logRequestTime(DateCache dateCache, StringBuilder b, Request request, Response response)
+ {
+ b.append('[');
+ append(b, dateCache.format(request.getTimeStamp()));
+ b.append(']');
+ }
+
+ @SuppressWarnings("unused")
+ private static void logLatencyMicroseconds(StringBuilder b, Request request, Response response)
+ {
+ long currentTime = System.currentTimeMillis();
+ long requestTime = request.getTimeStamp();
+
+ long latencyMs = currentTime - requestTime;
+ long latencyUs = TimeUnit.MILLISECONDS.toMicros(latencyMs);
+
+ b.append(latencyUs);
+ }
+
+ @SuppressWarnings("unused")
+ private static void logLatencyMilliseconds(StringBuilder b, Request request, Response response)
+ {
+ long latency = System.currentTimeMillis() - request.getTimeStamp();
+ b.append(latency);
+ }
+
+ @SuppressWarnings("unused")
+ private static void logLatencySeconds(StringBuilder b, Request request, Response response)
+ {
+ long latency = System.currentTimeMillis() - request.getTimeStamp();
+ b.append(TimeUnit.MILLISECONDS.toSeconds(latency));
+ }
+
+ @SuppressWarnings("unused")
+ private static void logRequestAuthentication(StringBuilder b, Request request, Response response)
+ {
+ append(b, getAuthentication(request, false));
+ }
+
+ @SuppressWarnings("unused")
+ private static void logRequestAuthenticationWithDeferred(StringBuilder b, Request request, Response response)
+ {
+ append(b, getAuthentication(request, true));
+ }
+
+ @SuppressWarnings("unused")
+ private static void logUrlRequestPath(StringBuilder b, Request request, Response response)
+ {
+ append(b, request.getRequestURI());
+ }
+
+ @SuppressWarnings("unused")
+ private static void logConnectionStatus(StringBuilder b, Request request, Response response)
+ {
+ b.append(request.getHttpChannel().isResponseCompleted() ? (request.getHttpChannel().isPersistent() ? '+' : '-') : 'X');
+ }
+
+ @SuppressWarnings("unused")
+ private static void logRequestTrailer(String arg, StringBuilder b, Request request, Response response)
+ {
+ HttpFields trailers = request.getTrailers();
+ if (trailers != null)
+ append(b, trailers.get(arg));
+ else
+ b.append('-');
+ }
+
+ @SuppressWarnings("unused")
+ private static void logResponseTrailer(String arg, StringBuilder b, Request request, Response response)
+ {
+ Supplier<HttpFields> supplier = response.getTrailers();
+ if (supplier != null)
+ {
+ HttpFields trailers = supplier.get();
+
+ if (trailers != null)
+ append(b, trailers.get(arg));
+ else
+ b.append('-');
+ }
+ else
+ b.append("-");
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/DebugListener.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/DebugListener.java
new file mode 100644
index 0000000..c433048
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/DebugListener.java
@@ -0,0 +1,335 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.Locale;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletRequestEvent;
+import javax.servlet.ServletRequestListener;
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandler.Context;
+import org.eclipse.jetty.server.handler.ContextHandler.ContextScopeListener;
+import org.eclipse.jetty.util.DateCache;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.Name;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * A Context Listener that produces additional debug.
+ * This listener if added to a ContextHandler, will produce additional debug information to
+ * either/or a specific log stream or the standard debug log.
+ * The events produced by {@link ServletContextListener}, {@link ServletRequestListener},
+ * {@link AsyncListener} and {@link ContextScopeListener} are logged.
+ */
+@ManagedObject("Debug Listener")
+public class DebugListener extends AbstractLifeCycle implements ServletContextListener
+{
+ private static final Logger LOG = Log.getLogger(DebugListener.class);
+ private static final DateCache __date = new DateCache("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
+
+ private final String _attr = String.format("__R%s@%x", this.getClass().getSimpleName(), System.identityHashCode(this));
+
+ private final PrintStream _out;
+ private boolean _renameThread;
+ private boolean _showHeaders;
+ private boolean _dumpContext;
+
+ public DebugListener()
+ {
+ this(null, false, false, false);
+ }
+
+ public DebugListener(@Name("renameThread") boolean renameThread, @Name("showHeaders") boolean showHeaders, @Name("dumpContext") boolean dumpContext)
+ {
+ this(null, renameThread, showHeaders, dumpContext);
+ }
+
+ public DebugListener(@Name("outputStream") OutputStream out, @Name("renameThread") boolean renameThread, @Name("showHeaders") boolean showHeaders, @Name("dumpContext") boolean dumpContext)
+ {
+ _out = out == null ? null : new PrintStream(out);
+ _renameThread = renameThread;
+ _showHeaders = showHeaders;
+ _dumpContext = dumpContext;
+ }
+
+ @ManagedAttribute("Rename thread within context scope")
+ public boolean isRenameThread()
+ {
+ return _renameThread;
+ }
+
+ public void setRenameThread(boolean renameThread)
+ {
+ _renameThread = renameThread;
+ }
+
+ @ManagedAttribute("Show request headers")
+ public boolean isShowHeaders()
+ {
+ return _showHeaders;
+ }
+
+ public void setShowHeaders(boolean showHeaders)
+ {
+ _showHeaders = showHeaders;
+ }
+
+ @ManagedAttribute("Dump contexts at start")
+ public boolean isDumpContext()
+ {
+ return _dumpContext;
+ }
+
+ public void setDumpContext(boolean dumpContext)
+ {
+ _dumpContext = dumpContext;
+ }
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce)
+ {
+ sce.getServletContext().addListener(_servletRequestListener);
+ ContextHandler handler = ContextHandler.getContextHandler(sce.getServletContext());
+ handler.addEventListener(_contextScopeListener);
+ String cname = findContextName(sce.getServletContext());
+ log("^ ctx=%s %s", cname, sce.getServletContext());
+ if (_dumpContext)
+ {
+ if (_out == null)
+ {
+ handler.dumpStdErr();
+ System.err.println(Dumpable.KEY);
+ }
+ else
+ {
+ try
+ {
+ handler.dump(_out);
+ _out.println(Dumpable.KEY);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent sce)
+ {
+ String cname = findContextName(sce.getServletContext());
+ log("v ctx=%s %s", cname, sce.getServletContext());
+ }
+
+ protected String findContextName(ServletContext context)
+ {
+ if (context == null)
+ return null;
+ String n = (String)context.getAttribute(_attr);
+ if (n == null)
+ {
+ n = String.format("%s@%x", context.getContextPath(), context.hashCode());
+ context.setAttribute(_attr, n);
+ }
+ return n;
+ }
+
+ protected String findRequestName(ServletRequest request)
+ {
+ if (request == null)
+ return null;
+ HttpServletRequest r = (HttpServletRequest)request;
+ String n = (String)request.getAttribute(_attr);
+ if (n == null)
+ {
+ n = String.format("%s@%x", r.getRequestURI(), request.hashCode());
+ request.setAttribute(_attr, n);
+ }
+ return n;
+ }
+
+ protected void log(String format, Object... arg)
+ {
+ if (!isRunning())
+ return;
+
+ String s = String.format(format, arg);
+
+ long now = System.currentTimeMillis();
+ long ms = now % 1000;
+ if (_out != null)
+ _out.printf("%s.%03d:%s%n", __date.formatNow(now), ms, s);
+ if (LOG.isDebugEnabled())
+ LOG.info(s);
+ }
+
+ final AsyncListener _asyncListener = new AsyncListener()
+ {
+ @Override
+ public void onTimeout(AsyncEvent event) throws IOException
+ {
+ String cname = findContextName(((AsyncContextEvent)event).getServletContext());
+ String rname = findRequestName(event.getAsyncContext().getRequest());
+ log("! ctx=%s r=%s onTimeout %s", cname, rname, ((AsyncContextEvent)event).getHttpChannelState());
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event) throws IOException
+ {
+ String cname = findContextName(((AsyncContextEvent)event).getServletContext());
+ String rname = findRequestName(event.getAsyncContext().getRequest());
+ log("! ctx=%s r=%s onStartAsync %s", cname, rname, ((AsyncContextEvent)event).getHttpChannelState());
+ }
+
+ @Override
+ public void onError(AsyncEvent event) throws IOException
+ {
+ String cname = findContextName(((AsyncContextEvent)event).getServletContext());
+ String rname = findRequestName(event.getAsyncContext().getRequest());
+ log("!! ctx=%s r=%s onError %s %s", cname, rname, event.getThrowable(), ((AsyncContextEvent)event).getHttpChannelState());
+ }
+
+ @Override
+ public void onComplete(AsyncEvent event) throws IOException
+ {
+ AsyncContextEvent ace = (AsyncContextEvent)event;
+ String cname = findContextName(ace.getServletContext());
+ String rname = findRequestName(ace.getAsyncContext().getRequest());
+
+ Request br = Request.getBaseRequest(ace.getAsyncContext().getRequest());
+ Response response = br.getResponse();
+ String headers = _showHeaders ? ("\n" + response.getHttpFields().toString()) : "";
+
+ log("! ctx=%s r=%s onComplete %s %d%s", cname, rname, ace.getHttpChannelState(), response.getStatus(), headers);
+ }
+ };
+
+ final ServletRequestListener _servletRequestListener = new ServletRequestListener()
+ {
+ @Override
+ public void requestInitialized(ServletRequestEvent sre)
+ {
+ String cname = findContextName(sre.getServletContext());
+ HttpServletRequest r = (HttpServletRequest)sre.getServletRequest();
+
+ String rname = findRequestName(r);
+ DispatcherType d = r.getDispatcherType();
+ if (d == DispatcherType.REQUEST)
+ {
+ Request br = Request.getBaseRequest(r);
+
+ String headers = _showHeaders ? ("\n" + br.getMetaData().getFields().toString()) : "";
+
+ StringBuffer url = r.getRequestURL();
+ if (r.getQueryString() != null)
+ url.append('?').append(r.getQueryString());
+ log(">> %s ctx=%s r=%s %s %s %s %s %s%s", d,
+ cname,
+ rname,
+ d,
+ r.getMethod(),
+ url.toString(),
+ r.getProtocol(),
+ br.getHttpChannel(),
+ headers);
+ }
+ else
+ log(">> %s ctx=%s r=%s", d, cname, rname);
+ }
+
+ @Override
+ public void requestDestroyed(ServletRequestEvent sre)
+ {
+ String cname = findContextName(sre.getServletContext());
+ HttpServletRequest r = (HttpServletRequest)sre.getServletRequest();
+ String rname = findRequestName(r);
+ DispatcherType d = r.getDispatcherType();
+ if (sre.getServletRequest().isAsyncStarted())
+ {
+ sre.getServletRequest().getAsyncContext().addListener(_asyncListener);
+ log("<< %s ctx=%s r=%s async=true", d, cname, rname);
+ }
+ else
+ {
+ Request br = Request.getBaseRequest(r);
+ String headers = _showHeaders ? ("\n" + br.getResponse().getHttpFields().toString()) : "";
+ log("<< %s ctx=%s r=%s async=false %d%s", d, cname, rname, Request.getBaseRequest(r).getResponse().getStatus(), headers);
+ }
+ }
+ };
+
+ final ContextHandler.ContextScopeListener _contextScopeListener = new ContextHandler.ContextScopeListener()
+ {
+ @Override
+ public void enterScope(Context context, Request request, Object reason)
+ {
+ String cname = findContextName(context);
+ if (request == null)
+ log("> ctx=%s %s", cname, reason);
+ else
+ {
+ String rname = findRequestName(request);
+
+ if (_renameThread)
+ {
+ Thread thread = Thread.currentThread();
+ thread.setName(String.format("%s#%s", thread.getName(), rname));
+ }
+
+ log("> ctx=%s r=%s %s", cname, rname, reason);
+ }
+ }
+
+ @Override
+ public void exitScope(Context context, Request request)
+ {
+ String cname = findContextName(context);
+ if (request == null)
+ log("< ctx=%s", cname);
+ else
+ {
+ String rname = findRequestName(request);
+
+ log("< ctx=%s r=%s", cname, rname);
+ if (_renameThread)
+ {
+ Thread thread = Thread.currentThread();
+ if (thread.getName().endsWith(rname))
+ thread.setName(thread.getName().substring(0, thread.getName().length() - rname.length() - 1));
+ }
+ }
+ }
+ };
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/DetectorConnectionFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/DetectorConnectionFactory.java
new file mode 100644
index 0000000..2cc7c0e
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/DetectorConnectionFactory.java
@@ -0,0 +1,308 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.eclipse.jetty.io.AbstractConnection;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * A {@link ConnectionFactory} combining multiple {@link Detecting} instances that will upgrade to
+ * the first one recognizing the bytes in the buffer.
+ */
+public class DetectorConnectionFactory extends AbstractConnectionFactory implements ConnectionFactory.Detecting
+{
+ private static final Logger LOG = Log.getLogger(DetectorConnectionFactory.class);
+
+ private final List<Detecting> _detectingConnectionFactories;
+
+ /**
+ * <p>When the first bytes are not recognized by the {@code detectingConnectionFactories}, the default behavior is to
+ * upgrade to the protocol returned by {@link #findNextProtocol(Connector)}.</p>
+ * @param detectingConnectionFactories the {@link Detecting} instances.
+ */
+ public DetectorConnectionFactory(Detecting... detectingConnectionFactories)
+ {
+ super(toProtocolString(detectingConnectionFactories));
+ _detectingConnectionFactories = Arrays.asList(detectingConnectionFactories);
+ for (Detecting detectingConnectionFactory : detectingConnectionFactories)
+ {
+ addBean(detectingConnectionFactory);
+ }
+ }
+
+ private static String toProtocolString(Detecting... detectingConnectionFactories)
+ {
+ if (detectingConnectionFactories.length == 0)
+ throw new IllegalArgumentException("At least one detecting instance is required");
+
+ // remove protocol duplicates while keeping their ordering -> use LinkedHashSet
+ LinkedHashSet<String> protocols = Arrays.stream(detectingConnectionFactories).map(ConnectionFactory::getProtocol).collect(Collectors.toCollection(LinkedHashSet::new));
+
+ String protocol = protocols.stream().collect(Collectors.joining("|", "[", "]"));
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector generated protocol name : {}", protocol);
+ return protocol;
+ }
+
+ /**
+ * Performs a detection using multiple {@link ConnectionFactory.Detecting} instances and returns the aggregated outcome.
+ * @param buffer the buffer to perform a detection against.
+ * @return A {@link Detecting.Detection} value with the detection outcome of the {@code detectingConnectionFactories}.
+ */
+ @Override
+ public Detecting.Detection detect(ByteBuffer buffer)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} detecting from buffer {} using {}", getProtocol(), BufferUtil.toHexString(buffer), _detectingConnectionFactories);
+ boolean needMoreBytes = true;
+ for (Detecting detectingConnectionFactory : _detectingConnectionFactories)
+ {
+ Detecting.Detection detection = detectingConnectionFactory.detect(buffer);
+ if (detection == Detecting.Detection.RECOGNIZED)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} recognized bytes using {}", getProtocol(), detection);
+ return Detecting.Detection.RECOGNIZED;
+ }
+ needMoreBytes &= detection == Detection.NEED_MORE_BYTES;
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} {}", getProtocol(), (needMoreBytes ? "requires more bytes" : "failed to recognize bytes"));
+ return needMoreBytes ? Detection.NEED_MORE_BYTES : Detection.NOT_RECOGNIZED;
+ }
+
+ /**
+ * Utility method that performs an upgrade to the specified connection factory, disposing of the given resources when needed.
+ * @param connectionFactory the connection factory to upgrade to.
+ * @param connector the connector.
+ * @param endPoint the endpoint.
+ */
+ protected static void upgradeToConnectionFactory(ConnectionFactory connectionFactory, Connector connector, EndPoint endPoint) throws IllegalStateException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Upgrading to connection factory {}", connectionFactory);
+ if (connectionFactory == null)
+ throw new IllegalStateException("Cannot upgrade: connection factory must not be null for " + endPoint);
+ Connection nextConnection = connectionFactory.newConnection(connector, endPoint);
+ if (!(nextConnection instanceof Connection.UpgradeTo))
+ throw new IllegalStateException("Cannot upgrade: " + nextConnection + " does not implement " + Connection.UpgradeTo.class.getName() + " for " + endPoint);
+ endPoint.upgrade(nextConnection);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Upgraded to connection factory {} and released buffer", connectionFactory);
+ }
+
+ /**
+ * <p>Callback method called when detection was unsuccessful.
+ * This implementation upgrades to the protocol returned by {@link #findNextProtocol(Connector)}.</p>
+ * @param connector the connector.
+ * @param endPoint the endpoint.
+ * @param buffer the buffer.
+ */
+ protected void nextProtocol(Connector connector, EndPoint endPoint, ByteBuffer buffer) throws IllegalStateException
+ {
+ String nextProtocol = findNextProtocol(connector);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} detection unsuccessful, found '{}' as the next protocol to upgrade to", getProtocol(), nextProtocol);
+ if (nextProtocol == null)
+ throw new IllegalStateException("Cannot find protocol following '" + getProtocol() + "' in connector's protocol list " + connector.getProtocols() + " for " + endPoint);
+ upgradeToConnectionFactory(connector.getConnectionFactory(nextProtocol), connector, endPoint);
+ }
+
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endPoint)
+ {
+ return configure(new DetectorConnection(endPoint, connector), connector, endPoint);
+ }
+
+ private class DetectorConnection extends AbstractConnection implements Connection.UpgradeFrom, Connection.UpgradeTo
+ {
+ private final Connector _connector;
+ private final ByteBuffer _buffer;
+
+ private DetectorConnection(EndPoint endp, Connector connector)
+ {
+ super(endp, connector.getExecutor());
+ _connector = connector;
+ _buffer = connector.getByteBufferPool().acquire(getInputBufferSize(), true);
+ }
+
+ @Override
+ public void onUpgradeTo(ByteBuffer buffer)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} copying unconsumed buffer {}", getProtocol(), BufferUtil.toDetailString(buffer));
+ BufferUtil.append(_buffer, buffer);
+ }
+
+ @Override
+ public ByteBuffer onUpgradeFrom()
+ {
+ if (_buffer.hasRemaining())
+ {
+ ByteBuffer unconsumed = ByteBuffer.allocateDirect(_buffer.remaining());
+ unconsumed.put(_buffer);
+ unconsumed.flip();
+ _connector.getByteBufferPool().release(_buffer);
+ return unconsumed;
+ }
+ return null;
+ }
+
+ @Override
+ public void onOpen()
+ {
+ super.onOpen();
+ if (!detectAndUpgrade())
+ fillInterested();
+ }
+
+ @Override
+ public void onFillable()
+ {
+ try
+ {
+ while (BufferUtil.space(_buffer) > 0)
+ {
+ // Read data
+ int fill = getEndPoint().fill(_buffer);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} filled buffer with {} bytes", getProtocol(), fill);
+ if (fill < 0)
+ {
+ _connector.getByteBufferPool().release(_buffer);
+ getEndPoint().shutdownOutput();
+ return;
+ }
+ if (fill == 0)
+ {
+ fillInterested();
+ return;
+ }
+
+ if (detectAndUpgrade())
+ return;
+ }
+
+ // all Detecting instances want more bytes than this buffer can store
+ LOG.warn("Detector {} failed to detect upgrade target on {} for {}", getProtocol(), _detectingConnectionFactories, getEndPoint());
+ releaseAndClose();
+ }
+ catch (Throwable x)
+ {
+ LOG.warn("Detector {} error for {}", getProtocol(), getEndPoint(), x);
+ releaseAndClose();
+ }
+ }
+
+ /**
+ * @return true when upgrade was performed, false otherwise.
+ */
+ private boolean detectAndUpgrade()
+ {
+ if (BufferUtil.isEmpty(_buffer))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} skipping detection on an empty buffer", getProtocol());
+ return false;
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} performing detection with {} bytes", getProtocol(), _buffer.remaining());
+ boolean notRecognized = true;
+ for (Detecting detectingConnectionFactory : _detectingConnectionFactories)
+ {
+ Detecting.Detection detection = detectingConnectionFactory.detect(_buffer);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} performed detection from {} with {} which returned {}", getProtocol(), BufferUtil.toDetailString(_buffer), detectingConnectionFactory, detection);
+ if (detection == Detecting.Detection.RECOGNIZED)
+ {
+ try
+ {
+ // This DetectingConnectionFactory recognized those bytes -> upgrade to the next one.
+ Connection nextConnection = detectingConnectionFactory.newConnection(_connector, getEndPoint());
+ if (!(nextConnection instanceof UpgradeTo))
+ throw new IllegalStateException("Cannot upgrade: " + nextConnection + " does not implement " + UpgradeTo.class.getName());
+ getEndPoint().upgrade(nextConnection);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} upgraded to {}", getProtocol(), nextConnection);
+ return true;
+ }
+ catch (DetectionFailureException e)
+ {
+ // It's just bubbling up from a nested Detector, so it's already handled, just rethrow it.
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} failed to upgrade, rethrowing", getProtocol(), e);
+ throw e;
+ }
+ catch (Exception e)
+ {
+ // Two reasons that can make us end up here:
+ // 1) detectingConnectionFactory.newConnection() failed? probably because it cannot find the next protocol
+ // 2) nextConnection is not instanceof UpgradeTo
+ // -> release the resources before rethrowing as DetectionFailureException
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} failed to upgrade", getProtocol());
+ releaseAndClose();
+ throw new DetectionFailureException(e);
+ }
+ }
+ notRecognized &= detection == Detecting.Detection.NOT_RECOGNIZED;
+ }
+
+ if (notRecognized)
+ {
+ // No DetectingConnectionFactory recognized those bytes -> call unsuccessful detection callback.
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} failed to detect a known protocol, falling back to nextProtocol()", getProtocol());
+ nextProtocol(_connector, getEndPoint(), _buffer);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} call to nextProtocol() succeeded, assuming upgrade performed", getProtocol());
+ return true;
+ }
+
+ return false;
+ }
+
+ private void releaseAndClose()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Detector {} releasing buffer and closing", getProtocol());
+ _connector.getByteBufferPool().release(_buffer);
+ close();
+ }
+ }
+
+ private static class DetectionFailureException extends RuntimeException
+ {
+ public DetectionFailureException(Throwable cause)
+ {
+ super(cause);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java
new file mode 100644
index 0000000..1720261
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java
@@ -0,0 +1,521 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import javax.servlet.DispatcherType;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HttpCompliance;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.Attributes;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class Dispatcher implements RequestDispatcher
+{
+ private static final Logger LOG = Log.getLogger(Dispatcher.class);
+
+ /**
+ * Dispatch include attribute names
+ */
+ public static final String __INCLUDE_PREFIX = "javax.servlet.include.";
+
+ /**
+ * Dispatch include attribute names
+ */
+ public static final String __FORWARD_PREFIX = "javax.servlet.forward.";
+
+ private final ContextHandler _contextHandler;
+ private final HttpURI _uri;
+ private final String _pathInContext;
+ private final String _named;
+
+ public Dispatcher(ContextHandler contextHandler, HttpURI uri, String pathInContext)
+ {
+ _contextHandler = contextHandler;
+ _uri = uri;
+ _pathInContext = pathInContext;
+ _named = null;
+ }
+
+ public Dispatcher(ContextHandler contextHandler, String name) throws IllegalStateException
+ {
+ _contextHandler = contextHandler;
+ _uri = null;
+ _pathInContext = null;
+ _named = name;
+ }
+
+ @Override
+ public void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException
+ {
+ forward(request, response, DispatcherType.FORWARD);
+ }
+
+ public void error(ServletRequest request, ServletResponse response) throws ServletException, IOException
+ {
+ forward(request, response, DispatcherType.ERROR);
+ }
+
+ @Override
+ public void include(ServletRequest request, ServletResponse response) throws ServletException, IOException
+ {
+ Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request));
+
+ if (!(request instanceof HttpServletRequest))
+ request = new ServletRequestHttpWrapper(request);
+ if (!(response instanceof HttpServletResponse))
+ response = new ServletResponseHttpWrapper(response);
+
+ final DispatcherType old_type = baseRequest.getDispatcherType();
+ final Attributes old_attr = baseRequest.getAttributes();
+ final MultiMap<String> old_query_params = baseRequest.getQueryParameters();
+ try
+ {
+ baseRequest.setDispatcherType(DispatcherType.INCLUDE);
+ baseRequest.getResponse().include();
+ if (_named != null)
+ {
+ _contextHandler.handle(_named, baseRequest, (HttpServletRequest)request, (HttpServletResponse)response);
+ }
+ else
+ {
+ Objects.requireNonNull(_uri);
+ // Check any URI violations against the compliance for this request
+ checkUriViolations(_uri, baseRequest);
+
+ IncludeAttributes attr = new IncludeAttributes(old_attr);
+
+ attr._requestURI = _uri.getPath();
+ attr._contextPath = _contextHandler.getRequestContextPath();
+ attr._servletPath = null; // set by ServletHandler
+ attr._pathInfo = _pathInContext;
+ attr._query = _uri.getQuery();
+
+ if (attr._query != null)
+ baseRequest.mergeQueryParameters(baseRequest.getQueryString(), attr._query, false);
+ baseRequest.setAttributes(attr);
+
+ _contextHandler.handle(_pathInContext, baseRequest, (HttpServletRequest)request, (HttpServletResponse)response);
+ }
+ }
+ finally
+ {
+ baseRequest.setAttributes(old_attr);
+ baseRequest.getResponse().included();
+ baseRequest.setQueryParameters(old_query_params);
+ baseRequest.resetParameters();
+ baseRequest.setDispatcherType(old_type);
+ }
+ }
+
+ protected void forward(ServletRequest request, ServletResponse response, DispatcherType dispatch) throws ServletException, IOException
+ {
+ Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request));
+ Response baseResponse = baseRequest.getResponse();
+ baseResponse.resetForForward();
+
+ if (!(request instanceof HttpServletRequest))
+ request = new ServletRequestHttpWrapper(request);
+ if (!(response instanceof HttpServletResponse))
+ response = new ServletResponseHttpWrapper(response);
+
+ final HttpURI old_uri = baseRequest.getHttpURI();
+ final String old_context_path = baseRequest.getContextPath();
+ final String old_servlet_path = baseRequest.getServletPath();
+ final String old_path_info = baseRequest.getPathInfo();
+
+ final MultiMap<String> old_query_params = baseRequest.getQueryParameters();
+ final Attributes old_attr = baseRequest.getAttributes();
+ final DispatcherType old_type = baseRequest.getDispatcherType();
+
+ try
+ {
+ baseRequest.setDispatcherType(dispatch);
+
+ if (_named != null)
+ {
+ _contextHandler.handle(_named, baseRequest, (HttpServletRequest)request, (HttpServletResponse)response);
+ }
+ else
+ {
+ Objects.requireNonNull(_uri);
+ // Check any URI violations against the compliance for this request
+ checkUriViolations(_uri, baseRequest);
+
+ ForwardAttributes attr = new ForwardAttributes(old_attr);
+
+ //If we have already been forwarded previously, then keep using the established
+ //original value. Otherwise, this is the first forward and we need to establish the values.
+ //Note: the established value on the original request for pathInfo and
+ //for queryString is allowed to be null, but cannot be null for the other values.
+ if (old_attr.getAttribute(FORWARD_REQUEST_URI) != null)
+ {
+ attr._pathInfo = (String)old_attr.getAttribute(FORWARD_PATH_INFO);
+ attr._query = (String)old_attr.getAttribute(FORWARD_QUERY_STRING);
+ attr._requestURI = (String)old_attr.getAttribute(FORWARD_REQUEST_URI);
+ attr._contextPath = (String)old_attr.getAttribute(FORWARD_CONTEXT_PATH);
+ attr._servletPath = (String)old_attr.getAttribute(FORWARD_SERVLET_PATH);
+ }
+ else
+ {
+ attr._pathInfo = old_path_info;
+ attr._query = old_uri.getQuery();
+ attr._requestURI = old_uri.getPath();
+ attr._contextPath = old_context_path;
+ attr._servletPath = old_servlet_path;
+ }
+
+ // Combine old and new URIs.
+ HttpURI uri = new HttpURI(old_uri, _uri);
+ baseRequest.setHttpURI(uri);
+
+ baseRequest.setContextPath(_contextHandler.getContextPath());
+ baseRequest.setServletPath(null);
+ baseRequest.setPathInfo(_pathInContext);
+
+ if (_uri.getQuery() != null || old_uri.getQuery() != null)
+ {
+ try
+ {
+ baseRequest.mergeQueryParameters(old_uri.getQuery(), _uri.getQuery(), true);
+ }
+ catch (BadMessageException e)
+ {
+ // Only throw BME if not in Error Dispatch Mode
+ // This allows application ErrorPageErrorHandler to handle BME messages
+ if (dispatch != DispatcherType.ERROR)
+ {
+ throw e;
+ }
+ else
+ {
+ LOG.warn("Ignoring Original Bad Request Query String: " + old_uri, e);
+ }
+ }
+ }
+
+ baseRequest.setAttributes(attr);
+
+ _contextHandler.handle(_pathInContext, baseRequest, (HttpServletRequest)request, (HttpServletResponse)response);
+
+ // If we are not async and not closed already, then close via the possibly wrapped response.
+ if (!baseRequest.getHttpChannelState().isAsync() && !baseResponse.getHttpOutput().isClosed())
+ {
+ try
+ {
+ response.getOutputStream().close();
+ }
+ catch (IllegalStateException e)
+ {
+ response.getWriter().close();
+ }
+ }
+ }
+ }
+ finally
+ {
+ baseRequest.setHttpURI(old_uri);
+ baseRequest.setContextPath(old_context_path);
+ baseRequest.setServletPath(old_servlet_path);
+ baseRequest.setPathInfo(old_path_info);
+ baseRequest.setQueryParameters(old_query_params);
+ baseRequest.resetParameters();
+ baseRequest.setAttributes(old_attr);
+ baseRequest.setDispatcherType(old_type);
+ }
+ }
+
+ private static void checkUriViolations(HttpURI uri, Request baseRequest)
+ {
+ if (uri.hasViolations())
+ {
+ HttpChannel channel = baseRequest.getHttpChannel();
+ Connection connection = channel == null ? null : channel.getConnection();
+ HttpCompliance compliance = connection instanceof HttpConnection
+ ? ((HttpConnection)connection).getHttpCompliance()
+ : channel != null ? channel.getConnector().getBean(HttpCompliance.class) : null;
+ String illegalState = HttpCompliance.checkUriCompliance(compliance, uri);
+ if (illegalState != null)
+ throw new IllegalStateException(illegalState);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("Dispatcher@0x%x{%s,%s}", hashCode(), _named, _uri);
+ }
+
+ private class ForwardAttributes extends Attributes.Wrapper
+ {
+ private String _requestURI;
+ private String _contextPath;
+ private String _servletPath;
+ private String _pathInfo;
+ private String _query;
+
+ ForwardAttributes(Attributes attributes)
+ {
+ super(attributes);
+ }
+
+ @Override
+ public Object getAttribute(String key)
+ {
+ if (Dispatcher.this._named == null)
+ {
+ switch (key)
+ {
+ case FORWARD_PATH_INFO:
+ return _pathInfo;
+ case FORWARD_REQUEST_URI:
+ return _requestURI;
+ case FORWARD_SERVLET_PATH:
+ return _servletPath;
+ case FORWARD_CONTEXT_PATH:
+ return _contextPath;
+ case FORWARD_QUERY_STRING:
+ return _query;
+ default:
+ break;
+ }
+ }
+
+ // TODO: should this be __FORWARD_PREFIX?
+ if (key.startsWith(__INCLUDE_PREFIX))
+ return null;
+
+ return _attributes.getAttribute(key);
+ }
+
+ @Override
+ public Set<String> getAttributeNameSet()
+ {
+ HashSet<String> set = new HashSet<>();
+ super.getAttributeNameSet().stream()
+ .filter(name -> !name.startsWith(__INCLUDE_PREFIX))
+ .filter(name -> !name.startsWith(__FORWARD_PREFIX))
+ .forEach(set::add);
+
+ if (_named == null)
+ {
+ if (_pathInfo != null)
+ set.add(FORWARD_PATH_INFO);
+ if (_requestURI != null)
+ set.add(FORWARD_REQUEST_URI);
+ if (_servletPath != null)
+ set.add(FORWARD_SERVLET_PATH);
+ if (_contextPath != null)
+ set.add(FORWARD_CONTEXT_PATH);
+ if (_query != null)
+ set.add(FORWARD_QUERY_STRING);
+ }
+
+ return set;
+ }
+
+ @Override
+ public void setAttribute(String key, Object value)
+ {
+ if (_named == null && key.startsWith("javax.servlet."))
+ {
+ switch (key)
+ {
+ case FORWARD_PATH_INFO:
+ _pathInfo = (String)value;
+ break;
+ case FORWARD_REQUEST_URI:
+ _requestURI = (String)value;
+ break;
+ case FORWARD_SERVLET_PATH:
+ _servletPath = (String)value;
+ break;
+ case FORWARD_CONTEXT_PATH:
+ _contextPath = (String)value;
+ break;
+ case FORWARD_QUERY_STRING:
+ _query = (String)value;
+ break;
+ default:
+ if (value == null)
+ _attributes.removeAttribute(key);
+ else
+ _attributes.setAttribute(key, value);
+ break;
+ }
+ }
+ else if (value == null)
+ _attributes.removeAttribute(key);
+ else
+ _attributes.setAttribute(key, value);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "FORWARD+" + _attributes.toString();
+ }
+
+ @Override
+ public void clearAttributes()
+ {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public void removeAttribute(String name)
+ {
+ setAttribute(name, null);
+ }
+ }
+
+ private class IncludeAttributes extends Attributes.Wrapper
+ {
+ private String _requestURI;
+ private String _contextPath;
+ private String _servletPath;
+ private String _pathInfo;
+ private String _query;
+
+ IncludeAttributes(Attributes attributes)
+ {
+ super(attributes);
+ }
+
+ @Override
+ public Object getAttribute(String key)
+ {
+ if (Dispatcher.this._named == null)
+ {
+ switch (key)
+ {
+ case INCLUDE_PATH_INFO:
+ return _pathInfo;
+ case INCLUDE_SERVLET_PATH:
+ return _servletPath;
+ case INCLUDE_CONTEXT_PATH:
+ return _contextPath;
+ case INCLUDE_QUERY_STRING:
+ return _query;
+ case INCLUDE_REQUEST_URI:
+ return _requestURI;
+ default:
+ break;
+ }
+ }
+ else if (key.startsWith(__INCLUDE_PREFIX))
+ return null;
+
+ return _attributes.getAttribute(key);
+ }
+
+ @Override
+ public Set<String> getAttributeNameSet()
+ {
+ HashSet<String> set = new HashSet<>();
+ super.getAttributeNameSet().stream()
+ .filter(name -> !name.startsWith(__INCLUDE_PREFIX))
+ .forEach(set::add);
+
+ if (_named == null)
+ {
+ if (_pathInfo != null)
+ set.add(INCLUDE_PATH_INFO);
+ if (_requestURI != null)
+ set.add(INCLUDE_REQUEST_URI);
+ if (_servletPath != null)
+ set.add(INCLUDE_SERVLET_PATH);
+ if (_contextPath != null)
+ set.add(INCLUDE_CONTEXT_PATH);
+ if (_query != null)
+ set.add(INCLUDE_QUERY_STRING);
+ }
+
+ return set;
+ }
+
+ @Override
+ public void setAttribute(String key, Object value)
+ {
+ if (_named == null && key.startsWith("javax.servlet."))
+ {
+ switch (key)
+ {
+ case INCLUDE_PATH_INFO:
+ _pathInfo = (String)value;
+ break;
+ case INCLUDE_REQUEST_URI:
+ _requestURI = (String)value;
+ break;
+ case INCLUDE_SERVLET_PATH:
+ _servletPath = (String)value;
+ break;
+ case INCLUDE_CONTEXT_PATH:
+ _contextPath = (String)value;
+ break;
+ case INCLUDE_QUERY_STRING:
+ _query = (String)value;
+ break;
+ default:
+ if (value == null)
+ _attributes.removeAttribute(key);
+ else
+ _attributes.setAttribute(key, value);
+ break;
+ }
+ }
+ else if (value == null)
+ _attributes.removeAttribute(key);
+ else
+ _attributes.setAttribute(key, value);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "INCLUDE+" + _attributes.toString();
+ }
+
+ @Override
+ public void clearAttributes()
+ {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public void removeAttribute(String name)
+ {
+ setAttribute(name, null);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/EncodingHttpWriter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/EncodingHttpWriter.java
new file mode 100644
index 0000000..a050b61
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/EncodingHttpWriter.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+
+/**
+ *
+ */
+public class EncodingHttpWriter extends HttpWriter
+{
+ final Writer _converter;
+
+ public EncodingHttpWriter(HttpOutput out, String encoding)
+ {
+ super(out);
+ try
+ {
+ _converter = new OutputStreamWriter(_bytes, encoding);
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void write(char[] s, int offset, int length) throws IOException
+ {
+ HttpOutput out = _out;
+
+ while (length > 0)
+ {
+ _bytes.reset();
+ int chars = Math.min(length, MAX_OUTPUT_CHARS);
+
+ _converter.write(s, offset, chars);
+ _converter.flush();
+ _bytes.writeTo(out);
+ length -= chars;
+ offset += chars;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java
new file mode 100644
index 0000000..cb37cb8
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java
@@ -0,0 +1,974 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.net.InetSocketAddress;
+import javax.servlet.ServletRequest;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HostPortHttpField;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.QuotedCSVParser;
+import org.eclipse.jetty.server.HttpConfiguration.Customizer;
+import org.eclipse.jetty.util.ArrayTrie;
+import org.eclipse.jetty.util.HostPort;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.Trie;
+
+import static java.lang.invoke.MethodType.methodType;
+
+/**
+ * Customize Requests for Proxy Forwarding.
+ * <p>
+ * This customizer looks at at HTTP request for headers that indicate
+ * it has been forwarded by one or more proxies. Specifically handled are
+ * <ul>
+ * <li>{@code Forwarded}, as defined by <a href="https://tools.ietf.org/html/rfc7239">rfc7239</a>
+ * <li>{@code X-Forwarded-Host}</li>
+ * <li>{@code X-Forwarded-Server}</li>
+ * <li>{@code X-Forwarded-For}</li>
+ * <li>{@code X-Forwarded-Proto}</li>
+ * <li>{@code X-Proxied-Https}</li>
+ * </ul>
+ * <p>If these headers are present, then the {@link Request} object is updated
+ * so that the proxy is not seen as the other end point of the connection on which
+ * the request came</p>
+ * <p>Headers can also be defined so that forwarded SSL Session IDs and Cipher
+ * suites may be customised</p>
+ * <p>
+ * The Authority (host and port) is updated on the {@link Request} object based
+ * on the host / port information in the following search order.
+ * </p>
+ * <table style="border: 1px solid black; border-collapse: separate; border-spacing: 0px;">
+ * <caption style="font-weight: bold; font-size: 1.2em">Request Authority Search Order</caption>
+ * <colgroup>
+ * <col><col style="width: 15%"><col><col><col><col>
+ * </colgroup>
+ * <thead style="background-color: lightgray">
+ * <tr>
+ * <th>#</th>
+ * <th>Value Origin</th>
+ * <th>Host</th>
+ * <th>Port</th>
+ * <th>Protocol</th>
+ * <th>Notes</th>
+ * </tr>
+ * </thead>
+ * <tbody style="text-align: left; vertical-align: top;">
+ * <tr>
+ * <td>1</td>
+ * <td><code>Forwarded</code> Header</td>
+ * <td>"{@code host=<host>}" param (Required)</td>
+ * <td>"{@code host=<host>:<port>} param (Implied)</td>
+ * <td>"{@code proto=<value>}" param (Optional)</td>
+ * <td>From left-most relevant parameter (see <a href="https://tools.ietf.org/html/rfc7239">rfc7239</a>)</td>
+ * </tr>
+ * <tr>
+ * <td>2</td>
+ * <td><code>X-Forwarded-Host</code> Header</td>
+ * <td>Required</td>
+ * <td>Implied</td>
+ * <td>n/a</td>
+ * <td>left-most value</td>
+ * </tr>
+ * <tr>
+ * <td>3</td>
+ * <td><code>X-Forwarded-Port</code> Header</td>
+ * <td>n/a</td>
+ * <td>Required</td>
+ * <td>n/a</td>
+ * <td>left-most value (only if {@link #getForwardedPortAsAuthority()} is true)</td>
+ * </tr>
+ * <tr>
+ * <td>4</td>
+ * <td><code>X-Forwarded-Server</code> Header</td>
+ * <td>Required</td>
+ * <td>Optional</td>
+ * <td>n/a</td>
+ * <td>left-most value</td>
+ * </tr>
+ * <tr>
+ * <td>5</td>
+ * <td><code>X-Forwarded-Proto</code> Header</td>
+ * <td>n/a</td>
+ * <td>Implied from value</td>
+ * <td>Required</td>
+ * <td>
+ * <p>left-most value becomes protocol.</p>
+ * <ul>
+ * <li>Value of "<code>http</code>" means port=80.</li>
+ * <li>Value of "{@link HttpConfiguration#getSecureScheme()}" means port={@link HttpConfiguration#getSecurePort()}.</li>
+ * </ul>
+ * </td>
+ * </tr>
+ * <tr>
+ * <td>6</td>
+ * <td><code>X-Proxied-Https</code> Header</td>
+ * <td>n/a</td>
+ * <td>Implied from value</td>
+ * <td>boolean</td>
+ * <td>
+ * <p>left-most value determines protocol and port.</p>
+ * <ul>
+ * <li>Value of "<code>on</code>" means port={@link HttpConfiguration#getSecurePort()}, and protocol={@link HttpConfiguration#getSecureScheme()}).</li>
+ * <li>Value of "<code>off</code>" means port=80, and protocol=http.</li>
+ * </ul>
+ * </td>
+ * </tr>
+ * </tbody>
+ * </table>
+ *
+ * @see <a href="http://en.wikipedia.org/wiki/X-Forwarded-For">Wikipedia: X-Forwarded-For</a>
+ * @see <a href="https://tools.ietf.org/html/rfc7239">RFC 7239: Forwarded HTTP Extension</a>
+ */
+public class ForwardedRequestCustomizer implements Customizer
+{
+ private HostPortHttpField _forcedHost;
+ private boolean _proxyAsAuthority = false;
+ private boolean _forwardedPortAsAuthority = true;
+ private String _forwardedHeader = HttpHeader.FORWARDED.toString();
+ private String _forwardedHostHeader = HttpHeader.X_FORWARDED_HOST.toString();
+ private String _forwardedServerHeader = HttpHeader.X_FORWARDED_SERVER.toString();
+ private String _forwardedProtoHeader = HttpHeader.X_FORWARDED_PROTO.toString();
+ private String _forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString();
+ private String _forwardedPortHeader = HttpHeader.X_FORWARDED_PORT.toString();
+ private String _forwardedHttpsHeader = "X-Proxied-Https";
+ private String _forwardedCipherSuiteHeader = "Proxy-auth-cert";
+ private String _forwardedSslSessionIdHeader = "Proxy-ssl-id";
+ private boolean _sslIsSecure = true;
+ private Trie<MethodHandle> _handles;
+
+ public ForwardedRequestCustomizer()
+ {
+ updateHandles();
+ }
+
+ /**
+ * @return true if the proxy address obtained via
+ * {@code X-Forwarded-Server} or RFC7239 "by" is used as
+ * the request authority. Default false
+ */
+ public boolean getProxyAsAuthority()
+ {
+ return _proxyAsAuthority;
+ }
+
+ /**
+ * @param proxyAsAuthority if true, use the proxy address obtained via
+ * {@code X-Forwarded-Server} or RFC7239 "by" as the request authority.
+ */
+ public void setProxyAsAuthority(boolean proxyAsAuthority)
+ {
+ _proxyAsAuthority = proxyAsAuthority;
+ }
+
+ /**
+ * @param rfc7239only Configure to only support the RFC7239 Forwarded header and to
+ * not support any {@code X-Forwarded-} headers. This convenience method
+ * clears all the non RFC headers if passed true and sets them to
+ * the default values (if not already set) if passed false.
+ */
+ public void setForwardedOnly(boolean rfc7239only)
+ {
+ if (rfc7239only)
+ {
+ if (_forwardedHeader == null)
+ _forwardedHeader = HttpHeader.FORWARDED.toString();
+ _forwardedHostHeader = null;
+ _forwardedServerHeader = null;
+ _forwardedForHeader = null;
+ _forwardedPortHeader = null;
+ _forwardedProtoHeader = null;
+ _forwardedHttpsHeader = null;
+ }
+ else
+ {
+ if (_forwardedHostHeader == null)
+ _forwardedHostHeader = HttpHeader.X_FORWARDED_HOST.toString();
+ if (_forwardedServerHeader == null)
+ _forwardedServerHeader = HttpHeader.X_FORWARDED_SERVER.toString();
+ if (_forwardedForHeader == null)
+ _forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString();
+ if (_forwardedPortHeader == null)
+ _forwardedPortHeader = HttpHeader.X_FORWARDED_PORT.toString();
+ if (_forwardedProtoHeader == null)
+ _forwardedProtoHeader = HttpHeader.X_FORWARDED_PROTO.toString();
+ if (_forwardedHttpsHeader == null)
+ _forwardedHttpsHeader = "X-Proxied-Https";
+ }
+
+ updateHandles();
+ }
+
+ public String getForcedHost()
+ {
+ return _forcedHost.getValue();
+ }
+
+ /**
+ * Set a forced valued for the host header to control what is returned by {@link ServletRequest#getServerName()} and {@link ServletRequest#getServerPort()}.
+ *
+ * @param hostAndPort The value of the host header to force.
+ */
+ public void setForcedHost(String hostAndPort)
+ {
+ _forcedHost = new HostPortHttpField(hostAndPort);
+ }
+
+ /**
+ * @return The header name for RFC forwarded (default Forwarded)
+ */
+ public String getForwardedHeader()
+ {
+ return _forwardedHeader;
+ }
+
+ /**
+ * @param forwardedHeader The header name for RFC forwarded (default Forwarded)
+ */
+ public void setForwardedHeader(String forwardedHeader)
+ {
+ if (_forwardedHeader == null || !_forwardedHeader.equals(forwardedHeader))
+ {
+ _forwardedHeader = forwardedHeader;
+ updateHandles();
+ }
+ }
+
+ public String getForwardedHostHeader()
+ {
+ return _forwardedHostHeader;
+ }
+
+ /**
+ * @param forwardedHostHeader The header name for forwarded hosts (default {@code X-Forwarded-Host})
+ */
+ public void setForwardedHostHeader(String forwardedHostHeader)
+ {
+ if (_forwardedHostHeader == null || !_forwardedHostHeader.equalsIgnoreCase(forwardedHostHeader))
+ {
+ _forwardedHostHeader = forwardedHostHeader;
+ updateHandles();
+ }
+ }
+
+ /**
+ * @return the header name for forwarded server.
+ */
+ public String getForwardedServerHeader()
+ {
+ return _forwardedServerHeader;
+ }
+
+ /**
+ * @param forwardedServerHeader The header name for forwarded server (default {@code X-Forwarded-Server})
+ */
+ public void setForwardedServerHeader(String forwardedServerHeader)
+ {
+ if (_forwardedServerHeader == null || !_forwardedServerHeader.equalsIgnoreCase(forwardedServerHeader))
+ {
+ _forwardedServerHeader = forwardedServerHeader;
+ updateHandles();
+ }
+ }
+
+ /**
+ * @return the forwarded for header
+ */
+ public String getForwardedForHeader()
+ {
+ return _forwardedForHeader;
+ }
+
+ /**
+ * @param forwardedRemoteAddressHeader The header name for forwarded for (default {@code X-Forwarded-For})
+ */
+ public void setForwardedForHeader(String forwardedRemoteAddressHeader)
+ {
+ if (_forwardedForHeader == null || !_forwardedForHeader.equalsIgnoreCase(forwardedRemoteAddressHeader))
+ {
+ _forwardedForHeader = forwardedRemoteAddressHeader;
+ updateHandles();
+ }
+ }
+
+ public String getForwardedPortHeader()
+ {
+ return _forwardedPortHeader;
+ }
+
+ /**
+ * @param forwardedPortHeader The header name for forwarded hosts (default {@code X-Forwarded-Port})
+ */
+ public void setForwardedPortHeader(String forwardedPortHeader)
+ {
+ if (_forwardedPortHeader == null || !_forwardedPortHeader.equalsIgnoreCase(forwardedPortHeader))
+ {
+ _forwardedPortHeader = forwardedPortHeader;
+ updateHandles();
+ }
+ }
+
+ /**
+ * @return if true, the X-Forwarded-Port header applies to the authority,
+ * else it applies to the remote client address
+ */
+ public boolean getForwardedPortAsAuthority()
+ {
+ return _forwardedPortAsAuthority;
+ }
+
+ /**
+ * Set if the X-Forwarded-Port header will be used for Authority
+ *
+ * @param forwardedPortAsAuthority if true, the X-Forwarded-Port header applies to the authority,
+ * else it applies to the remote client address
+ */
+ public void setForwardedPortAsAuthority(boolean forwardedPortAsAuthority)
+ {
+ _forwardedPortAsAuthority = forwardedPortAsAuthority;
+ }
+
+ /**
+ * Get the forwardedProtoHeader.
+ *
+ * @return the forwardedProtoHeader (default {@code X-Forwarded-Proto})
+ */
+ public String getForwardedProtoHeader()
+ {
+ return _forwardedProtoHeader;
+ }
+
+ /**
+ * Set the forwardedProtoHeader.
+ *
+ * @param forwardedProtoHeader the forwardedProtoHeader to set (default {@code X-Forwarded-Proto})
+ */
+ public void setForwardedProtoHeader(String forwardedProtoHeader)
+ {
+ if (_forwardedProtoHeader == null || !_forwardedProtoHeader.equalsIgnoreCase(forwardedProtoHeader))
+ {
+ _forwardedProtoHeader = forwardedProtoHeader;
+ updateHandles();
+ }
+ }
+
+ /**
+ * @return The header name holding a forwarded cipher suite (default {@code Proxy-auth-cert})
+ */
+ public String getForwardedCipherSuiteHeader()
+ {
+ return _forwardedCipherSuiteHeader;
+ }
+
+ /**
+ * @param forwardedCipherSuiteHeader The header name holding a forwarded cipher suite (default {@code Proxy-auth-cert})
+ */
+ public void setForwardedCipherSuiteHeader(String forwardedCipherSuiteHeader)
+ {
+ if (_forwardedCipherSuiteHeader == null || !_forwardedCipherSuiteHeader.equalsIgnoreCase(forwardedCipherSuiteHeader))
+ {
+ _forwardedCipherSuiteHeader = forwardedCipherSuiteHeader;
+ updateHandles();
+ }
+ }
+
+ /**
+ * @return The header name holding a forwarded SSL Session ID (default {@code Proxy-ssl-id})
+ */
+ public String getForwardedSslSessionIdHeader()
+ {
+ return _forwardedSslSessionIdHeader;
+ }
+
+ /**
+ * @param forwardedSslSessionIdHeader The header name holding a forwarded SSL Session ID (default {@code Proxy-ssl-id})
+ */
+ public void setForwardedSslSessionIdHeader(String forwardedSslSessionIdHeader)
+ {
+ if (_forwardedSslSessionIdHeader == null || !_forwardedSslSessionIdHeader.equalsIgnoreCase(forwardedSslSessionIdHeader))
+ {
+ _forwardedSslSessionIdHeader = forwardedSslSessionIdHeader;
+ updateHandles();
+ }
+ }
+
+ /**
+ * @return The header name holding a forwarded Https status indicator (on|off true|false) (default {@code X-Proxied-Https})
+ */
+ public String getForwardedHttpsHeader()
+ {
+ return _forwardedHttpsHeader;
+ }
+
+ /**
+ * @param forwardedHttpsHeader the header name holding a forwarded Https status indicator(default {@code X-Proxied-Https})
+ */
+ public void setForwardedHttpsHeader(String forwardedHttpsHeader)
+ {
+ if (_forwardedHttpsHeader == null || !_forwardedHttpsHeader.equalsIgnoreCase(forwardedHttpsHeader))
+ {
+ _forwardedHttpsHeader = forwardedHttpsHeader;
+ updateHandles();
+ }
+ }
+
+ /**
+ * @return true if the presence of an SSL session or certificate header is sufficient
+ * to indicate a secure request (default is true)
+ */
+ public boolean isSslIsSecure()
+ {
+ return _sslIsSecure;
+ }
+
+ /**
+ * @param sslIsSecure true if the presence of an SSL session or certificate header is sufficient
+ * to indicate a secure request (default is true)
+ */
+ public void setSslIsSecure(boolean sslIsSecure)
+ {
+ _sslIsSecure = sslIsSecure;
+ }
+
+ @Override
+ public void customize(Connector connector, HttpConfiguration config, Request request)
+ {
+ HttpFields httpFields = request.getHttpFields();
+
+ // Do a single pass through the header fields as it is a more efficient single iteration.
+ Forwarded forwarded = new Forwarded(request, config);
+ boolean match = false;
+ for (HttpField field : httpFields)
+ {
+ try
+ {
+ MethodHandle handle = _handles.get(field.getName());
+ if (handle != null)
+ {
+ match = true;
+ handle.invoke(forwarded, field);
+ }
+ }
+ catch (Throwable t)
+ {
+ onError(field, t);
+ }
+ }
+
+ if (match)
+ {
+ // Is secure status configured from headers?
+ if (forwarded.isSecure())
+ {
+ request.setSecure(true);
+ }
+
+ // Set Scheme from configured protocol
+ if (forwarded._proto != null)
+ {
+ request.setScheme(forwarded._proto);
+ }
+ // Set scheme if header implies secure scheme is to be used (see #isSslIsSecure())
+ else if (forwarded._secureScheme)
+ {
+ request.setScheme(config.getSecureScheme());
+ }
+
+ // Use authority from headers, if configured.
+ if (forwarded._authority != null)
+ {
+ String host = forwarded._authority._host;
+ int port = forwarded._authority._port;
+
+ HttpURI requestURI = request.getMetaData().getURI();
+
+ if (requestURI != null)
+ {
+ // Fall back to request metadata if needed.
+ if (host == null)
+ {
+ host = requestURI.getHost();
+ }
+
+ if (port == MutableHostPort.UNSET) // is unset by headers
+ {
+ port = requestURI.getPort();
+ }
+
+ // Don't change port if port == IMPLIED.
+
+ // Update authority if different from metadata
+ if (!host.equalsIgnoreCase(requestURI.getHost()) ||
+ port != requestURI.getPort())
+ {
+ httpFields.put(new HostPortHttpField(host, port));
+ request.setAuthority(host, port);
+ }
+ }
+ }
+
+ // Set Remote Address
+ if (forwarded.hasFor())
+ {
+ int forPort = forwarded._for._port > 0 ? forwarded._for._port : request.getRemotePort();
+ request.setRemoteAddr(InetSocketAddress.createUnresolved(forwarded._for._host, forPort));
+ }
+ }
+ }
+
+ protected static int getSecurePort(HttpConfiguration config)
+ {
+ return config.getSecurePort() > 0 ? config.getSecurePort() : 443;
+ }
+
+ protected void onError(HttpField field, Throwable t)
+ {
+ throw new BadMessageException("Bad header value for " + field.getName(), t);
+ }
+
+ protected static String getLeftMost(String headerValue)
+ {
+ if (headerValue == null)
+ return null;
+
+ int commaIndex = headerValue.indexOf(',');
+
+ if (commaIndex == -1)
+ {
+ // Single value
+ return headerValue;
+ }
+
+ // The left-most value is the farthest downstream client
+ return headerValue.substring(0, commaIndex).trim();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x", this.getClass().getSimpleName(), hashCode());
+ }
+
+ @Deprecated
+ public String getHostHeader()
+ {
+ return _forcedHost.getValue();
+ }
+
+ /**
+ * Set a forced valued for the host header to control what is returned by {@link ServletRequest#getServerName()} and {@link ServletRequest#getServerPort()}.
+ *
+ * @param hostHeader The value of the host header to force.
+ */
+ @Deprecated
+ public void setHostHeader(String hostHeader)
+ {
+ _forcedHost = new HostPortHttpField(hostHeader);
+ }
+
+ private void updateHandles()
+ {
+ int size = 0;
+ MethodHandles.Lookup lookup = MethodHandles.lookup();
+
+ // Loop to grow capacity of ArrayTrie for all headers
+ while (true)
+ {
+ try
+ {
+ size += 128; // experimented good baseline size
+ _handles = new ArrayTrie<>(size);
+
+ if (updateForwardedHandle(lookup, getForwardedHeader(), "handleRFC7239"))
+ continue;
+ if (updateForwardedHandle(lookup, getForwardedHostHeader(), "handleForwardedHost"))
+ continue;
+ if (updateForwardedHandle(lookup, getForwardedForHeader(), "handleForwardedFor"))
+ continue;
+ if (updateForwardedHandle(lookup, getForwardedPortHeader(), "handleForwardedPort"))
+ continue;
+ if (updateForwardedHandle(lookup, getForwardedProtoHeader(), "handleProto"))
+ continue;
+ if (updateForwardedHandle(lookup, getForwardedHttpsHeader(), "handleHttps"))
+ continue;
+ if (updateForwardedHandle(lookup, getForwardedServerHeader(), "handleForwardedServer"))
+ continue;
+ if (updateForwardedHandle(lookup, getForwardedCipherSuiteHeader(), "handleCipherSuite"))
+ continue;
+ if (updateForwardedHandle(lookup, getForwardedSslSessionIdHeader(), "handleSslSessionId"))
+ continue;
+ break;
+ }
+ catch (NoSuchMethodException | IllegalAccessException e)
+ {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ private boolean updateForwardedHandle(MethodHandles.Lookup lookup, String headerName, String forwardedMethodName) throws NoSuchMethodException, IllegalAccessException
+ {
+ final MethodType type = methodType(void.class, HttpField.class);
+
+ if (StringUtil.isBlank(headerName))
+ return false;
+
+ return !_handles.put(headerName, lookup.findVirtual(Forwarded.class, forwardedMethodName, type));
+ }
+
+ private static class MutableHostPort
+ {
+ public static final int UNSET = -1;
+ public static final int IMPLIED = 0;
+
+ String _host;
+ Source _hostSource = Source.UNSET;
+ int _port = UNSET;
+ Source _portSource = Source.UNSET;
+
+ public void setHostPort(String host, int port, Source source)
+ {
+ setHost(host, source);
+ setPort(port, source);
+ }
+
+ public void setHost(String host, Source source)
+ {
+ if (source.priority() > _hostSource.priority())
+ {
+ _host = host;
+ _hostSource = source;
+ }
+ }
+
+ public void setPort(int port, Source source)
+ {
+ if (source.priority() > _portSource.priority())
+ {
+ _port = port;
+ _portSource = source;
+ }
+ }
+
+ public void setHostPort(HostPort hostPort, Source source)
+ {
+ if (source.priority() > _hostSource.priority())
+ {
+ _host = hostPort.getHost();
+ _hostSource = source;
+ }
+
+ int port = hostPort.getPort();
+ // Is port supplied?
+ if (port > 0 && source.priority() > _portSource.priority())
+ {
+ _port = hostPort.getPort();
+ _portSource = source;
+ }
+ // Since we are Host:Port pair, the port could be unspecified
+ // Meaning it's implied.
+ // Make sure that we switch the tracked port from unset to implied
+ else if (_port == UNSET)
+ {
+ // set port to implied (with no priority)
+ _port = IMPLIED;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ final StringBuilder sb = new StringBuilder("MutableHostPort{");
+ sb.append("host='").append(_host).append("'/").append(_hostSource);
+ sb.append(", port=").append(_port);
+ sb.append("/").append(_portSource);
+ sb.append('}');
+ return sb.toString();
+ }
+ }
+
+ /**
+ * Ordered Source Enum.
+ * <p>
+ * Lowest first, Last/Highest priority wins
+ * </p>
+ */
+ public enum Source
+ {
+ UNSET,
+ XPROXIED_HTTPS,
+ XFORWARDED_PROTO,
+ XFORWARDED_SERVER,
+ XFORWARDED_PORT,
+ XFORWARDED_FOR,
+ XFORWARDED_HOST,
+ FORWARDED,
+ FORCED;
+
+ int priority()
+ {
+ return ordinal();
+ }
+ }
+
+ private class Forwarded extends QuotedCSVParser
+ {
+ HttpConfiguration _config;
+ Request _request;
+
+ MutableHostPort _authority;
+ MutableHostPort _for;
+ String _proto;
+ Source _protoSource = Source.UNSET;
+ Boolean _secure;
+ boolean _secureScheme = false;
+
+ public Forwarded(Request request, HttpConfiguration config)
+ {
+ super(false);
+ _request = request;
+ _config = config;
+ if (_forcedHost != null)
+ {
+ getAuthority().setHostPort(
+ _forcedHost.getHostPort().getHost(),
+ _forcedHost.getHostPort().getPort(),
+ Source.FORCED);
+ }
+ }
+
+ public boolean isSecure()
+ {
+ return (_secure != null && _secure);
+ }
+
+ public boolean hasFor()
+ {
+ return _for != null && _for._host != null;
+ }
+
+ private MutableHostPort getAuthority()
+ {
+ if (_authority == null)
+ {
+ _authority = new MutableHostPort();
+ }
+ return _authority;
+ }
+
+ private MutableHostPort getFor()
+ {
+ if (_for == null)
+ {
+ _for = new MutableHostPort();
+ }
+ return _for;
+ }
+
+ /**
+ * Called if header is <code>Proxy-auth-cert</code>
+ */
+ public void handleCipherSuite(HttpField field)
+ {
+ _request.setAttribute("javax.servlet.request.cipher_suite", field.getValue());
+
+ // Is ForwardingRequestCustomizer configured to trigger isSecure and scheme change on this header?
+ if (isSslIsSecure())
+ {
+ _secure = true;
+ // track desire for secure scheme, actual protocol will be resolved later.
+ _secureScheme = true;
+ }
+ }
+
+ /**
+ * Called if header is <code>Proxy-Ssl-Id</code>
+ */
+ public void handleSslSessionId(HttpField field)
+ {
+ _request.setAttribute("javax.servlet.request.ssl_session_id", field.getValue());
+
+ // Is ForwardingRequestCustomizer configured to trigger isSecure and scheme change on this header?
+ if (isSslIsSecure())
+ {
+ _secure = true;
+ // track desire for secure scheme, actual protocol will be resolved later.
+ _secureScheme = true;
+ }
+ }
+
+ /**
+ * Called if header is <code>X-Forwarded-Host</code>
+ */
+ public void handleForwardedHost(HttpField field)
+ {
+ updateAuthority(getLeftMost(field.getValue()), Source.XFORWARDED_HOST);
+ }
+
+ /**
+ * Called if header is <code>X-Forwarded-For</code>
+ */
+ public void handleForwardedFor(HttpField field)
+ {
+ HostPort hostField = new HostPort(getLeftMost(field.getValue()));
+ getFor().setHostPort(hostField, Source.XFORWARDED_FOR);
+ }
+
+ /**
+ * Called if header is <code>X-Forwarded-Server</code>
+ */
+ public void handleForwardedServer(HttpField field)
+ {
+ if (getProxyAsAuthority())
+ return;
+ updateAuthority(getLeftMost(field.getValue()), Source.XFORWARDED_SERVER);
+ }
+
+ /**
+ * Called if header is <code>X-Forwarded-Port</code>
+ */
+ public void handleForwardedPort(HttpField field)
+ {
+ int port = HostPort.parsePort(getLeftMost(field.getValue()));
+
+ updatePort(port, Source.XFORWARDED_PORT);
+ }
+
+ /**
+ * Called if header is <code>X-Forwarded-Proto</code>
+ */
+ public void handleProto(HttpField field)
+ {
+ updateProto(getLeftMost(field.getValue()), Source.XFORWARDED_PROTO);
+ }
+
+ /**
+ * Called if header is <code>X-Proxied-Https</code>
+ */
+ public void handleHttps(HttpField field)
+ {
+ if ("on".equalsIgnoreCase(field.getValue()) || "true".equalsIgnoreCase(field.getValue()))
+ {
+ _secure = true;
+ updateProto(HttpScheme.HTTPS.asString(), Source.XPROXIED_HTTPS);
+ updatePort(getSecurePort(_config), Source.XPROXIED_HTTPS);
+ }
+ else if ("off".equalsIgnoreCase(field.getValue()) || "false".equalsIgnoreCase(field.getValue()))
+ {
+ _secure = false;
+ updateProto(HttpScheme.HTTP.asString(), Source.XPROXIED_HTTPS);
+ updatePort(MutableHostPort.IMPLIED, Source.XPROXIED_HTTPS);
+ }
+ else
+ {
+ throw new BadMessageException("Invalid value for " + field.getName());
+ }
+ }
+
+ /**
+ * Called if header is <code>Forwarded</code>
+ */
+ public void handleRFC7239(HttpField field)
+ {
+ addValue(field.getValue());
+ }
+
+ @Override
+ protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
+ {
+ if (valueLength == 0 && paramValue > paramName)
+ {
+ String name = StringUtil.asciiToLowerCase(buffer.substring(paramName, paramValue - 1));
+ String value = buffer.substring(paramValue);
+ switch (name)
+ {
+ case "by":
+ {
+ if (!getProxyAsAuthority())
+ break;
+ if (value.startsWith("_") || "unknown".equals(value))
+ break;
+ HostPort hostField = new HostPort(value);
+ getAuthority().setHostPort(hostField.getHost(), hostField.getPort(), Source.FORWARDED);
+ break;
+ }
+ case "for":
+ {
+ if (value.startsWith("_") || "unknown".equals(value))
+ break;
+ HostPort hostField = new HostPort(value);
+ getFor().setHostPort(hostField.getHost(), hostField.getPort(), Source.FORWARDED);
+ break;
+ }
+ case "host":
+ {
+ if (value.startsWith("_") || "unknown".equals(value))
+ break;
+ HostPort hostField = new HostPort(value);
+ getAuthority().setHostPort(hostField.getHost(), hostField.getPort(), Source.FORWARDED);
+ break;
+ }
+ case "proto":
+ updateProto(value, Source.FORWARDED);
+ break;
+ }
+ }
+ }
+
+ private void updateAuthority(String value, Source source)
+ {
+ HostPort hostField = new HostPort(value);
+ getAuthority().setHostPort(hostField, source);
+ }
+
+ private void updatePort(int port, Source source)
+ {
+ if (getForwardedPortAsAuthority())
+ {
+ getAuthority().setPort(port, source);
+ }
+ else
+ {
+ getFor().setPort(port, source);
+ }
+ }
+
+ private void updateProto(String proto, Source source)
+ {
+ if (source.priority() > _protoSource.priority())
+ {
+ _proto = proto;
+ _protoSource = source;
+
+ if (_proto.equalsIgnoreCase(_config.getSecureScheme()))
+ {
+ _secure = true;
+ }
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java
new file mode 100644
index 0000000..17528f1
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java
@@ -0,0 +1,81 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.server.handler.HandlerWrapper;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.component.Destroyable;
+import org.eclipse.jetty.util.component.LifeCycle;
+
+/**
+ * A Jetty Server Handler.
+ * <p>
+ * A Handler instance is required by a {@link Server} to handle incoming
+ * HTTP requests.
+ * <p>
+ * A Handler may:
+ * <ul>
+ * <li>Completely generate the HTTP Response</li>
+ * <li>Examine/modify the request and call another Handler (see {@link HandlerWrapper}).
+ * <li>Pass the request to one or more other Handlers (see {@link HandlerCollection}).
+ * </ul>
+ *
+ * Handlers are passed the servlet API request and response object, but are
+ * not Servlets. The servlet container is implemented by handlers for
+ * context, security, session and servlet that modify the request object
+ * before passing it to the next stage of handling.
+ */
+@ManagedObject("Jetty Handler")
+public interface Handler extends LifeCycle, Destroyable
+{
+ /**
+ * Handle a request.
+ *
+ * @param target The target of the request - either a URI or a name.
+ * @param baseRequest The original unwrapped request object.
+ * @param request The request either as the {@link Request} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getRequest() getRequest()}</code>
+ * method can be used access the Request object if required.
+ * @param response The response as the {@link Response} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getResponse() getResponse()}</code>
+ * method can be used access the Response object if required.
+ * @throws IOException if unable to handle the request or response processing
+ * @throws ServletException if unable to handle the request or response due to underlying servlet issue
+ */
+ void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException;
+
+ void setServer(Server server);
+
+ @ManagedAttribute(value = "the jetty server for this handler", readonly = true)
+ Server getServer();
+
+ @ManagedOperation(value = "destroy associated resources", impact = "ACTION")
+ @Override
+ void destroy();
+}
+
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HandlerContainer.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HandlerContainer.java
new file mode 100644
index 0000000..1cdbc2b
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HandlerContainer.java
@@ -0,0 +1,59 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.LifeCycle;
+
+/**
+ * A Handler that contains other Handlers.
+ * <p>
+ * The contained handlers may be one (see @{link {@link org.eclipse.jetty.server.handler.HandlerWrapper})
+ * or many (see {@link org.eclipse.jetty.server.handler.HandlerList} or {@link org.eclipse.jetty.server.handler.HandlerCollection}.
+ */
+@ManagedObject("Handler of Multiple Handlers")
+public interface HandlerContainer extends LifeCycle
+{
+
+ /**
+ * @return array of handlers directly contained by this handler.
+ */
+ @ManagedAttribute("handlers in this container")
+ Handler[] getHandlers();
+
+ /**
+ * @return array of all handlers contained by this handler and it's children
+ */
+ @ManagedAttribute("all contained handlers")
+ Handler[] getChildHandlers();
+
+ /**
+ * @param byclass the child handler class to get
+ * @return array of all handlers contained by this handler and it's children of the passed type.
+ */
+ Handler[] getChildHandlersByClass(Class<?> byclass);
+
+ /**
+ * @param byclass the child handler class to get
+ * @param <T> the type of handler
+ * @return first handler of all handlers contained by this handler and it's children of the passed type.
+ */
+ <T extends Handler> T getChildHandlerByClass(Class<T> byclass);
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HomeBaseWarning.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HomeBaseWarning.java
new file mode 100644
index 0000000..9ee3887
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HomeBaseWarning.java
@@ -0,0 +1,75 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Display an optional Warning Message if the {jetty.home} and {jetty.base} are the same directory.
+ * <p>
+ * This is to warn about not recommended approach to setting up the Jetty Distribution.
+ */
+public class HomeBaseWarning
+{
+ private static final Logger LOG = Log.getLogger(HomeBaseWarning.class);
+
+ public HomeBaseWarning()
+ {
+ boolean showWarn = false;
+
+ String home = System.getProperty("jetty.home");
+ String base = System.getProperty("jetty.base");
+
+ if (StringUtil.isBlank(base))
+ {
+ // no base defined? then we are likely running
+ // via direct command line.
+ return;
+ }
+
+ Path homePath = new File(home).toPath();
+ Path basePath = new File(base).toPath();
+
+ try
+ {
+ showWarn = Files.isSameFile(homePath, basePath);
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ // Can't definitively determine this state
+ return;
+ }
+
+ if (showWarn)
+ {
+ StringBuilder warn = new StringBuilder();
+ warn.append("This instance of Jetty is not running from a separate {jetty.base} directory");
+ warn.append(", this is not recommended. See documentation at https://www.eclipse.org/jetty/documentation/current/startup.html");
+ LOG.warn("{}", warn.toString());
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HostHeaderCustomizer.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HostHeaderCustomizer.java
new file mode 100644
index 0000000..6f4678c
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HostHeaderCustomizer.java
@@ -0,0 +1,68 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.Objects;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Customizes requests that lack the {@code Host} header (for example, HTTP 1.0 requests).
+ * <p>
+ * In case of HTTP 1.0 requests that lack the {@code Host} header, the application may issue
+ * a redirect, and the {@code Location} header is usually constructed from the {@code Host}
+ * header; if the {@code Host} header is missing, the server may query the connector for its
+ * IP address in order to construct the {@code Location} header, and thus leak to clients
+ * internal IP addresses.
+ * <p>
+ * This {@link HttpConfiguration.Customizer} is configured with a {@code serverName} and
+ * optionally a {@code serverPort}.
+ * If the {@code Host} header is absent, the configured {@code serverName} will be set on
+ * the request so that {@link HttpServletRequest#getServerName()} will return that value,
+ * and likewise for {@code serverPort} and {@link HttpServletRequest#getServerPort()}.
+ */
+public class HostHeaderCustomizer implements HttpConfiguration.Customizer
+{
+ private final String serverName;
+ private final int serverPort;
+
+ /**
+ * @param serverName the {@code serverName} to set on the request (the {@code serverPort} will not be set)
+ */
+ public HostHeaderCustomizer(String serverName)
+ {
+ this(serverName, 0);
+ }
+
+ /**
+ * @param serverName the {@code serverName} to set on the request
+ * @param serverPort the {@code serverPort} to set on the request
+ */
+ public HostHeaderCustomizer(String serverName, int serverPort)
+ {
+ this.serverName = Objects.requireNonNull(serverName);
+ this.serverPort = serverPort;
+ }
+
+ @Override
+ public void customize(Connector connector, HttpConfiguration channelConfig, Request request)
+ {
+ if (request.getHeader("Host") == null)
+ request.setAuthority(serverName, serverPort); // TODO set the field as well?
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java
new file mode 100644
index 0000000..492c23c
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java
@@ -0,0 +1,1447 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.EventListener;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.servlet.DispatcherType;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpGenerator;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpHeaderValue;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.http.MetaData;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.ChannelEndPoint;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.QuietException;
+import org.eclipse.jetty.server.HttpChannelState.Action;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.HostPort;
+import org.eclipse.jetty.util.SharedBlockingCallback.Blocker;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+import static org.eclipse.jetty.util.thread.Invocable.InvocationType.NON_BLOCKING;
+
+/**
+ * HttpChannel represents a single endpoint for HTTP semantic processing.
+ * The HttpChannel is both an HttpParser.RequestHandler, where it passively receives events from
+ * an incoming HTTP request, and a Runnable, where it actively takes control of the request/response
+ * life cycle and calls the application (perhaps suspending and resuming with multiple calls to run).
+ * The HttpChannel signals the switch from passive mode to active mode by returning true to one of the
+ * HttpParser.RequestHandler callbacks. The completion of the active phase is signalled by a call to
+ * HttpTransport.completed().
+ */
+public class HttpChannel implements Runnable, HttpOutput.Interceptor
+{
+ public static Listener NOOP_LISTENER = new Listener(){};
+ private static final Logger LOG = Log.getLogger(HttpChannel.class);
+
+ private final AtomicLong _requests = new AtomicLong();
+ private final Connector _connector;
+ private final Executor _executor;
+ private final HttpConfiguration _configuration;
+ private final EndPoint _endPoint;
+ private final HttpTransport _transport;
+ private final HttpChannelState _state;
+ private final Request _request;
+ private final Response _response;
+ private final HttpChannel.Listener _combinedListener;
+ @Deprecated
+ private final List<Listener> _transientListeners = new ArrayList<>();
+ private HttpFields _trailers;
+ private final Supplier<HttpFields> _trailerSupplier = () -> _trailers;
+ private MetaData.Response _committedMetaData;
+ private RequestLog _requestLog;
+ private long _oldIdleTimeout;
+
+ /**
+ * Bytes written after interception (eg after compression)
+ */
+ private long _written;
+
+ public HttpChannel(Connector connector, HttpConfiguration configuration, EndPoint endPoint, HttpTransport transport)
+ {
+ _connector = connector;
+ _configuration = configuration;
+ _endPoint = endPoint;
+ _transport = transport;
+
+ _state = new HttpChannelState(this);
+ _request = new Request(this, newHttpInput(_state));
+ _response = new Response(this, newHttpOutput());
+
+ _executor = connector.getServer().getThreadPool();
+ _requestLog = connector.getServer().getRequestLog();
+ _combinedListener = (connector instanceof AbstractConnector)
+ ? ((AbstractConnector)connector).getHttpChannelListeners()
+ : NOOP_LISTENER;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("new {} -> {},{},{}",
+ this,
+ _endPoint,
+ _endPoint == null ? null : _endPoint.getConnection(),
+ _state);
+ }
+
+ public boolean isSendError()
+ {
+ return _state.isSendError();
+ }
+
+ /** Format the address or host returned from Request methods
+ * @param addr The address or host
+ * @return Default implementation returns {@link HostPort#normalizeHost(String)}
+ */
+ protected String formatAddrOrHost(String addr)
+ {
+ return HostPort.normalizeHost(addr);
+ }
+
+ protected HttpInput newHttpInput(HttpChannelState state)
+ {
+ return new HttpInput(state);
+ }
+
+ protected HttpOutput newHttpOutput()
+ {
+ return new HttpOutput(this);
+ }
+
+ public HttpChannelState getState()
+ {
+ return _state;
+ }
+
+ /**
+ * Add a transient Listener to the HttpChannel.
+ * <p>Listeners added by this method will only be notified
+ * if the HttpChannel has been constructed with an instance of
+ * {@link TransientListeners} as an {@link AbstractConnector}
+ * provided listener</p>
+ * <p>Transient listeners are removed after every request cycle</p>
+ * @param listener the listener to add
+ * @return true if the listener was added.
+ */
+ @Deprecated
+ public boolean addListener(Listener listener)
+ {
+ return _transientListeners.add(listener);
+ }
+
+ @Deprecated
+ public boolean removeListener(Listener listener)
+ {
+ return _transientListeners.remove(listener);
+ }
+
+ @Deprecated
+ public List<Listener> getTransientListeners()
+ {
+ return _transientListeners;
+ }
+
+ public long getBytesWritten()
+ {
+ return _written;
+ }
+
+ /**
+ * @return the number of requests handled by this connection
+ */
+ public long getRequests()
+ {
+ return _requests.get();
+ }
+
+ public Connector getConnector()
+ {
+ return _connector;
+ }
+
+ public HttpTransport getHttpTransport()
+ {
+ return _transport;
+ }
+
+ public RequestLog getRequestLog()
+ {
+ return _requestLog;
+ }
+
+ public void setRequestLog(RequestLog requestLog)
+ {
+ _requestLog = requestLog;
+ }
+
+ public void addRequestLog(RequestLog requestLog)
+ {
+ if (_requestLog == null)
+ _requestLog = requestLog;
+ else if (_requestLog instanceof RequestLogCollection)
+ ((RequestLogCollection)_requestLog).add(requestLog);
+ else
+ _requestLog = new RequestLogCollection(_requestLog, requestLog);
+ }
+
+ public MetaData.Response getCommittedMetaData()
+ {
+ return _committedMetaData;
+ }
+
+ /**
+ * Get the idle timeout.
+ * <p>This is implemented as a call to {@link EndPoint#getIdleTimeout()}, but may be
+ * overridden by channels that have timeouts different from their connections.
+ *
+ * @return the idle timeout (in milliseconds)
+ */
+ public long getIdleTimeout()
+ {
+ return _endPoint.getIdleTimeout();
+ }
+
+ /**
+ * Set the idle timeout.
+ * <p>This is implemented as a call to {@link EndPoint#setIdleTimeout(long)}, but may be
+ * overridden by channels that have timeouts different from their connections.
+ *
+ * @param timeoutMs the idle timeout in milliseconds
+ */
+ public void setIdleTimeout(long timeoutMs)
+ {
+ _endPoint.setIdleTimeout(timeoutMs);
+ }
+
+ public ByteBufferPool getByteBufferPool()
+ {
+ return _connector.getByteBufferPool();
+ }
+
+ public HttpConfiguration getHttpConfiguration()
+ {
+ return _configuration;
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return getHttpTransport().isOptimizedForDirectBuffers();
+ }
+
+ public Server getServer()
+ {
+ return _connector.getServer();
+ }
+
+ public Request getRequest()
+ {
+ return _request;
+ }
+
+ public Response getResponse()
+ {
+ return _response;
+ }
+
+ public Connection getConnection()
+ {
+ return _endPoint.getConnection();
+ }
+
+ public EndPoint getEndPoint()
+ {
+ return _endPoint;
+ }
+
+ public InetSocketAddress getLocalAddress()
+ {
+ return _endPoint.getLocalAddress();
+ }
+
+ public InetSocketAddress getRemoteAddress()
+ {
+ return _endPoint.getRemoteAddress();
+ }
+
+ /**
+ * If the associated response has the Expect header set to 100 Continue,
+ * then accessing the input stream indicates that the handler/servlet
+ * is ready for the request body and thus a 100 Continue response is sent.
+ *
+ * @param available estimate of the number of bytes that are available
+ * @throws IOException if the InputStream cannot be created
+ */
+ public void continue100(int available) throws IOException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ public void recycle()
+ {
+ _request.recycle();
+ _response.recycle();
+ _committedMetaData = null;
+ _requestLog = _connector == null ? null : _connector.getServer().getRequestLog();
+ _written = 0;
+ _trailers = null;
+ _oldIdleTimeout = 0;
+ _transientListeners.clear();
+ }
+
+ public void onAsyncWaitForContent()
+ {
+ }
+
+ public void onBlockWaitForContent()
+ {
+ }
+
+ public void onBlockWaitForContentFailure(Throwable failure)
+ {
+ getRequest().getHttpInput().failed(failure);
+ }
+
+ @Override
+ public void run()
+ {
+ handle();
+ }
+
+ /**
+ * @return True if the channel is ready to continue handling (ie it is not suspended)
+ */
+ public boolean handle()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("handle {} {} ", _request.getHttpURI(), this);
+
+ HttpChannelState.Action action = _state.handling();
+
+ // Loop here to handle async request redispatches.
+ // The loop is controlled by the call to async.unhandle in the
+ // finally block below. Unhandle will return false only if an async dispatch has
+ // already happened when unhandle is called.
+ loop:
+ while (!getServer().isStopped())
+ {
+ try
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("action {} {}", action, this);
+
+ switch (action)
+ {
+ case TERMINATED:
+ onCompleted();
+ break loop;
+
+ case WAIT:
+ // break loop without calling unhandle
+ break loop;
+
+ case DISPATCH:
+ {
+ if (!_request.hasMetaData())
+ throw new IllegalStateException("state=" + _state);
+
+ dispatch(DispatcherType.REQUEST, () ->
+ {
+ for (HttpConfiguration.Customizer customizer : _configuration.getCustomizers())
+ {
+ customizer.customize(getConnector(), _configuration, _request);
+ if (_request.isHandled())
+ return;
+ }
+ getServer().handle(HttpChannel.this);
+ });
+
+ break;
+ }
+
+ case ASYNC_DISPATCH:
+ {
+ dispatch(DispatcherType.ASYNC, () -> getServer().handleAsync(this));
+ break;
+ }
+
+ case ASYNC_TIMEOUT:
+ _state.onTimeout();
+ break;
+
+ case SEND_ERROR:
+ {
+ try
+ {
+ // Get ready to send an error response
+ _response.resetContent();
+
+ // the following is needed as you cannot trust the response code and reason
+ // as those could have been modified after calling sendError
+ Integer code = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
+ if (code == null)
+ code = HttpStatus.INTERNAL_SERVER_ERROR_500;
+ _response.setStatus(code);
+
+ // The handling of the original dispatch failed and we are now going to either generate
+ // and error response ourselves or dispatch for an error page. If there is content left over
+ // from the failed dispatch, then we try to consume it here and if we fail we add a
+ // Connection:close. This can't be deferred to COMPLETE as the response will be committed
+ // by then.
+ ensureConsumeAllOrNotPersistent();
+
+ ContextHandler.Context context = (ContextHandler.Context)_request.getAttribute(ErrorHandler.ERROR_CONTEXT);
+ ErrorHandler errorHandler = ErrorHandler.getErrorHandler(getServer(), context == null ? null : context.getContextHandler());
+
+ // If we can't have a body, then create a minimal error response.
+ if (HttpStatus.hasNoBody(_response.getStatus()) || errorHandler == null || !errorHandler.errorPageForMethod(_request.getMethod()))
+ {
+ sendResponseAndComplete();
+ break;
+ }
+
+ dispatch(DispatcherType.ERROR, () ->
+ {
+ errorHandler.handle(null, _request, _request, _response);
+ _request.setHandled(true);
+ });
+ }
+ catch (Throwable x)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Could not perform ERROR dispatch, aborting", x);
+ if (_state.isResponseCommitted())
+ abort(x);
+ else
+ {
+ try
+ {
+ _response.resetContent();
+ sendResponseAndComplete();
+ }
+ catch (Throwable t)
+ {
+ if (x != t)
+ x.addSuppressed(t);
+ abort(x);
+ }
+ }
+ }
+ finally
+ {
+ // clean up the context that was set in Response.sendError
+ _request.removeAttribute(ErrorHandler.ERROR_CONTEXT);
+ }
+ break;
+ }
+
+ case ASYNC_ERROR:
+ {
+ throw _state.getAsyncContextEvent().getThrowable();
+ }
+
+ case READ_REGISTER:
+ {
+ onAsyncWaitForContent();
+ break;
+ }
+
+ case READ_PRODUCE:
+ {
+ _request.getHttpInput().asyncReadProduce();
+ break;
+ }
+
+ case READ_CALLBACK:
+ {
+ ContextHandler handler = _state.getContextHandler();
+ if (handler != null)
+ handler.handle(_request, _request.getHttpInput());
+ else
+ _request.getHttpInput().run();
+ break;
+ }
+
+ case WRITE_CALLBACK:
+ {
+ ContextHandler handler = _state.getContextHandler();
+ if (handler != null)
+ handler.handle(_request, _response.getHttpOutput());
+ else
+ _response.getHttpOutput().run();
+ break;
+ }
+
+ case COMPLETE:
+ {
+ if (!_response.isCommitted())
+ {
+ if (!_request.isHandled() && !_response.getHttpOutput().isClosed())
+ {
+ // The request was not actually handled
+ _response.sendError(HttpStatus.NOT_FOUND_404);
+ break;
+ }
+
+ // Indicate Connection:close if we can't consume all.
+ if (_response.getStatus() >= 200)
+ ensureConsumeAllOrNotPersistent();
+ }
+
+ // RFC 7230, section 3.3.
+ if (!_request.isHead() &&
+ _response.getStatus() != HttpStatus.NOT_MODIFIED_304 &&
+ !_response.isContentComplete(_response.getHttpOutput().getWritten()))
+ {
+ if (isCommitted())
+ abort(new IOException("insufficient content written"));
+ else
+ {
+ _response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, "insufficient content written");
+ break;
+ }
+ }
+
+ // Set a close callback on the HttpOutput to make it an async callback
+ _response.completeOutput(Callback.from(NON_BLOCKING, () -> _state.completed(null), _state::completed));
+
+ break;
+ }
+
+ default:
+ throw new IllegalStateException(this.toString());
+ }
+ }
+ catch (Throwable failure)
+ {
+ if ("org.eclipse.jetty.continuation.ContinuationThrowable".equals(failure.getClass().getName()))
+ LOG.ignore(failure);
+ else
+ handleException(failure);
+ }
+
+ action = _state.unhandle();
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("!handle {} {}", action, this);
+
+ boolean suspended = action == Action.WAIT;
+ return !suspended;
+ }
+
+ public void ensureConsumeAllOrNotPersistent()
+ {
+ switch (_request.getHttpVersion())
+ {
+ case HTTP_1_0:
+ if (_request.getHttpInput().consumeAll())
+ return;
+
+ // Remove any keep-alive value in Connection headers
+ _response.getHttpFields().computeField(HttpHeader.CONNECTION, (h, fields) ->
+ {
+ if (fields == null || fields.isEmpty())
+ return null;
+ String v = fields.stream()
+ .flatMap(field -> Stream.of(field.getValues()).filter(s -> !HttpHeaderValue.KEEP_ALIVE.is(s)))
+ .collect(Collectors.joining(", "));
+ if (StringUtil.isEmpty(v))
+ return null;
+
+ return new HttpField(HttpHeader.CONNECTION, v);
+ });
+ break;
+
+ case HTTP_1_1:
+ if (_request.getHttpInput().consumeAll())
+ return;
+
+ // Add close value to Connection headers
+ _response.getHttpFields().computeField(HttpHeader.CONNECTION, (h, fields) ->
+ {
+ if (fields == null || fields.isEmpty())
+ return HttpConnection.CONNECTION_CLOSE;
+
+ if (fields.stream().anyMatch(f -> f.contains(HttpHeaderValue.CLOSE.asString())))
+ {
+ if (fields.size() == 1)
+ {
+ HttpField f = fields.get(0);
+ if (HttpConnection.CONNECTION_CLOSE.equals(f))
+ return f;
+ }
+
+ return new HttpField(HttpHeader.CONNECTION, fields.stream()
+ .flatMap(field -> Stream.of(field.getValues()).filter(s -> !HttpHeaderValue.KEEP_ALIVE.is(s)))
+ .collect(Collectors.joining(", ")));
+ }
+
+ return new HttpField(HttpHeader.CONNECTION,
+ Stream.concat(fields.stream()
+ .flatMap(field -> Stream.of(field.getValues()).filter(s -> !HttpHeaderValue.KEEP_ALIVE.is(s))),
+ Stream.of(HttpHeaderValue.CLOSE.asString()))
+ .collect(Collectors.joining(", ")));
+ });
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ private void dispatch(DispatcherType type, Dispatchable dispatchable) throws IOException, ServletException
+ {
+ try
+ {
+ _request.setHandled(false);
+ _response.reopen();
+ _request.setDispatcherType(type);
+ _combinedListener.onBeforeDispatch(_request);
+ dispatchable.dispatch();
+ }
+ catch (Throwable x)
+ {
+ _combinedListener.onDispatchFailure(_request, x);
+ throw x;
+ }
+ finally
+ {
+ _combinedListener.onAfterDispatch(_request);
+ _request.setDispatcherType(null);
+ }
+ }
+
+ /**
+ * <p>Sends an error 500, performing a special logic to detect whether the request is suspended,
+ * to avoid concurrent writes from the application.</p>
+ * <p>It may happen that the application suspends, and then throws an exception, while an application
+ * spawned thread writes the response content; in such case, we attempt to commit the error directly
+ * bypassing the {@link ErrorHandler} mechanisms and the response OutputStream.</p>
+ *
+ * @param failure the Throwable that caused the problem
+ */
+ protected void handleException(Throwable failure)
+ {
+ // Unwrap wrapping Jetty and Servlet exceptions.
+ Throwable quiet = unwrap(failure, QuietException.class);
+ Throwable noStack = unwrap(failure, BadMessageException.class, IOException.class, TimeoutException.class);
+
+ if (quiet != null || !getServer().isRunning())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(_request.getRequestURI(), failure);
+ }
+ else if (noStack != null)
+ {
+ // No stack trace unless there is debug turned on
+ if (LOG.isDebugEnabled())
+ LOG.warn("handleException " + _request.getRequestURI(), failure);
+ else
+ LOG.warn("handleException {} {}", _request.getRequestURI(), noStack.toString());
+ }
+ else
+ {
+ LOG.warn(_request.getRequestURI(), failure);
+ }
+
+ if (isCommitted())
+ abort(failure);
+ else
+ _state.onError(failure);
+ }
+
+ /**
+ * Unwrap failure causes to find target class
+ *
+ * @param failure The throwable to have its causes unwrapped
+ * @param targets Exception classes that we should not unwrap
+ * @return A target throwable or null
+ */
+ protected Throwable unwrap(Throwable failure, Class<?>... targets)
+ {
+ while (failure != null)
+ {
+ for (Class<?> x : targets)
+ {
+ if (x.isInstance(failure))
+ return failure;
+ }
+ failure = failure.getCause();
+ }
+ return null;
+ }
+
+ public void sendResponseAndComplete()
+ {
+ try
+ {
+ _request.setHandled(true);
+ _state.completing();
+ sendResponse(null, _response.getHttpOutput().getBuffer(), true, Callback.from(() -> _state.completed(null), _state::completed));
+ }
+ catch (Throwable x)
+ {
+ abort(x);
+ }
+ }
+
+ public boolean isExpecting100Continue()
+ {
+ return false;
+ }
+
+ public boolean isExpecting102Processing()
+ {
+ return false;
+ }
+
+ @Override
+ public String toString()
+ {
+ long timeStamp = _request.getTimeStamp();
+ return String.format("%s@%x{s=%s,r=%s,c=%b/%b,a=%s,uri=%s,age=%d}",
+ getClass().getSimpleName(),
+ hashCode(),
+ _state,
+ _requests,
+ isRequestCompleted(),
+ isResponseCompleted(),
+ _state.getState(),
+ _request.getHttpURI(),
+ timeStamp == 0 ? 0 : System.currentTimeMillis() - timeStamp);
+ }
+
+ public void onRequest(MetaData.Request request)
+ {
+ _requests.incrementAndGet();
+ _request.setTimeStamp(System.currentTimeMillis());
+ HttpFields fields = _response.getHttpFields();
+ if (_configuration.getSendDateHeader() && !fields.contains(HttpHeader.DATE))
+ fields.put(_connector.getServer().getDateField());
+
+ long idleTO = _configuration.getIdleTimeout();
+ _oldIdleTimeout = getIdleTimeout();
+ if (idleTO >= 0 && _oldIdleTimeout != idleTO)
+ setIdleTimeout(idleTO);
+
+ request.setTrailerSupplier(_trailerSupplier);
+ _request.setMetaData(request);
+
+ _request.setSecure(HttpScheme.HTTPS.is(request.getURI().getScheme()));
+
+ _combinedListener.onRequestBegin(_request);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("REQUEST for {} on {}{}{} {} {}{}{}", request.getURIString(), this, System.lineSeparator(),
+ request.getMethod(), request.getURIString(), request.getHttpVersion(), System.lineSeparator(),
+ request.getFields());
+ }
+
+ public boolean onContent(HttpInput.Content content)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onContent {} {}", this, content);
+ _combinedListener.onRequestContent(_request, content.getByteBuffer());
+ return _request.getHttpInput().addContent(content);
+ }
+
+ public boolean onContentComplete()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onContentComplete {}", this);
+ _combinedListener.onRequestContentEnd(_request);
+ return false;
+ }
+
+ public void onTrailers(HttpFields trailers)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onTrailers {} {}", this, trailers);
+ _trailers = trailers;
+ _combinedListener.onRequestTrailers(_request);
+ }
+
+ public boolean onRequestComplete()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onRequestComplete {}", this);
+ boolean result = _request.getHttpInput().eof();
+ _combinedListener.onRequestEnd(_request);
+ return result;
+ }
+
+ public void onCompleted()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onCompleted for {} written={}", getRequest().getRequestURI(), getBytesWritten());
+
+ if (_requestLog != null)
+ _requestLog.log(_request, _response);
+
+ long idleTO = _configuration.getIdleTimeout();
+ if (idleTO >= 0 && getIdleTimeout() != _oldIdleTimeout)
+ setIdleTimeout(_oldIdleTimeout);
+
+ _request.onCompleted();
+ _combinedListener.onComplete(_request);
+ _transport.onCompleted();
+ }
+
+ public boolean onEarlyEOF()
+ {
+ return _request.getHttpInput().earlyEOF();
+ }
+
+ public void onBadMessage(BadMessageException failure)
+ {
+ int status = failure.getCode();
+ String reason = failure.getReason();
+ if (status < HttpStatus.BAD_REQUEST_400 || status > 599)
+ failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, reason, failure);
+
+ _combinedListener.onRequestFailure(_request, failure);
+
+ Action action;
+ try
+ {
+ action = _state.handling();
+ }
+ catch (Throwable e)
+ {
+ // The bad message cannot be handled in the current state,
+ // so rethrow, hopefully somebody will be able to handle.
+ abort(e);
+ throw failure;
+ }
+
+ try
+ {
+ if (action == Action.DISPATCH)
+ {
+ ByteBuffer content = null;
+ HttpFields fields = new HttpFields();
+
+ ErrorHandler handler = getServer().getBean(ErrorHandler.class);
+ if (handler != null)
+ content = handler.badMessageError(status, reason, fields);
+
+ sendResponse(new MetaData.Response(HttpVersion.HTTP_1_1, status, reason, fields, BufferUtil.length(content)), content, true);
+ }
+ }
+ catch (IOException e)
+ {
+ LOG.debug(e);
+ }
+ finally
+ {
+ try
+ {
+ onCompleted();
+ }
+ catch (Throwable e)
+ {
+ LOG.debug(e);
+ abort(e);
+ }
+ }
+ }
+
+ public boolean sendResponse(MetaData.Response info, ByteBuffer content, boolean complete, final Callback callback)
+ {
+ boolean committing = _state.commitResponse();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("sendResponse info={} content={} complete={} committing={} callback={}",
+ info,
+ BufferUtil.toDetailString(content),
+ complete,
+ committing,
+ callback);
+
+ if (committing)
+ {
+ // We need an info to commit
+ if (info == null)
+ info = _response.newResponseMetaData();
+ commit(info);
+ _combinedListener.onResponseBegin(_request);
+ _request.onResponseCommit();
+
+ // wrap callback to process 100 responses
+ final int status = info.getStatus();
+ final Callback committed = (status < HttpStatus.OK_200 && status >= HttpStatus.CONTINUE_100)
+ ? new Send100Callback(callback)
+ : new SendCallback(callback, content, true, complete);
+
+ // committing write
+ _transport.send(info, _request.isHead(), content, complete, committed);
+ }
+ else if (info == null)
+ {
+ // This is a normal write
+ _transport.send(null, _request.isHead(), content, complete, new SendCallback(callback, content, false, complete));
+ }
+ else
+ {
+ callback.failed(new IllegalStateException("committed"));
+ }
+ return committing;
+ }
+
+ public boolean sendResponse(MetaData.Response info, ByteBuffer content, boolean complete) throws IOException
+ {
+ try (Blocker blocker = _response.getHttpOutput().acquireWriteBlockingCallback())
+ {
+ boolean committing = sendResponse(info, content, complete, blocker);
+ blocker.block();
+ return committing;
+ }
+ catch (Throwable failure)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(failure);
+ abort(failure);
+ throw failure;
+ }
+ }
+
+ protected void commit(MetaData.Response info)
+ {
+ _committedMetaData = info;
+ if (LOG.isDebugEnabled())
+ LOG.debug("COMMIT for {} on {}{}{} {} {}{}{}", getRequest().getRequestURI(), this, System.lineSeparator(),
+ info.getStatus(), info.getReason(), info.getHttpVersion(), System.lineSeparator(),
+ info.getFields());
+ }
+
+ public boolean isCommitted()
+ {
+ return _state.isResponseCommitted();
+ }
+
+ /**
+ * @return True if the request lifecycle is completed
+ */
+ public boolean isRequestCompleted()
+ {
+ return _state.isCompleted();
+ }
+
+ /**
+ * @return True if the response is completely written.
+ */
+ public boolean isResponseCompleted()
+ {
+ return _state.isResponseCompleted();
+ }
+
+ public boolean isPersistent()
+ {
+ return _endPoint.isOpen();
+ }
+
+ /**
+ * <p>Non-Blocking write, committing the response if needed.</p>
+ * Called as last link in HttpOutput.Filter chain
+ *
+ * @param content the content buffer to write
+ * @param complete whether the content is complete for the response
+ * @param callback Callback when complete or failed
+ */
+ @Override
+ public void write(ByteBuffer content, boolean complete, Callback callback)
+ {
+ sendResponse(null, content, complete, callback);
+ }
+
+ @Override
+ public void resetBuffer()
+ {
+ if (isCommitted())
+ throw new IllegalStateException("Committed");
+ }
+
+ @Override
+ public HttpOutput.Interceptor getNextInterceptor()
+ {
+ return null;
+ }
+
+ protected void execute(Runnable task)
+ {
+ _executor.execute(task);
+ }
+
+ public Scheduler getScheduler()
+ {
+ return _connector.getScheduler();
+ }
+
+ /**
+ * @return true if the HttpChannel can efficiently use direct buffer (typically this means it is not over SSL or a multiplexed protocol)
+ */
+ public boolean useDirectBuffers()
+ {
+ return getEndPoint() instanceof ChannelEndPoint;
+ }
+
+ /**
+ * If a write or similar operation to this channel fails,
+ * then this method should be called.
+ * <p>
+ * The standard implementation calls {@link HttpTransport#abort(Throwable)}.
+ *
+ * @param failure the failure that caused the abort.
+ */
+ public void abort(Throwable failure)
+ {
+ if (_state.abortResponse())
+ {
+ _combinedListener.onResponseFailure(_request, failure);
+ _transport.abort(failure);
+ }
+ }
+
+ private void notifyEvent1(Function<Listener, Consumer<Request>> function, Request request)
+ {
+ for (Listener listener : _transientListeners)
+ {
+ try
+ {
+ function.apply(listener).accept(request);
+ }
+ catch (Throwable x)
+ {
+ LOG.debug("Failure invoking listener " + listener, x);
+ }
+ }
+ }
+
+ private void notifyEvent2(Function<Listener, BiConsumer<Request, ByteBuffer>> function, Request request, ByteBuffer content)
+ {
+ for (Listener listener : _transientListeners)
+ {
+ ByteBuffer view = content.slice();
+ try
+ {
+ function.apply(listener).accept(request, view);
+ }
+ catch (Throwable x)
+ {
+ LOG.debug("Failure invoking listener " + listener, x);
+ }
+ }
+ }
+
+ private void notifyEvent2(Function<Listener, BiConsumer<Request, Throwable>> function, Request request, Throwable failure)
+ {
+ for (Listener listener : _transientListeners)
+ {
+ try
+ {
+ function.apply(listener).accept(request, failure);
+ }
+ catch (Throwable x)
+ {
+ LOG.debug("Failure invoking listener " + listener, x);
+ }
+ }
+ }
+
+ interface Dispatchable
+ {
+ void dispatch() throws IOException, ServletException;
+ }
+
+ /**
+ * <p>Listener for {@link HttpChannel} events.</p>
+ * <p>HttpChannel will emit events for the various phases it goes through while
+ * processing an HTTP request and response.</p>
+ * <p>Implementations of this interface may listen to those events to track
+ * timing and/or other values such as request URI, etc.</p>
+ * <p>The events parameters, especially the {@link Request} object, may be
+ * in a transient state depending on the event, and not all properties/features
+ * of the parameters may be available inside a listener method.</p>
+ * <p>It is recommended that the event parameters are <em>not</em> acted upon
+ * in the listener methods, or undefined behavior may result. For example, it
+ * would be a bad idea to try to read some content from the
+ * {@link javax.servlet.ServletInputStream} in listener methods. On the other
+ * hand, it is legit to store request attributes in one listener method that
+ * may be possibly retrieved in another listener method in a later event.</p>
+ * <p>Listener methods are invoked synchronously from the thread that is
+ * performing the request processing, and they should not call blocking code
+ * (otherwise the request processing will be blocked as well).</p>
+ * <p>Listener instances that are set as a bean on the {@link Connector} are
+ * efficiently added to {@link HttpChannel}. If additional listeners are added
+ * using the deprecated {@link HttpChannel#addListener(Listener)}</p> method,
+ * then an instance of {@link TransientListeners} must be added to the connector
+ * in order for them to be invoked.
+ */
+ public interface Listener extends EventListener
+ {
+ /**
+ * Invoked just after the HTTP request line and headers have been parsed.
+ *
+ * @param request the request object
+ */
+ default void onRequestBegin(Request request)
+ {
+ }
+
+ /**
+ * Invoked just before calling the application.
+ *
+ * @param request the request object
+ */
+ default void onBeforeDispatch(Request request)
+ {
+ }
+
+ /**
+ * Invoked when the application threw an exception.
+ *
+ * @param request the request object
+ * @param failure the exception thrown by the application
+ */
+ default void onDispatchFailure(Request request, Throwable failure)
+ {
+ }
+
+ /**
+ * Invoked just after the application returns from the first invocation.
+ *
+ * @param request the request object
+ */
+ default void onAfterDispatch(Request request)
+ {
+ }
+
+ /**
+ * Invoked every time a request content chunk has been parsed, just before
+ * making it available to the application.
+ *
+ * @param request the request object
+ * @param content a {@link ByteBuffer#slice() slice} of the request content chunk
+ */
+ default void onRequestContent(Request request, ByteBuffer content)
+ {
+ }
+
+ /**
+ * Invoked when the end of the request content is detected.
+ *
+ * @param request the request object
+ */
+ default void onRequestContentEnd(Request request)
+ {
+ }
+
+ /**
+ * Invoked when the request trailers have been parsed.
+ *
+ * @param request the request object
+ */
+ default void onRequestTrailers(Request request)
+ {
+ }
+
+ /**
+ * Invoked when the request has been fully parsed.
+ *
+ * @param request the request object
+ */
+ default void onRequestEnd(Request request)
+ {
+ }
+
+ /**
+ * Invoked when the request processing failed.
+ *
+ * @param request the request object
+ * @param failure the request failure
+ */
+ default void onRequestFailure(Request request, Throwable failure)
+ {
+ }
+
+ /**
+ * Invoked just before the response line is written to the network.
+ *
+ * @param request the request object
+ */
+ default void onResponseBegin(Request request)
+ {
+ }
+
+ /**
+ * Invoked just after the response is committed (that is, the response
+ * line, headers and possibly some content have been written to the
+ * network).
+ *
+ * @param request the request object
+ */
+ default void onResponseCommit(Request request)
+ {
+ }
+
+ /**
+ * Invoked after a response content chunk has been written to the network.
+ *
+ * @param request the request object
+ * @param content a {@link ByteBuffer#slice() slice} of the response content chunk
+ */
+ default void onResponseContent(Request request, ByteBuffer content)
+ {
+ }
+
+ /**
+ * Invoked when the response has been fully written.
+ *
+ * @param request the request object
+ */
+ default void onResponseEnd(Request request)
+ {
+ }
+
+ /**
+ * Invoked when the response processing failed.
+ *
+ * @param request the request object
+ * @param failure the response failure
+ */
+ default void onResponseFailure(Request request, Throwable failure)
+ {
+ }
+
+ /**
+ * Invoked when the request <em>and</em> response processing are complete.
+ *
+ * @param request the request object
+ */
+ default void onComplete(Request request)
+ {
+ }
+ }
+
+ private class SendCallback extends Callback.Nested
+ {
+ private final ByteBuffer _content;
+ private final int _length;
+ private final boolean _commit;
+ private final boolean _complete;
+
+ private SendCallback(Callback callback, ByteBuffer content, boolean commit, boolean complete)
+ {
+ super(callback);
+ _content = content == null ? BufferUtil.EMPTY_BUFFER : content.slice();
+ _length = _content.remaining();
+ _commit = commit;
+ _complete = complete;
+ }
+
+ @Override
+ public void succeeded()
+ {
+ _written += _length;
+ if (_commit)
+ _combinedListener.onResponseCommit(_request);
+ if (_length > 0)
+ _combinedListener.onResponseContent(_request, _content);
+ if (_complete && _state.completeResponse())
+ _combinedListener.onResponseEnd(_request);
+ super.succeeded();
+ }
+
+ @Override
+ public void failed(final Throwable x)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Commit failed", x);
+
+ if (x instanceof BadMessageException)
+ {
+ _transport.send(HttpGenerator.RESPONSE_500_INFO, false, null, true, new Callback.Nested(this)
+ {
+ @Override
+ public void succeeded()
+ {
+ _response.getHttpOutput().completed(null);
+ super.failed(x);
+ }
+
+ @Override
+ public void failed(Throwable th)
+ {
+ abort(x);
+ super.failed(x);
+ }
+ });
+ }
+ else
+ {
+ abort(x);
+ super.failed(x);
+ }
+ }
+ }
+
+ private class Send100Callback extends SendCallback
+ {
+ private Send100Callback(Callback callback)
+ {
+ super(callback, null, false, false);
+ }
+
+ @Override
+ public void succeeded()
+ {
+ if (_state.partialResponse())
+ super.succeeded();
+ else
+ super.failed(new IllegalStateException());
+ }
+ }
+
+ /**
+ * A Listener instance that can be added as a bean to {@link AbstractConnector} so that
+ * the listeners obtained from HttpChannel{@link #getTransientListeners()}
+ */
+ @Deprecated
+ public static class TransientListeners implements Listener
+ {
+ @Override
+ public void onRequestBegin(Request request)
+ {
+ request.getHttpChannel().notifyEvent1(listener -> listener::onRequestBegin, request);
+ }
+
+ @Override
+ public void onBeforeDispatch(Request request)
+ {
+ request.getHttpChannel().notifyEvent1(listener -> listener::onBeforeDispatch, request);
+ }
+
+ @Override
+ public void onDispatchFailure(Request request, Throwable failure)
+ {
+ request.getHttpChannel().notifyEvent2(listener -> listener::onDispatchFailure, request, failure);
+ }
+
+ @Override
+ public void onAfterDispatch(Request request)
+ {
+ request.getHttpChannel().notifyEvent1(listener -> listener::onAfterDispatch, request);
+ }
+
+ @Override
+ public void onRequestContent(Request request, ByteBuffer content)
+ {
+ request.getHttpChannel().notifyEvent2(listener -> listener::onRequestContent, request, content);
+ }
+
+ @Override
+ public void onRequestContentEnd(Request request)
+ {
+ request.getHttpChannel().notifyEvent1(listener -> listener::onRequestContentEnd, request);
+ }
+
+ @Override
+ public void onRequestTrailers(Request request)
+ {
+ request.getHttpChannel().notifyEvent1(listener -> listener::onRequestTrailers, request);
+ }
+
+ @Override
+ public void onRequestEnd(Request request)
+ {
+ request.getHttpChannel().notifyEvent1(listener -> listener::onRequestEnd, request);
+ }
+
+ @Override
+ public void onRequestFailure(Request request, Throwable failure)
+ {
+ request.getHttpChannel().notifyEvent2(listener -> listener::onRequestFailure, request, failure);
+ }
+
+ @Override
+ public void onResponseBegin(Request request)
+ {
+ request.getHttpChannel().notifyEvent1(listener -> listener::onResponseBegin, request);
+ }
+
+ @Override
+ public void onResponseCommit(Request request)
+ {
+ request.getHttpChannel().notifyEvent1(listener -> listener::onResponseCommit, request);
+ }
+
+ @Override
+ public void onResponseContent(Request request, ByteBuffer content)
+ {
+ request.getHttpChannel().notifyEvent2(listener -> listener::onResponseContent, request, content);
+ }
+
+ @Override
+ public void onResponseEnd(Request request)
+ {
+ request.getHttpChannel().notifyEvent1(listener -> listener::onResponseEnd, request);
+ }
+
+ @Override
+ public void onResponseFailure(Request request, Throwable failure)
+ {
+ request.getHttpChannel().notifyEvent2(listener -> listener::onResponseFailure, request, failure);
+ }
+
+ @Override
+ public void onComplete(Request request)
+ {
+ request.getHttpChannel().notifyEvent1(listener -> listener::onComplete, request);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelListeners.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelListeners.java
new file mode 100644
index 0000000..e542200
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelListeners.java
@@ -0,0 +1,286 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * A {@link HttpChannel.Listener} that holds a collection of
+ * other {@link HttpChannel.Listener} instances that are efficiently
+ * invoked without iteration.
+ * @see AbstractConnector
+ */
+public class HttpChannelListeners implements HttpChannel.Listener
+{
+ static final Logger LOG = Log.getLogger(HttpChannel.class);
+ public static HttpChannel.Listener NOOP = new HttpChannel.Listener() {};
+
+ private final NotifyRequest onRequestBegin;
+ private final NotifyRequest onBeforeDispatch;
+ private final NotifyFailure onDispatchFailure;
+ private final NotifyRequest onAfterDispatch;
+ private final NotifyContent onRequestContent;
+ private final NotifyRequest onRequestContentEnd;
+ private final NotifyRequest onRequestTrailers;
+ private final NotifyRequest onRequestEnd;
+ private final NotifyFailure onRequestFailure;
+ private final NotifyRequest onResponseBegin;
+ private final NotifyRequest onResponseCommit;
+ private final NotifyContent onResponseContent;
+ private final NotifyRequest onResponseEnd;
+ private final NotifyFailure onResponseFailure;
+ private final NotifyRequest onComplete;
+
+ public HttpChannelListeners(Collection<HttpChannel.Listener> listeners)
+ {
+ try
+ {
+ NotifyRequest onRequestBegin = NotifyRequest.NOOP;
+ NotifyRequest onBeforeDispatch = NotifyRequest.NOOP;
+ NotifyFailure onDispatchFailure = NotifyFailure.NOOP;
+ NotifyRequest onAfterDispatch = NotifyRequest.NOOP;
+ NotifyContent onRequestContent = NotifyContent.NOOP;
+ NotifyRequest onRequestContentEnd = NotifyRequest.NOOP;
+ NotifyRequest onRequestTrailers = NotifyRequest.NOOP;
+ NotifyRequest onRequestEnd = NotifyRequest.NOOP;
+ NotifyFailure onRequestFailure = NotifyFailure.NOOP;
+ NotifyRequest onResponseBegin = NotifyRequest.NOOP;
+ NotifyRequest onResponseCommit = NotifyRequest.NOOP;
+ NotifyContent onResponseContent = NotifyContent.NOOP;
+ NotifyRequest onResponseEnd = NotifyRequest.NOOP;
+ NotifyFailure onResponseFailure = NotifyFailure.NOOP;
+ NotifyRequest onComplete = NotifyRequest.NOOP;
+
+ for (HttpChannel.Listener listener : listeners)
+ {
+ if (!listener.getClass().getMethod("onRequestBegin", Request.class).isDefault())
+ onRequestBegin = combine(onRequestBegin, listener::onRequestBegin);
+ if (!listener.getClass().getMethod("onBeforeDispatch", Request.class).isDefault())
+ onBeforeDispatch = combine(onBeforeDispatch, listener::onBeforeDispatch);
+ if (!listener.getClass().getMethod("onDispatchFailure", Request.class, Throwable.class).isDefault())
+ onDispatchFailure = combine(onDispatchFailure, listener::onDispatchFailure);
+ if (!listener.getClass().getMethod("onAfterDispatch", Request.class).isDefault())
+ onAfterDispatch = combine(onAfterDispatch, listener::onAfterDispatch);
+ if (!listener.getClass().getMethod("onRequestContent", Request.class, ByteBuffer.class).isDefault())
+ onRequestContent = combine(onRequestContent, listener::onRequestContent);
+ if (!listener.getClass().getMethod("onRequestContentEnd", Request.class).isDefault())
+ onRequestContentEnd = combine(onRequestContentEnd, listener::onRequestContentEnd);
+ if (!listener.getClass().getMethod("onRequestTrailers", Request.class).isDefault())
+ onRequestTrailers = combine(onRequestTrailers, listener::onRequestTrailers);
+ if (!listener.getClass().getMethod("onRequestEnd", Request.class).isDefault())
+ onRequestEnd = combine(onRequestEnd, listener::onRequestEnd);
+ if (!listener.getClass().getMethod("onRequestFailure", Request.class, Throwable.class).isDefault())
+ onRequestFailure = combine(onRequestFailure, listener::onRequestFailure);
+ if (!listener.getClass().getMethod("onResponseBegin", Request.class).isDefault())
+ onResponseBegin = combine(onResponseBegin, listener::onResponseBegin);
+ if (!listener.getClass().getMethod("onResponseCommit", Request.class).isDefault())
+ onResponseCommit = combine(onResponseCommit, listener::onResponseCommit);
+ if (!listener.getClass().getMethod("onResponseContent", Request.class, ByteBuffer.class).isDefault())
+ onResponseContent = combine(onResponseContent, listener::onResponseContent);
+ if (!listener.getClass().getMethod("onResponseEnd", Request.class).isDefault())
+ onResponseEnd = combine(onResponseEnd, listener::onResponseEnd);
+ if (!listener.getClass().getMethod("onResponseFailure", Request.class, Throwable.class).isDefault())
+ onResponseFailure = combine(onResponseFailure, listener::onResponseFailure);
+ if (!listener.getClass().getMethod("onComplete", Request.class).isDefault())
+ onComplete = combine(onComplete, listener::onComplete);
+ }
+
+ this.onRequestBegin = onRequestBegin;
+ this.onBeforeDispatch = onBeforeDispatch;
+ this.onDispatchFailure = onDispatchFailure;
+ this.onAfterDispatch = onAfterDispatch;
+ this.onRequestContent = onRequestContent;
+ this.onRequestContentEnd = onRequestContentEnd;
+ this.onRequestTrailers = onRequestTrailers;
+ this.onRequestEnd = onRequestEnd;
+ this.onRequestFailure = onRequestFailure;
+ this.onResponseBegin = onResponseBegin;
+ this.onResponseCommit = onResponseCommit;
+ this.onResponseContent = onResponseContent;
+ this.onResponseEnd = onResponseEnd;
+ this.onResponseFailure = onResponseFailure;
+ this.onComplete = onComplete;
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void onRequestBegin(Request request)
+ {
+ onRequestBegin.onRequest(request);
+ }
+
+ @Override
+ public void onBeforeDispatch(Request request)
+ {
+ onBeforeDispatch.onRequest(request);
+ }
+
+ @Override
+ public void onDispatchFailure(Request request, Throwable failure)
+ {
+ onDispatchFailure.onFailure(request, failure);
+ }
+
+ @Override
+ public void onAfterDispatch(Request request)
+ {
+ onAfterDispatch.onRequest(request);
+ }
+
+ @Override
+ public void onRequestContent(Request request, ByteBuffer content)
+ {
+ onRequestContent.onContent(request, content);
+ }
+
+ @Override
+ public void onRequestContentEnd(Request request)
+ {
+ onRequestContentEnd.onRequest(request);
+ }
+
+ @Override
+ public void onRequestTrailers(Request request)
+ {
+ onRequestTrailers.onRequest(request);
+ }
+
+ @Override
+ public void onRequestEnd(Request request)
+ {
+ onRequestEnd.onRequest(request);
+ }
+
+ @Override
+ public void onRequestFailure(Request request, Throwable failure)
+ {
+ onRequestFailure.onFailure(request, failure);
+ }
+
+ @Override
+ public void onResponseBegin(Request request)
+ {
+ onResponseBegin.onRequest(request);
+ }
+
+ @Override
+ public void onResponseCommit(Request request)
+ {
+ onResponseCommit.onRequest(request);
+ }
+
+ @Override
+ public void onResponseContent(Request request, ByteBuffer content)
+ {
+ onResponseContent.onContent(request, content);
+ }
+
+ @Override
+ public void onResponseEnd(Request request)
+ {
+ onResponseEnd.onRequest(request);
+ }
+
+ @Override
+ public void onResponseFailure(Request request, Throwable failure)
+ {
+ onResponseFailure.onFailure(request, failure);
+ }
+
+ @Override
+ public void onComplete(Request request)
+ {
+ onComplete.onRequest(request);
+ }
+
+ private interface NotifyRequest
+ {
+ void onRequest(Request request);
+
+ NotifyRequest NOOP = request ->
+ {
+ };
+ }
+
+ private interface NotifyFailure
+ {
+ void onFailure(Request request, Throwable failure);
+
+ NotifyFailure NOOP = (request, failure) ->
+ {
+ };
+ }
+
+ private interface NotifyContent
+ {
+ void onContent(Request request, ByteBuffer content);
+
+ NotifyContent NOOP = (request, content) ->
+ {
+ };
+ }
+
+ private static NotifyRequest combine(NotifyRequest first, NotifyRequest second)
+ {
+ if (first == NotifyRequest.NOOP)
+ return second;
+ if (second == NotifyRequest.NOOP)
+ return first;
+ return request ->
+ {
+ first.onRequest(request);
+ second.onRequest(request);
+ };
+ }
+
+ private static NotifyFailure combine(NotifyFailure first, NotifyFailure second)
+ {
+ if (first == NotifyFailure.NOOP)
+ return second;
+ if (second == NotifyFailure.NOOP)
+ return first;
+ return (request, throwable) ->
+ {
+ first.onFailure(request, throwable);
+ second.onFailure(request, throwable);
+ };
+ }
+
+ private static NotifyContent combine(NotifyContent first, NotifyContent second)
+ {
+ if (first == NotifyContent.NOOP)
+ return (request, content) -> second.onContent(request, content.slice());
+ if (second == NotifyContent.NOOP)
+ return (request, content) -> first.onContent(request, content.slice());
+ return (request, content) ->
+ {
+ content = content.slice();
+ first.onContent(request, content);
+ second.onContent(request, content);
+ };
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java
new file mode 100644
index 0000000..655c06d
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java
@@ -0,0 +1,536 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HostPortHttpField;
+import org.eclipse.jetty.http.HttpCompliance;
+import org.eclipse.jetty.http.HttpComplianceSection;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpGenerator;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpHeaderValue;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpParser;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.http.MetaData;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * An HttpChannel customized to be transported over the HTTP/1 protocol
+ */
+public class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandler, HttpParser.ComplianceHandler
+{
+ private static final Logger LOG = Log.getLogger(HttpChannelOverHttp.class);
+ private static final HttpField PREAMBLE_UPGRADE_H2C = new HttpField(HttpHeader.UPGRADE, "h2c");
+ private final HttpFields _fields = new HttpFields();
+ private final MetaData.Request _metadata = new MetaData.Request(_fields);
+ private final HttpConnection _httpConnection;
+ private HttpField _connection;
+ private HttpField _upgrade = null;
+ private boolean _delayedForContent;
+ private boolean _unknownExpectation = false;
+ private boolean _expect100Continue = false;
+ private boolean _expect102Processing = false;
+ private List<String> _complianceViolations;
+ private HttpFields _trailers;
+
+ public HttpChannelOverHttp(HttpConnection httpConnection, Connector connector, HttpConfiguration config, EndPoint endPoint, HttpTransport transport)
+ {
+ super(connector, config, endPoint, transport);
+ _httpConnection = httpConnection;
+ _metadata.setURI(new HttpURI());
+ }
+
+ @Override
+ protected HttpInput newHttpInput(HttpChannelState state)
+ {
+ return new HttpInputOverHTTP(state);
+ }
+
+ @Override
+ public void recycle()
+ {
+ super.recycle();
+ _unknownExpectation = false;
+ _expect100Continue = false;
+ _expect102Processing = false;
+ _metadata.recycle();
+ _connection = null;
+ _fields.clear();
+ _upgrade = null;
+ _trailers = null;
+ }
+
+ @Override
+ public boolean isExpecting100Continue()
+ {
+ return _expect100Continue;
+ }
+
+ @Override
+ public boolean isExpecting102Processing()
+ {
+ return _expect102Processing;
+ }
+
+ @Override
+ public boolean startRequest(String method, String uri, HttpVersion version)
+ {
+ _metadata.setMethod(method);
+ _metadata.getURI().parseRequestTarget(method, uri);
+ _metadata.setHttpVersion(version);
+ _unknownExpectation = false;
+ _expect100Continue = false;
+ _expect102Processing = false;
+ return false;
+ }
+
+ @Override
+ public void parsedHeader(HttpField field)
+ {
+ HttpHeader header = field.getHeader();
+ String value = field.getValue();
+ if (header != null)
+ {
+ switch (header)
+ {
+ case CONNECTION:
+ _connection = field;
+ break;
+
+ case HOST:
+ if (!_metadata.getURI().isAbsolute() && field instanceof HostPortHttpField)
+ {
+ HostPortHttpField hp = (HostPortHttpField)field;
+ _metadata.getURI().setAuthority(hp.getHost(), hp.getPort());
+ }
+ break;
+
+ case EXPECT:
+ {
+ if (_metadata.getHttpVersion() == HttpVersion.HTTP_1_1)
+ {
+ HttpHeaderValue expect = HttpHeaderValue.CACHE.get(value);
+ switch (expect == null ? HttpHeaderValue.UNKNOWN : expect)
+ {
+ case CONTINUE:
+ _expect100Continue = true;
+ break;
+
+ case PROCESSING:
+ _expect102Processing = true;
+ break;
+
+ default:
+ String[] values = field.getValues();
+ for (int i = 0; values != null && i < values.length; i++)
+ {
+ expect = HttpHeaderValue.CACHE.get(values[i].trim());
+ if (expect == null)
+ _unknownExpectation = true;
+ else
+ {
+ switch (expect)
+ {
+ case CONTINUE:
+ _expect100Continue = true;
+ break;
+ case PROCESSING:
+ _expect102Processing = true;
+ break;
+ default:
+ _unknownExpectation = true;
+ }
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ case UPGRADE:
+ _upgrade = field;
+ break;
+
+ default:
+ break;
+ }
+ }
+ _fields.add(field);
+ }
+
+ @Override
+ public void parsedTrailer(HttpField field)
+ {
+ if (_trailers == null)
+ _trailers = new HttpFields();
+ _trailers.add(field);
+ }
+
+ /**
+ * If the associated response has the Expect header set to 100 Continue,
+ * then accessing the input stream indicates that the handler/servlet
+ * is ready for the request body and thus a 100 Continue response is sent.
+ *
+ * @throws IOException if the InputStream cannot be created
+ */
+ @Override
+ public void continue100(int available) throws IOException
+ {
+ // If the client is expecting 100 CONTINUE, then send it now.
+ // TODO: consider using an AtomicBoolean ?
+ if (isExpecting100Continue())
+ {
+ _expect100Continue = false;
+
+ // is content missing?
+ if (available == 0)
+ {
+ if (getResponse().isCommitted())
+ throw new IOException("Committed before 100 Continues");
+
+ boolean committed = sendResponse(HttpGenerator.CONTINUE_100_INFO, null, false);
+ if (!committed)
+ throw new IOException("Concurrent commit while trying to send 100-Continue");
+ }
+ }
+ }
+
+ @Override
+ public void earlyEOF()
+ {
+ _httpConnection.getGenerator().setPersistent(false);
+ // If we have no request yet, just close
+ if (_metadata.getMethod() == null)
+ _httpConnection.close();
+ else if (onEarlyEOF() || _delayedForContent)
+ {
+ _delayedForContent = false;
+ handle();
+ }
+ }
+
+ @Override
+ public boolean content(ByteBuffer content)
+ {
+ HttpInput.Content c = _httpConnection.newContent(content);
+ boolean handle = onContent(c) || _delayedForContent;
+ _delayedForContent = false;
+ return handle;
+ }
+
+ @Override
+ public void onAsyncWaitForContent()
+ {
+ _httpConnection.asyncReadFillInterested();
+ }
+
+ @Override
+ public void onBlockWaitForContent()
+ {
+ _httpConnection.blockingReadFillInterested();
+ }
+
+ @Override
+ public void onBlockWaitForContentFailure(Throwable failure)
+ {
+ _httpConnection.blockingReadFailure(failure);
+ }
+
+ @Override
+ public void badMessage(BadMessageException failure)
+ {
+ _httpConnection.getGenerator().setPersistent(false);
+ try
+ {
+ // Need to call onRequest, so RequestLog can reports as much as possible
+ onRequest(_metadata);
+ getRequest().getHttpInput().earlyEOF();
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+
+ onBadMessage(failure);
+ }
+
+ @Override
+ public boolean headerComplete()
+ {
+ if (_complianceViolations != null && !_complianceViolations.isEmpty())
+ {
+ this.getRequest().setAttribute(HttpCompliance.VIOLATIONS_ATTR, _complianceViolations);
+ _complianceViolations = null;
+ }
+
+ boolean persistent;
+
+ switch (_metadata.getHttpVersion())
+ {
+ case HTTP_0_9:
+ {
+ persistent = false;
+ break;
+ }
+ case HTTP_1_0:
+ {
+ if (getHttpConfiguration().isPersistentConnectionsEnabled())
+ {
+ if (_connection != null)
+ {
+ if (_connection.contains(HttpHeaderValue.KEEP_ALIVE.asString()))
+ persistent = true;
+ else
+ persistent = _fields.contains(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.asString());
+ }
+ else
+ persistent = false;
+ }
+ else
+ persistent = false;
+
+ if (!persistent)
+ persistent = HttpMethod.CONNECT.is(_metadata.getMethod());
+ if (persistent)
+ getResponse().getHttpFields().add(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE);
+
+ break;
+ }
+
+ case HTTP_1_1:
+ {
+ if (_unknownExpectation)
+ {
+ badMessage(new BadMessageException(HttpStatus.EXPECTATION_FAILED_417));
+ return false;
+ }
+
+ if (getHttpConfiguration().isPersistentConnectionsEnabled())
+ {
+ if (_connection != null)
+ {
+ if (_connection.contains(HttpHeaderValue.CLOSE.asString()))
+ persistent = false;
+ else
+ persistent = !_fields.contains(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()); // handle multiple connection fields
+ }
+ else
+ persistent = true;
+ }
+ else
+ persistent = false;
+
+ if (!persistent)
+ persistent = HttpMethod.CONNECT.is(_metadata.getMethod());
+ if (!persistent)
+ getResponse().getHttpFields().add(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE);
+
+ if (_upgrade != null && upgrade())
+ return true;
+
+ break;
+ }
+
+ case HTTP_2:
+ {
+ // Allow direct "upgrade" to HTTP_2_0 only if the connector supports h2c.
+ _upgrade = PREAMBLE_UPGRADE_H2C;
+
+ if (HttpMethod.PRI.is(_metadata.getMethod()) &&
+ "*".equals(_metadata.getURI().toString()) &&
+ _fields.size() == 0 &&
+ upgrade())
+ return true;
+
+ badMessage(new BadMessageException(HttpStatus.UPGRADE_REQUIRED_426));
+ _httpConnection.getParser().close();
+ return false;
+ }
+
+ default:
+ {
+ throw new IllegalStateException("unsupported version " + _metadata.getHttpVersion());
+ }
+ }
+
+ if (!persistent)
+ _httpConnection.getGenerator().setPersistent(false);
+
+ onRequest(_metadata);
+
+ // Should we delay dispatch until we have some content?
+ // We should not delay if there is no content expect or client is expecting 100 or the response is already committed or the request buffer already has something in it to parse
+ _delayedForContent = (getHttpConfiguration().isDelayDispatchUntilContent() &&
+ (_httpConnection.getParser().getContentLength() > 0 || _httpConnection.getParser().isChunking()) &&
+ !isExpecting100Continue() &&
+ !isCommitted() &&
+ _httpConnection.isRequestBufferEmpty());
+
+ return !_delayedForContent;
+ }
+
+ boolean onIdleTimeout(Throwable timeout)
+ {
+ if (_delayedForContent)
+ {
+ _delayedForContent = false;
+ getRequest().getHttpInput().onIdleTimeout(timeout);
+ execute(this);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * <p>Attempts to perform an HTTP/1.1 upgrade.</p>
+ * <p>The upgrade looks up a {@link ConnectionFactory.Upgrading} from the connector
+ * matching the protocol specified in the {@code Upgrade} header.</p>
+ * <p>The upgrade may succeed, be ignored (which can allow a later handler to implement)
+ * or fail with a {@link BadMessageException}.</p>
+ *
+ * @return true if the upgrade was performed, false if it was ignored
+ * @throws BadMessageException if the upgrade failed
+ */
+ private boolean upgrade() throws BadMessageException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("upgrade {} {}", this, _upgrade);
+
+ @SuppressWarnings("ReferenceEquality")
+ boolean isUpgradedH2C = (_upgrade == PREAMBLE_UPGRADE_H2C);
+
+ if (!isUpgradedH2C && (_connection == null || !_connection.contains("upgrade")))
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400);
+
+ // Find the upgrade factory
+ ConnectionFactory.Upgrading factory = null;
+ for (ConnectionFactory f : getConnector().getConnectionFactories())
+ {
+ if (f instanceof ConnectionFactory.Upgrading)
+ {
+ if (f.getProtocols().contains(_upgrade.getValue()))
+ {
+ factory = (ConnectionFactory.Upgrading)f;
+ break;
+ }
+ }
+ }
+
+ if (factory == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("No factory for {} in {}", _upgrade, getConnector());
+ return false;
+ }
+
+ // Create new connection
+ HttpFields response101 = new HttpFields();
+ Connection upgradeConnection = factory.upgradeConnection(getConnector(), getEndPoint(), _metadata, response101);
+ if (upgradeConnection == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Upgrade ignored for {} by {}", _upgrade, factory);
+ return false;
+ }
+
+ // Send 101 if needed
+ try
+ {
+ if (!isUpgradedH2C)
+ sendResponse(new MetaData.Response(HttpVersion.HTTP_1_1, HttpStatus.SWITCHING_PROTOCOLS_101, response101, 0), null, true);
+ }
+ catch (IOException e)
+ {
+ throw new BadMessageException(HttpStatus.INTERNAL_SERVER_ERROR_500, null, e);
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Upgrade from {} to {}", getEndPoint().getConnection(), upgradeConnection);
+ getRequest().setAttribute(HttpConnection.UPGRADE_CONNECTION_ATTRIBUTE, upgradeConnection);
+ getResponse().setStatus(101);
+ getHttpTransport().onCompleted();
+ return true;
+ }
+
+ @Override
+ protected void handleException(Throwable x)
+ {
+ _httpConnection.getGenerator().setPersistent(false);
+ super.handleException(x);
+ }
+
+ @Override
+ public void abort(Throwable failure)
+ {
+ super.abort(failure);
+ _httpConnection.getGenerator().setPersistent(false);
+ }
+
+ @Override
+ public boolean contentComplete()
+ {
+ boolean handle = onContentComplete() || _delayedForContent;
+ _delayedForContent = false;
+ return handle;
+ }
+
+ @Override
+ public boolean messageComplete()
+ {
+ if (_trailers != null)
+ onTrailers(_trailers);
+ return onRequestComplete();
+ }
+
+ @Override
+ public int getHeaderCacheSize()
+ {
+ return getHttpConfiguration().getHeaderCacheSize();
+ }
+
+ @Override
+ public void onComplianceViolation(HttpCompliance compliance, HttpComplianceSection violation, String reason)
+ {
+ if (_httpConnection.isRecordHttpComplianceViolations())
+ {
+ if (_complianceViolations == null)
+ {
+ _complianceViolations = new ArrayList<>();
+ }
+ String record = String.format("%s (see %s) in mode %s for %s in %s",
+ violation.getDescription(), violation.getURL(), compliance, reason, getHttpTransport());
+ _complianceViolations.add(record);
+ if (LOG.isDebugEnabled())
+ LOG.debug(record);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java
new file mode 100644
index 0000000..e96b2e9
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java
@@ -0,0 +1,1439 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.AsyncListener;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletResponse;
+import javax.servlet.UnavailableException;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.io.QuietException;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandler.Context;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION;
+import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION_TYPE;
+import static javax.servlet.RequestDispatcher.ERROR_MESSAGE;
+import static javax.servlet.RequestDispatcher.ERROR_REQUEST_URI;
+import static javax.servlet.RequestDispatcher.ERROR_SERVLET_NAME;
+import static javax.servlet.RequestDispatcher.ERROR_STATUS_CODE;
+
+/**
+ * Implementation of AsyncContext interface that holds the state of request-response cycle.
+ */
+public class HttpChannelState
+{
+ private static final Logger LOG = Log.getLogger(HttpChannelState.class);
+
+ private static final long DEFAULT_TIMEOUT = Long.getLong("org.eclipse.jetty.server.HttpChannelState.DEFAULT_TIMEOUT", 30000L);
+
+ /*
+ * The state of the HttpChannel,used to control the overall lifecycle.
+ * <pre>
+ * IDLE <-----> HANDLING ----> WAITING
+ * | ^ /
+ * | \ /
+ * v \ v
+ * UPGRADED WOKEN
+ * </pre>
+ */
+ public enum State
+ {
+ IDLE, // Idle request
+ HANDLING, // Request dispatched to filter/servlet or Async IO callback
+ WAITING, // Suspended and waiting
+ WOKEN, // Dispatch to handle from ASYNC_WAIT
+ UPGRADED // Request upgraded the connection
+ }
+
+ /*
+ * The state of the request processing lifecycle.
+ * <pre>
+ * BLOCKING <----> COMPLETING ---> COMPLETED
+ * ^ | ^ ^
+ * / | \ |
+ * | | DISPATCH |
+ * | | ^ ^ |
+ * | v / | |
+ * | ASYNC -------> COMPLETE
+ * | | | ^
+ * | v | |
+ * | EXPIRE | |
+ * \ | / |
+ * \ v / |
+ * EXPIRING ----------+
+ * </pre>
+ */
+ private enum RequestState
+ {
+ BLOCKING, // Blocking request dispatched
+ ASYNC, // AsyncContext.startAsync() has been called
+ DISPATCH, // AsyncContext.dispatch() has been called
+ EXPIRE, // AsyncContext timeout has happened
+ EXPIRING, // AsyncListeners are being called
+ COMPLETE, // AsyncContext.complete() has been called
+ COMPLETING, // Request is being closed (maybe asynchronously)
+ COMPLETED // Response is completed
+ }
+
+ /*
+ * The input readiness state, which works together with {@link HttpInput.State}
+ */
+ private enum InputState
+ {
+ IDLE, // No isReady; No data
+ REGISTER, // isReady()==false handling; No data
+ REGISTERED, // isReady()==false !handling; No data
+ POSSIBLE, // isReady()==false async read callback called (http/1 only)
+ PRODUCING, // isReady()==false READ_PRODUCE action is being handled (http/1 only)
+ READY // isReady() was false, onContentAdded has been called
+ }
+
+ /*
+ * The output committed state, which works together with {@link HttpOutput.State}
+ */
+ private enum OutputState
+ {
+ OPEN,
+ COMMITTED,
+ COMPLETED,
+ ABORTED,
+ }
+
+ /**
+ * The actions to take as the channel moves from state to state.
+ */
+ public enum Action
+ {
+ DISPATCH, // handle a normal request dispatch
+ ASYNC_DISPATCH, // handle an async request dispatch
+ SEND_ERROR, // Generate an error page or error dispatch
+ ASYNC_ERROR, // handle an async error
+ ASYNC_TIMEOUT, // call asyncContext onTimeout
+ WRITE_CALLBACK, // handle an IO write callback
+ READ_REGISTER, // Register for fill interest
+ READ_PRODUCE, // Check is a read is possible by parsing/filling
+ READ_CALLBACK, // handle an IO read callback
+ COMPLETE, // Complete the response by closing output
+ TERMINATED, // No further actions
+ WAIT, // Wait for further events
+ }
+
+ private final HttpChannel _channel;
+ private List<AsyncListener> _asyncListeners;
+ private State _state = State.IDLE;
+ private RequestState _requestState = RequestState.BLOCKING;
+ private OutputState _outputState = OutputState.OPEN;
+ private InputState _inputState = InputState.IDLE;
+ private boolean _initial = true;
+ private boolean _sendError;
+ private boolean _asyncWritePossible;
+ private long _timeoutMs = DEFAULT_TIMEOUT;
+ private AsyncContextEvent _event;
+ private Thread _onTimeoutThread;
+
+ protected HttpChannelState(HttpChannel channel)
+ {
+ _channel = channel;
+ }
+
+ public State getState()
+ {
+ synchronized (this)
+ {
+ return _state;
+ }
+ }
+
+ public void addListener(AsyncListener listener)
+ {
+ synchronized (this)
+ {
+ if (_asyncListeners == null)
+ _asyncListeners = new ArrayList<>();
+ _asyncListeners.add(listener);
+ }
+ }
+
+ public boolean hasListener(AsyncListener listener)
+ {
+ synchronized (this)
+ {
+ if (_asyncListeners == null)
+ return false;
+ for (AsyncListener l : _asyncListeners)
+ {
+ if (l == listener)
+ return true;
+
+ if (l instanceof AsyncContextState.WrappedAsyncListener && ((AsyncContextState.WrappedAsyncListener)l).getListener() == listener)
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ public boolean isSendError()
+ {
+ synchronized (this)
+ {
+ return _sendError;
+ }
+ }
+
+ public void setTimeout(long ms)
+ {
+ synchronized (this)
+ {
+ _timeoutMs = ms;
+ }
+ }
+
+ public long getTimeout()
+ {
+ synchronized (this)
+ {
+ return _timeoutMs;
+ }
+ }
+
+ public AsyncContextEvent getAsyncContextEvent()
+ {
+ synchronized (this)
+ {
+ return _event;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ synchronized (this)
+ {
+ return toStringLocked();
+ }
+ }
+
+ private String toStringLocked()
+ {
+ return String.format("%s@%x{%s}",
+ getClass().getSimpleName(),
+ hashCode(),
+ getStatusStringLocked());
+ }
+
+ private String getStatusStringLocked()
+ {
+ return String.format("s=%s rs=%s os=%s is=%s awp=%b se=%b i=%b al=%d",
+ _state,
+ _requestState,
+ _outputState,
+ _inputState,
+ _asyncWritePossible,
+ _sendError,
+ _initial,
+ _asyncListeners == null ? 0 : _asyncListeners.size());
+ }
+
+ public String getStatusString()
+ {
+ synchronized (this)
+ {
+ return getStatusStringLocked();
+ }
+ }
+
+ public boolean commitResponse()
+ {
+ synchronized (this)
+ {
+ switch (_outputState)
+ {
+ case OPEN:
+ _outputState = OutputState.COMMITTED;
+ return true;
+
+ default:
+ return false;
+ }
+ }
+ }
+
+ public boolean partialResponse()
+ {
+ synchronized (this)
+ {
+ switch (_outputState)
+ {
+ case COMMITTED:
+ _outputState = OutputState.OPEN;
+ return true;
+
+ default:
+ return false;
+ }
+ }
+ }
+
+ public boolean completeResponse()
+ {
+ synchronized (this)
+ {
+ switch (_outputState)
+ {
+ case OPEN:
+ case COMMITTED:
+ _outputState = OutputState.COMPLETED;
+ return true;
+
+ default:
+ return false;
+ }
+ }
+ }
+
+ public boolean isResponseCommitted()
+ {
+ synchronized (this)
+ {
+ switch (_outputState)
+ {
+ case OPEN:
+ return false;
+ default:
+ return true;
+ }
+ }
+ }
+
+ public boolean isResponseCompleted()
+ {
+ synchronized (this)
+ {
+ return _outputState == OutputState.COMPLETED;
+ }
+ }
+
+ public boolean abortResponse()
+ {
+ synchronized (this)
+ {
+ switch (_outputState)
+ {
+ case ABORTED:
+ return false;
+
+ case OPEN:
+ _channel.getResponse().setStatus(500);
+ _outputState = OutputState.ABORTED;
+ return true;
+
+ default:
+ _outputState = OutputState.ABORTED;
+ return true;
+ }
+ }
+ }
+
+ /**
+ * @return Next handling of the request should proceed
+ */
+ public Action handling()
+ {
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("handling {}", toStringLocked());
+
+ switch (_state)
+ {
+ case IDLE:
+ if (_requestState != RequestState.BLOCKING)
+ throw new IllegalStateException(getStatusStringLocked());
+ _initial = true;
+ _state = State.HANDLING;
+ return Action.DISPATCH;
+
+ case WOKEN:
+ if (_event != null && _event.getThrowable() != null && !_sendError)
+ {
+ _state = State.HANDLING;
+ return Action.ASYNC_ERROR;
+ }
+
+ Action action = nextAction(true);
+ if (LOG.isDebugEnabled())
+ LOG.debug("nextAction(true) {} {}", action, toStringLocked());
+ return action;
+
+ default:
+ throw new IllegalStateException(getStatusStringLocked());
+ }
+ }
+ }
+
+ /**
+ * Signal that the HttpConnection has finished handling the request.
+ * For blocking connectors, this call may block if the request has
+ * been suspended (startAsync called).
+ *
+ * @return next actions
+ * be handled again (eg because of a resume that happened before unhandle was called)
+ */
+ protected Action unhandle()
+ {
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("unhandle {}", toStringLocked());
+
+ if (_state != State.HANDLING)
+ throw new IllegalStateException(this.getStatusStringLocked());
+
+ _initial = false;
+
+ Action action = nextAction(false);
+ if (LOG.isDebugEnabled())
+ LOG.debug("nextAction(false) {} {}", action, toStringLocked());
+ return action;
+ }
+ }
+
+ private Action nextAction(boolean handling)
+ {
+ // Assume we can keep going, but exceptions are below
+ _state = State.HANDLING;
+
+ if (_sendError)
+ {
+ switch (_requestState)
+ {
+ case BLOCKING:
+ case ASYNC:
+ case COMPLETE:
+ case DISPATCH:
+ case COMPLETING:
+ _requestState = RequestState.BLOCKING;
+ _sendError = false;
+ return Action.SEND_ERROR;
+
+ default:
+ break;
+ }
+ }
+
+ switch (_requestState)
+ {
+ case BLOCKING:
+ if (handling)
+ throw new IllegalStateException(getStatusStringLocked());
+ _requestState = RequestState.COMPLETING;
+ return Action.COMPLETE;
+
+ case ASYNC:
+ switch (_inputState)
+ {
+ case POSSIBLE:
+ _inputState = InputState.PRODUCING;
+ return Action.READ_PRODUCE;
+ case READY:
+ _inputState = InputState.IDLE;
+ return Action.READ_CALLBACK;
+ case REGISTER:
+ case PRODUCING:
+ _inputState = InputState.REGISTERED;
+ return Action.READ_REGISTER;
+ case IDLE:
+ case REGISTERED:
+ break;
+ default:
+ throw new IllegalStateException(getStatusStringLocked());
+ }
+
+ if (_asyncWritePossible)
+ {
+ _asyncWritePossible = false;
+ return Action.WRITE_CALLBACK;
+ }
+
+ Scheduler scheduler = _channel.getScheduler();
+ if (scheduler != null && _timeoutMs > 0 && !_event.hasTimeoutTask())
+ _event.setTimeoutTask(scheduler.schedule(_event, _timeoutMs, TimeUnit.MILLISECONDS));
+ _state = State.WAITING;
+ return Action.WAIT;
+
+ case DISPATCH:
+ _requestState = RequestState.BLOCKING;
+ return Action.ASYNC_DISPATCH;
+
+ case EXPIRE:
+ _requestState = RequestState.EXPIRING;
+ return Action.ASYNC_TIMEOUT;
+
+ case EXPIRING:
+ if (handling)
+ throw new IllegalStateException(getStatusStringLocked());
+ sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, "AsyncContext timeout");
+ // handle sendError immediately
+ _requestState = RequestState.BLOCKING;
+ _sendError = false;
+ return Action.SEND_ERROR;
+
+ case COMPLETE:
+ _requestState = RequestState.COMPLETING;
+ return Action.COMPLETE;
+
+ case COMPLETING:
+ _state = State.WAITING;
+ return Action.WAIT;
+
+ case COMPLETED:
+ _state = State.IDLE;
+ return Action.TERMINATED;
+
+ default:
+ throw new IllegalStateException(getStatusStringLocked());
+ }
+ }
+
+ public void startAsync(AsyncContextEvent event)
+ {
+ final List<AsyncListener> lastAsyncListeners;
+
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("startAsync {}", toStringLocked());
+ if (_state != State.HANDLING || _requestState != RequestState.BLOCKING)
+ throw new IllegalStateException(this.getStatusStringLocked());
+
+ _requestState = RequestState.ASYNC;
+ _event = event;
+ lastAsyncListeners = _asyncListeners;
+ _asyncListeners = null;
+ }
+
+ if (lastAsyncListeners != null)
+ {
+ Runnable callback = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ for (AsyncListener listener : lastAsyncListeners)
+ {
+ try
+ {
+ listener.onStartAsync(event);
+ }
+ catch (Throwable e)
+ {
+ // TODO Async Dispatch Error
+ LOG.warn(e);
+ }
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return "startAsync";
+ }
+ };
+
+ runInContext(event, callback);
+ }
+ }
+
+ public void dispatch(ServletContext context, String path)
+ {
+ boolean dispatch = false;
+ AsyncContextEvent event;
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("dispatch {} -> {}", toStringLocked(), path);
+
+ switch (_requestState)
+ {
+ case ASYNC:
+ break;
+ case EXPIRING:
+ if (Thread.currentThread() != _onTimeoutThread)
+ throw new IllegalStateException(this.getStatusStringLocked());
+ break;
+ default:
+ throw new IllegalStateException(this.getStatusStringLocked());
+ }
+
+ if (context != null)
+ _event.setDispatchContext(context);
+ if (path != null)
+ _event.setDispatchPath(path);
+
+ if (_requestState == RequestState.ASYNC && _state == State.WAITING)
+ {
+ _state = State.WOKEN;
+ dispatch = true;
+ }
+ _requestState = RequestState.DISPATCH;
+ event = _event;
+ }
+
+ cancelTimeout(event);
+ if (dispatch)
+ scheduleDispatch();
+ }
+
+ protected void timeout()
+ {
+ boolean dispatch = false;
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Timeout {}", toStringLocked());
+
+ if (_requestState != RequestState.ASYNC)
+ return;
+ _requestState = RequestState.EXPIRE;
+
+ if (_state == State.WAITING)
+ {
+ _state = State.WOKEN;
+ dispatch = true;
+ }
+ }
+
+ if (dispatch)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Dispatch after async timeout {}", this);
+ scheduleDispatch();
+ }
+ }
+
+ protected void onTimeout()
+ {
+ final List<AsyncListener> listeners;
+ AsyncContextEvent event;
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onTimeout {}", toStringLocked());
+ if (_requestState != RequestState.EXPIRING || _state != State.HANDLING)
+ throw new IllegalStateException(toStringLocked());
+ event = _event;
+ listeners = _asyncListeners;
+ _onTimeoutThread = Thread.currentThread();
+ }
+
+ try
+ {
+ if (listeners != null)
+ {
+ Runnable task = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ for (AsyncListener listener : listeners)
+ {
+ try
+ {
+ listener.onTimeout(event);
+ }
+ catch (Throwable x)
+ {
+ LOG.warn("{} while invoking onTimeout listener {}", x, listener, x);
+ }
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return "onTimeout";
+ }
+ };
+
+ runInContext(event, task);
+ }
+ }
+ finally
+ {
+ synchronized (this)
+ {
+ _onTimeoutThread = null;
+ }
+ }
+ }
+
+ public void complete()
+ {
+ boolean handle = false;
+ AsyncContextEvent event;
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("complete {}", toStringLocked());
+
+ event = _event;
+ switch (_requestState)
+ {
+ case EXPIRING:
+ if (Thread.currentThread() != _onTimeoutThread)
+ throw new IllegalStateException(this.getStatusStringLocked());
+ _requestState = _sendError ? RequestState.BLOCKING : RequestState.COMPLETE;
+ break;
+
+ case ASYNC:
+ _requestState = _sendError ? RequestState.BLOCKING : RequestState.COMPLETE;
+ break;
+
+ case COMPLETE:
+ return;
+ default:
+ throw new IllegalStateException(this.getStatusStringLocked());
+ }
+ if (_state == State.WAITING)
+ {
+ handle = true;
+ _state = State.WOKEN;
+ }
+ }
+
+ cancelTimeout(event);
+ if (handle)
+ runInContext(event, _channel);
+ }
+
+ public void asyncError(Throwable failure)
+ {
+ // This method is called when an failure occurs asynchronously to
+ // normal handling. If the request is async, we arrange for the
+ // exception to be thrown from the normal handling loop and then
+ // actually handled by #thrownException
+
+ AsyncContextEvent event = null;
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("asyncError " + toStringLocked(), failure);
+
+ if (_state == State.WAITING && _requestState == RequestState.ASYNC)
+ {
+ _state = State.WOKEN;
+ _event.addThrowable(failure);
+ event = _event;
+ }
+ else
+ {
+ if (!(failure instanceof QuietException))
+ LOG.warn(failure.toString());
+ if (LOG.isDebugEnabled())
+ LOG.debug(failure);
+ }
+ }
+
+ if (event != null)
+ {
+ cancelTimeout(event);
+ runInContext(event, _channel);
+ }
+ }
+
+ protected void onError(Throwable th)
+ {
+ final AsyncContextEvent asyncEvent;
+ final List<AsyncListener> asyncListeners;
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("thrownException " + getStatusStringLocked(), th);
+
+ // This can only be called from within the handle loop
+ if (_state != State.HANDLING)
+ throw new IllegalStateException(getStatusStringLocked());
+
+ // If sendError has already been called, we can only handle one failure at a time!
+ if (_sendError)
+ {
+ LOG.warn("unhandled due to prior sendError", th);
+ return;
+ }
+
+ // Check async state to determine type of handling
+ switch (_requestState)
+ {
+ case BLOCKING:
+ // handle the exception with a sendError
+ sendError(th);
+ return;
+
+ case DISPATCH: // Dispatch has already been called but we ignore and handle exception below
+ case COMPLETE: // Complete has already been called but we ignore and handle exception below
+ case ASYNC:
+ if (_asyncListeners == null || _asyncListeners.isEmpty())
+ {
+ sendError(th);
+ return;
+ }
+ asyncEvent = _event;
+ asyncEvent.addThrowable(th);
+ asyncListeners = _asyncListeners;
+ break;
+
+ default:
+ LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th));
+ return;
+ }
+ }
+
+ // If we are async and have async listeners
+ // call onError
+ runInContext(asyncEvent, () ->
+ {
+ for (AsyncListener listener : asyncListeners)
+ {
+ try
+ {
+ listener.onError(asyncEvent);
+ }
+ catch (Throwable x)
+ {
+ LOG.warn(x + " while invoking onError listener " + listener);
+ LOG.debug(x);
+ }
+ }
+ });
+
+ // check the actions of the listeners
+ synchronized (this)
+ {
+ if (_requestState == RequestState.ASYNC && !_sendError)
+ {
+ // The listeners did not invoke API methods and the
+ // container must provide a default error dispatch.
+ sendError(th);
+ }
+ else if (_requestState != RequestState.COMPLETE)
+ {
+ LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th));
+ }
+ }
+ }
+
+ private void sendError(Throwable th)
+ {
+ // No sync as this is always called with lock held
+
+ // Determine the actual details of the exception
+ final Request request = _channel.getRequest();
+ final int code;
+ final String message;
+ Throwable cause = _channel.unwrap(th, BadMessageException.class, UnavailableException.class);
+ if (cause == null)
+ {
+ code = HttpStatus.INTERNAL_SERVER_ERROR_500;
+ message = th.toString();
+ }
+ else if (cause instanceof BadMessageException)
+ {
+ BadMessageException bme = (BadMessageException)cause;
+ code = bme.getCode();
+ message = bme.getReason();
+ }
+ else if (cause instanceof UnavailableException)
+ {
+ message = cause.toString();
+ if (((UnavailableException)cause).isPermanent())
+ code = HttpStatus.NOT_FOUND_404;
+ else
+ code = HttpStatus.SERVICE_UNAVAILABLE_503;
+ }
+ else
+ {
+ code = HttpStatus.INTERNAL_SERVER_ERROR_500;
+ message = null;
+ }
+
+ sendError(code, message);
+
+ // No ISE, so good to modify request/state
+ request.setAttribute(ERROR_EXCEPTION, th);
+ request.setAttribute(ERROR_EXCEPTION_TYPE, th.getClass());
+ // Ensure any async lifecycle is ended!
+ _requestState = RequestState.BLOCKING;
+ }
+
+ public void sendError(int code, String message)
+ {
+ // This method is called by Response.sendError to organise for an error page to be generated when it is possible:
+ // + The response is reset and temporarily closed.
+ // + The details of the error are saved as request attributes
+ // + The _sendError boolean is set to true so that an ERROR_DISPATCH action will be generated:
+ // - after unhandle for sync
+ // - after both unhandle and complete for async
+
+ final Request request = _channel.getRequest();
+ final Response response = _channel.getResponse();
+ if (message == null)
+ message = HttpStatus.getMessage(code);
+
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("sendError {}", toStringLocked());
+
+ if (_outputState != OutputState.OPEN)
+ throw new IllegalStateException(_outputState.toString());
+
+ switch (_state)
+ {
+ case HANDLING:
+ case WOKEN:
+ case WAITING:
+ break;
+ default:
+ throw new IllegalStateException(getStatusStringLocked());
+ }
+
+ response.setStatus(code);
+ response.errorClose();
+
+ request.setAttribute(ErrorHandler.ERROR_CONTEXT, request.getErrorContext());
+ request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI());
+ request.setAttribute(ERROR_SERVLET_NAME, request.getServletName());
+ request.setAttribute(ERROR_STATUS_CODE, code);
+ request.setAttribute(ERROR_MESSAGE, message);
+
+ _sendError = true;
+ if (_event != null)
+ {
+ Throwable cause = (Throwable)request.getAttribute(ERROR_EXCEPTION);
+ if (cause != null)
+ _event.addThrowable(cause);
+ }
+ }
+ }
+
+ protected void completing()
+ {
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("completing {}", toStringLocked());
+
+ switch (_requestState)
+ {
+ case COMPLETED:
+ throw new IllegalStateException(getStatusStringLocked());
+ default:
+ _requestState = RequestState.COMPLETING;
+ }
+ }
+ }
+
+ protected void completed(Throwable failure)
+ {
+ final List<AsyncListener> aListeners;
+ final AsyncContextEvent event;
+ boolean handle = false;
+
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("completed {}", toStringLocked());
+
+ if (_requestState != RequestState.COMPLETING)
+ throw new IllegalStateException(this.getStatusStringLocked());
+
+ if (_event == null)
+ {
+ _requestState = RequestState.COMPLETED;
+ aListeners = null;
+ event = null;
+ if (_state == State.WAITING)
+ {
+ _state = State.WOKEN;
+ handle = true;
+ }
+ }
+ else
+ {
+ aListeners = _asyncListeners;
+ event = _event;
+ }
+ }
+
+ // release any aggregate buffer from a closing flush
+ _channel.getResponse().getHttpOutput().completed(failure);
+
+ if (event != null)
+ {
+ cancelTimeout(event);
+ if (aListeners != null)
+ {
+ runInContext(event, () ->
+ {
+ for (AsyncListener listener : aListeners)
+ {
+ try
+ {
+ listener.onComplete(event);
+ }
+ catch (Throwable e)
+ {
+ LOG.warn(e + " while invoking onComplete listener " + listener);
+ LOG.debug(e);
+ }
+ }
+ });
+ }
+ event.completed();
+
+ synchronized (this)
+ {
+ _requestState = RequestState.COMPLETED;
+ if (_state == State.WAITING)
+ {
+ _state = State.WOKEN;
+ handle = true;
+ }
+ }
+ }
+
+ if (handle)
+ _channel.handle();
+ }
+
+ protected void recycle()
+ {
+ cancelTimeout();
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("recycle {}", toStringLocked());
+
+ switch (_state)
+ {
+ case HANDLING:
+ throw new IllegalStateException(getStatusStringLocked());
+ case UPGRADED:
+ return;
+ default:
+ break;
+ }
+ _asyncListeners = null;
+ _state = State.IDLE;
+ _requestState = RequestState.BLOCKING;
+ _outputState = OutputState.OPEN;
+ _initial = true;
+ _inputState = InputState.IDLE;
+ _asyncWritePossible = false;
+ _timeoutMs = DEFAULT_TIMEOUT;
+ _event = null;
+ }
+ }
+
+ public void upgrade()
+ {
+ cancelTimeout();
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("upgrade {}", toStringLocked());
+
+ switch (_state)
+ {
+ case IDLE:
+ break;
+ default:
+ throw new IllegalStateException(getStatusStringLocked());
+ }
+ _asyncListeners = null;
+ _state = State.UPGRADED;
+ _requestState = RequestState.BLOCKING;
+ _initial = true;
+ _inputState = InputState.IDLE;
+ _asyncWritePossible = false;
+ _timeoutMs = DEFAULT_TIMEOUT;
+ _event = null;
+ }
+ }
+
+ protected void scheduleDispatch()
+ {
+ _channel.execute(_channel);
+ }
+
+ protected void cancelTimeout()
+ {
+ final AsyncContextEvent event;
+ synchronized (this)
+ {
+ event = _event;
+ }
+ cancelTimeout(event);
+ }
+
+ protected void cancelTimeout(AsyncContextEvent event)
+ {
+ if (event != null)
+ event.cancelTimeoutTask();
+ }
+
+ public boolean isIdle()
+ {
+ synchronized (this)
+ {
+ return _state == State.IDLE;
+ }
+ }
+
+ public boolean isExpired()
+ {
+ synchronized (this)
+ {
+ // TODO review
+ return _requestState == RequestState.EXPIRE || _requestState == RequestState.EXPIRING;
+ }
+ }
+
+ public boolean isInitial()
+ {
+ synchronized (this)
+ {
+ return _initial;
+ }
+ }
+
+ public boolean isSuspended()
+ {
+ synchronized (this)
+ {
+ return _state == State.WAITING || _state == State.HANDLING && _requestState == RequestState.ASYNC;
+ }
+ }
+
+ boolean isCompleted()
+ {
+ synchronized (this)
+ {
+ return _requestState == RequestState.COMPLETED;
+ }
+ }
+
+ public boolean isAsyncStarted()
+ {
+ synchronized (this)
+ {
+ if (_state == State.HANDLING)
+ return _requestState != RequestState.BLOCKING;
+ return _requestState == RequestState.ASYNC || _requestState == RequestState.EXPIRING;
+ }
+ }
+
+ public boolean isAsync()
+ {
+ synchronized (this)
+ {
+ return !_initial || _requestState != RequestState.BLOCKING;
+ }
+ }
+
+ public Request getBaseRequest()
+ {
+ return _channel.getRequest();
+ }
+
+ public HttpChannel getHttpChannel()
+ {
+ return _channel;
+ }
+
+ public ContextHandler getContextHandler()
+ {
+ final AsyncContextEvent event;
+ synchronized (this)
+ {
+ event = _event;
+ }
+ return getContextHandler(event);
+ }
+
+ ContextHandler getContextHandler(AsyncContextEvent event)
+ {
+ if (event != null)
+ {
+ Context context = ((Context)event.getServletContext());
+ if (context != null)
+ return context.getContextHandler();
+ }
+ return null;
+ }
+
+ public ServletResponse getServletResponse()
+ {
+ final AsyncContextEvent event;
+ synchronized (this)
+ {
+ event = _event;
+ }
+ return getServletResponse(event);
+ }
+
+ public ServletResponse getServletResponse(AsyncContextEvent event)
+ {
+ if (event != null && event.getSuppliedResponse() != null)
+ return event.getSuppliedResponse();
+ return _channel.getResponse();
+ }
+
+ void runInContext(AsyncContextEvent event, Runnable runnable)
+ {
+ ContextHandler contextHandler = getContextHandler(event);
+ if (contextHandler == null)
+ runnable.run();
+ else
+ contextHandler.handle(_channel.getRequest(), runnable);
+ }
+
+ public Object getAttribute(String name)
+ {
+ return _channel.getRequest().getAttribute(name);
+ }
+
+ public void removeAttribute(String name)
+ {
+ _channel.getRequest().removeAttribute(name);
+ }
+
+ public void setAttribute(String name, Object attribute)
+ {
+ _channel.getRequest().setAttribute(name, attribute);
+ }
+
+ /**
+ * Called to signal async read isReady() has returned false.
+ * This indicates that there is no content available to be consumed
+ * and that once the channel enters the ASYNC_WAIT state it will
+ * register for read interest by calling {@link HttpChannel#onAsyncWaitForContent()}
+ * either from this method or from a subsequent call to {@link #unhandle()}.
+ */
+ public void onReadUnready()
+ {
+ boolean interested = false;
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onReadUnready {}", toStringLocked());
+
+ switch (_inputState)
+ {
+ case IDLE:
+ case READY:
+ if (_state == State.WAITING)
+ {
+ interested = true;
+ _inputState = InputState.REGISTERED;
+ }
+ else
+ {
+ _inputState = InputState.REGISTER;
+ }
+ break;
+
+ case REGISTER:
+ case REGISTERED:
+ case POSSIBLE:
+ case PRODUCING:
+ break;
+ }
+ }
+
+ if (interested)
+ _channel.onAsyncWaitForContent();
+ }
+
+ /**
+ * Called to signal that content is now available to read.
+ * If the channel is in ASYNC_WAIT state and unready (ie isReady() has
+ * returned false), then the state is changed to ASYNC_WOKEN and true
+ * is returned.
+ *
+ * @return True IFF the channel was unready and in ASYNC_WAIT state
+ */
+ public boolean onContentAdded()
+ {
+ boolean woken = false;
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onContentAdded {}", toStringLocked());
+
+ switch (_inputState)
+ {
+ case IDLE:
+ case READY:
+ break;
+
+ case PRODUCING:
+ _inputState = InputState.READY;
+ break;
+
+ case REGISTER:
+ case REGISTERED:
+ _inputState = InputState.READY;
+ if (_state == State.WAITING)
+ {
+ woken = true;
+ _state = State.WOKEN;
+ }
+ break;
+
+ case POSSIBLE:
+ throw new IllegalStateException(toStringLocked());
+ }
+ }
+ return woken;
+ }
+
+ /**
+ * Called to signal that the channel is ready for a callback.
+ * This is similar to calling {@link #onReadUnready()} followed by
+ * {@link #onContentAdded()}, except that as content is already
+ * available, read interest is never set.
+ *
+ * @return true if woken
+ */
+ public boolean onReadReady()
+ {
+ boolean woken = false;
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onReadReady {}", toStringLocked());
+
+ switch (_inputState)
+ {
+ case IDLE:
+ _inputState = InputState.READY;
+ if (_state == State.WAITING)
+ {
+ woken = true;
+ _state = State.WOKEN;
+ }
+ break;
+
+ default:
+ throw new IllegalStateException(toStringLocked());
+ }
+ }
+ return woken;
+ }
+
+ /**
+ * Called to indicate that more content may be available,
+ * but that a handling thread may need to produce (fill/parse)
+ * it. Typically called by the async read success callback.
+ *
+ * @return {@code true} if more content may be available
+ */
+ public boolean onReadPossible()
+ {
+ boolean woken = false;
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onReadPossible {}", toStringLocked());
+
+ switch (_inputState)
+ {
+ case REGISTERED:
+ _inputState = InputState.POSSIBLE;
+ if (_state == State.WAITING)
+ {
+ woken = true;
+ _state = State.WOKEN;
+ }
+ break;
+
+ default:
+ throw new IllegalStateException(toStringLocked());
+ }
+ }
+ return woken;
+ }
+
+ /**
+ * Called to signal that a read has read -1.
+ * Will wake if the read was called while in ASYNC_WAIT state
+ *
+ * @return {@code true} if woken
+ */
+ public boolean onReadEof()
+ {
+ boolean woken = false;
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onEof {}", toStringLocked());
+
+ // Force read ready so onAllDataRead can be called
+ _inputState = InputState.READY;
+ if (_state == State.WAITING)
+ {
+ woken = true;
+ _state = State.WOKEN;
+ }
+ }
+ return woken;
+ }
+
+ public boolean onWritePossible()
+ {
+ boolean wake = false;
+
+ synchronized (this)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onWritePossible {}", toStringLocked());
+
+ _asyncWritePossible = true;
+ if (_state == State.WAITING)
+ {
+ _state = State.WOKEN;
+ wake = true;
+ }
+ }
+
+ return wake;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java
new file mode 100644
index 0000000..e932f78
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java
@@ -0,0 +1,715 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.jetty.http.CookieCompliance;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.util.Jetty;
+import org.eclipse.jetty.util.TreeTrie;
+import org.eclipse.jetty.util.Trie;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.component.DumpableCollection;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * HTTP Configuration.
+ * <p>This class is a holder of HTTP configuration for use by the
+ * {@link HttpChannel} class. Typically an HTTPConfiguration instance
+ * is instantiated and passed to a {@link ConnectionFactory} that can
+ * create HTTP channels (e.g. HTTP, AJP or FCGI).</p>
+ * <p>The configuration held by this class is not for the wire protocol,
+ * but for the interpretation and handling of HTTP requests that could
+ * be transported by a variety of protocols.
+ * </p>
+ */
+@ManagedObject("HTTP Configuration")
+public class HttpConfiguration implements Dumpable
+{
+ private static final Logger LOG = Log.getLogger(HttpConfiguration.class);
+
+ public static final String SERVER_VERSION = "Jetty(" + Jetty.VERSION + ")";
+ private final List<Customizer> _customizers = new CopyOnWriteArrayList<>();
+ private final Trie<Boolean> _formEncodedMethods = new TreeTrie<>();
+ private int _outputBufferSize = 32 * 1024;
+ private int _outputAggregationSize = _outputBufferSize / 4;
+ private int _requestHeaderSize = 8 * 1024;
+ private int _responseHeaderSize = 8 * 1024;
+ private int _headerCacheSize = 1024;
+ private int _securePort;
+ private long _idleTimeout = -1;
+ private long _blockingTimeout = -1;
+ private String _secureScheme = HttpScheme.HTTPS.asString();
+ private boolean _sendServerVersion = true;
+ private boolean _sendXPoweredBy = false;
+ private boolean _sendDateHeader = true;
+ private boolean _delayDispatchUntilContent = true;
+ private boolean _persistentConnectionsEnabled = true;
+ private int _maxErrorDispatches = 10;
+ private long _minRequestDataRate;
+ private long _minResponseDataRate;
+ private CookieCompliance _requestCookieCompliance = CookieCompliance.RFC6265;
+ private CookieCompliance _responseCookieCompliance = CookieCompliance.RFC6265;
+ private MultiPartFormDataCompliance _multiPartCompliance = MultiPartFormDataCompliance.LEGACY; // TODO change default in jetty-10
+ private boolean _notifyRemoteAsyncErrors = true;
+ private boolean _relativeRedirectAllowed;
+
+ /**
+ * <p>An interface that allows a request object to be customized
+ * for a particular HTTP connector configuration. Unlike Filters, customizer are
+ * applied before the request is submitted for processing and can be specific to the
+ * connector on which the request was received.</p>
+ *
+ * <p>Typically Customizers perform tasks such as:</p>
+ * <ul>
+ * <li>process header fields that may be injected by a proxy or load balancer.
+ * <li>setup attributes that may come from the connection/connector such as SSL Session IDs
+ * <li>Allow a request to be marked as secure or authenticated if those have been offloaded
+ * and communicated by header, cookie or other out-of-band mechanism
+ * <li>Set request attributes/fields that are determined by the connector on which the
+ * request was received
+ * </ul>
+ */
+ public interface Customizer
+ {
+ void customize(Connector connector, HttpConfiguration channelConfig, Request request);
+ }
+
+ public interface ConnectionFactory
+ {
+ HttpConfiguration getHttpConfiguration();
+ }
+
+ public HttpConfiguration()
+ {
+ _formEncodedMethods.put(HttpMethod.POST.asString(), Boolean.TRUE);
+ _formEncodedMethods.put(HttpMethod.PUT.asString(), Boolean.TRUE);
+ }
+
+ /**
+ * Creates a configuration from another.
+ *
+ * @param config The configuration to copy.
+ */
+ public HttpConfiguration(HttpConfiguration config)
+ {
+ _customizers.addAll(config._customizers);
+ for (String s : config._formEncodedMethods.keySet())
+ {
+ _formEncodedMethods.put(s, Boolean.TRUE);
+ }
+ _outputBufferSize = config._outputBufferSize;
+ _outputAggregationSize = config._outputAggregationSize;
+ _requestHeaderSize = config._requestHeaderSize;
+ _responseHeaderSize = config._responseHeaderSize;
+ _headerCacheSize = config._headerCacheSize;
+ _secureScheme = config._secureScheme;
+ _securePort = config._securePort;
+ _idleTimeout = config._idleTimeout;
+ _blockingTimeout = config._blockingTimeout;
+ _sendDateHeader = config._sendDateHeader;
+ _sendServerVersion = config._sendServerVersion;
+ _sendXPoweredBy = config._sendXPoweredBy;
+ _delayDispatchUntilContent = config._delayDispatchUntilContent;
+ _persistentConnectionsEnabled = config._persistentConnectionsEnabled;
+ _maxErrorDispatches = config._maxErrorDispatches;
+ _minRequestDataRate = config._minRequestDataRate;
+ _minResponseDataRate = config._minResponseDataRate;
+ _requestCookieCompliance = config._requestCookieCompliance;
+ _responseCookieCompliance = config._responseCookieCompliance;
+ _multiPartCompliance = config._multiPartCompliance;
+ _notifyRemoteAsyncErrors = config._notifyRemoteAsyncErrors;
+ _relativeRedirectAllowed = config._relativeRedirectAllowed;
+ }
+
+ /**
+ * <p>Adds a {@link Customizer} that is invoked for every
+ * request received.</p>
+ * <p>Customizers are often used to interpret optional headers (eg {@link ForwardedRequestCustomizer}) or
+ * optional protocol semantics (eg {@link SecureRequestCustomizer}).
+ *
+ * @param customizer A request customizer
+ */
+ public void addCustomizer(Customizer customizer)
+ {
+ _customizers.add(customizer);
+ }
+
+ public List<Customizer> getCustomizers()
+ {
+ return _customizers;
+ }
+
+ public <T> T getCustomizer(Class<T> type)
+ {
+ for (Customizer c : _customizers)
+ {
+ if (type.isAssignableFrom(c.getClass()))
+ return (T)c;
+ }
+ return null;
+ }
+
+ @ManagedAttribute("The size in bytes of the output buffer used to aggregate HTTP output")
+ public int getOutputBufferSize()
+ {
+ return _outputBufferSize;
+ }
+
+ @ManagedAttribute("The maximum size in bytes for HTTP output to be aggregated")
+ public int getOutputAggregationSize()
+ {
+ return _outputAggregationSize;
+ }
+
+ @ManagedAttribute("The maximum allowed size in bytes for an HTTP request header")
+ public int getRequestHeaderSize()
+ {
+ return _requestHeaderSize;
+ }
+
+ @ManagedAttribute("The maximum allowed size in bytes for an HTTP response header")
+ public int getResponseHeaderSize()
+ {
+ return _responseHeaderSize;
+ }
+
+ @ManagedAttribute("The maximum allowed size in Trie nodes for an HTTP header field cache")
+ public int getHeaderCacheSize()
+ {
+ return _headerCacheSize;
+ }
+
+ @ManagedAttribute("The port to which Integral or Confidential security constraints are redirected")
+ public int getSecurePort()
+ {
+ return _securePort;
+ }
+
+ @ManagedAttribute("The scheme with which Integral or Confidential security constraints are redirected")
+ public String getSecureScheme()
+ {
+ return _secureScheme;
+ }
+
+ @ManagedAttribute("Whether persistent connections are enabled")
+ public boolean isPersistentConnectionsEnabled()
+ {
+ return _persistentConnectionsEnabled;
+ }
+
+ /**
+ * <p>The max idle time is applied to an HTTP request for IO operations and
+ * delayed dispatch.</p>
+ *
+ * @return the max idle time in ms or if == 0 implies an infinite timeout, <0
+ * implies no HTTP channel timeout and the connection timeout is used instead.
+ */
+ @ManagedAttribute("The idle timeout in ms for I/O operations during the handling of an HTTP request")
+ public long getIdleTimeout()
+ {
+ return _idleTimeout;
+ }
+
+ /**
+ * <p>The max idle time is applied to an HTTP request for IO operations and
+ * delayed dispatch.</p>
+ *
+ * @param timeoutMs the max idle time in ms or if == 0 implies an infinite timeout, <0
+ * implies no HTTP channel timeout and the connection timeout is used instead.
+ */
+ public void setIdleTimeout(long timeoutMs)
+ {
+ _idleTimeout = timeoutMs;
+ }
+
+ /**
+ * <p>This timeout is in addition to the {@link Connector#getIdleTimeout()}, and applies
+ * to the total operation (as opposed to the idle timeout that applies to the time no
+ * data is being sent). This applies only to blocking operations and does not affect
+ * asynchronous read and write.</p>
+ *
+ * @return -1, for no blocking timeout (default), 0 for a blocking timeout equal to the
+ * idle timeout; >0 for a timeout in ms applied to the total blocking operation.
+ * @deprecated Replaced by {@link #getMinResponseDataRate()} and {@link #getMinRequestDataRate()}
+ */
+ @ManagedAttribute(value = "Total timeout in ms for blocking I/O operations. DEPRECATED!", readonly = true)
+ @Deprecated
+ public long getBlockingTimeout()
+ {
+ return _blockingTimeout;
+ }
+
+ /**
+ * <p>This timeout is in addition to the {@link Connector#getIdleTimeout()}, and applies
+ * to the total operation (as opposed to the idle timeout that applies to the time no
+ * data is being sent).This applies only to blocking operations and does not affect
+ * asynchronous read and write.</p>
+ *
+ * @param blockingTimeout -1, for no blocking timeout (default), 0 for a blocking timeout equal to the
+ * idle timeout; >0 for a timeout in ms applied to the total blocking operation.
+ * @deprecated Replaced by {@link #setMinResponseDataRate(long)} and {@link #setMinRequestDataRate(long)}
+ */
+ @Deprecated
+ public void setBlockingTimeout(long blockingTimeout)
+ {
+ if (blockingTimeout > 0)
+ LOG.warn("HttpConfiguration.setBlockingTimeout is deprecated!");
+ _blockingTimeout = blockingTimeout;
+ }
+
+ public void setPersistentConnectionsEnabled(boolean persistentConnectionsEnabled)
+ {
+ _persistentConnectionsEnabled = persistentConnectionsEnabled;
+ }
+
+ public void setSendServerVersion(boolean sendServerVersion)
+ {
+ _sendServerVersion = sendServerVersion;
+ }
+
+ @ManagedAttribute("Whether to send the Server header in responses")
+ public boolean getSendServerVersion()
+ {
+ return _sendServerVersion;
+ }
+
+ public void writePoweredBy(Appendable out, String preamble, String postamble) throws IOException
+ {
+ if (getSendServerVersion())
+ {
+ if (preamble != null)
+ out.append(preamble);
+ out.append(Jetty.POWERED_BY);
+ if (postamble != null)
+ out.append(postamble);
+ }
+ }
+
+ public void setSendXPoweredBy(boolean sendXPoweredBy)
+ {
+ _sendXPoweredBy = sendXPoweredBy;
+ }
+
+ @ManagedAttribute("Whether to send the X-Powered-By header in responses")
+ public boolean getSendXPoweredBy()
+ {
+ return _sendXPoweredBy;
+ }
+
+ /**
+ * Indicates if the {@code Date} header should be sent in responses.
+ *
+ * @param sendDateHeader true if the {@code Date} header should be sent in responses
+ * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.2">HTTP/1.1 Standard Header: Date</a>
+ * @see #getSendDateHeader()
+ */
+ public void setSendDateHeader(boolean sendDateHeader)
+ {
+ _sendDateHeader = sendDateHeader;
+ }
+
+ /**
+ * Indicates if the {@code Date} header will be sent in responses.
+ *
+ * @return true by default
+ */
+ @ManagedAttribute("Whether to send the Date header in responses")
+ public boolean getSendDateHeader()
+ {
+ return _sendDateHeader;
+ }
+
+ /**
+ * @param delay if true, delays the application dispatch until content is available (defaults to true)
+ */
+ public void setDelayDispatchUntilContent(boolean delay)
+ {
+ _delayDispatchUntilContent = delay;
+ }
+
+ @ManagedAttribute("Whether to delay the application dispatch until content is available")
+ public boolean isDelayDispatchUntilContent()
+ {
+ return _delayDispatchUntilContent;
+ }
+
+ /**
+ * <p>Sets the {@link Customizer}s that are invoked for every
+ * request received.</p>
+ * <p>Customizers are often used to interpret optional headers (eg {@link ForwardedRequestCustomizer}) or
+ * optional protocol semantics (eg {@link SecureRequestCustomizer}).</p>
+ *
+ * @param customizers the list of customizers
+ */
+ public void setCustomizers(List<Customizer> customizers)
+ {
+ _customizers.clear();
+ _customizers.addAll(customizers);
+ }
+
+ /**
+ * Set the size of the buffer into which response content is aggregated
+ * before being sent to the client. A larger buffer can improve performance by allowing
+ * a content producer to run without blocking, however larger buffers consume more memory and
+ * may induce some latency before a client starts processing the content.
+ *
+ * @param outputBufferSize buffer size in bytes.
+ */
+ public void setOutputBufferSize(int outputBufferSize)
+ {
+ _outputBufferSize = outputBufferSize;
+ setOutputAggregationSize(outputBufferSize / 4);
+ }
+
+ /**
+ * Set the max size of the response content write that is copied into the aggregate buffer.
+ * Writes that are smaller of this size are copied into the aggregate buffer, while
+ * writes that are larger of this size will cause the aggregate buffer to be flushed
+ * and the write to be executed without being copied.
+ *
+ * @param outputAggregationSize the max write size that is aggregated
+ */
+ public void setOutputAggregationSize(int outputAggregationSize)
+ {
+ _outputAggregationSize = outputAggregationSize;
+ }
+
+ /**
+ * <p>Larger headers will allow for more and/or larger cookies plus larger form content encoded
+ * in a URL. However, larger headers consume more memory and can make a server more vulnerable to denial of service
+ * attacks.</p>
+ *
+ * @param requestHeaderSize the maximum size in bytes of the request header
+ */
+ public void setRequestHeaderSize(int requestHeaderSize)
+ {
+ _requestHeaderSize = requestHeaderSize;
+ }
+
+ /**
+ * <p>Larger headers will allow for more and/or larger cookies and longer HTTP headers (eg for redirection).
+ * However, larger headers will also consume more memory.</p>
+ *
+ * @param responseHeaderSize the maximum size in bytes of the response header
+ */
+ public void setResponseHeaderSize(int responseHeaderSize)
+ {
+ _responseHeaderSize = responseHeaderSize;
+ }
+
+ /**
+ * @param headerCacheSize The size of the header field cache, in terms of unique characters branches
+ * in the lookup {@link Trie} and associated data structures.
+ */
+ public void setHeaderCacheSize(int headerCacheSize)
+ {
+ _headerCacheSize = headerCacheSize;
+ }
+
+ /**
+ * <p>Sets the TCP/IP port used for CONFIDENTIAL and INTEGRAL redirections.</p>
+ *
+ * @param securePort the secure port to redirect to.
+ */
+ public void setSecurePort(int securePort)
+ {
+ _securePort = securePort;
+ }
+
+ /**
+ * <p>Set the URI scheme used for CONFIDENTIAL and INTEGRAL redirections.</p>
+ *
+ * @param secureScheme A scheme string like "https"
+ */
+ public void setSecureScheme(String secureScheme)
+ {
+ _secureScheme = secureScheme;
+ }
+
+ /**
+ * Sets the form encoded HTTP methods.
+ *
+ * @param methods the HTTP methods of requests that can be decoded as
+ * {@code x-www-form-urlencoded} content to be made available via the
+ * {@link Request#getParameter(String)} and associated APIs
+ */
+ public void setFormEncodedMethods(String... methods)
+ {
+ _formEncodedMethods.clear();
+ for (String method : methods)
+ {
+ addFormEncodedMethod(method);
+ }
+ }
+
+ /**
+ * @return the set of HTTP methods of requests that can be decoded as
+ * {@code x-www-form-urlencoded} content to be made available via the
+ * {@link Request#getParameter(String)} and associated APIs
+ */
+ public Set<String> getFormEncodedMethods()
+ {
+ return _formEncodedMethods.keySet();
+ }
+
+ /**
+ * Adds a form encoded HTTP Method
+ *
+ * @param method the HTTP method of requests that can be decoded as
+ * {@code x-www-form-urlencoded} content to be made available via the
+ * {@link Request#getParameter(String)} and associated APIs
+ */
+ public void addFormEncodedMethod(String method)
+ {
+ _formEncodedMethods.put(method, Boolean.TRUE);
+ }
+
+ /**
+ * Tests whether the HTTP method supports {@code x-www-form-urlencoded} content
+ *
+ * @param method the HTTP method
+ * @return true if requests with this method can be
+ * decoded as {@code x-www-form-urlencoded} content to be made available via the
+ * {@link Request#getParameter(String)} and associated APIs
+ */
+ public boolean isFormEncodedMethod(String method)
+ {
+ return Boolean.TRUE.equals(_formEncodedMethods.get(method));
+ }
+
+ /**
+ * @return The maximum error dispatches for a request to prevent looping on an error
+ */
+ @ManagedAttribute("The maximum ERROR dispatches for a request for loop prevention (default 10)")
+ public int getMaxErrorDispatches()
+ {
+ return _maxErrorDispatches;
+ }
+
+ /**
+ * @param max The maximum error dispatches for a request to prevent looping on an error
+ */
+ public void setMaxErrorDispatches(int max)
+ {
+ _maxErrorDispatches = max;
+ }
+
+ /**
+ * @return The minimum request data rate in bytes per second; or <=0 for no limit
+ */
+ @ManagedAttribute("The minimum request content data rate in bytes per second")
+ public long getMinRequestDataRate()
+ {
+ return _minRequestDataRate;
+ }
+
+ /**
+ * @param bytesPerSecond The minimum request data rate in bytes per second; or <=0 for no limit
+ */
+ public void setMinRequestDataRate(long bytesPerSecond)
+ {
+ _minRequestDataRate = bytesPerSecond;
+ }
+
+ /**
+ * @return The minimum response data rate in bytes per second; or <=0 for no limit
+ */
+ @ManagedAttribute("The minimum response content data rate in bytes per second")
+ public long getMinResponseDataRate()
+ {
+ return _minResponseDataRate;
+ }
+
+ /**
+ * <p>Sets an minimum response content data rate.</p>
+ * <p>The value is enforced only approximately - not precisely - due to the fact that
+ * for efficiency reasons buffer writes may be comprised of both response headers and
+ * response content.</p>
+ *
+ * @param bytesPerSecond The minimum response data rate in bytes per second; or <=0 for no limit
+ */
+ public void setMinResponseDataRate(long bytesPerSecond)
+ {
+ _minResponseDataRate = bytesPerSecond;
+ }
+
+ /**
+ * @return The CookieCompliance used for parsing request <code>Cookie</code> headers.
+ * @see #getResponseCookieCompliance()
+ */
+ public CookieCompliance getRequestCookieCompliance()
+ {
+ return _requestCookieCompliance;
+ }
+
+ /**
+ * @return The CookieCompliance used for generating response <code>Set-Cookie</code> headers
+ * @see #getRequestCookieCompliance()
+ */
+ public CookieCompliance getResponseCookieCompliance()
+ {
+ return _responseCookieCompliance;
+ }
+
+ /**
+ * @param cookieCompliance The CookieCompliance to use for parsing request <code>Cookie</code> headers.
+ * @see #setRequestCookieCompliance(CookieCompliance)
+ */
+ public void setRequestCookieCompliance(CookieCompliance cookieCompliance)
+ {
+ _requestCookieCompliance = cookieCompliance == null ? CookieCompliance.RFC6265 : cookieCompliance;
+ }
+
+ /**
+ * @param cookieCompliance The CookieCompliance to use for generating response <code>Set-Cookie</code> headers
+ * @see #setResponseCookieCompliance(CookieCompliance)
+ */
+ public void setResponseCookieCompliance(CookieCompliance cookieCompliance)
+ {
+ _responseCookieCompliance = cookieCompliance == null ? CookieCompliance.RFC6265 : cookieCompliance;
+ }
+
+ @Deprecated
+ public void setCookieCompliance(CookieCompliance compliance)
+ {
+ setRequestCookieCompliance(compliance);
+ }
+
+ @Deprecated
+ public CookieCompliance getCookieCompliance()
+ {
+ return getRequestCookieCompliance();
+ }
+
+ @Deprecated
+ public boolean isCookieCompliance(CookieCompliance compliance)
+ {
+ return _requestCookieCompliance.equals(compliance);
+ }
+
+ /**
+ * Sets the compliance level for multipart/form-data handling.
+ *
+ * @param multiPartCompliance The multipart/form-data compliance level.
+ */
+ public void setMultiPartFormDataCompliance(MultiPartFormDataCompliance multiPartCompliance)
+ {
+ // TODO change default in jetty-10
+ _multiPartCompliance = multiPartCompliance == null ? MultiPartFormDataCompliance.LEGACY : multiPartCompliance;
+ }
+
+ public MultiPartFormDataCompliance getMultipartFormDataCompliance()
+ {
+ return _multiPartCompliance;
+ }
+
+ /**
+ * @param notifyRemoteAsyncErrors whether remote errors, when detected, are notified to async applications
+ */
+ public void setNotifyRemoteAsyncErrors(boolean notifyRemoteAsyncErrors)
+ {
+ this._notifyRemoteAsyncErrors = notifyRemoteAsyncErrors;
+ }
+
+ /**
+ * @return whether remote errors, when detected, are notified to async applications
+ */
+ @ManagedAttribute("Whether remote errors, when detected, are notified to async applications")
+ public boolean isNotifyRemoteAsyncErrors()
+ {
+ return _notifyRemoteAsyncErrors;
+ }
+
+ /**
+ * @param allowed True if relative redirection locations are allowed
+ */
+ public void setRelativeRedirectAllowed(boolean allowed)
+ {
+ _relativeRedirectAllowed = allowed;
+ }
+
+ /**
+ * @return True if relative redirection locations are allowed
+ */
+ @ManagedAttribute("Whether relative redirection locations are allowed")
+ public boolean isRelativeRedirectAllowed()
+ {
+ return _relativeRedirectAllowed;
+ }
+
+ @Override
+ public String dump()
+ {
+ return Dumpable.dump(this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ Dumpable.dumpObjects(out, indent, this,
+ new DumpableCollection("customizers", _customizers),
+ new DumpableCollection("formEncodedMethods", _formEncodedMethods.keySet()),
+ "outputBufferSize=" + _outputBufferSize,
+ "outputAggregationSize=" + _outputAggregationSize,
+ "requestHeaderSize=" + _requestHeaderSize,
+ "responseHeaderSize=" + _responseHeaderSize,
+ "headerCacheSize=" + _headerCacheSize,
+ "secureScheme=" + _secureScheme,
+ "securePort=" + _securePort,
+ "idleTimeout=" + _idleTimeout,
+ "blockingTimeout=" + _blockingTimeout,
+ "sendDateHeader=" + _sendDateHeader,
+ "sendServerVersion=" + _sendServerVersion,
+ "sendXPoweredBy=" + _sendXPoweredBy,
+ "delayDispatchUntilContent=" + _delayDispatchUntilContent,
+ "persistentConnectionsEnabled=" + _persistentConnectionsEnabled,
+ "maxErrorDispatches=" + _maxErrorDispatches,
+ "minRequestDataRate=" + _minRequestDataRate,
+ "minResponseDataRate=" + _minResponseDataRate,
+ "cookieCompliance=" + _requestCookieCompliance,
+ "setRequestCookieCompliance=" + _responseCookieCompliance,
+ "notifyRemoteAsyncErrors=" + _notifyRemoteAsyncErrors,
+ "relativeRedirectAllowed=" + _relativeRedirectAllowed
+ );
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{%d/%d,%d/%d,%s://:%d,%s}",
+ this.getClass().getSimpleName(),
+ hashCode(),
+ _outputBufferSize,
+ _outputAggregationSize,
+ _requestHeaderSize,
+ _responseHeaderSize,
+ _secureScheme,
+ _securePort,
+ _customizers);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java
new file mode 100644
index 0000000..e08bc79
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java
@@ -0,0 +1,919 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritePendingException;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.LongAdder;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HttpCompliance;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpGenerator;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpHeaderValue;
+import org.eclipse.jetty.http.HttpParser;
+import org.eclipse.jetty.http.HttpParser.RequestHandler;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.MetaData;
+import org.eclipse.jetty.http.PreEncodedHttpField;
+import org.eclipse.jetty.io.AbstractConnection;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.EofException;
+import org.eclipse.jetty.io.WriteFlusher;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.IteratingCallback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+import static org.eclipse.jetty.http.HttpStatus.INTERNAL_SERVER_ERROR_500;
+
+/**
+ * <p>A {@link Connection} that handles the HTTP protocol.</p>
+ */
+public class HttpConnection extends AbstractConnection implements Runnable, HttpTransport, WriteFlusher.Listener, Connection.UpgradeFrom, Connection.UpgradeTo
+{
+ private static final Logger LOG = Log.getLogger(HttpConnection.class);
+ public static final HttpField CONNECTION_CLOSE = new PreEncodedHttpField(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString());
+ public static final String UPGRADE_CONNECTION_ATTRIBUTE = "org.eclipse.jetty.server.HttpConnection.UPGRADE";
+ private static final boolean REQUEST_BUFFER_DIRECT = false;
+ private static final boolean HEADER_BUFFER_DIRECT = false;
+ private static final boolean CHUNK_BUFFER_DIRECT = false;
+ private static final ThreadLocal<HttpConnection> __currentConnection = new ThreadLocal<>();
+
+ private final HttpConfiguration _config;
+ private final Connector _connector;
+ private final ByteBufferPool _bufferPool;
+ private final HttpInput _input;
+ private final HttpGenerator _generator;
+ private final HttpChannelOverHttp _channel;
+ private final HttpParser _parser;
+ private final AtomicInteger _contentBufferReferences = new AtomicInteger();
+ private volatile ByteBuffer _requestBuffer = null;
+ private final BlockingReadCallback _blockingReadCallback = new BlockingReadCallback();
+ private final AsyncReadCallback _asyncReadCallback = new AsyncReadCallback();
+ private final SendCallback _sendCallback = new SendCallback();
+ private final boolean _recordHttpComplianceViolations;
+ private final LongAdder bytesIn = new LongAdder();
+ private final LongAdder bytesOut = new LongAdder();
+
+ /**
+ * Get the current connection that this thread is dispatched to.
+ * Note that a thread may be processing a request asynchronously and
+ * thus not be dispatched to the connection.
+ *
+ * @return the current HttpConnection or null
+ * @see Request#getAttribute(String) for a more general way to access the HttpConnection
+ */
+ public static HttpConnection getCurrentConnection()
+ {
+ return __currentConnection.get();
+ }
+
+ protected static HttpConnection setCurrentConnection(HttpConnection connection)
+ {
+ HttpConnection last = __currentConnection.get();
+ __currentConnection.set(connection);
+ return last;
+ }
+
+ public HttpConnection(HttpConfiguration config, Connector connector, EndPoint endPoint, HttpCompliance compliance, boolean recordComplianceViolations)
+ {
+ super(endPoint, connector.getExecutor());
+ _config = config;
+ _connector = connector;
+ _bufferPool = _connector.getByteBufferPool();
+ _generator = newHttpGenerator();
+ _channel = newHttpChannel();
+ _input = _channel.getRequest().getHttpInput();
+ _parser = newHttpParser(compliance);
+ _recordHttpComplianceViolations = recordComplianceViolations;
+ if (LOG.isDebugEnabled())
+ LOG.debug("New HTTP Connection {}", this);
+ }
+
+ @Deprecated
+ public HttpCompliance getHttpCompliance()
+ {
+ return _parser.getHttpCompliance();
+ }
+
+ public HttpConfiguration getHttpConfiguration()
+ {
+ return _config;
+ }
+
+ public boolean isRecordHttpComplianceViolations()
+ {
+ return _recordHttpComplianceViolations;
+ }
+
+ protected HttpGenerator newHttpGenerator()
+ {
+ return new HttpGenerator(_config.getSendServerVersion(), _config.getSendXPoweredBy());
+ }
+
+ protected HttpChannelOverHttp newHttpChannel()
+ {
+ return new HttpChannelOverHttp(this, _connector, _config, getEndPoint(), this);
+ }
+
+ protected HttpParser newHttpParser(HttpCompliance compliance)
+ {
+ return new HttpParser(newRequestHandler(), getHttpConfiguration().getRequestHeaderSize(), compliance);
+ }
+
+ protected HttpParser.RequestHandler newRequestHandler()
+ {
+ return _channel;
+ }
+
+ public Server getServer()
+ {
+ return _connector.getServer();
+ }
+
+ public Connector getConnector()
+ {
+ return _connector;
+ }
+
+ public HttpChannel getHttpChannel()
+ {
+ return _channel;
+ }
+
+ public HttpParser getParser()
+ {
+ return _parser;
+ }
+
+ public HttpGenerator getGenerator()
+ {
+ return _generator;
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return getEndPoint().isOptimizedForDirectBuffers();
+ }
+
+ @Override
+ public long getMessagesIn()
+ {
+ return getHttpChannel().getRequests();
+ }
+
+ @Override
+ public long getMessagesOut()
+ {
+ return getHttpChannel().getRequests();
+ }
+
+ @Override
+ public ByteBuffer onUpgradeFrom()
+ {
+ if (BufferUtil.hasContent(_requestBuffer))
+ {
+ ByteBuffer unconsumed = ByteBuffer.allocateDirect(_requestBuffer.remaining());
+ unconsumed.put(_requestBuffer);
+ unconsumed.flip();
+ releaseRequestBuffer();
+ return unconsumed;
+ }
+ return null;
+ }
+
+ @Override
+ public void onUpgradeTo(ByteBuffer buffer)
+ {
+ BufferUtil.append(getRequestBuffer(), buffer);
+ }
+
+ @Override
+ public void onFlushed(long bytes) throws IOException
+ {
+ // Unfortunately cannot distinguish between header and content
+ // bytes, and for content bytes whether they are chunked or not.
+ _channel.getResponse().getHttpOutput().onFlushed(bytes);
+ }
+
+ void releaseRequestBuffer()
+ {
+ if (_requestBuffer != null && !_requestBuffer.hasRemaining())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("releaseRequestBuffer {}", this);
+ ByteBuffer buffer = _requestBuffer;
+ _requestBuffer = null;
+ _bufferPool.release(buffer);
+ }
+ }
+
+ public ByteBuffer getRequestBuffer()
+ {
+ if (_requestBuffer == null)
+ _requestBuffer = _bufferPool.acquire(getInputBufferSize(), REQUEST_BUFFER_DIRECT);
+ return _requestBuffer;
+ }
+
+ public boolean isRequestBufferEmpty()
+ {
+ return BufferUtil.isEmpty(_requestBuffer);
+ }
+
+ @Override
+ public void onFillable()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} onFillable enter {} {}", this, _channel.getState(), BufferUtil.toDetailString(_requestBuffer));
+
+ HttpConnection last = setCurrentConnection(this);
+ try
+ {
+ while (getEndPoint().isOpen())
+ {
+ // Fill the request buffer (if needed).
+ int filled = fillRequestBuffer();
+ if (filled < 0 && getEndPoint().isOutputShutdown())
+ close();
+
+ // Parse the request buffer.
+ boolean handle = parseRequestBuffer();
+
+ // There could be a connection upgrade before handling
+ // the HTTP/1.1 request, for example PRI * HTTP/2.
+ // If there was a connection upgrade, the other
+ // connection took over, nothing more to do here.
+ if (getEndPoint().getConnection() != this)
+ break;
+
+ // Handle channel event
+ if (handle)
+ {
+ boolean suspended = !_channel.handle();
+
+ // We should break iteration if we have suspended or upgraded the connection.
+ if (suspended || getEndPoint().getConnection() != this)
+ break;
+ }
+ else if (filled == 0)
+ {
+ fillInterested();
+ break;
+ }
+ else if (filled < 0)
+ {
+ if (_channel.getState().isIdle())
+ getEndPoint().shutdownOutput();
+ break;
+ }
+ }
+ }
+ catch (Throwable x)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} caught exception {}", this, _channel.getState(), x);
+ BufferUtil.clear(_requestBuffer);
+ releaseRequestBuffer();
+ close();
+ }
+ finally
+ {
+ setCurrentConnection(last);
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} onFillable exit {} {}", this, _channel.getState(), BufferUtil.toDetailString(_requestBuffer));
+ }
+ }
+
+ /**
+ * Fill and parse data looking for content
+ *
+ * @return true if an {@link RequestHandler} method was called and it returned true;
+ */
+ protected boolean fillAndParseForContent()
+ {
+ // Defensive check to avoid an infinite select/wakeup/fillAndParseForContent/wait loop
+ // in case the parser was mistakenly closed and the connection was not aborted.
+ if (_parser.isTerminated())
+ throw new IllegalStateException("Parser is terminated: " + _parser);
+
+ boolean handled = false;
+ while (_parser.inContentState())
+ {
+ int filled = fillRequestBuffer();
+ handled = parseRequestBuffer();
+ if (handled || filled <= 0 || _input.hasContent())
+ break;
+ }
+ return handled;
+ }
+
+ private int fillRequestBuffer()
+ {
+ if (_contentBufferReferences.get() > 0)
+ throw new IllegalStateException("fill with unconsumed content on " + this);
+
+ if (BufferUtil.isEmpty(_requestBuffer))
+ {
+ // Get a buffer
+ // We are not in a race here for the request buffer as we have not yet received a request,
+ // so there are not an possible legal threads calling #parseContent or #completed.
+ _requestBuffer = getRequestBuffer();
+
+ // fill
+ try
+ {
+ int filled = getEndPoint().fill(_requestBuffer);
+ if (filled == 0) // Do a retry on fill 0 (optimization for SSL connections)
+ filled = getEndPoint().fill(_requestBuffer);
+
+ if (filled > 0)
+ bytesIn.add(filled);
+ else if (filled < 0)
+ _parser.atEOF();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} filled {} {}", this, filled, BufferUtil.toDetailString(_requestBuffer));
+
+ return filled;
+ }
+ catch (IOException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(e);
+ _parser.atEOF();
+ return -1;
+ }
+ }
+
+ return 0;
+ }
+
+ private boolean parseRequestBuffer()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} parse {} {}", this, BufferUtil.toDetailString(_requestBuffer));
+
+ boolean handle = _parser.parseNext(_requestBuffer == null ? BufferUtil.EMPTY_BUFFER : _requestBuffer);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} parsed {} {}", this, handle, _parser);
+
+ // recycle buffer ?
+ if (_contentBufferReferences.get() == 0)
+ releaseRequestBuffer();
+
+ return handle;
+ }
+
+ @Override
+ public void onCompleted()
+ {
+ // If we are fill interested, then a read is pending and we must abort
+ if (isFillInterested())
+ {
+ LOG.warn("Pending read in onCompleted {} {}", this, getEndPoint());
+ _channel.abort(new IOException("Pending read in onCompleted"));
+ }
+
+ // Handle connection upgrades
+ else if (_channel.getResponse().getStatus() == HttpStatus.SWITCHING_PROTOCOLS_101)
+ {
+ Connection connection = (Connection)_channel.getRequest().getAttribute(UPGRADE_CONNECTION_ATTRIBUTE);
+ if (connection != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Upgrade from {} to {}", this, connection);
+ _channel.getState().upgrade();
+ getEndPoint().upgrade(connection);
+ _channel.recycle();
+ _parser.reset();
+ _generator.reset();
+ if (_contentBufferReferences.get() == 0)
+ releaseRequestBuffer();
+ else
+ {
+ LOG.warn("{} lingering content references?!?!", this);
+ _requestBuffer = null; // Not returned to pool!
+ _contentBufferReferences.set(0);
+ }
+ return;
+ }
+ }
+
+ // Drive to EOF, EarlyEOF or Error
+ boolean complete = _input.consumeAll();
+
+ // Finish consuming the request
+ // If we are still expecting
+ if (_channel.isExpecting100Continue())
+ {
+ // close to seek EOF
+ _parser.close();
+ }
+ // else abort if we can't consume all
+ else if (_generator.isPersistent() && !complete)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("unconsumed input {} {}", this, _parser);
+ _channel.abort(new IOException("unconsumed input"));
+ }
+
+ // Reset the channel, parsers and generator
+ _channel.recycle();
+ if (!_parser.isClosed())
+ {
+ if (_generator.isPersistent())
+ _parser.reset();
+ else
+ _parser.close();
+ }
+
+ _generator.reset();
+
+ // if we are not called from the onfillable thread, schedule completion
+ if (getCurrentConnection() != this)
+ {
+ // If we are looking for the next request
+ if (_parser.isStart())
+ {
+ // if the buffer is empty
+ if (BufferUtil.isEmpty(_requestBuffer))
+ {
+ // look for more data
+ fillInterested();
+ }
+ // else if we are still running
+ else if (getConnector().isRunning())
+ {
+ // Dispatched to handle a pipelined request
+ try
+ {
+ getExecutor().execute(this);
+ }
+ catch (RejectedExecutionException e)
+ {
+ if (getConnector().isRunning())
+ LOG.warn(e);
+ else
+ LOG.ignore(e);
+ getEndPoint().close();
+ }
+ }
+ else
+ {
+ getEndPoint().close();
+ }
+ }
+ // else the parser must be closed, so seek the EOF if we are still open
+ else if (getEndPoint().isOpen())
+ fillInterested();
+ }
+ }
+
+ @Override
+ protected boolean onReadTimeout(Throwable timeout)
+ {
+ return _channel.onIdleTimeout(timeout);
+ }
+
+ @Override
+ protected void onFillInterestedFailed(Throwable cause)
+ {
+ _parser.close();
+ super.onFillInterestedFailed(cause);
+ }
+
+ @Override
+ public void onOpen()
+ {
+ super.onOpen();
+ if (isRequestBufferEmpty())
+ fillInterested();
+ else
+ getExecutor().execute(this);
+ }
+
+ @Override
+ public void onClose()
+ {
+ _sendCallback.close();
+ super.onClose();
+ }
+
+ @Override
+ public void run()
+ {
+ onFillable();
+ }
+
+ @Override
+ public void send(MetaData.Response info, boolean head, ByteBuffer content, boolean lastContent, Callback callback)
+ {
+ if (info == null)
+ {
+ if (!lastContent && BufferUtil.isEmpty(content))
+ {
+ callback.succeeded();
+ return;
+ }
+ }
+ else
+ {
+ // If we are still expecting a 100 continues when we commit
+ if (_channel.isExpecting100Continue())
+ // then we can't be persistent
+ _generator.setPersistent(false);
+ }
+
+ if (_sendCallback.reset(info, head, content, lastContent, callback))
+ {
+ _sendCallback.iterate();
+ }
+ }
+
+ HttpInput.Content newContent(ByteBuffer c)
+ {
+ return new Content(c);
+ }
+
+ @Override
+ public void abort(Throwable failure)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("abort {} {}", this, failure);
+ // Do a direct close of the output, as this may indicate to a client that the
+ // response is bad either with RST or by abnormal completion of chunked response.
+ getEndPoint().close();
+ }
+
+ @Override
+ public boolean isPushSupported()
+ {
+ return false;
+ }
+
+ @Override
+ public void push(org.eclipse.jetty.http.MetaData.Request request)
+ {
+ LOG.debug("ignore push in {}", this);
+ }
+
+ public void asyncReadFillInterested()
+ {
+ getEndPoint().fillInterested(_asyncReadCallback);
+ }
+
+ public void blockingReadFillInterested()
+ {
+ // We try fillInterested here because of SSL and
+ // spurious wakeups. With blocking reads, we read in a loop
+ // that tries to read/parse content and blocks waiting if there is
+ // none available. The loop can be woken up by incoming encrypted
+ // bytes, which due to SSL might not produce any decrypted bytes.
+ // Thus the loop needs to register fill interest again. However if
+ // the loop is woken up spuriously, then the register interest again
+ // can result in a pending read exception, unless we use tryFillInterested.
+ getEndPoint().tryFillInterested(_blockingReadCallback);
+ }
+
+ public void blockingReadFailure(Throwable e)
+ {
+ _blockingReadCallback.failed(e);
+ }
+
+ @Override
+ public long getBytesIn()
+ {
+ return bytesIn.longValue();
+ }
+
+ @Override
+ public long getBytesOut()
+ {
+ return bytesOut.longValue();
+ }
+
+ @Override
+ public String toConnectionString()
+ {
+ return String.format("%s@%x[p=%s,g=%s]=>%s",
+ getClass().getSimpleName(),
+ hashCode(),
+ _parser,
+ _generator,
+ _channel);
+ }
+
+ private class Content extends HttpInput.Content
+ {
+ public Content(ByteBuffer content)
+ {
+ super(content);
+ _contentBufferReferences.incrementAndGet();
+ }
+
+ @Override
+ public void succeeded()
+ {
+ if (_contentBufferReferences.decrementAndGet() == 0)
+ releaseRequestBuffer();
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ succeeded();
+ }
+ }
+
+ private class BlockingReadCallback implements Callback
+ {
+ @Override
+ public void succeeded()
+ {
+ _input.unblock();
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ _input.failed(x);
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ // This callback does not block, rather it wakes up the
+ // thread that is blocked waiting on the read.
+ return InvocationType.NON_BLOCKING;
+ }
+ }
+
+ private class AsyncReadCallback implements Callback
+ {
+ @Override
+ public void succeeded()
+ {
+ if (_channel.getState().onReadPossible())
+ _channel.handle();
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ if (_input.failed(x))
+ _channel.handle();
+ }
+ }
+
+ private class SendCallback extends IteratingCallback
+ {
+ private MetaData.Response _info;
+ private boolean _head;
+ private ByteBuffer _content;
+ private boolean _lastContent;
+ private Callback _callback;
+ private ByteBuffer _header;
+ private ByteBuffer _chunk;
+ private boolean _shutdownOut;
+
+ private SendCallback()
+ {
+ super(true);
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return _callback.getInvocationType();
+ }
+
+ private boolean reset(MetaData.Response info, boolean head, ByteBuffer content, boolean last, Callback callback)
+ {
+ if (reset())
+ {
+ _info = info;
+ _head = head;
+ _content = content;
+ _lastContent = last;
+ _callback = callback;
+ _header = null;
+ _shutdownOut = false;
+
+ if (getConnector().isShutdown())
+ _generator.setPersistent(false);
+
+ return true;
+ }
+
+ if (isClosed())
+ callback.failed(new EofException());
+ else
+ callback.failed(new WritePendingException());
+ return false;
+ }
+
+ @Override
+ public Action process() throws Exception
+ {
+ if (_callback == null)
+ throw new IllegalStateException();
+
+ while (true)
+ {
+ HttpGenerator.Result result = _generator.generateResponse(_info, _head, _header, _chunk, _content, _lastContent);
+ if (LOG.isDebugEnabled())
+ LOG.debug("generate: {} for {} ({},{},{})@{}",
+ result,
+ this,
+ BufferUtil.toSummaryString(_header),
+ BufferUtil.toSummaryString(_content),
+ _lastContent,
+ _generator.getState());
+
+ switch (result)
+ {
+ case NEED_INFO:
+ throw new EofException("request lifecycle violation");
+
+ case NEED_HEADER:
+ {
+ _header = _bufferPool.acquire(Math.min(_config.getResponseHeaderSize(), _config.getOutputBufferSize()), HEADER_BUFFER_DIRECT);
+ continue;
+ }
+
+ case HEADER_OVERFLOW:
+ {
+ if (_header.capacity() >= _config.getResponseHeaderSize())
+ throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Response header too large");
+ releaseHeader();
+ _header = _bufferPool.acquire(_config.getResponseHeaderSize(), HEADER_BUFFER_DIRECT);
+ continue;
+ }
+ case NEED_CHUNK:
+ {
+ _chunk = _bufferPool.acquire(HttpGenerator.CHUNK_SIZE, CHUNK_BUFFER_DIRECT);
+ continue;
+ }
+ case NEED_CHUNK_TRAILER:
+ {
+ releaseChunk();
+ _chunk = _bufferPool.acquire(_config.getResponseHeaderSize(), CHUNK_BUFFER_DIRECT);
+ continue;
+ }
+ case FLUSH:
+ {
+ // Don't write the chunk or the content if this is a HEAD response, or any other type of response that should have no content
+ if (_head || _generator.isNoContent())
+ {
+ BufferUtil.clear(_chunk);
+ BufferUtil.clear(_content);
+ }
+
+ byte gatherWrite = 0;
+ long bytes = 0;
+ if (BufferUtil.hasContent(_header))
+ {
+ gatherWrite += 4;
+ bytes += _header.remaining();
+ }
+ if (BufferUtil.hasContent(_chunk))
+ {
+ gatherWrite += 2;
+ bytes += _chunk.remaining();
+ }
+ if (BufferUtil.hasContent(_content))
+ {
+ gatherWrite += 1;
+ bytes += _content.remaining();
+ }
+ HttpConnection.this.bytesOut.add(bytes);
+ switch (gatherWrite)
+ {
+ case 7:
+ getEndPoint().write(this, _header, _chunk, _content);
+ break;
+ case 6:
+ getEndPoint().write(this, _header, _chunk);
+ break;
+ case 5:
+ getEndPoint().write(this, _header, _content);
+ break;
+ case 4:
+ getEndPoint().write(this, _header);
+ break;
+ case 3:
+ getEndPoint().write(this, _chunk, _content);
+ break;
+ case 2:
+ getEndPoint().write(this, _chunk);
+ break;
+ case 1:
+ getEndPoint().write(this, _content);
+ break;
+ default:
+ succeeded();
+ }
+
+ return Action.SCHEDULED;
+ }
+ case SHUTDOWN_OUT:
+ {
+ _shutdownOut = true;
+ continue;
+ }
+ case DONE:
+ {
+ // If this is the end of the response and the connector was shutdown after response was committed,
+ // we can't add the Connection:close header, but we are still allowed to close the connection
+ // by shutting down the output.
+ if (getConnector().isShutdown() && _generator.isEnd() && _generator.isPersistent())
+ _shutdownOut = true;
+
+ return Action.SUCCEEDED;
+ }
+ case CONTINUE:
+ {
+ break;
+ }
+ default:
+ {
+ throw new IllegalStateException("generateResponse=" + result);
+ }
+ }
+ }
+ }
+
+ private Callback release()
+ {
+ Callback complete = _callback;
+ _callback = null;
+ _info = null;
+ _content = null;
+ releaseHeader();
+ releaseChunk();
+ return complete;
+ }
+
+ private void releaseHeader()
+ {
+ if (_header != null)
+ _bufferPool.release(_header);
+ _header = null;
+ }
+
+ private void releaseChunk()
+ {
+ if (_chunk != null)
+ _bufferPool.release(_chunk);
+ _chunk = null;
+ }
+
+ @Override
+ protected void onCompleteSuccess()
+ {
+ release().succeeded();
+ if (_shutdownOut)
+ getEndPoint().shutdownOutput();
+ }
+
+ @Override
+ public void onCompleteFailure(final Throwable x)
+ {
+ failedCallback(release(), x);
+ if (_shutdownOut)
+ getEndPoint().shutdownOutput();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[i=%s,cb=%s]", super.toString(), _info, _callback);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java
new file mode 100644
index 0000000..c4dac53
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java
@@ -0,0 +1,94 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import org.eclipse.jetty.http.HttpCompliance;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.annotation.Name;
+
+/**
+ * A Connection Factory for HTTP Connections.
+ * <p>Accepts connections either directly or via SSL and/or ALPN chained connection factories. The accepted
+ * {@link HttpConnection}s are configured by a {@link HttpConfiguration} instance that is either created by
+ * default or passed in to the constructor.
+ */
+public class HttpConnectionFactory extends AbstractConnectionFactory implements HttpConfiguration.ConnectionFactory
+{
+ private final HttpConfiguration _config;
+ private HttpCompliance _httpCompliance;
+ private boolean _recordHttpComplianceViolations = false;
+
+ public HttpConnectionFactory()
+ {
+ this(new HttpConfiguration());
+ }
+
+ public HttpConnectionFactory(@Name("config") HttpConfiguration config)
+ {
+ this(config, null);
+ }
+
+ public HttpConnectionFactory(@Name("config") HttpConfiguration config, @Name("compliance") HttpCompliance compliance)
+ {
+ super(HttpVersion.HTTP_1_1.asString());
+ _config = config;
+ _httpCompliance = compliance == null ? HttpCompliance.RFC7230 : compliance;
+ if (config == null)
+ throw new IllegalArgumentException("Null HttpConfiguration");
+ addBean(_config);
+ }
+
+ @Override
+ public HttpConfiguration getHttpConfiguration()
+ {
+ return _config;
+ }
+
+ public HttpCompliance getHttpCompliance()
+ {
+ return _httpCompliance;
+ }
+
+ public boolean isRecordHttpComplianceViolations()
+ {
+ return _recordHttpComplianceViolations;
+ }
+
+ /**
+ * @param httpCompliance String value of {@link HttpCompliance}
+ */
+ public void setHttpCompliance(HttpCompliance httpCompliance)
+ {
+ _httpCompliance = httpCompliance;
+ }
+
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endPoint)
+ {
+ HttpConnection conn = new HttpConnection(_config, connector, endPoint, _httpCompliance, isRecordHttpComplianceViolations());
+ return configure(conn, connector, endPoint);
+ }
+
+ public void setRecordHttpComplianceViolations(boolean recordHttpComplianceViolations)
+ {
+ this._recordHttpComplianceViolations = recordHttpComplianceViolations;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java
new file mode 100644
index 0000000..e324474
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java
@@ -0,0 +1,1228 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.io.EofException;
+import org.eclipse.jetty.io.RuntimeIOException;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.component.Destroyable;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * {@link HttpInput} provides an implementation of {@link ServletInputStream} for {@link HttpChannel}.
+ * <p>
+ * Content may arrive in patterns such as [content(), content(), messageComplete()]
+ * so that this class maintains two states: the content state that tells
+ * whether there is content to consume and the EOF state that tells whether an EOF has arrived.
+ * Only once the content has been consumed the content state is moved to the EOF state.
+ * </p>
+ */
+public class HttpInput extends ServletInputStream implements Runnable
+{
+ /**
+ * An interceptor for HTTP Request input.
+ * <p>
+ * Unlike InputStream wrappers that can be applied by filters, an interceptor
+ * is entirely transparent and works with async IO APIs.
+ * </p>
+ * <p>
+ * An Interceptor may consume data from the passed content and the interceptor
+ * will continue to be called for the same content until the interceptor returns
+ * null or an empty content. Thus even if the passed content is completely consumed
+ * the interceptor will be called with the same content until it can no longer
+ * produce more content.
+ * </p>
+ *
+ * @see HttpInput#setInterceptor(Interceptor)
+ * @see HttpInput#addInterceptor(Interceptor)
+ */
+ public interface Interceptor
+ {
+ /**
+ * @param content The content to be intercepted (may be empty or a {@link SentinelContent}.
+ * The content will be modified with any data the interceptor consumes, but there is no requirement
+ * that all the data is consumed by the interceptor.
+ * @return The intercepted content or null if interception is completed for that content.
+ */
+ Content readFrom(Content content);
+ }
+
+ /**
+ * An {@link Interceptor} that chains two other {@link Interceptor}s together.
+ * The {@link Interceptor#readFrom(Content)} calls the previous {@link Interceptor}'s
+ * {@link Interceptor#readFrom(Content)} and then passes any {@link Content} returned
+ * to the next {@link Interceptor}.
+ */
+ public static class ChainedInterceptor implements Interceptor, Destroyable
+ {
+ private final Interceptor _prev;
+ private final Interceptor _next;
+
+ public ChainedInterceptor(Interceptor prev, Interceptor next)
+ {
+ _prev = prev;
+ _next = next;
+ }
+
+ public Interceptor getPrev()
+ {
+ return _prev;
+ }
+
+ public Interceptor getNext()
+ {
+ return _next;
+ }
+
+ @Override
+ public Content readFrom(Content content)
+ {
+ return getNext().readFrom(getPrev().readFrom(content));
+ }
+
+ @Override
+ public void destroy()
+ {
+ if (_prev instanceof Destroyable)
+ ((Destroyable)_prev).destroy();
+ if (_next instanceof Destroyable)
+ ((Destroyable)_next).destroy();
+ }
+ }
+
+ private static final Logger LOG = Log.getLogger(HttpInput.class);
+ static final Content EOF_CONTENT = new EofContent("EOF");
+ static final Content EARLY_EOF_CONTENT = new EofContent("EARLY_EOF");
+
+ private final byte[] _oneByteBuffer = new byte[1];
+ private Content _content;
+ private Content _intercepted;
+ private final Deque<Content> _inputQ = new ArrayDeque<>();
+ private final HttpChannelState _channelState;
+ private ReadListener _listener;
+ private State _state = STREAM;
+ private long _firstByteTimeStamp = -1;
+ private long _contentArrived;
+ private long _contentConsumed;
+ private long _blockUntil;
+ private boolean _waitingForContent;
+ private Interceptor _interceptor;
+
+ public HttpInput(HttpChannelState state)
+ {
+ _channelState = state;
+ }
+
+ protected HttpChannelState getHttpChannelState()
+ {
+ return _channelState;
+ }
+
+ public void recycle()
+ {
+ synchronized (_inputQ)
+ {
+ Throwable failure = fail(_intercepted, null);
+ _intercepted = null;
+ failure = fail(_content, failure);
+ _content = null;
+ Content item = _inputQ.poll();
+ while (item != null)
+ {
+ failure = fail(item, failure);
+ item = _inputQ.poll();
+ }
+ _listener = null;
+ _state = STREAM;
+ _contentArrived = 0;
+ _contentConsumed = 0;
+ _firstByteTimeStamp = -1;
+ _blockUntil = 0;
+ _waitingForContent = false;
+ if (_interceptor instanceof Destroyable)
+ ((Destroyable)_interceptor).destroy();
+ _interceptor = null;
+ }
+ }
+
+ private Throwable fail(Content content, Throwable failure)
+ {
+ if (content != null)
+ {
+ if (failure == null)
+ failure = new IOException("unconsumed input");
+ content.failed(failure);
+ }
+ return failure;
+ }
+
+ /**
+ * @return The current Interceptor, or null if none set
+ */
+ public Interceptor getInterceptor()
+ {
+ return _interceptor;
+ }
+
+ /**
+ * Set the interceptor.
+ *
+ * @param interceptor The interceptor to use.
+ */
+ public void setInterceptor(Interceptor interceptor)
+ {
+ _interceptor = interceptor;
+ }
+
+ /**
+ * Set the {@link Interceptor}, using a {@link ChainedInterceptor} if
+ * an {@link Interceptor} is already set.
+ *
+ * @param interceptor the next {@link Interceptor} in a chain
+ */
+ public void addInterceptor(Interceptor interceptor)
+ {
+ if (_interceptor == null)
+ _interceptor = interceptor;
+ else
+ _interceptor = new ChainedInterceptor(_interceptor, interceptor);
+ }
+
+ @Override
+ public int available()
+ {
+ int available = 0;
+ boolean woken = false;
+ synchronized (_inputQ)
+ {
+ if (_content == null)
+ _content = _inputQ.poll();
+ if (_content == null)
+ {
+ try
+ {
+ produceContent();
+ }
+ catch (Throwable e)
+ {
+ woken = failed(e);
+ }
+ if (_content == null)
+ _content = _inputQ.poll();
+ }
+
+ if (_content != null)
+ available = _content.remaining();
+ }
+
+ if (woken)
+ wake();
+ return available;
+ }
+
+ protected void wake()
+ {
+ HttpChannel channel = _channelState.getHttpChannel();
+ Executor executor = channel.getConnector().getServer().getThreadPool();
+ executor.execute(channel);
+ }
+
+ private long getBlockingTimeout()
+ {
+ return getHttpChannelState().getHttpChannel().getHttpConfiguration().getBlockingTimeout();
+ }
+
+ @Override
+ public int read() throws IOException
+ {
+ int read = read(_oneByteBuffer, 0, 1);
+ if (read == 0)
+ throw new IllegalStateException("unready read=0");
+ return read < 0 ? -1 : _oneByteBuffer[0] & 0xFF;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException
+ {
+ boolean wake = false;
+ int l;
+ synchronized (_inputQ)
+ {
+ if (!isAsync())
+ {
+ // Setup blocking only if not async
+ if (_blockUntil == 0)
+ {
+ long blockingTimeout = getBlockingTimeout();
+ if (blockingTimeout > 0)
+ _blockUntil = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(blockingTimeout);
+ }
+ }
+
+ // Calculate minimum request rate for DOS protection
+ long minRequestDataRate = _channelState.getHttpChannel().getHttpConfiguration().getMinRequestDataRate();
+ if (minRequestDataRate > 0 && _firstByteTimeStamp != -1)
+ {
+ long period = System.nanoTime() - _firstByteTimeStamp;
+ if (period > 0)
+ {
+ long minimumData = minRequestDataRate * TimeUnit.NANOSECONDS.toMillis(period) / TimeUnit.SECONDS.toMillis(1);
+ if (_contentArrived < minimumData)
+ {
+ BadMessageException bad = new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408,
+ String.format("Request content data rate < %d B/s", minRequestDataRate));
+ if (_channelState.isResponseCommitted())
+ _channelState.getHttpChannel().abort(bad);
+ throw bad;
+ }
+ }
+ }
+
+ // Consume content looking for bytes to read
+ while (true)
+ {
+ Content item = nextContent();
+ if (item != null)
+ {
+ l = get(item, b, off, len);
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} read {} from {}", this, l, item);
+
+ // Consume any following poison pills
+ if (item.isEmpty())
+ nextInterceptedContent();
+ break;
+ }
+
+ // No content, so should we block?
+ if (!_state.blockForContent(this))
+ {
+ // Not blocking, so what should we return?
+ l = _state.noContent();
+
+ if (l < 0)
+ // If EOF do we need to wake for allDataRead callback?
+ wake = _channelState.onReadEof();
+ break;
+ }
+ }
+ }
+
+ if (wake)
+ wake();
+ return l;
+ }
+
+ /**
+ * Called when derived implementations should attempt to produce more Content and add it via {@link #addContent(Content)}. For protocols that are constantly
+ * producing (eg HTTP2) this can be left as a noop;
+ *
+ * @throws IOException if unable to produce content
+ */
+ protected void produceContent() throws IOException
+ {
+ }
+
+ /**
+ * Called by channel when asynchronous IO needs to produce more content
+ *
+ * @throws IOException if unable to produce content
+ */
+ public void asyncReadProduce() throws IOException
+ {
+ synchronized (_inputQ)
+ {
+ produceContent();
+ }
+ }
+
+ /**
+ * Get the next content from the inputQ, calling {@link #produceContent()} if need be. EOF is processed and state changed.
+ *
+ * @return the content or null if none available.
+ * @throws IOException if retrieving the content fails
+ */
+ protected Content nextContent() throws IOException
+ {
+ Content content = nextNonSentinelContent();
+ if (content == null && !isFinished())
+ {
+ produceContent();
+ content = nextNonSentinelContent();
+ }
+ return content;
+ }
+
+ /**
+ * Poll the inputQ for Content. Consumed buffers and {@link SentinelContent}s are removed and EOF state updated if need be.
+ *
+ * @return Content or null
+ */
+ protected Content nextNonSentinelContent() throws IOException
+ {
+ while (true)
+ {
+ // Get the next content (or EOF)
+ Content content = nextInterceptedContent();
+
+ // If it is EOF, consume it here
+ if (content instanceof SentinelContent)
+ {
+ // Consume the EOF content, either if it was original content
+ // or if it was produced by interception
+ consume(content);
+ continue;
+ }
+
+ return content;
+ }
+ }
+
+ /**
+ * Get the next readable from the inputQ, calling {@link #produceContent()} if need be. EOF is NOT processed and state is not changed.
+ *
+ * @return the content or EOF or null if none available.
+ * @throws IOException if retrieving the content fails
+ */
+ protected Content produceNextContent() throws IOException
+ {
+ Content content = nextInterceptedContent();
+ if (content == null && !isFinished())
+ {
+ produceContent();
+ content = nextInterceptedContent();
+ }
+ return content;
+ }
+
+ /**
+ * Poll the inputQ for Content or EOF. Consumed buffers and non EOF {@link SentinelContent}s are removed. EOF state is not updated.
+ * Interception is done within this method.
+ *
+ * @return Content with remaining, a {@link SentinelContent}, or null
+ */
+ protected Content nextInterceptedContent() throws IOException
+ {
+ // If we have a chunk produced by interception
+ if (_intercepted != null)
+ {
+ // Use it if it has any remaining content
+ if (_intercepted.hasContent())
+ return _intercepted;
+
+ // succeed the chunk
+ _intercepted.succeeded();
+ _intercepted = null;
+ }
+
+ // If we don't have a Content under consideration, get
+ // the next one off the input Q.
+ if (_content == null)
+ _content = _inputQ.poll();
+
+ // While we have content to consider.
+ while (_content != null)
+ {
+ // Are we intercepting?
+ if (_interceptor != null)
+ {
+ // Intercept the current content.
+ // The interceptor may be called several
+ // times for the same content.
+ _intercepted = intercept(_content);
+
+ // If interception produced new content
+ if (_intercepted != null && _intercepted != _content)
+ {
+ // if it is not empty use it
+ if (_intercepted.hasContent())
+ return _intercepted;
+ _intercepted.succeeded();
+ }
+
+ // intercepted content consumed
+ _intercepted = null;
+
+ // fall through so that the unintercepted _content is
+ // considered for any remaining content, for EOF and to
+ // succeed it if it is entirely consumed.
+ }
+
+ // If the content has content or is an EOF marker, use it
+ if (_content.hasContent() || _content instanceof SentinelContent)
+ return _content;
+
+ // The content is consumed, so get the next one. Note that EOF
+ // content is never consumed here, but in #pollContent
+ _content.succeeded();
+ _content = _inputQ.poll();
+ }
+
+ return null;
+ }
+
+ private Content intercept(Content content) throws IOException
+ {
+ try
+ {
+ return _interceptor.readFrom(content);
+ }
+ catch (Throwable x)
+ {
+ IOException failure = new IOException("Bad content", x);
+ content.failed(failure);
+ HttpChannel channel = _channelState.getHttpChannel();
+ Response response = channel.getResponse();
+ if (response.isCommitted())
+ channel.abort(failure);
+ throw failure;
+ }
+ }
+
+ private void consume(Content content)
+ {
+ if (!isError() && content instanceof EofContent)
+ {
+ if (content == EARLY_EOF_CONTENT)
+ _state = EARLY_EOF;
+ else if (_listener == null)
+ _state = EOF;
+ else
+ _state = AEOF;
+ }
+
+ // Consume the content, either if it was original content
+ // or if it was produced by interception
+ content.succeeded();
+ if (_content == content)
+ _content = null;
+ else if (_intercepted == content)
+ _intercepted = null;
+ }
+
+ /**
+ * Copies the given content into the given byte buffer.
+ *
+ * @param content the content to copy from
+ * @param buffer the buffer to copy into
+ * @param offset the buffer offset to start copying from
+ * @param length the space available in the buffer
+ * @return the number of bytes actually copied
+ */
+ protected int get(Content content, byte[] buffer, int offset, int length)
+ {
+ int l = content.get(buffer, offset, length);
+ _contentConsumed += l;
+ return l;
+ }
+
+ /**
+ * Blocks until some content or some end-of-file event arrives.
+ *
+ * @throws IOException if the wait is interrupted
+ */
+ protected void blockForContent() throws IOException
+ {
+ try
+ {
+ _waitingForContent = true;
+ _channelState.getHttpChannel().onBlockWaitForContent();
+
+ boolean loop = false;
+ long timeout = 0;
+ while (true)
+ {
+ if (_blockUntil != 0)
+ {
+ timeout = TimeUnit.NANOSECONDS.toMillis(_blockUntil - System.nanoTime());
+ if (timeout <= 0)
+ throw new TimeoutException(String.format("Blocking timeout %d ms", getBlockingTimeout()));
+ }
+
+ // This method is called from a loop, so we just
+ // need to check the timeout before and after waiting.
+ if (loop)
+ break;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} blocking for content timeout={}", this, timeout);
+ if (timeout > 0)
+ _inputQ.wait(timeout);
+ else
+ _inputQ.wait();
+
+ loop = true;
+ }
+ }
+ catch (Throwable x)
+ {
+ _channelState.getHttpChannel().onBlockWaitForContentFailure(x);
+ }
+ }
+
+ /**
+ * Adds some content to this input stream.
+ *
+ * @param content the content to add
+ * @return true if content channel woken for read
+ */
+ public boolean addContent(Content content)
+ {
+ synchronized (_inputQ)
+ {
+ _waitingForContent = false;
+ if (_firstByteTimeStamp == -1)
+ _firstByteTimeStamp = System.nanoTime();
+
+ if (isFinished())
+ {
+ Throwable failure = isError() ? _state.getError() : new EOFException("Content after EOF");
+ content.failed(failure);
+ return false;
+ }
+ else
+ {
+ _contentArrived += content.remaining();
+
+ if (_content == null && _inputQ.isEmpty())
+ _content = content;
+ else
+ _inputQ.offer(content);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} addContent {}", this, content);
+
+ try
+ {
+ if (nextInterceptedContent() != null)
+ return wakeup();
+ else
+ return false;
+ }
+ catch (Throwable x)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("", x);
+ return failed(x);
+ }
+ }
+ }
+ }
+
+ public boolean hasContent()
+ {
+ synchronized (_inputQ)
+ {
+ return _content != null || _inputQ.size() > 0;
+ }
+ }
+
+ public void unblock()
+ {
+ synchronized (_inputQ)
+ {
+ _inputQ.notify();
+ }
+ }
+
+ public long getContentConsumed()
+ {
+ synchronized (_inputQ)
+ {
+ return _contentConsumed;
+ }
+ }
+
+ public long getContentReceived()
+ {
+ synchronized (_inputQ)
+ {
+ return _contentArrived;
+ }
+ }
+
+ /**
+ * This method should be called to signal that an EOF has been detected before all the expected content arrived.
+ * <p>
+ * Typically this will result in an EOFException being thrown from a subsequent read rather than a -1 return.
+ *
+ * @return true if content channel woken for read
+ */
+ public boolean earlyEOF()
+ {
+ return addContent(EARLY_EOF_CONTENT);
+ }
+
+ /**
+ * This method should be called to signal that all the expected content arrived.
+ *
+ * @return true if content channel woken for read
+ */
+ public boolean eof()
+ {
+ return addContent(EOF_CONTENT);
+ }
+
+ /**
+ * Consume all available content without blocking.
+ * Raw content is counted in the {@link #getContentReceived()} statistics, but
+ * is not intercepted nor counted in the {@link #getContentConsumed()} statistics
+ *
+ * @return True if EOF was reached, false otherwise.
+ */
+ public boolean consumeAll()
+ {
+ while (true)
+ {
+ synchronized (_inputQ)
+ {
+ if (_intercepted != null)
+ {
+ _intercepted.skip(_intercepted.remaining());
+ consume(_intercepted);
+ }
+
+ if (_content != null)
+ {
+ _content.skip(_content.remaining());
+ consume(_content);
+ }
+
+ Content content = _inputQ.poll();
+ while (content != null)
+ {
+ consume(content);
+ content = _inputQ.poll();
+ }
+
+ if (_state instanceof EOFState)
+ return !(_state instanceof ErrorState);
+
+ try
+ {
+ produceContent();
+ if (_content == null && _intercepted == null && _inputQ.isEmpty())
+ {
+ _state = EARLY_EOF;
+ _inputQ.notify();
+ return false;
+ }
+ }
+ catch (Throwable e)
+ {
+ LOG.debug(e);
+ _state = new ErrorState(e);
+ _inputQ.notify();
+ return false;
+ }
+ }
+ }
+ }
+
+ public boolean isError()
+ {
+ synchronized (_inputQ)
+ {
+ return _state instanceof ErrorState;
+ }
+ }
+
+ public boolean isAsync()
+ {
+ synchronized (_inputQ)
+ {
+ return _state == ASYNC;
+ }
+ }
+
+ @Override
+ public boolean isFinished()
+ {
+ synchronized (_inputQ)
+ {
+ return _state instanceof EOFState;
+ }
+ }
+
+ @Override
+ public boolean isReady()
+ {
+ synchronized (_inputQ)
+ {
+ try
+ {
+ if (_listener == null)
+ return true;
+ if (_state instanceof EOFState)
+ return true;
+ if (_waitingForContent)
+ return false;
+ if (produceNextContent() != null)
+ return true;
+ _channelState.onReadUnready();
+ _waitingForContent = true;
+ return false;
+ }
+ catch (Throwable e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("", e);
+ failed(e);
+ return true;
+ }
+ }
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener)
+ {
+ boolean woken = false;
+ synchronized (_inputQ)
+ {
+ try
+ {
+ if (_listener != null)
+ throw new IllegalStateException("ReadListener already set");
+
+ _listener = Objects.requireNonNull(readListener);
+
+ if (isError())
+ {
+ woken = _channelState.onReadReady();
+ }
+ else
+ {
+ Content content = produceNextContent();
+ if (content != null)
+ {
+ _state = ASYNC;
+ woken = _channelState.onReadReady();
+ }
+ else if (_state == EOF)
+ {
+ _state = AEOF;
+ woken = _channelState.onReadEof();
+ }
+ else
+ {
+ _state = ASYNC;
+ _channelState.onReadUnready();
+ _waitingForContent = true;
+ }
+ }
+ }
+ catch (Throwable e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("", e);
+ failed(e);
+ woken = _channelState.onReadReady();
+ }
+ }
+
+ if (woken)
+ wake();
+ }
+
+ public boolean onIdleTimeout(Throwable x)
+ {
+ synchronized (_inputQ)
+ {
+ boolean neverDispatched = getHttpChannelState().isIdle();
+ if ((_waitingForContent || neverDispatched) && !isError())
+ {
+ x.addSuppressed(new Throwable("HttpInput idle timeout"));
+ _state = new ErrorState(x);
+ return wakeup();
+ }
+ return false;
+ }
+ }
+
+ public boolean failed(Throwable x)
+ {
+ synchronized (_inputQ)
+ {
+ // Errors may be reported multiple times, for example
+ // a local idle timeout and a remote I/O failure.
+ if (isError())
+ {
+ if (LOG.isDebugEnabled())
+ {
+ // Log both the original and current failure
+ // without modifying the original failure.
+ Throwable failure = new Throwable(_state.getError());
+ failure.addSuppressed(x);
+ LOG.debug(failure);
+ }
+ }
+ else
+ {
+ // Add a suppressed throwable to capture this stack
+ // trace without wrapping/hiding the original failure.
+ x.addSuppressed(new Throwable("HttpInput failure"));
+ _state = new ErrorState(x);
+ }
+ return wakeup();
+ }
+ }
+
+ private boolean wakeup()
+ {
+ if (_listener != null)
+ return _channelState.onContentAdded();
+ _inputQ.notify();
+ return false;
+ }
+
+ /*
+ * <p> While this class is-a Runnable, it should never be dispatched in it's own thread. It is a runnable only so that the calling thread can use {@link
+ * ContextHandler#handle(Runnable)} to setup classloaders etc. </p>
+ */
+ @Override
+ public void run()
+ {
+ ReadListener listener = null;
+ Throwable error = null;
+ boolean aeof = false;
+
+ try
+ {
+ synchronized (_inputQ)
+ {
+ listener = _listener;
+
+ if (_state == EOF)
+ return;
+
+ if (_state == AEOF)
+ {
+ _state = EOF;
+ aeof = true;
+ }
+
+ error = _state.getError();
+
+ if (!aeof && error == null)
+ {
+ Content content = nextInterceptedContent();
+ if (content == null)
+ return;
+
+ // Consume a directly received EOF without first calling onDataAvailable
+ // So -1 will never be read and only onAddDataRread or onError will be called
+ if (content instanceof EofContent)
+ {
+ consume(content);
+ if (_state == EARLY_EOF)
+ error = _state.getError();
+ else if (_state == AEOF)
+ {
+ aeof = true;
+ _state = EOF;
+ }
+ }
+ }
+ }
+
+ if (error != null)
+ {
+ // TODO is this necessary to add here?
+ _channelState.getHttpChannel().getResponse().getHttpFields().add(HttpConnection.CONNECTION_CLOSE);
+ listener.onError(error);
+ }
+ else if (aeof)
+ {
+ listener.onAllDataRead();
+ }
+ else
+ {
+ listener.onDataAvailable();
+ // If -1 was read, then HttpChannelState#onEOF will have been called and a subsequent
+ // unhandle will call run again so onAllDataRead() can be called.
+ }
+ }
+ catch (Throwable e)
+ {
+ LOG.warn(e.toString());
+ if (LOG.isDebugEnabled())
+ LOG.debug("", e);
+ try
+ {
+ if (aeof || error == null)
+ {
+ _channelState.getHttpChannel().getResponse().getHttpFields().add(HttpConnection.CONNECTION_CLOSE);
+ listener.onError(e);
+ }
+ }
+ catch (Throwable ex2)
+ {
+ LOG.warn(ex2.toString());
+ LOG.debug(ex2);
+ throw new RuntimeIOException(ex2);
+ }
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ State state;
+ long consumed;
+ int q;
+ Content content;
+ synchronized (_inputQ)
+ {
+ state = _state;
+ consumed = _contentConsumed;
+ q = _inputQ.size();
+ content = _inputQ.peekFirst();
+ }
+ return String.format("%s@%x[c=%d,q=%d,[0]=%s,s=%s]",
+ getClass().getSimpleName(),
+ hashCode(),
+ consumed,
+ q,
+ content,
+ state);
+ }
+
+ /**
+ * A Sentinel Content, which has zero length content but
+ * indicates some other event in the input stream (eg EOF)
+ */
+ public static class SentinelContent extends Content
+ {
+ private final String _name;
+
+ public SentinelContent(String name)
+ {
+ super(BufferUtil.EMPTY_BUFFER);
+ _name = name;
+ }
+
+ @Override
+ public String toString()
+ {
+ return _name;
+ }
+ }
+
+ public static class EofContent extends SentinelContent
+ {
+ EofContent(String name)
+ {
+ super(name);
+ }
+ }
+
+ public static class Content implements Callback
+ {
+ protected final ByteBuffer _content;
+
+ public Content(ByteBuffer content)
+ {
+ _content = content;
+ }
+
+ public ByteBuffer getByteBuffer()
+ {
+ return _content;
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return InvocationType.NON_BLOCKING;
+ }
+
+ public int get(byte[] buffer, int offset, int length)
+ {
+ length = Math.min(_content.remaining(), length);
+ _content.get(buffer, offset, length);
+ return length;
+ }
+
+ public int skip(int length)
+ {
+ length = Math.min(_content.remaining(), length);
+ _content.position(_content.position() + length);
+ return length;
+ }
+
+ public boolean hasContent()
+ {
+ return _content.hasRemaining();
+ }
+
+ public int remaining()
+ {
+ return _content.remaining();
+ }
+
+ public boolean isEmpty()
+ {
+ return !_content.hasRemaining();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("Content@%x{%s}", hashCode(), BufferUtil.toDetailString(_content));
+ }
+ }
+
+ protected abstract static class State
+ {
+ public boolean blockForContent(HttpInput in) throws IOException
+ {
+ return false;
+ }
+
+ public int noContent() throws IOException
+ {
+ return -1;
+ }
+
+ public Throwable getError()
+ {
+ return null;
+ }
+ }
+
+ protected static class EOFState extends State
+ {
+ }
+
+ protected static class ErrorState extends EOFState
+ {
+ final Throwable _error;
+
+ ErrorState(Throwable error)
+ {
+ _error = error;
+ }
+
+ @Override
+ public Throwable getError()
+ {
+ return _error;
+ }
+
+ @Override
+ public int noContent() throws IOException
+ {
+ if (_error instanceof IOException)
+ throw (IOException)_error;
+ throw new IOException(_error);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ERROR:" + _error;
+ }
+ }
+
+ protected static final State STREAM = new State()
+ {
+ @Override
+ public boolean blockForContent(HttpInput input) throws IOException
+ {
+ input.blockForContent();
+ return true;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "STREAM";
+ }
+ };
+
+ protected static final State ASYNC = new State()
+ {
+ @Override
+ public int noContent()
+ {
+ return 0;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ASYNC";
+ }
+ };
+
+ protected static final State EARLY_EOF = new EOFState()
+ {
+ @Override
+ public int noContent() throws IOException
+ {
+ throw getError();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "EARLY_EOF";
+ }
+
+ @Override
+ public IOException getError()
+ {
+ return new EofException("Early EOF");
+ }
+ };
+
+ protected static final State EOF = new EOFState()
+ {
+ @Override
+ public String toString()
+ {
+ return "EOF";
+ }
+ };
+
+ protected static final State AEOF = new EOFState()
+ {
+ @Override
+ public String toString()
+ {
+ return "AEOF";
+ }
+ };
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputOverHTTP.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputOverHTTP.java
new file mode 100644
index 0000000..3236249
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputOverHTTP.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+
+public class HttpInputOverHTTP extends HttpInput
+{
+ public HttpInputOverHTTP(HttpChannelState state)
+ {
+ super(state);
+ }
+
+ @Override
+ protected void produceContent() throws IOException
+ {
+ ((HttpConnection)getHttpChannelState().getHttpChannel().getEndPoint().getConnection()).fillAndParseForContent();
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java
new file mode 100644
index 0000000..2b67359
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java
@@ -0,0 +1,1953 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritePendingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+import java.util.ResourceBundle;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.WriteListener;
+
+import org.eclipse.jetty.http.HttpContent;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.EofException;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.IteratingCallback;
+import org.eclipse.jetty.util.SharedBlockingCallback;
+import org.eclipse.jetty.util.SharedBlockingCallback.Blocker;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * <p>{@link HttpOutput} implements {@link ServletOutputStream}
+ * as required by the Servlet specification.</p>
+ * <p>{@link HttpOutput} buffers content written by the application until a
+ * further write will overflow the buffer, at which point it triggers a commit
+ * of the response.</p>
+ * <p>{@link HttpOutput} can be closed and reopened, to allow requests included
+ * via {@link RequestDispatcher#include(ServletRequest, ServletResponse)} to
+ * close the stream, to be reopened after the inclusion ends.</p>
+ */
+public class HttpOutput extends ServletOutputStream implements Runnable
+{
+ private static final String LSTRING_FILE = "javax.servlet.LocalStrings";
+ private static ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE);
+
+ /**
+ * The output state
+ */
+ enum State
+ {
+ OPEN, // Open
+ CLOSE, // Close needed from onWriteComplete
+ CLOSING, // Close in progress after close API called
+ CLOSED // Closed
+ }
+
+ /**
+ * The API State which combines with the output State:
+ * <pre>
+ OPEN/BLOCKING---last----------------------------+ CLOSED/BLOCKING
+ / | ^ \ ^ ^
+ / w | \ / |
+ / | owc +--owcL------------------->--owcL-----\---------------------+ |
+ | v | / / V |
+ swl OPEN/BLOCKED----last---->CLOSE/BLOCKED----owc----->CLOSING/BLOCKED--owcL------+
+ |
+ \
+ \
+ V
+ +-->OPEN/READY------last---------------------------+
+ / ^ | \
+ / / w \
+ | / | +--owcL------------------->--owcL----\---------------------------+
+ | / v / / V |
+ | irt OPEN/PENDING----last---->CLOSE/PENDING----owc---->CLOSING/PENDING--owcL----+ |
+ | \ / | | ^ | | |
+ owc \/ owc irf / irf | |
+ | /\ | | / | | |
+ | / \ V | / | V V
+ | irf OPEN/ASYNC------last----------|----------------+ | CLOSED/ASYNC
+ | \ | | ^ ^
+ \ \ | | | |
+ \ \ | | | |
+ \ v v v | |
+ +--OPEN/UNREADY----last---->CLOSE/UNREADY----owc----->CLOSING/UNREADY--owcL---+ |
+ \ \ |
+ +--owcL------------------->--owcL--------------------------------+
+
+ swl : setWriteListener
+ w : write
+ owc : onWriteComplete last == false
+ owcL : onWriteComplete last == true
+ irf : isReady() == false
+ irt : isReady() == true
+ last : close() or complete(Callback) or write of known last content
+ </pre>
+ */
+ enum ApiState
+ {
+ BLOCKING, // Open in blocking mode
+ BLOCKED, // Blocked in blocking operation
+ ASYNC, // Open in async mode
+ READY, // isReady() has returned true
+ PENDING, // write operating in progress
+ UNREADY, // write operating in progress, isReady has returned false
+ }
+
+ /**
+ * The HttpOutput.Interceptor is a single intercept point for all
+ * output written to the HttpOutput: via writer; via output stream;
+ * asynchronously; or blocking.
+ * <p>
+ * The Interceptor can be used to implement translations (eg Gzip) or
+ * additional buffering that acts on all output. Interceptors are
+ * created in a chain, so that multiple concerns may intercept.
+ * <p>
+ * The {@link HttpChannel} is an {@link Interceptor} and is always the
+ * last link in any Interceptor chain.
+ * <p>
+ * Responses are committed by the first call to
+ * {@link #write(ByteBuffer, boolean, Callback)}
+ * and closed by a call to {@link #write(ByteBuffer, boolean, Callback)}
+ * with the last boolean set true. If no content is available to commit
+ * or close, then a null buffer is passed.
+ */
+ public interface Interceptor
+ {
+ /**
+ * Write content.
+ * The response is committed by the first call to write and is closed by
+ * a call with last == true. Empty content buffers may be passed to
+ * force a commit or close.
+ *
+ * @param content The content to be written or an empty buffer.
+ * @param last True if this is the last call to write
+ * @param callback The callback to use to indicate {@link Callback#succeeded()}
+ * or {@link Callback#failed(Throwable)}.
+ */
+ void write(ByteBuffer content, boolean last, Callback callback);
+
+ /**
+ * @return The next Interceptor in the chain or null if this is the
+ * last Interceptor in the chain.
+ */
+ Interceptor getNextInterceptor();
+
+ /**
+ * @return True if the Interceptor is optimized to receive direct
+ * {@link ByteBuffer}s in the {@link #write(ByteBuffer, boolean, Callback)}
+ * method. If false is returned, then passing direct buffers may cause
+ * inefficiencies.
+ */
+ boolean isOptimizedForDirectBuffers();
+
+ /**
+ * Reset the buffers.
+ * <p>If the Interceptor contains buffers then reset them.
+ *
+ * @throws IllegalStateException Thrown if the response has been
+ * committed and buffers and/or headers cannot be reset.
+ */
+ default void resetBuffer() throws IllegalStateException
+ {
+ Interceptor next = getNextInterceptor();
+ if (next != null)
+ next.resetBuffer();
+ }
+ }
+
+ private static Logger LOG = Log.getLogger(HttpOutput.class);
+ private static final ThreadLocal<CharsetEncoder> _encoder = new ThreadLocal<>();
+
+ private final HttpChannel _channel;
+ private final HttpChannelState _channelState;
+ private final SharedBlockingCallback _writeBlocker;
+ private ApiState _apiState = ApiState.BLOCKING;
+ private State _state = State.OPEN;
+ private boolean _softClose = false;
+ private Interceptor _interceptor;
+ private long _written;
+ private long _flushed;
+ private long _firstByteTimeStamp = -1;
+ private ByteBuffer _aggregate;
+ private int _bufferSize;
+ private int _commitSize;
+ private WriteListener _writeListener;
+ private volatile Throwable _onError;
+ private Callback _closedCallback;
+
+ public HttpOutput(HttpChannel channel)
+ {
+ _channel = channel;
+ _channelState = channel.getState();
+ _interceptor = channel;
+ _writeBlocker = new WriteBlocker(channel);
+ HttpConfiguration config = channel.getHttpConfiguration();
+ _bufferSize = config.getOutputBufferSize();
+ _commitSize = config.getOutputAggregationSize();
+ if (_commitSize > _bufferSize)
+ {
+ LOG.warn("OutputAggregationSize {} exceeds bufferSize {}", _commitSize, _bufferSize);
+ _commitSize = _bufferSize;
+ }
+ }
+
+ public HttpChannel getHttpChannel()
+ {
+ return _channel;
+ }
+
+ public Interceptor getInterceptor()
+ {
+ return _interceptor;
+ }
+
+ public void setInterceptor(Interceptor interceptor)
+ {
+ _interceptor = interceptor;
+ }
+
+ public boolean isWritten()
+ {
+ return _written > 0;
+ }
+
+ public long getWritten()
+ {
+ return _written;
+ }
+
+ public void reopen()
+ {
+ synchronized (_channelState)
+ {
+ _softClose = false;
+ }
+ }
+
+ protected Blocker acquireWriteBlockingCallback() throws IOException
+ {
+ return _writeBlocker.acquire();
+ }
+
+ private void channelWrite(ByteBuffer content, boolean complete) throws IOException
+ {
+ try (Blocker blocker = _writeBlocker.acquire())
+ {
+ channelWrite(content, complete, blocker);
+ blocker.block();
+ }
+ }
+
+ private void channelWrite(ByteBuffer content, boolean last, Callback callback)
+ {
+ if (_firstByteTimeStamp == -1)
+ {
+ long minDataRate = getHttpChannel().getHttpConfiguration().getMinResponseDataRate();
+ if (minDataRate > 0)
+ _firstByteTimeStamp = System.nanoTime();
+ else
+ _firstByteTimeStamp = Long.MAX_VALUE;
+ }
+
+ _interceptor.write(content, last, callback);
+ }
+
+ private void onWriteComplete(boolean last, Throwable failure)
+ {
+ String state = null;
+ boolean wake = false;
+ Callback closedCallback = null;
+ ByteBuffer closeContent = null;
+ synchronized (_channelState)
+ {
+ if (LOG.isDebugEnabled())
+ state = stateString();
+
+ // Transition to CLOSED state if we were the last write or we have failed
+ if (last || failure != null)
+ {
+ _state = State.CLOSED;
+ closedCallback = _closedCallback;
+ _closedCallback = null;
+ releaseBuffer(failure);
+ wake = updateApiState(failure);
+ }
+ else if (_state == State.CLOSE)
+ {
+ // Somebody called close or complete while we were writing.
+ // We can now send a (probably empty) last buffer and then when it completes
+ // onWriteComplete will be called again to actually execute the _completeCallback
+ _state = State.CLOSING;
+ closeContent = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER;
+ }
+ else
+ {
+ wake = updateApiState(null);
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("onWriteComplete({},{}) {}->{} c={} cb={} w={}",
+ last, failure, state, stateString(), BufferUtil.toDetailString(closeContent), closedCallback, wake);
+
+ try
+ {
+ if (failure != null)
+ _channel.abort(failure);
+
+ if (closedCallback != null)
+ {
+ if (failure == null)
+ closedCallback.succeeded();
+ else
+ closedCallback.failed(failure);
+ }
+ else if (closeContent != null)
+ {
+ channelWrite(closeContent, true, new WriteCompleteCB());
+ }
+ }
+ finally
+ {
+ if (wake)
+ _channel.execute(_channel); // TODO review in jetty-10 if execute is needed
+ }
+ }
+
+ private boolean updateApiState(Throwable failure)
+ {
+ boolean wake = false;
+ switch (_apiState)
+ {
+ case BLOCKED:
+ _apiState = ApiState.BLOCKING;
+ break;
+
+ case PENDING:
+ _apiState = ApiState.ASYNC;
+ if (failure != null)
+ {
+ _onError = failure;
+ wake = _channelState.onWritePossible();
+ }
+ break;
+
+ case UNREADY:
+ _apiState = ApiState.READY;
+ if (failure != null)
+ _onError = failure;
+ wake = _channelState.onWritePossible();
+ break;
+
+ default:
+ if (_state == State.CLOSED)
+ break;
+ throw new IllegalStateException(stateString());
+ }
+ return wake;
+ }
+
+ private int maximizeAggregateSpace()
+ {
+ // If no aggregate, we can allocate one of bufferSize
+ if (_aggregate == null)
+ return getBufferSize();
+
+ // compact to maximize space
+ BufferUtil.compact(_aggregate);
+
+ return BufferUtil.space(_aggregate);
+ }
+
+ public void softClose()
+ {
+ synchronized (_channelState)
+ {
+ _softClose = true;
+ }
+ }
+
+ public void complete(Callback callback)
+ {
+ // This method is invoked for the COMPLETE action handling in
+ // HttpChannel.handle. The callback passed typically will call completed
+ // to finish the request cycle and so may need to asynchronously wait for:
+ // a pending/blocked operation to finish and then either an async close or
+ // wait for an application close to complete.
+ boolean succeeded = false;
+ Throwable error = null;
+ ByteBuffer content = null;
+ synchronized (_channelState)
+ {
+ // First check the API state for any unrecoverable situations
+ switch (_apiState)
+ {
+ case UNREADY: // isReady() has returned false so a call to onWritePossible may happen at any time
+ error = new CancellationException("Completed whilst write unready");
+ break;
+
+ case PENDING: // an async write is pending and may complete at any time
+ // If this is not the last write, then we must abort
+ if (!_channel.getResponse().isContentComplete(_written))
+ error = new CancellationException("Completed whilst write pending");
+ break;
+
+ case BLOCKED: // another thread is blocked in a write or a close
+ error = new CancellationException("Completed whilst write blocked");
+ break;
+
+ default:
+ break;
+ }
+
+ // If we can't complete due to the API state, then abort
+ if (error != null)
+ {
+ _channel.abort(error);
+ _writeBlocker.fail(error);
+ _state = State.CLOSED;
+ }
+ else
+ {
+ // Otherwise check the output state to determine how to complete
+ switch (_state)
+ {
+ case CLOSED:
+ succeeded = true;
+ break;
+
+ case CLOSE:
+ case CLOSING:
+ _closedCallback = Callback.combine(_closedCallback, callback);
+ break;
+
+ case OPEN:
+ if (_onError != null)
+ {
+ error = _onError;
+ break;
+ }
+
+ _closedCallback = Callback.combine(_closedCallback, callback);
+
+ switch (_apiState)
+ {
+ case BLOCKING:
+ // Output is idle blocking state, but we still do an async close
+ _apiState = ApiState.BLOCKED;
+ _state = State.CLOSING;
+ content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER;
+ break;
+
+ case ASYNC:
+ case READY:
+ // Output is idle in async state, so we can do an async close
+ _apiState = ApiState.PENDING;
+ _state = State.CLOSING;
+ content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER;
+ break;
+
+ case UNREADY:
+ case PENDING:
+ // An operation is in progress, so we soft close now
+ _softClose = true;
+ // then trigger a close from onWriteComplete
+ _state = State.CLOSE;
+ break;
+
+ default:
+ throw new IllegalStateException();
+ }
+ break;
+ }
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("complete({}) {} s={} e={}, c={}", callback, stateString(), succeeded, error, BufferUtil.toDetailString(content));
+
+ if (succeeded)
+ {
+ callback.succeeded();
+ return;
+ }
+
+ if (error != null)
+ {
+ callback.failed(error);
+ return;
+ }
+
+ if (content != null)
+ channelWrite(content, true, new WriteCompleteCB());
+ }
+
+ /**
+ * Called to indicate that the request cycle has been completed.
+ */
+ public void completed(Throwable failure)
+ {
+ synchronized (_channelState)
+ {
+ _state = State.CLOSED;
+ releaseBuffer(failure);
+ }
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ ByteBuffer content = null;
+ Blocker blocker = null;
+ synchronized (_channelState)
+ {
+ if (_onError != null)
+ {
+ if (_onError instanceof IOException)
+ throw (IOException)_onError;
+ if (_onError instanceof RuntimeException)
+ throw (RuntimeException)_onError;
+ if (_onError instanceof Error)
+ throw (Error)_onError;
+ throw new IOException(_onError);
+ }
+
+ switch (_state)
+ {
+ case CLOSED:
+ break;
+
+ case CLOSE:
+ case CLOSING:
+ switch (_apiState)
+ {
+ case BLOCKING:
+ case BLOCKED:
+ // block until CLOSED state reached.
+ blocker = _writeBlocker.acquire();
+ _closedCallback = Callback.combine(_closedCallback, blocker);
+ break;
+
+ default:
+ // async close with no callback, so nothing to do
+ break;
+ }
+ break;
+
+ case OPEN:
+ switch (_apiState)
+ {
+ case BLOCKING:
+ // Output is idle blocking state, but we still do an async close
+ _apiState = ApiState.BLOCKED;
+ _state = State.CLOSING;
+ blocker = _writeBlocker.acquire();
+ content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER;
+ break;
+
+ case BLOCKED:
+ // An blocking operation is in progress, so we soft close now
+ _softClose = true;
+ // then trigger a close from onWriteComplete
+ _state = State.CLOSE;
+ // and block until it is complete
+ blocker = _writeBlocker.acquire();
+ _closedCallback = Callback.combine(_closedCallback, blocker);
+ break;
+
+ case ASYNC:
+ case READY:
+ // Output is idle in async state, so we can do an async close
+ _apiState = ApiState.PENDING;
+ _state = State.CLOSING;
+ content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER;
+ break;
+
+ case UNREADY:
+ case PENDING:
+ // An async operation is in progress, so we soft close now
+ _softClose = true;
+ // then trigger a close from onWriteComplete
+ _state = State.CLOSE;
+ break;
+ }
+ break;
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("close() {} c={} b={}", stateString(), BufferUtil.toDetailString(content), blocker);
+
+ if (content == null)
+ {
+ if (blocker == null)
+ // nothing to do or block for.
+ return;
+
+ // Just wait for some other close to finish.
+ try (Blocker b = blocker)
+ {
+ b.block();
+ }
+ }
+ else
+ {
+ if (blocker == null)
+ {
+ // Do an async close
+ channelWrite(content, true, new WriteCompleteCB());
+ }
+ else
+ {
+ // Do a blocking close
+ try (Blocker b = blocker)
+ {
+ channelWrite(content, true, blocker);
+ b.block();
+ onWriteComplete(true, null);
+ }
+ catch (Throwable t)
+ {
+ onWriteComplete(true, t);
+ throw t;
+ }
+ }
+ }
+ }
+
+ public ByteBuffer getBuffer()
+ {
+ synchronized (_channelState)
+ {
+ return acquireBuffer();
+ }
+ }
+
+ private ByteBuffer acquireBuffer()
+ {
+ if (_aggregate == null)
+ _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers());
+ return _aggregate;
+ }
+
+ private void releaseBuffer(Throwable failure)
+ {
+ if (_aggregate != null)
+ {
+ ByteBufferPool bufferPool = _channel.getConnector().getByteBufferPool();
+ if (failure == null)
+ bufferPool.release(_aggregate);
+ else
+ bufferPool.remove(_aggregate);
+ _aggregate = null;
+ }
+ }
+
+ public boolean isClosed()
+ {
+ synchronized (_channelState)
+ {
+ return _softClose || (_state != State.OPEN);
+ }
+ }
+
+ public boolean isAsync()
+ {
+ synchronized (_channelState)
+ {
+ switch (_apiState)
+ {
+ case ASYNC:
+ case READY:
+ case PENDING:
+ case UNREADY:
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public void flush() throws IOException
+ {
+ ByteBuffer content = null;
+ synchronized (_channelState)
+ {
+ switch (_state)
+ {
+ case CLOSED:
+ case CLOSING:
+ return;
+
+ default:
+ {
+ switch (_apiState)
+ {
+ case BLOCKING:
+ _apiState = ApiState.BLOCKED;
+ content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER;
+ break;
+
+ case ASYNC:
+ case PENDING:
+ throw new IllegalStateException("isReady() not called: " + stateString());
+
+ case READY:
+ _apiState = ApiState.PENDING;
+ break;
+
+ case UNREADY:
+ throw new WritePendingException();
+
+ default:
+ throw new IllegalStateException(stateString());
+ }
+ }
+ }
+ }
+
+ if (content == null)
+ {
+ new AsyncFlush(false).iterate();
+ }
+ else
+ {
+ try
+ {
+ channelWrite(content, false);
+ onWriteComplete(false, null);
+ }
+ catch (Throwable t)
+ {
+ onWriteComplete(false, t);
+ throw t;
+ }
+ }
+ }
+
+ private void checkWritable() throws EofException
+ {
+ if (_softClose)
+ throw new EofException("Closed");
+
+ switch (_state)
+ {
+ case CLOSED:
+ case CLOSING:
+ throw new EofException("Closed");
+
+ default:
+ break;
+ }
+
+ if (_onError != null)
+ throw new EofException(_onError);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("write(array {})", BufferUtil.toDetailString(ByteBuffer.wrap(b, off, len)));
+
+ boolean last;
+ boolean aggregate;
+ boolean flush;
+
+ // Async or Blocking ?
+ boolean async;
+ synchronized (_channelState)
+ {
+ checkWritable();
+ long written = _written + len;
+ int space = maximizeAggregateSpace();
+ last = _channel.getResponse().isAllContentWritten(written);
+ // Write will be aggregated if:
+ // + it is smaller than the commitSize
+ // + is not the last one, or is last but will fit in an already allocated aggregate buffer.
+ aggregate = len <= _commitSize && (!last || BufferUtil.hasContent(_aggregate) && len <= space);
+ flush = last || !aggregate || len >= space;
+
+ if (last && _state == State.OPEN)
+ _state = State.CLOSING;
+
+ switch (_apiState)
+ {
+ case BLOCKING:
+ _apiState = flush ? ApiState.BLOCKED : ApiState.BLOCKING;
+ async = false;
+ break;
+
+ case ASYNC:
+ throw new IllegalStateException("isReady() not called: " + stateString());
+
+ case READY:
+ async = true;
+ _apiState = flush ? ApiState.PENDING : ApiState.ASYNC;
+ break;
+
+ case PENDING:
+ case UNREADY:
+ throw new WritePendingException();
+
+ default:
+ throw new IllegalStateException(stateString());
+ }
+
+ _written = written;
+
+ // Should we aggregate?
+ if (aggregate)
+ {
+ acquireBuffer();
+ int filled = BufferUtil.fill(_aggregate, b, off, len);
+
+ // return if we are not complete, not full and filled all the content
+ if (!flush)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("write(array) {} aggregated !flush {}",
+ stateString(), BufferUtil.toDetailString(_aggregate));
+ return;
+ }
+
+ // adjust offset/length
+ off += filled;
+ len -= filled;
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("write(array) {} last={} agg={} flush=true async={}, len={} {}",
+ stateString(), last, aggregate, async, len, BufferUtil.toDetailString(_aggregate));
+
+ if (async)
+ {
+ // Do the asynchronous writing from the callback
+ new AsyncWrite(b, off, len, last).iterate();
+ return;
+ }
+
+ // Blocking write
+ try
+ {
+ boolean complete = false;
+ // flush any content from the aggregate
+ if (BufferUtil.hasContent(_aggregate))
+ {
+ complete = last && len == 0;
+ channelWrite(_aggregate, complete);
+
+ // should we fill aggregate again from the buffer?
+ if (len > 0 && !last && len <= _commitSize && len <= maximizeAggregateSpace())
+ {
+ BufferUtil.append(_aggregate, b, off, len);
+ onWriteComplete(false, null);
+ return;
+ }
+ }
+
+ // write any remaining content in the buffer directly
+ if (len > 0)
+ {
+ // write a buffer capacity at a time to avoid JVM pooling large direct buffers
+ // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6210541
+ ByteBuffer view = ByteBuffer.wrap(b, off, len);
+
+ while (len > getBufferSize())
+ {
+ int p = view.position();
+ int l = p + getBufferSize();
+ view.limit(l);
+ channelWrite(view, false);
+ view.limit(p + len);
+ view.position(l);
+ len -= getBufferSize();
+ }
+ channelWrite(view, last);
+ }
+ else if (last && !complete)
+ {
+ channelWrite(BufferUtil.EMPTY_BUFFER, true);
+ }
+
+ onWriteComplete(last, null);
+ }
+ catch (Throwable t)
+ {
+ onWriteComplete(last, t);
+ throw t;
+ }
+ }
+
+ public void write(ByteBuffer buffer) throws IOException
+ {
+ // This write always bypasses aggregate buffer
+ int len = BufferUtil.length(buffer);
+ boolean flush;
+ boolean last;
+
+ // Async or Blocking ?
+ boolean async;
+ synchronized (_channelState)
+ {
+ checkWritable();
+ long written = _written + len;
+ last = _channel.getResponse().isAllContentWritten(written);
+ flush = last || len > 0 || BufferUtil.hasContent(_aggregate);
+
+ if (last && _state == State.OPEN)
+ _state = State.CLOSING;
+
+ switch (_apiState)
+ {
+ case BLOCKING:
+ async = false;
+ _apiState = flush ? ApiState.BLOCKED : ApiState.BLOCKING;
+ break;
+
+ case ASYNC:
+ throw new IllegalStateException("isReady() not called: " + stateString());
+
+ case READY:
+ async = true;
+ _apiState = flush ? ApiState.PENDING : ApiState.ASYNC;
+ break;
+
+ case PENDING:
+ case UNREADY:
+ throw new WritePendingException();
+
+ default:
+ throw new IllegalStateException(stateString());
+ }
+ _written = written;
+ }
+
+ if (!flush)
+ return;
+
+ if (async)
+ {
+ new AsyncWrite(buffer, last).iterate();
+ }
+ else
+ {
+ try
+ {
+ // Blocking write
+ // flush any content from the aggregate
+ boolean complete = false;
+ if (BufferUtil.hasContent(_aggregate))
+ {
+ complete = last && len == 0;
+ channelWrite(_aggregate, complete);
+ }
+
+ // write any remaining content in the buffer directly
+ if (len > 0)
+ channelWrite(buffer, last);
+ else if (last && !complete)
+ channelWrite(BufferUtil.EMPTY_BUFFER, true);
+
+ onWriteComplete(last, null);
+ }
+ catch (Throwable t)
+ {
+ onWriteComplete(last, t);
+ throw t;
+ }
+ }
+ }
+
+ @Override
+ public void write(int b) throws IOException
+ {
+ boolean flush;
+ boolean last;
+ // Async or Blocking ?
+
+ boolean async = false;
+ synchronized (_channelState)
+ {
+ checkWritable();
+ long written = _written + 1;
+ int space = maximizeAggregateSpace();
+ last = _channel.getResponse().isAllContentWritten(written);
+ flush = last || space == 1;
+
+ if (last && _state == State.OPEN)
+ _state = State.CLOSING;
+
+ switch (_apiState)
+ {
+ case BLOCKING:
+ _apiState = flush ? ApiState.BLOCKED : ApiState.BLOCKING;
+ break;
+
+ case ASYNC:
+ throw new IllegalStateException("isReady() not called: " + stateString());
+
+ case READY:
+ async = true;
+ _apiState = flush ? ApiState.PENDING : ApiState.ASYNC;
+ break;
+
+ case PENDING:
+ case UNREADY:
+ throw new WritePendingException();
+
+ default:
+ throw new IllegalStateException(stateString());
+ }
+ _written = written;
+
+ acquireBuffer();
+ BufferUtil.append(_aggregate, (byte)b);
+ }
+
+ // Check if all written or full
+ if (!flush)
+ return;
+
+ if (async)
+ // Do the asynchronous writing from the callback
+ new AsyncFlush(last).iterate();
+ else
+ {
+ try
+ {
+ channelWrite(_aggregate, last);
+ onWriteComplete(last, null);
+ }
+ catch (Throwable t)
+ {
+ onWriteComplete(last, t);
+ throw t;
+ }
+ }
+ }
+
+ @Override
+ public void print(String s) throws IOException
+ {
+ print(s, false);
+ }
+
+ @Override
+ public void println(String s) throws IOException
+ {
+ print(s, true);
+ }
+
+ private void print(String s, boolean eoln) throws IOException
+ {
+ if (isClosed())
+ throw new IOException("Closed");
+
+ String charset = _channel.getResponse().getCharacterEncoding();
+ CharsetEncoder encoder = _encoder.get();
+ if (encoder == null || !encoder.charset().name().equalsIgnoreCase(charset))
+ {
+ encoder = Charset.forName(charset).newEncoder();
+ encoder.onMalformedInput(CodingErrorAction.REPLACE);
+ encoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
+ _encoder.set(encoder);
+ }
+ else
+ {
+ encoder.reset();
+ }
+
+ CharBuffer in = CharBuffer.wrap(s);
+ CharBuffer crlf = eoln ? CharBuffer.wrap("\r\n") : null;
+ ByteBuffer out = getHttpChannel().getByteBufferPool().acquire((int)(1 + (s.length() + 2) * encoder.averageBytesPerChar()), false);
+ BufferUtil.flipToFill(out);
+
+ for (; ; )
+ {
+ CoderResult result;
+ if (in.hasRemaining())
+ {
+ result = encoder.encode(in, out, crlf == null);
+ if (result.isUnderflow())
+ if (crlf == null)
+ break;
+ else
+ continue;
+ }
+ else if (crlf != null && crlf.hasRemaining())
+ {
+ result = encoder.encode(crlf, out, true);
+ if (result.isUnderflow())
+ {
+ if (!encoder.flush(out).isUnderflow())
+ result.throwException();
+ break;
+ }
+ }
+ else
+ break;
+
+ if (result.isOverflow())
+ {
+ BufferUtil.flipToFlush(out, 0);
+ ByteBuffer bigger = BufferUtil.ensureCapacity(out, out.capacity() + s.length() + 2);
+ getHttpChannel().getByteBufferPool().release(out);
+ BufferUtil.flipToFill(bigger);
+ out = bigger;
+ continue;
+ }
+
+ result.throwException();
+ }
+ BufferUtil.flipToFlush(out, 0);
+ write(out.array(), out.arrayOffset(), out.remaining());
+ getHttpChannel().getByteBufferPool().release(out);
+ }
+
+ @Override
+ public void println(boolean b) throws IOException
+ {
+ println(lStrings.getString(b ? "value.true" : "value.false"));
+ }
+
+ @Override
+ public void println(char c) throws IOException
+ {
+ println(String.valueOf(c));
+ }
+
+ @Override
+ public void println(int i) throws IOException
+ {
+ println(String.valueOf(i));
+ }
+
+ @Override
+ public void println(long l) throws IOException
+ {
+ println(String.valueOf(l));
+ }
+
+ @Override
+ public void println(float f) throws IOException
+ {
+ println(String.valueOf(f));
+ }
+
+ @Override
+ public void println(double d) throws IOException
+ {
+ println(String.valueOf(d));
+ }
+
+ /**
+ * Blocking send of whole content.
+ *
+ * @param content The whole content to send
+ * @throws IOException if the send fails
+ */
+ public void sendContent(ByteBuffer content) throws IOException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("sendContent({})", BufferUtil.toDetailString(content));
+
+ _written += content.remaining();
+ channelWrite(content, true);
+ }
+
+ /**
+ * Blocking send of stream content.
+ *
+ * @param in The stream content to send
+ * @throws IOException if the send fails
+ */
+ public void sendContent(InputStream in) throws IOException
+ {
+ try (Blocker blocker = _writeBlocker.acquire())
+ {
+ new InputStreamWritingCB(in, blocker).iterate();
+ blocker.block();
+ }
+ }
+
+ /**
+ * Blocking send of channel content.
+ *
+ * @param in The channel content to send
+ * @throws IOException if the send fails
+ */
+ public void sendContent(ReadableByteChannel in) throws IOException
+ {
+ try (Blocker blocker = _writeBlocker.acquire())
+ {
+ new ReadableByteChannelWritingCB(in, blocker).iterate();
+ blocker.block();
+ }
+ }
+
+ /**
+ * Blocking send of HTTP content.
+ *
+ * @param content The HTTP content to send
+ * @throws IOException if the send fails
+ */
+ public void sendContent(HttpContent content) throws IOException
+ {
+ try (Blocker blocker = _writeBlocker.acquire())
+ {
+ sendContent(content, blocker);
+ blocker.block();
+ }
+ }
+
+ /**
+ * Asynchronous send of whole content.
+ *
+ * @param content The whole content to send
+ * @param callback The callback to use to notify success or failure
+ */
+ public void sendContent(ByteBuffer content, final Callback callback)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("sendContent(buffer={},{})", BufferUtil.toDetailString(content), callback);
+
+ if (prepareSendContent(content.remaining(), callback))
+ channelWrite(content, true,
+ new Callback.Nested(callback)
+ {
+ @Override
+ public void succeeded()
+ {
+ onWriteComplete(true, null);
+ super.succeeded();
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ onWriteComplete(true, x);
+ super.failed(x);
+ }
+ });
+ }
+
+ /**
+ * Asynchronous send of stream content.
+ * The stream will be closed after reading all content.
+ *
+ * @param in The stream content to send
+ * @param callback The callback to use to notify success or failure
+ */
+ public void sendContent(InputStream in, Callback callback)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("sendContent(stream={},{})", in, callback);
+
+ if (prepareSendContent(0, callback))
+ new InputStreamWritingCB(in, callback).iterate();
+ }
+
+ /**
+ * Asynchronous send of channel content.
+ * The channel will be closed after reading all content.
+ *
+ * @param in The channel content to send
+ * @param callback The callback to use to notify success or failure
+ */
+ public void sendContent(ReadableByteChannel in, Callback callback)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("sendContent(channel={},{})", in, callback);
+
+ if (prepareSendContent(0, callback))
+ new ReadableByteChannelWritingCB(in, callback).iterate();
+ }
+
+ private boolean prepareSendContent(int len, Callback callback)
+ {
+ synchronized (_channelState)
+ {
+ if (BufferUtil.hasContent(_aggregate))
+ {
+ callback.failed(new IOException("cannot sendContent() after write()"));
+ return false;
+ }
+ if (_channel.isCommitted())
+ {
+ callback.failed(new IOException("cannot sendContent(), output already committed"));
+ return false;
+ }
+
+ switch (_state)
+ {
+ case CLOSED:
+ case CLOSING:
+ callback.failed(new EofException("Closed"));
+ return false;
+
+ default:
+ _state = State.CLOSING;
+ break;
+ }
+
+ if (_onError != null)
+ {
+ callback.failed(_onError);
+ return false;
+ }
+
+ if (_apiState != ApiState.BLOCKING)
+ throw new IllegalStateException(stateString());
+ _apiState = ApiState.PENDING;
+ if (len > 0)
+ _written += len;
+ return true;
+ }
+ }
+
+ /**
+ * Asynchronous send of HTTP content.
+ *
+ * @param httpContent The HTTP content to send
+ * @param callback The callback to use to notify success or failure
+ */
+ public void sendContent(HttpContent httpContent, Callback callback)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("sendContent(http={},{})", httpContent, callback);
+
+ ByteBuffer buffer = _channel.useDirectBuffers() ? httpContent.getDirectBuffer() : null;
+ if (buffer == null)
+ buffer = httpContent.getIndirectBuffer();
+
+ if (buffer != null)
+ {
+ sendContent(buffer, callback);
+ return;
+ }
+
+ ReadableByteChannel rbc = null;
+ try
+ {
+ rbc = httpContent.getReadableByteChannel();
+ }
+ catch (Throwable x)
+ {
+ LOG.debug(x);
+ }
+ if (rbc != null)
+ {
+ // Close of the rbc is done by the async sendContent
+ sendContent(rbc, callback);
+ return;
+ }
+
+ InputStream in = null;
+ try
+ {
+ in = httpContent.getInputStream();
+ }
+ catch (Throwable x)
+ {
+ LOG.debug(x);
+ }
+ if (in != null)
+ {
+ sendContent(in, callback);
+ return;
+ }
+
+ Throwable cause = new IllegalArgumentException("unknown content for " + httpContent);
+ _channel.abort(cause);
+ callback.failed(cause);
+ }
+
+ public int getBufferSize()
+ {
+ return _bufferSize;
+ }
+
+ public void setBufferSize(int size)
+ {
+ _bufferSize = size;
+ _commitSize = size;
+ }
+
+ /**
+ * <p>Invoked when bytes have been flushed to the network.</p>
+ * <p>The number of flushed bytes may be different from the bytes written
+ * by the application if an {@link Interceptor} changed them, for example
+ * by compressing them.</p>
+ *
+ * @param bytes the number of bytes flushed
+ * @throws IOException if the minimum data rate, when set, is not respected
+ * @see org.eclipse.jetty.io.WriteFlusher.Listener
+ */
+ public void onFlushed(long bytes) throws IOException
+ {
+ if (_firstByteTimeStamp == -1 || _firstByteTimeStamp == Long.MAX_VALUE)
+ return;
+ long minDataRate = getHttpChannel().getHttpConfiguration().getMinResponseDataRate();
+ _flushed += bytes;
+ long elapsed = System.nanoTime() - _firstByteTimeStamp;
+ long minFlushed = minDataRate * TimeUnit.NANOSECONDS.toMillis(elapsed) / TimeUnit.SECONDS.toMillis(1);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Flushed bytes min/actual {}/{}", minFlushed, _flushed);
+ if (_flushed < minFlushed)
+ {
+ IOException ioe = new IOException(String.format("Response content data rate < %d B/s", minDataRate));
+ _channel.abort(ioe);
+ throw ioe;
+ }
+ }
+
+ public void recycle()
+ {
+ synchronized (_channelState)
+ {
+ _state = State.OPEN;
+ _apiState = ApiState.BLOCKING;
+ _softClose = true; // Stay closed until next request
+ _interceptor = _channel;
+ HttpConfiguration config = _channel.getHttpConfiguration();
+ _bufferSize = config.getOutputBufferSize();
+ _commitSize = config.getOutputAggregationSize();
+ if (_commitSize > _bufferSize)
+ _commitSize = _bufferSize;
+ releaseBuffer(null);
+ _written = 0;
+ _writeListener = null;
+ _onError = null;
+ _firstByteTimeStamp = -1;
+ _flushed = 0;
+ _closedCallback = null;
+ }
+ }
+
+ public void resetBuffer()
+ {
+ synchronized (_channelState)
+ {
+ _interceptor.resetBuffer();
+ if (BufferUtil.hasContent(_aggregate))
+ BufferUtil.clear(_aggregate);
+ _written = 0;
+ }
+ }
+
+ @Override
+ public void setWriteListener(WriteListener writeListener)
+ {
+ if (!_channel.getState().isAsync())
+ throw new IllegalStateException("!ASYNC: " + stateString());
+ boolean wake;
+ synchronized (_channelState)
+ {
+ if (_apiState != ApiState.BLOCKING)
+ throw new IllegalStateException("!OPEN" + stateString());
+ _apiState = ApiState.READY;
+ _writeListener = writeListener;
+ wake = _channel.getState().onWritePossible();
+ }
+ if (wake)
+ _channel.execute(_channel);
+ }
+
+ @Override
+ public boolean isReady()
+ {
+ synchronized (_channelState)
+ {
+ switch (_apiState)
+ {
+ case BLOCKING:
+ case READY:
+ return true;
+
+ case ASYNC:
+ _apiState = ApiState.READY;
+ return true;
+
+ case PENDING:
+ _apiState = ApiState.UNREADY;
+ return false;
+
+ case BLOCKED:
+ case UNREADY:
+ return false;
+
+ default:
+ throw new IllegalStateException(stateString());
+ }
+ }
+ }
+
+ @Override
+ public void run()
+ {
+ Throwable error = null;
+
+ synchronized (_channelState)
+ {
+ if (_onError != null)
+ {
+ error = _onError;
+ _onError = null;
+ }
+ }
+
+ try
+ {
+ if (error == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onWritePossible");
+ _writeListener.onWritePossible();
+ return;
+ }
+ }
+ catch (Throwable t)
+ {
+ error = t;
+ }
+ try
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("onError", error);
+ _writeListener.onError(error);
+ }
+ catch (Throwable t)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug(t);
+ }
+ finally
+ {
+ IO.close(this);
+ }
+ }
+
+ private String stateString()
+ {
+ return String.format("s=%s,api=%s,sc=%b,e=%s", _state, _apiState, _softClose, _onError);
+ }
+
+ @Override
+ public String toString()
+ {
+ synchronized (_channelState)
+ {
+ return String.format("%s@%x{%s}", this.getClass().getSimpleName(), hashCode(), stateString());
+ }
+ }
+
+ private abstract class ChannelWriteCB extends IteratingCallback
+ {
+ final boolean _last;
+
+ private ChannelWriteCB(boolean last)
+ {
+ _last = last;
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return InvocationType.NON_BLOCKING;
+ }
+
+ @Override
+ protected void onCompleteSuccess()
+ {
+ onWriteComplete(_last, null);
+ }
+
+ @Override
+ public void onCompleteFailure(Throwable e)
+ {
+ onWriteComplete(_last, e);
+ }
+ }
+
+ private abstract class NestedChannelWriteCB extends ChannelWriteCB
+ {
+ private final Callback _callback;
+
+ private NestedChannelWriteCB(Callback callback, boolean last)
+ {
+ super(last);
+ _callback = callback;
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return _callback.getInvocationType();
+ }
+
+ @Override
+ protected void onCompleteSuccess()
+ {
+ try
+ {
+ super.onCompleteSuccess();
+ }
+ finally
+ {
+ _callback.succeeded();
+ }
+ }
+
+ @Override
+ public void onCompleteFailure(Throwable e)
+ {
+ try
+ {
+ super.onCompleteFailure(e);
+ }
+ catch (Throwable t)
+ {
+ if (t != e)
+ e.addSuppressed(t);
+ }
+ finally
+ {
+ _callback.failed(e);
+ }
+ }
+ }
+
+ private class AsyncFlush extends ChannelWriteCB
+ {
+ private volatile boolean _flushed;
+
+ private AsyncFlush(boolean last)
+ {
+ super(last);
+ }
+
+ @Override
+ protected Action process() throws Exception
+ {
+ if (BufferUtil.hasContent(_aggregate))
+ {
+ _flushed = true;
+ channelWrite(_aggregate, false, this);
+ return Action.SCHEDULED;
+ }
+
+ if (!_flushed)
+ {
+ _flushed = true;
+ channelWrite(BufferUtil.EMPTY_BUFFER, false, this);
+ return Action.SCHEDULED;
+ }
+
+ return Action.SUCCEEDED;
+ }
+ }
+
+ private class AsyncWrite extends ChannelWriteCB
+ {
+ private final ByteBuffer _buffer;
+ private final ByteBuffer _slice;
+ private final int _len;
+ private boolean _completed;
+
+ private AsyncWrite(byte[] b, int off, int len, boolean last)
+ {
+ super(last);
+ _buffer = ByteBuffer.wrap(b, off, len);
+ _len = len;
+ // always use a view for large byte arrays to avoid JVM pooling large direct buffers
+ _slice = _len < getBufferSize() ? null : _buffer.duplicate();
+ }
+
+ private AsyncWrite(ByteBuffer buffer, boolean last)
+ {
+ super(last);
+ _buffer = buffer;
+ _len = buffer.remaining();
+ // Use a slice buffer for large indirect to avoid JVM pooling large direct buffers
+ if (_buffer.isDirect() || _len < getBufferSize())
+ _slice = null;
+ else
+ {
+ _slice = _buffer.duplicate();
+ }
+ }
+
+ @Override
+ protected Action process() throws Exception
+ {
+ // flush any content from the aggregate
+ if (BufferUtil.hasContent(_aggregate))
+ {
+ _completed = _len == 0;
+ channelWrite(_aggregate, _last && _completed, this);
+ return Action.SCHEDULED;
+ }
+
+ // Can we just aggregate the remainder?
+ if (!_last && _aggregate != null && _len < maximizeAggregateSpace() && _len < _commitSize)
+ {
+ int position = BufferUtil.flipToFill(_aggregate);
+ BufferUtil.put(_buffer, _aggregate);
+ BufferUtil.flipToFlush(_aggregate, position);
+ return Action.SUCCEEDED;
+ }
+
+ // Is there data left to write?
+ if (_buffer.hasRemaining())
+ {
+ // if there is no slice, just write it
+ if (_slice == null)
+ {
+ _completed = true;
+ channelWrite(_buffer, _last, this);
+ return Action.SCHEDULED;
+ }
+
+ // otherwise take a slice
+ int p = _buffer.position();
+ int l = Math.min(getBufferSize(), _buffer.remaining());
+ int pl = p + l;
+ _slice.limit(pl);
+ _buffer.position(pl);
+ _slice.position(p);
+ _completed = !_buffer.hasRemaining();
+ channelWrite(_slice, _last && _completed, this);
+ return Action.SCHEDULED;
+ }
+
+ // all content written, but if we have not yet signal completion, we
+ // need to do so
+ if (_last && !_completed)
+ {
+ _completed = true;
+ channelWrite(BufferUtil.EMPTY_BUFFER, true, this);
+ return Action.SCHEDULED;
+ }
+
+ if (LOG.isDebugEnabled() && _completed)
+ LOG.debug("EOF of {}", this);
+
+ return Action.SUCCEEDED;
+ }
+ }
+
+ /**
+ * An iterating callback that will take content from an
+ * InputStream and write it to the associated {@link HttpChannel}.
+ * A non direct buffer of size {@link HttpOutput#getBufferSize()} is used.
+ * This callback is passed to the {@link HttpChannel#write(ByteBuffer, boolean, Callback)} to
+ * be notified as each buffer is written and only once all the input is consumed will the
+ * wrapped {@link Callback#succeeded()} method be called.
+ */
+ private class InputStreamWritingCB extends NestedChannelWriteCB
+ {
+ private final InputStream _in;
+ private final ByteBuffer _buffer;
+ private boolean _eof;
+ private boolean _closed;
+
+ private InputStreamWritingCB(InputStream in, Callback callback)
+ {
+ super(callback, true);
+ _in = in;
+ _buffer = _channel.getByteBufferPool().acquire(getBufferSize(), false);
+ }
+
+ @Override
+ protected Action process() throws Exception
+ {
+ // Only return if EOF has previously been read and thus
+ // a write done with EOF=true
+ if (_eof)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("EOF of {}", this);
+ if (!_closed)
+ {
+ _closed = true;
+ _channel.getByteBufferPool().release(_buffer);
+ IO.close(_in);
+ }
+
+ return Action.SUCCEEDED;
+ }
+
+ // Read until buffer full or EOF
+ int len = 0;
+ while (len < _buffer.capacity() && !_eof)
+ {
+ int r = _in.read(_buffer.array(), _buffer.arrayOffset() + len, _buffer.capacity() - len);
+ if (r < 0)
+ _eof = true;
+ else
+ len += r;
+ }
+
+ // write what we have
+ _buffer.position(0);
+ _buffer.limit(len);
+ _written += len;
+ channelWrite(_buffer, _eof, this);
+ return Action.SCHEDULED;
+ }
+
+ @Override
+ public void onCompleteFailure(Throwable x)
+ {
+ try
+ {
+ _channel.getByteBufferPool().release(_buffer);
+ }
+ finally
+ {
+ super.onCompleteFailure(x);
+ }
+ }
+ }
+
+ /**
+ * An iterating callback that will take content from a
+ * ReadableByteChannel and write it to the {@link HttpChannel}.
+ * A {@link ByteBuffer} of size {@link HttpOutput#getBufferSize()} is used that will be direct if
+ * {@link HttpChannel#useDirectBuffers()} is true.
+ * This callback is passed to the {@link HttpChannel#write(ByteBuffer, boolean, Callback)} to
+ * be notified as each buffer is written and only once all the input is consumed will the
+ * wrapped {@link Callback#succeeded()} method be called.
+ */
+ private class ReadableByteChannelWritingCB extends NestedChannelWriteCB
+ {
+ private final ReadableByteChannel _in;
+ private final ByteBuffer _buffer;
+ private boolean _eof;
+ private boolean _closed;
+
+ private ReadableByteChannelWritingCB(ReadableByteChannel in, Callback callback)
+ {
+ super(callback, true);
+ _in = in;
+ _buffer = _channel.getByteBufferPool().acquire(getBufferSize(), _channel.useDirectBuffers());
+ }
+
+ @Override
+ protected Action process() throws Exception
+ {
+ // Only return if EOF has previously been read and thus
+ // a write done with EOF=true
+ if (_eof)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("EOF of {}", this);
+ if (!_closed)
+ {
+ _closed = true;
+ _channel.getByteBufferPool().release(_buffer);
+ IO.close(_in);
+ }
+ return Action.SUCCEEDED;
+ }
+
+ // Read from stream until buffer full or EOF
+ BufferUtil.clearToFill(_buffer);
+ while (_buffer.hasRemaining() && !_eof)
+ {
+ _eof = (_in.read(_buffer)) < 0;
+ }
+
+ // write what we have
+ BufferUtil.flipToFlush(_buffer, 0);
+ _written += _buffer.remaining();
+ channelWrite(_buffer, _eof, this);
+
+ return Action.SCHEDULED;
+ }
+
+ @Override
+ public void onCompleteFailure(Throwable x)
+ {
+ _channel.getByteBufferPool().release(_buffer);
+ IO.close(_in);
+ super.onCompleteFailure(x);
+ }
+ }
+
+ private static class WriteBlocker extends SharedBlockingCallback
+ {
+ private final HttpChannel _channel;
+
+ private WriteBlocker(HttpChannel channel)
+ {
+ _channel = channel;
+ }
+
+ @Override
+ protected long getIdleTimeout()
+ {
+ long blockingTimeout = _channel.getHttpConfiguration().getBlockingTimeout();
+ if (blockingTimeout == 0)
+ return _channel.getIdleTimeout();
+ return blockingTimeout;
+ }
+ }
+
+ private class WriteCompleteCB implements Callback
+ {
+ @Override
+ public void succeeded()
+ {
+ onWriteComplete(true, null);
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ onWriteComplete(true, x);
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return InvocationType.NON_BLOCKING;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpTransport.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpTransport.java
new file mode 100644
index 0000000..f981716
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpTransport.java
@@ -0,0 +1,81 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.http.MetaData;
+import org.eclipse.jetty.util.Callback;
+
+/**
+ * Abstraction of the outbound HTTP transport.
+ */
+public interface HttpTransport
+{
+ /**
+ * Asynchronous call to send a response (or part) over the transport
+ *
+ * @param info The header info to send, or null if just sending more data.
+ * The first call to send for a response must have a non null info.
+ * @param head True if the response if for a HEAD request (and the data should not be sent).
+ * @param content A buffer of content to be sent.
+ * @param lastContent True if the content is the last content for the current response.
+ * @param callback The Callback instance that success or failure of the send is notified on
+ */
+ void send(MetaData.Response info, boolean head, ByteBuffer content, boolean lastContent, Callback callback);
+
+ /**
+ * @return true if responses can be pushed over this transport
+ */
+ boolean isPushSupported();
+
+ /**
+ * @param request A request to use as the basis for generating a pushed response.
+ */
+ void push(MetaData.Request request);
+
+ /**
+ * Called to indicated the end of the current request/response cycle (which may be
+ * some time after the last content is sent).
+ */
+ void onCompleted();
+
+ /**
+ * Aborts this transport.
+ * <p>
+ * This method should terminate the transport in a way that
+ * can indicate an abnormal response to the client, for example
+ * by abruptly close the connection.
+ * <p>
+ * This method is called when an error response needs to be sent,
+ * but the response is already committed, or when a write failure
+ * is detected. If abort is called, {@link #onCompleted()} is not
+ * called
+ *
+ * @param failure the failure that caused the abort.
+ */
+ void abort(Throwable failure);
+
+ /**
+ * Is the underlying transport optimized for DirectBuffer usage
+ *
+ * @return True if direct buffers can be used optimally.
+ */
+ boolean isOptimizedForDirectBuffers();
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpWriter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpWriter.java
new file mode 100644
index 0000000..d45aacc
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/HttpWriter.java
@@ -0,0 +1,81 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.eclipse.jetty.util.ByteArrayOutputStream2;
+import org.eclipse.jetty.util.Callback;
+
+/**
+ *
+ */
+public abstract class HttpWriter extends Writer
+{
+ public static final int MAX_OUTPUT_CHARS = 512; // TODO should this be configurable? super size is 1024
+
+ final HttpOutput _out;
+ final ByteArrayOutputStream2 _bytes;
+ final char[] _chars;
+
+ public HttpWriter(HttpOutput out)
+ {
+ _out = out;
+ _chars = new char[MAX_OUTPUT_CHARS];
+ _bytes = new ByteArrayOutputStream2(MAX_OUTPUT_CHARS); // TODO should this be pooled - or do we just recycle the writer?
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ _out.close();
+ }
+
+ public void complete(Callback callback)
+ {
+ _out.complete(callback);
+ }
+
+ @Override
+ public void flush() throws IOException
+ {
+ _out.flush();
+ }
+
+ @Override
+ public void write(String s, int offset, int length) throws IOException
+ {
+ while (length > MAX_OUTPUT_CHARS)
+ {
+ write(s, offset, MAX_OUTPUT_CHARS);
+ offset += MAX_OUTPUT_CHARS;
+ length -= MAX_OUTPUT_CHARS;
+ }
+
+ s.getChars(offset, offset + length, _chars, 0);
+ write(_chars, 0, length);
+ }
+
+ @Override
+ public void write(char[] s, int offset, int length) throws IOException
+ {
+ throw new AbstractMethodError();
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/InclusiveByteRange.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/InclusiveByteRange.java
new file mode 100644
index 0000000..eaff33f
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/InclusiveByteRange.java
@@ -0,0 +1,264 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Byte range inclusive of end points.
+ * <PRE>
+ *
+ * parses the following types of byte ranges:
+ *
+ * bytes=100-499
+ * bytes=-300
+ * bytes=100-
+ * bytes=1-2,2-3,6-,-2
+ *
+ * given an entity length, converts range to string
+ *
+ * bytes 100-499/500
+ *
+ * </PRE>
+ *
+ * Based on RFC2616 3.12, 14.16, 14.35.1, 14.35.2
+ * <p>
+ * And yes the spec does strangely say that while 10-20, is bytes 10 to 20 and 10- is bytes 10 until the end that -20 IS NOT bytes 0-20, but the last 20 bytes of the content.
+ *
+ * @version $version$
+ */
+public class InclusiveByteRange
+{
+ private static final Logger LOG = Log.getLogger(InclusiveByteRange.class);
+
+ private long first;
+ private long last;
+
+ public InclusiveByteRange(long first, long last)
+ {
+ this.first = first;
+ this.last = last;
+ }
+
+ public long getFirst()
+ {
+ return first;
+ }
+
+ public long getLast()
+ {
+ return last;
+ }
+
+ private void coalesce(InclusiveByteRange r)
+ {
+ first = Math.min(first, r.first);
+ last = Math.max(last, r.last);
+ }
+
+ private boolean overlaps(InclusiveByteRange range)
+ {
+ return (range.first >= this.first && range.first <= this.last) ||
+ (range.last >= this.first && range.last <= this.last) ||
+ (range.first < this.first && range.last > this.last);
+ }
+
+ public long getSize()
+ {
+ return last - first + 1;
+ }
+
+ public String toHeaderRangeString(long size)
+ {
+ StringBuilder sb = new StringBuilder(40);
+ sb.append("bytes ");
+ sb.append(first);
+ sb.append('-');
+ sb.append(last);
+ sb.append("/");
+ sb.append(size);
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return (int)(first ^ last);
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (obj == null)
+ return false;
+
+ if (!(obj instanceof InclusiveByteRange))
+ return false;
+
+ return ((InclusiveByteRange)obj).first == this.first &&
+ ((InclusiveByteRange)obj).last == this.last;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder(60);
+ sb.append(first);
+ sb.append(":");
+ sb.append(last);
+ return sb.toString();
+ }
+
+ /**
+ * @param headers Enumeration of Range header fields.
+ * @param size Size of the resource.
+ * @return List of satisfiable ranges
+ */
+ public static List<InclusiveByteRange> satisfiableRanges(Enumeration<String> headers, long size)
+ {
+ List<InclusiveByteRange> ranges = null;
+ final long end = size - 1;
+
+ // walk through all Range headers
+ while (headers.hasMoreElements())
+ {
+ String header = headers.nextElement();
+ StringTokenizer tok = new StringTokenizer(header, "=,", false);
+ String t = null;
+ try
+ {
+ // read all byte ranges for this header
+ while (tok.hasMoreTokens())
+ {
+ try
+ {
+ t = tok.nextToken().trim();
+ if ("bytes".equals(t))
+ continue;
+
+ long first = -1;
+ long last = -1;
+ int dash = t.indexOf('-');
+ if (dash < 0 || t.indexOf("-", dash + 1) >= 0)
+ {
+ LOG.warn("Bad range format: {}", t);
+ break;
+ }
+
+ if (dash > 0)
+ first = Long.parseLong(t.substring(0, dash).trim());
+ if (dash < (t.length() - 1))
+ last = Long.parseLong(t.substring(dash + 1).trim());
+
+ if (first == -1)
+ {
+ if (last == -1)
+ {
+ LOG.warn("Bad range format: {}", t);
+ break;
+ }
+
+ if (last == 0)
+ continue;
+
+ // This is a suffix range
+ first = Math.max(0, size - last);
+ last = end;
+ }
+ else
+ {
+ // Range starts after end
+ if (first >= size)
+ continue;
+
+ if (last == -1)
+ last = end;
+ else if (last >= end)
+ last = end;
+ }
+
+ if (last < first)
+ {
+ LOG.warn("Bad range format: {}", t);
+ break;
+ }
+
+ InclusiveByteRange range = new InclusiveByteRange(first, last);
+ if (ranges == null)
+ ranges = new ArrayList<>();
+
+ boolean coalesced = false;
+ for (Iterator<InclusiveByteRange> i = ranges.listIterator(); i.hasNext(); )
+ {
+ InclusiveByteRange r = i.next();
+ if (range.overlaps(r))
+ {
+ coalesced = true;
+ r.coalesce(range);
+ while (i.hasNext())
+ {
+ InclusiveByteRange r2 = i.next();
+
+ if (r2.overlaps(r))
+ {
+ r.coalesce(r2);
+ i.remove();
+ }
+ }
+ }
+ }
+
+ if (!coalesced)
+ ranges.add(range);
+ }
+ catch (NumberFormatException e)
+ {
+ LOG.warn("Bad range format: {}", t);
+ LOG.ignore(e);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Bad range format: {}", t);
+ LOG.ignore(e);
+ }
+ }
+
+ return ranges;
+ }
+
+ public static String to416HeaderRangeString(long size)
+ {
+ StringBuilder sb = new StringBuilder(40);
+ sb.append("bytes */");
+ sb.append(size);
+ return sb.toString();
+ }
+}
+
+
+
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Iso88591HttpWriter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Iso88591HttpWriter.java
new file mode 100644
index 0000000..fe75dfd
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Iso88591HttpWriter.java
@@ -0,0 +1,70 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+
+/**
+ *
+ */
+public class Iso88591HttpWriter extends HttpWriter
+{
+
+ public Iso88591HttpWriter(HttpOutput out)
+ {
+ super(out);
+ }
+
+ @Override
+ public void write(char[] s, int offset, int length) throws IOException
+ {
+ HttpOutput out = _out;
+
+ if (length == 1)
+ {
+ int c = s[offset];
+ out.write(c < 256 ? c : '?');
+ return;
+ }
+
+ while (length > 0)
+ {
+ _bytes.reset();
+ int chars = Math.min(length, MAX_OUTPUT_CHARS);
+
+ byte[] buffer = _bytes.getBuf();
+ int bytes = _bytes.getCount();
+
+ if (chars > buffer.length - bytes)
+ chars = buffer.length - bytes;
+
+ for (int i = 0; i < chars; i++)
+ {
+ int c = s[offset + i];
+ buffer[bytes++] = (byte)(c < 256 ? c : '?');
+ }
+ if (bytes >= 0)
+ _bytes.setCount(bytes);
+
+ _bytes.writeTo(out);
+ length -= chars;
+ offset += chars;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java
new file mode 100644
index 0000000..a5688cc
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java
@@ -0,0 +1,556 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpParser;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.io.ByteArrayEndPoint;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.ByteArrayOutputStream2;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * A local connector, mostly for testing purposes.
+ * <pre>
+ * HttpTester.Request request = HttpTester.newRequest();
+ * request.setURI("/some/resource");
+ * HttpTester.Response response =
+ * HttpTester.parseResponse(HttpTester.from(localConnector.getResponse(request.generate())));
+ * </pre>
+ */
+public class LocalConnector extends AbstractConnector
+{
+ private final BlockingQueue<LocalEndPoint> _connects = new LinkedBlockingQueue<>();
+
+ public LocalConnector(Server server, Executor executor, Scheduler scheduler, ByteBufferPool pool, int acceptors, ConnectionFactory... factories)
+ {
+ super(server, executor, scheduler, pool, acceptors, factories);
+ setIdleTimeout(30000);
+ }
+
+ public LocalConnector(Server server)
+ {
+ this(server, null, null, null, -1, new HttpConnectionFactory());
+ }
+
+ public LocalConnector(Server server, SslContextFactory sslContextFactory)
+ {
+ this(server, null, null, null, -1, AbstractConnectionFactory.getFactories(sslContextFactory, new HttpConnectionFactory()));
+ }
+
+ public LocalConnector(Server server, ConnectionFactory connectionFactory)
+ {
+ this(server, null, null, null, -1, connectionFactory);
+ }
+
+ public LocalConnector(Server server, ConnectionFactory connectionFactory, SslContextFactory sslContextFactory)
+ {
+ this(server, null, null, null, -1, AbstractConnectionFactory.getFactories(sslContextFactory, connectionFactory));
+ }
+
+ @Override
+ public Object getTransport()
+ {
+ return this;
+ }
+
+ /**
+ * Sends requests and get responses based on thread activity.
+ * Returns all the responses received once the thread activity has
+ * returned to the level it was before the requests.
+ * <p>
+ * This methods waits until the connection is closed or
+ * is idle for 5s before returning the responses.
+ * <p>Use {@link #getResponse(String)} for an alternative that does not wait for idle.
+ *
+ * @param requests the requests
+ * @return the responses
+ * @throws Exception if the requests fail
+ * @deprecated Use {@link #getResponse(String)}
+ */
+ @Deprecated
+ public String getResponses(String requests) throws Exception
+ {
+ return getResponses(requests, 5, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Sends requests and get responses based on thread activity.
+ * Returns all the responses received once the thread activity has
+ * returned to the level it was before the requests.
+ * <p>
+ * This methods waits until the connection is closed or
+ * an idle period before returning the responses.
+ * <p>Use {@link #getResponse(String)} for an alternative that does not wait for idle.
+ *
+ * @param requests the requests
+ * @param idleFor The time the response stream must be idle for before returning
+ * @param units The units of idleFor
+ * @return the responses
+ * @throws Exception if the requests fail
+ * @deprecated Use {@link #getResponse(String, boolean, long, TimeUnit)}
+ */
+ @Deprecated
+ public String getResponses(String requests, long idleFor, TimeUnit units) throws Exception
+ {
+ ByteBuffer result = getResponses(BufferUtil.toBuffer(requests, StandardCharsets.UTF_8), idleFor, units);
+ return result == null ? null : BufferUtil.toString(result, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Sends requests and get's responses based on thread activity.
+ * Returns all the responses received once the thread activity has
+ * returned to the level it was before the requests.
+ * <p>
+ * This methods waits until the connection is closed or
+ * is idle for 5s before returning the responses.
+ * <p>Use {@link #getResponse(ByteBuffer)} for an alternative that does not wait for idle.
+ *
+ * @param requestsBuffer the requests
+ * @return the responses
+ * @throws Exception if the requests fail
+ * @deprecated Use {@link #getResponse(ByteBuffer)}
+ */
+ @Deprecated
+ public ByteBuffer getResponses(ByteBuffer requestsBuffer) throws Exception
+ {
+ return getResponses(requestsBuffer, 5, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Sends requests and get's responses based on thread activity.
+ * Returns all the responses received once the thread activity has
+ * returned to the level it was before the requests.
+ * <p>
+ * This methods waits until the connection is closed or
+ * an idle period before returning the responses.
+ *
+ * @param requestsBuffer the requests
+ * @param idleFor The time the response stream must be idle for before returning
+ * @param units The units of idleFor
+ * @return the responses
+ * @throws Exception if the requests fail
+ * @deprecated Use {@link #getResponse(ByteBuffer, boolean, long, TimeUnit)}
+ */
+ @Deprecated
+ public ByteBuffer getResponses(ByteBuffer requestsBuffer, long idleFor, TimeUnit units) throws Exception
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("requests {}", BufferUtil.toUTF8String(requestsBuffer));
+ LocalEndPoint endp = executeRequest(requestsBuffer);
+ endp.waitUntilClosedOrIdleFor(idleFor, units);
+ ByteBuffer responses = endp.takeOutput();
+ if (endp.isOutputShutdown())
+ endp.close();
+ if (LOG.isDebugEnabled())
+ LOG.debug("responses {}", BufferUtil.toUTF8String(responses));
+ return responses;
+ }
+
+ /**
+ * Execute a request and return the EndPoint through which
+ * multiple responses can be received or more input provided.
+ *
+ * @param rawRequest the request
+ * @return the local endpoint
+ */
+ public LocalEndPoint executeRequest(String rawRequest)
+ {
+ return executeRequest(BufferUtil.toBuffer(rawRequest, StandardCharsets.UTF_8));
+ }
+
+ private LocalEndPoint executeRequest(ByteBuffer rawRequest)
+ {
+ if (!isStarted())
+ throw new IllegalStateException("!STARTED");
+ LocalEndPoint endp = new LocalEndPoint();
+ endp.addInput(rawRequest);
+ _connects.add(endp);
+ return endp;
+ }
+
+ public LocalEndPoint connect()
+ {
+ LocalEndPoint endp = new LocalEndPoint();
+ _connects.add(endp);
+ return endp;
+ }
+
+ @Override
+ protected void accept(int acceptorID) throws IOException, InterruptedException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("accepting {}", acceptorID);
+ LocalEndPoint endPoint = _connects.take();
+
+ Connection connection = getDefaultConnectionFactory().newConnection(this, endPoint);
+ endPoint.setConnection(connection);
+
+ endPoint.onOpen();
+ onEndPointOpened(endPoint);
+
+ connection.onOpen();
+ }
+
+ /**
+ * Get a single response using a parser to search for the end of the message.
+ *
+ * @param requestsBuffer The request to send
+ * @return ByteBuffer containing response or null.
+ * @throws Exception If there is a problem
+ */
+ public ByteBuffer getResponse(ByteBuffer requestsBuffer) throws Exception
+ {
+ return getResponse(requestsBuffer, false, 10, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Get a single response using a parser to search for the end of the message.
+ *
+ * @param requestBuffer The request to send
+ * @param time The time to wait
+ * @param unit The units of the wait
+ * @return ByteBuffer containing response or null.
+ * @throws Exception If there is a problem
+ */
+ public ByteBuffer getResponse(ByteBuffer requestBuffer, long time, TimeUnit unit) throws Exception
+ {
+ boolean head = BufferUtil.toString(requestBuffer).toLowerCase().startsWith("head ");
+ if (LOG.isDebugEnabled())
+ LOG.debug("requests {}", BufferUtil.toUTF8String(requestBuffer));
+ LocalEndPoint endp = executeRequest(requestBuffer);
+ return endp.waitForResponse(head, time, unit);
+ }
+
+ /**
+ * Get a single response using a parser to search for the end of the message.
+ *
+ * @param requestBuffer The request to send
+ * @param head True if the response is for a head request
+ * @param time The time to wait
+ * @param unit The units of the wait
+ * @return ByteBuffer containing response or null.
+ * @throws Exception If there is a problem
+ */
+ public ByteBuffer getResponse(ByteBuffer requestBuffer, boolean head, long time, TimeUnit unit) throws Exception
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("requests {}", BufferUtil.toUTF8String(requestBuffer));
+ LocalEndPoint endp = executeRequest(requestBuffer);
+ return endp.waitForResponse(head, time, unit);
+ }
+
+ /**
+ * Get a single response using a parser to search for the end of the message.
+ *
+ * @param rawRequest The request to send
+ * @return ByteBuffer containing response or null.
+ * @throws Exception If there is a problem
+ */
+ public String getResponse(String rawRequest) throws Exception
+ {
+ return getResponse(rawRequest, false, 30, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Get a single response using a parser to search for the end of the message.
+ *
+ * @param rawRequest The request to send
+ * @param time The time to wait
+ * @param unit The units of the wait
+ * @return ByteBuffer containing response or null.
+ * @throws Exception If there is a problem
+ */
+ public String getResponse(String rawRequest, long time, TimeUnit unit) throws Exception
+ {
+ boolean head = rawRequest.toLowerCase().startsWith("head ");
+ ByteBuffer requestsBuffer = BufferUtil.toBuffer(rawRequest, StandardCharsets.ISO_8859_1);
+ if (LOG.isDebugEnabled())
+ LOG.debug("request {}", BufferUtil.toUTF8String(requestsBuffer));
+ LocalEndPoint endp = executeRequest(requestsBuffer);
+
+ return BufferUtil.toString(endp.waitForResponse(head, time, unit), StandardCharsets.ISO_8859_1);
+ }
+
+ /**
+ * Get a single response using a parser to search for the end of the message.
+ *
+ * @param rawRequest The request to send
+ * @param head True if the response is for a head request
+ * @param time The time to wait
+ * @param unit The units of the wait
+ * @return ByteBuffer containing response or null.
+ * @throws Exception If there is a problem
+ */
+ public String getResponse(String rawRequest, boolean head, long time, TimeUnit unit) throws Exception
+ {
+ ByteBuffer requestsBuffer = BufferUtil.toBuffer(rawRequest, StandardCharsets.ISO_8859_1);
+ if (LOG.isDebugEnabled())
+ LOG.debug("request {}", BufferUtil.toUTF8String(requestsBuffer));
+ LocalEndPoint endp = executeRequest(requestsBuffer);
+
+ return BufferUtil.toString(endp.waitForResponse(head, time, unit), StandardCharsets.ISO_8859_1);
+ }
+
+ /**
+ * Local EndPoint
+ */
+ public class LocalEndPoint extends ByteArrayEndPoint
+ {
+ private final CountDownLatch _closed = new CountDownLatch(1);
+ private ByteBuffer _responseData;
+
+ public LocalEndPoint()
+ {
+ super(LocalConnector.this.getScheduler(), LocalConnector.this.getIdleTimeout());
+ setGrowOutput(true);
+ }
+
+ @Override
+ protected void execute(Runnable task)
+ {
+ getExecutor().execute(task);
+ }
+
+ @Override
+ public void onClose()
+ {
+ Connection connection = getConnection();
+ if (connection != null)
+ connection.onClose();
+ LocalConnector.this.onEndPointClosed(this);
+ super.onClose();
+ _closed.countDown();
+ }
+
+ @Override
+ public void doShutdownOutput()
+ {
+ super.shutdownOutput();
+ close();
+ }
+
+ public void waitUntilClosed()
+ {
+ while (isOpen())
+ {
+ try
+ {
+ if (!_closed.await(10, TimeUnit.SECONDS))
+ break;
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+ }
+
+ public void waitUntilClosedOrIdleFor(long idleFor, TimeUnit units)
+ {
+ Thread.yield();
+ int size = getOutput().remaining();
+ while (isOpen())
+ {
+ try
+ {
+ if (!_closed.await(idleFor, units))
+ {
+ if (size == getOutput().remaining())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("idle for {} {}", idleFor, units);
+ return;
+ }
+ size = getOutput().remaining();
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+ }
+
+ /**
+ * Remaining output ByteBuffer after calls to {@link #getResponse()} or {@link #waitForResponse(boolean, long, TimeUnit)}
+ *
+ * @return the remaining response data buffer
+ */
+ public ByteBuffer getResponseData()
+ {
+ return _responseData;
+ }
+
+ /**
+ * Wait for a response using a parser to detect the end of message
+ *
+ * @return Buffer containing full response or null for EOF;
+ * @throws Exception if the response cannot be parsed
+ */
+ public String getResponse() throws Exception
+ {
+ return getResponse(false, 30, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Wait for a response using a parser to detect the end of message
+ *
+ * @param head whether the request is a HEAD request
+ * @param time the maximum time to wait
+ * @param unit the time unit of the {@code timeout} argument
+ * @return Buffer containing full response or null for EOF;
+ * @throws Exception if the response cannot be parsed
+ */
+ public String getResponse(boolean head, long time, TimeUnit unit) throws Exception
+ {
+ ByteBuffer response = waitForResponse(head, time, unit);
+ if (response != null)
+ return BufferUtil.toString(response);
+ return null;
+ }
+
+ /**
+ * Wait for a response using a parser to detect the end of message
+ *
+ * @param head whether the request is a HEAD request
+ * @param time the maximum time to wait
+ * @param unit the time unit of the {@code timeout} argument
+ * @return Buffer containing full response or null for EOF;
+ * @throws Exception if the response cannot be parsed
+ */
+ public ByteBuffer waitForResponse(boolean head, long time, TimeUnit unit) throws Exception
+ {
+ HttpParser.ResponseHandler handler = new HttpParser.ResponseHandler()
+ {
+ @Override
+ public void parsedHeader(HttpField field)
+ {
+ }
+
+ @Override
+ public boolean contentComplete()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean messageComplete()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean headerComplete()
+ {
+ return false;
+ }
+
+ @Override
+ public int getHeaderCacheSize()
+ {
+ return 0;
+ }
+
+ @Override
+ public void earlyEOF()
+ {
+ }
+
+ @Override
+ public boolean content(ByteBuffer item)
+ {
+ return false;
+ }
+
+ @Override
+ public boolean startResponse(HttpVersion version, int status, String reason)
+ {
+ return false;
+ }
+ };
+
+ HttpParser parser = new HttpParser(handler);
+ parser.setHeadResponse(head);
+ try (ByteArrayOutputStream2 bout = new ByteArrayOutputStream2())
+ {
+ loop:
+ while (true)
+ {
+ // read a chunk of response
+ ByteBuffer chunk;
+ if (BufferUtil.hasContent(_responseData))
+ chunk = _responseData;
+ else
+ {
+ chunk = waitForOutput(time, unit);
+ if (BufferUtil.isEmpty(chunk) && (!isOpen() || isOutputShutdown()))
+ {
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ break loop;
+ }
+ }
+
+ // Parse the content of this chunk
+ while (BufferUtil.hasContent(chunk))
+ {
+ int pos = chunk.position();
+ boolean complete = parser.parseNext(chunk);
+ if (chunk.position() == pos)
+ {
+ // Nothing consumed
+ if (BufferUtil.isEmpty(chunk))
+ break;
+ return null;
+ }
+
+ // Add all consumed bytes to the output stream
+ bout.write(chunk.array(), chunk.arrayOffset() + pos, chunk.position() - pos);
+
+ // If we are complete then break the outer loop
+ if (complete)
+ {
+ if (BufferUtil.hasContent(chunk))
+ _responseData = chunk;
+ break loop;
+ }
+ }
+ }
+
+ if (bout.getCount() == 0 && isOutputShutdown())
+ return null;
+ return ByteBuffer.wrap(bout.getBuf(), 0, bout.getCount());
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/LowResourceMonitor.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/LowResourceMonitor.java
new file mode 100644
index 0000000..1753cc2
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/LowResourceMonitor.java
@@ -0,0 +1,647 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.Name;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.eclipse.jetty.util.thread.ThreadPool;
+
+/**
+ * A monitor for low resources, low resources can be detected by:
+ * <ul>
+ * <li>{@link ThreadPool#isLowOnThreads()} if {@link Connector#getExecutor()} is
+ * an instance of {@link ThreadPool} and {@link #setMonitorThreads(boolean)} is true.</li>
+ * <li>If {@link #setMaxMemory(long)} is non zero then low resources is detected if the JVMs
+ * {@link Runtime} instance has {@link Runtime#totalMemory()} minus {@link Runtime#freeMemory()}
+ * greater than {@link #getMaxMemory()}</li>
+ * <li>If {@link #setMaxConnections(int)} is non zero then low resources is detected if the total number
+ * of connections exceeds {@link #getMaxConnections()}. This feature is deprecated and replaced by
+ * {@link ConnectionLimit}</li>
+ * </ul>
+ */
+@ManagedObject("Monitor for low resource conditions and activate a low resource mode if detected")
+public class LowResourceMonitor extends ContainerLifeCycle
+{
+ private static final Logger LOG = Log.getLogger(LowResourceMonitor.class);
+
+ protected final Server _server;
+ private Scheduler _scheduler;
+ private Connector[] _monitoredConnectors;
+ private Set<AbstractConnector> _acceptingConnectors = new HashSet<>();
+ private int _period = 1000;
+
+ private int _lowResourcesIdleTimeout = 1000;
+ private int _maxLowResourcesTime = 0;
+
+ private final AtomicBoolean _low = new AtomicBoolean();
+
+ private String _reasons;
+
+ private long _lowStarted;
+ private boolean _acceptingInLowResources = true;
+
+ private Set<LowResourceCheck> _lowResourceChecks = new HashSet<>();
+
+ private final Runnable _monitor = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ if (isRunning())
+ {
+ monitor();
+ _scheduler.schedule(_monitor, _period, TimeUnit.MILLISECONDS);
+ }
+ }
+ };
+
+ public LowResourceMonitor(@Name("server") Server server)
+ {
+ _server = server;
+ }
+
+ @ManagedAttribute("True if low available threads status is monitored")
+ public boolean getMonitorThreads()
+ {
+ return !getBeans(ConnectorsThreadPoolLowResourceCheck.class).isEmpty();
+ }
+
+ /**
+ * @param monitorThreads If true, check connectors executors to see if they are
+ * {@link ThreadPool} instances that are low on threads.
+ */
+ public void setMonitorThreads(boolean monitorThreads)
+ {
+ if (monitorThreads)
+ // already configured?
+ if (!getMonitorThreads())
+ addLowResourceCheck(new ConnectorsThreadPoolLowResourceCheck());
+ else
+ getBeans(ConnectorsThreadPoolLowResourceCheck.class).forEach(this::removeBean);
+ }
+
+ /**
+ * @return The maximum connections allowed for the monitored connectors before low resource handling is activated
+ * @deprecated Replaced by ConnectionLimit
+ */
+ @ManagedAttribute("The maximum connections allowed for the monitored connectors before low resource handling is activated")
+ @Deprecated
+ public int getMaxConnections()
+ {
+ for (MaxConnectionsLowResourceCheck lowResourceCheck : getBeans(MaxConnectionsLowResourceCheck.class))
+ {
+ if (lowResourceCheck.getMaxConnections() > 0)
+ {
+ return lowResourceCheck.getMaxConnections();
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * @param maxConnections The maximum connections before low resources state is triggered
+ * @deprecated Replaced by {@link ConnectionLimit}
+ */
+ @Deprecated
+ public void setMaxConnections(int maxConnections)
+ {
+ if (maxConnections > 0)
+ {
+ if (getBeans(MaxConnectionsLowResourceCheck.class).isEmpty())
+ {
+ addLowResourceCheck(new MaxConnectionsLowResourceCheck(maxConnections));
+ }
+ else
+ {
+ getBeans(MaxConnectionsLowResourceCheck.class).forEach(c -> c.setMaxConnections(maxConnections));
+ }
+ }
+ else
+ {
+ getBeans(ConnectorsThreadPoolLowResourceCheck.class).forEach(this::removeBean);
+ }
+ }
+
+ @ManagedAttribute("The reasons the monitored connectors are low on resources")
+ public String getReasons()
+ {
+ return _reasons;
+ }
+
+ protected void setReasons(String reasons)
+ {
+ _reasons = reasons;
+ }
+
+ @ManagedAttribute("Are the monitored connectors low on resources?")
+ public boolean isLowOnResources()
+ {
+ return _low.get();
+ }
+
+ protected boolean enableLowOnResources(boolean expectedValue, boolean newValue)
+ {
+ return _low.compareAndSet(expectedValue, newValue);
+ }
+
+ @ManagedAttribute("The reason(s) the monitored connectors are low on resources")
+ public String getLowResourcesReasons()
+ {
+ return _reasons;
+ }
+
+ protected void setLowResourcesReasons(String reasons)
+ {
+ _reasons = reasons;
+ }
+
+ @ManagedAttribute("Get the timestamp in ms since epoch that low resources state started")
+ public long getLowResourcesStarted()
+ {
+ return _lowStarted;
+ }
+
+ public void setLowResourcesStarted(long lowStarted)
+ {
+ _lowStarted = lowStarted;
+ }
+
+ @ManagedAttribute("The monitored connectors. If null then all server connectors are monitored")
+ public Collection<Connector> getMonitoredConnectors()
+ {
+ if (_monitoredConnectors == null)
+ return Collections.emptyList();
+ return Arrays.asList(_monitoredConnectors);
+ }
+
+ /**
+ * @param monitoredConnectors The collections of Connectors that should be monitored for low resources.
+ */
+ public void setMonitoredConnectors(Collection<Connector> monitoredConnectors)
+ {
+ if (monitoredConnectors == null || monitoredConnectors.size() == 0)
+ _monitoredConnectors = null;
+ else
+ _monitoredConnectors = monitoredConnectors.toArray(new Connector[monitoredConnectors.size()]);
+ }
+
+ protected Connector[] getMonitoredOrServerConnectors()
+ {
+ if (_monitoredConnectors != null && _monitoredConnectors.length > 0)
+ return _monitoredConnectors;
+ return _server.getConnectors();
+ }
+
+ @ManagedAttribute("If false, new connections are not accepted while in low resources")
+ public boolean isAcceptingInLowResources()
+ {
+ return _acceptingInLowResources;
+ }
+
+ public void setAcceptingInLowResources(boolean acceptingInLowResources)
+ {
+ _acceptingInLowResources = acceptingInLowResources;
+ }
+
+ @ManagedAttribute("The monitor period in ms")
+ public int getPeriod()
+ {
+ return _period;
+ }
+
+ /**
+ * @param periodMS The period in ms to monitor for low resources
+ */
+ public void setPeriod(int periodMS)
+ {
+ _period = periodMS;
+ }
+
+ @ManagedAttribute("The idletimeout in ms to apply to all existing connections when low resources is detected")
+ public int getLowResourcesIdleTimeout()
+ {
+ return _lowResourcesIdleTimeout;
+ }
+
+ /**
+ * @param lowResourcesIdleTimeoutMS The timeout in ms to apply to EndPoints when in the low resources state.
+ */
+ public void setLowResourcesIdleTimeout(int lowResourcesIdleTimeoutMS)
+ {
+ _lowResourcesIdleTimeout = lowResourcesIdleTimeoutMS;
+ }
+
+ @ManagedAttribute("The maximum time in ms that low resources condition can persist before lowResourcesIdleTimeout is applied to new connections as well as existing connections")
+ public int getMaxLowResourcesTime()
+ {
+ return _maxLowResourcesTime;
+ }
+
+ /**
+ * @param maxLowResourcesTimeMS The time in milliseconds that a low resource state can persist before the low resource idle timeout is reapplied to all connections
+ */
+ public void setMaxLowResourcesTime(int maxLowResourcesTimeMS)
+ {
+ _maxLowResourcesTime = maxLowResourcesTimeMS;
+ }
+
+ @ManagedAttribute("The maximum memory (in bytes) that can be used before low resources is triggered. Memory used is calculated as (totalMemory-freeMemory).")
+ public long getMaxMemory()
+ {
+ Collection<MemoryLowResourceCheck> beans = getBeans(MemoryLowResourceCheck.class);
+ if (beans.isEmpty())
+ {
+ return 0;
+ }
+ return beans.stream().findFirst().get().getMaxMemory();
+ }
+
+ /**
+ * @param maxMemoryBytes The maximum memory in bytes in use before low resources is triggered.
+ */
+ public void setMaxMemory(long maxMemoryBytes)
+ {
+ if (maxMemoryBytes <= 0)
+ {
+ return;
+ }
+ Collection<MemoryLowResourceCheck> beans = getBeans(MemoryLowResourceCheck.class);
+ if (beans.isEmpty())
+ addLowResourceCheck(new MemoryLowResourceCheck(maxMemoryBytes));
+ else
+ beans.forEach(lowResourceCheck -> lowResourceCheck.setMaxMemory(maxMemoryBytes));
+ }
+
+ public Set<LowResourceCheck> getLowResourceChecks()
+ {
+ return _lowResourceChecks;
+ }
+
+ public void setLowResourceChecks(Set<LowResourceCheck> lowResourceChecks)
+ {
+ updateBeans(_lowResourceChecks.toArray(), lowResourceChecks.toArray());
+ this._lowResourceChecks = lowResourceChecks;
+ }
+
+ public void addLowResourceCheck(LowResourceCheck lowResourceCheck)
+ {
+ addBean(lowResourceCheck);
+ this._lowResourceChecks.add(lowResourceCheck);
+ }
+
+ protected void monitor()
+ {
+
+ String reasons = null;
+
+ for (LowResourceCheck lowResourceCheck : _lowResourceChecks)
+ {
+ if (lowResourceCheck.isLowOnResources())
+ {
+ reasons = lowResourceCheck.toString();
+ break;
+ }
+ }
+
+ if (reasons != null)
+ {
+ // Log the reasons if there is any change in the cause
+ if (!reasons.equals(getReasons()))
+ {
+ LOG.warn("Low Resources: {}", reasons);
+ setReasons(reasons);
+ }
+
+ // Enter low resources state?
+ if (enableLowOnResources(false, true))
+ {
+ setLowResourcesReasons(reasons);
+ setLowResourcesStarted(System.currentTimeMillis());
+ setLowResources();
+ }
+
+ // Too long in low resources state?
+ if (getMaxLowResourcesTime() > 0 && (System.currentTimeMillis() - getLowResourcesStarted()) > getMaxLowResourcesTime())
+ setLowResources();
+ }
+ else
+ {
+ if (enableLowOnResources(true, false))
+ {
+ LOG.info("Low Resources cleared");
+ setLowResourcesReasons(null);
+ setLowResourcesStarted(0);
+ setReasons(null);
+ clearLowResources();
+ }
+ }
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ _scheduler = _server.getBean(Scheduler.class);
+
+ if (_scheduler == null)
+ {
+ _scheduler = new LRMScheduler();
+ _scheduler.start();
+ }
+
+ super.doStart();
+
+ _scheduler.schedule(_monitor, _period, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ if (_scheduler instanceof LRMScheduler)
+ _scheduler.stop();
+ super.doStop();
+ }
+
+ protected void setLowResources()
+ {
+ for (Connector connector : getMonitoredOrServerConnectors())
+ {
+ if (connector instanceof AbstractConnector)
+ {
+ AbstractConnector c = (AbstractConnector)connector;
+ if (!isAcceptingInLowResources() && c.isAccepting())
+ {
+ _acceptingConnectors.add(c);
+ c.setAccepting(false);
+ }
+ }
+
+ for (EndPoint endPoint : connector.getConnectedEndPoints())
+ {
+ endPoint.setIdleTimeout(_lowResourcesIdleTimeout);
+ }
+ }
+ }
+
+ protected void clearLowResources()
+ {
+ for (Connector connector : getMonitoredOrServerConnectors())
+ {
+ for (EndPoint endPoint : connector.getConnectedEndPoints())
+ {
+ endPoint.setIdleTimeout(connector.getIdleTimeout());
+ }
+ }
+
+ for (AbstractConnector connector : _acceptingConnectors)
+ {
+ connector.setAccepting(true);
+ }
+ _acceptingConnectors.clear();
+ }
+
+ protected String low(String reasons, String newReason)
+ {
+ if (reasons == null)
+ return newReason;
+ return reasons + ", " + newReason;
+ }
+
+ private static class LRMScheduler extends ScheduledExecutorScheduler
+ {
+ }
+
+ public interface LowResourceCheck
+ {
+ boolean isLowOnResources();
+
+ String getReason();
+ }
+
+ //------------------------------------------------------
+ // default implementations for backward compat
+ //------------------------------------------------------
+
+ public class MainThreadPoolLowResourceCheck implements LowResourceCheck
+ {
+ private String reason;
+
+ public MainThreadPoolLowResourceCheck()
+ {
+ // no op
+ }
+
+ @Override
+ public boolean isLowOnResources()
+ {
+ ThreadPool serverThreads = _server.getThreadPool();
+ if (serverThreads.isLowOnThreads())
+ {
+ reason = "Server low on threads: " + serverThreads;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String getReason()
+ {
+ return reason;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Check if the server ThreadPool is lowOnThreads";
+ }
+ }
+
+ public class ConnectorsThreadPoolLowResourceCheck implements LowResourceCheck
+ {
+ private String reason;
+
+ public ConnectorsThreadPoolLowResourceCheck()
+ {
+ // no op
+ }
+
+ @Override
+ public boolean isLowOnResources()
+ {
+ ThreadPool serverThreads = _server.getThreadPool();
+ if (serverThreads.isLowOnThreads())
+ {
+ reason = "Server low on threads: " + serverThreads.getThreads() + ", idleThreads:" + serverThreads.getIdleThreads();
+ return true;
+ }
+ for (Connector connector : getMonitoredConnectors())
+ {
+ Executor executor = connector.getExecutor();
+ if (executor instanceof ThreadPool && executor != serverThreads)
+ {
+ ThreadPool connectorThreads = (ThreadPool)executor;
+ if (connectorThreads.isLowOnThreads())
+ {
+ reason = "Connector low on threads: " + connectorThreads;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String getReason()
+ {
+ return reason;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Check if the ThreadPool from monitored connectors are lowOnThreads and if all connections number is higher than the allowed maxConnection";
+ }
+ }
+
+ @ManagedObject("Check max allowed connections on connectors")
+ public class MaxConnectionsLowResourceCheck implements LowResourceCheck
+ {
+ private String reason;
+ private int maxConnections;
+
+ public MaxConnectionsLowResourceCheck(int maxConnections)
+ {
+ this.maxConnections = maxConnections;
+ }
+
+ /**
+ * @return The maximum connections allowed for the monitored connectors before low resource handling is activated
+ * @deprecated Replaced by ConnectionLimit
+ */
+ @ManagedAttribute("The maximum connections allowed for the monitored connectors before low resource handling is activated")
+ @Deprecated
+ public int getMaxConnections()
+ {
+ return maxConnections;
+ }
+
+ /**
+ * @param maxConnections The maximum connections before low resources state is triggered
+ * @deprecated Replaced by ConnectionLimit
+ */
+ @Deprecated
+ public void setMaxConnections(int maxConnections)
+ {
+ if (maxConnections > 0)
+ LOG.warn("LowResourceMonitor.setMaxConnections is deprecated. Use ConnectionLimit.");
+ this.maxConnections = maxConnections;
+ }
+
+ @Override
+ public boolean isLowOnResources()
+ {
+ int connections = 0;
+ for (Connector connector : getMonitoredConnectors())
+ {
+ connections += connector.getConnectedEndPoints().size();
+ }
+ if (maxConnections > 0 && connections > maxConnections)
+ {
+ reason = "Max Connections exceeded: " + connections + ">" + maxConnections;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String getReason()
+ {
+ return reason;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "All connections number is higher than the allowed maxConnection";
+ }
+ }
+
+ public class MemoryLowResourceCheck implements LowResourceCheck
+ {
+ private String reason;
+ private long maxMemory;
+
+ public MemoryLowResourceCheck(long maxMemory)
+ {
+ this.maxMemory = maxMemory;
+ }
+
+ @Override
+ public boolean isLowOnResources()
+ {
+ long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
+ if (maxMemory > 0 && memory > maxMemory)
+ {
+ reason = "Max memory exceeded: " + memory + ">" + maxMemory;
+ return true;
+ }
+ return false;
+ }
+
+ public long getMaxMemory()
+ {
+ return maxMemory;
+ }
+
+ /**
+ * @param maxMemoryBytes The maximum memory in bytes in use before low resources is triggered.
+ */
+ public void setMaxMemory(long maxMemoryBytes)
+ {
+ this.maxMemory = maxMemoryBytes;
+ }
+
+ @Override
+ public String getReason()
+ {
+ return reason;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Check if used memory is higher than the allowed max memory";
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/MultiPartCleanerListener.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/MultiPartCleanerListener.java
new file mode 100644
index 0000000..90abef6
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/MultiPartCleanerListener.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import javax.servlet.ServletRequestEvent;
+import javax.servlet.ServletRequestListener;
+
+import org.eclipse.jetty.server.handler.ContextHandler;
+
+public class MultiPartCleanerListener implements ServletRequestListener
+{
+ public static final MultiPartCleanerListener INSTANCE = new MultiPartCleanerListener();
+
+ protected MultiPartCleanerListener()
+ {
+ }
+
+ @Override
+ public void requestDestroyed(ServletRequestEvent sre)
+ {
+ //Clean up any tmp files created by MultiPartInputStream
+ MultiParts parts = (MultiParts)sre.getServletRequest().getAttribute(Request.MULTIPARTS);
+ if (parts != null)
+ {
+ ContextHandler.Context context = parts.getContext();
+
+ //Only do the cleanup if we are exiting from the context in which a servlet parsed the multipart files
+ if (context == sre.getServletContext())
+ {
+ try
+ {
+ parts.close();
+ }
+ catch (Throwable e)
+ {
+ sre.getServletContext().log("Errors deleting multipart tmp files", e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void requestInitialized(ServletRequestEvent sre)
+ {
+ //nothing to do, multipart config set up by ServletHolder.handle()
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/MultiPartFormDataCompliance.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/MultiPartFormDataCompliance.java
new file mode 100644
index 0000000..e3f90b0
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/MultiPartFormDataCompliance.java
@@ -0,0 +1,39 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+/**
+ * The compliance level for parsing <code>multiPart/form-data</code>
+ */
+public enum MultiPartFormDataCompliance
+{
+ /**
+ * Legacy <code>multiPart/form-data</code> parsing which is slow but forgiving.
+ * It will accept non compliant preambles and inconsistent line termination.
+ *
+ * @see org.eclipse.jetty.util.MultiPartInputStreamParser
+ */
+ LEGACY,
+ /**
+ * RFC7578 compliant parsing that is a fast but strict parser.
+ *
+ * @see org.eclipse.jetty.http.MultiPartFormInputStream
+ */
+ RFC7578
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java
new file mode 100644
index 0000000..33c7f6d
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java
@@ -0,0 +1,159 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import javax.servlet.MultipartConfigElement;
+import javax.servlet.http.Part;
+
+import org.eclipse.jetty.http.HttpCompliance;
+import org.eclipse.jetty.http.MultiPartFormInputStream;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandler.Context;
+import org.eclipse.jetty.util.MultiPartInputStreamParser;
+import org.eclipse.jetty.util.MultiPartInputStreamParser.NonCompliance;
+
+/*
+ * Used to switch between the old and new implementation of MultiPart Form InputStream Parsing.
+ * The new implementation is preferred will be used as default unless specified otherwise constructor.
+ */
+public interface MultiParts extends Closeable
+{
+ Collection<Part> getParts() throws IOException;
+
+ Part getPart(String name) throws IOException;
+
+ boolean isEmpty();
+
+ ContextHandler.Context getContext();
+
+ class MultiPartsHttpParser implements MultiParts
+ {
+ private final MultiPartFormInputStream _httpParser;
+ private final ContextHandler.Context _context;
+
+ public MultiPartsHttpParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request) throws IOException
+ {
+ _httpParser = new MultiPartFormInputStream(in, contentType, config, contextTmpDir);
+ _context = request.getContext();
+ }
+
+ @Override
+ public Collection<Part> getParts() throws IOException
+ {
+ return _httpParser.getParts();
+ }
+
+ @Override
+ public Part getPart(String name) throws IOException
+ {
+ return _httpParser.getPart(name);
+ }
+
+ @Override
+ public void close()
+ {
+ _httpParser.deleteParts();
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return _httpParser.isEmpty();
+ }
+
+ @Override
+ public Context getContext()
+ {
+ return _context;
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ class MultiPartsUtilParser implements MultiParts
+ {
+ private final MultiPartInputStreamParser _utilParser;
+ private final ContextHandler.Context _context;
+ private final Request _request;
+
+ public MultiPartsUtilParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request) throws IOException
+ {
+ _utilParser = new MultiPartInputStreamParser(in, contentType, config, contextTmpDir);
+ _context = request.getContext();
+ _request = request;
+ }
+
+ @Override
+ public Collection<Part> getParts() throws IOException
+ {
+ Collection<Part> parts = _utilParser.getParts();
+ setNonComplianceViolationsOnRequest();
+ return parts;
+ }
+
+ @Override
+ public Part getPart(String name) throws IOException
+ {
+ Part part = _utilParser.getPart(name);
+ setNonComplianceViolationsOnRequest();
+ return part;
+ }
+
+ @Override
+ public void close()
+ {
+ _utilParser.deleteParts();
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return _utilParser.getParsedParts().isEmpty();
+ }
+
+ @Override
+ public Context getContext()
+ {
+ return _context;
+ }
+
+ private void setNonComplianceViolationsOnRequest()
+ {
+ @SuppressWarnings("unchecked")
+ List<String> violations = (List<String>)_request.getAttribute(HttpCompliance.VIOLATIONS_ATTR);
+ if (violations != null)
+ return;
+
+ EnumSet<NonCompliance> nonComplianceWarnings = _utilParser.getNonComplianceWarnings();
+ violations = new ArrayList<>();
+ for (NonCompliance nc : nonComplianceWarnings)
+ {
+ violations.add(nc.name() + ": " + nc.getURL());
+ }
+ _request.setAttribute(HttpCompliance.VIOLATIONS_ATTR, violations);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NCSARequestLog.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NCSARequestLog.java
new file mode 100644
index 0000000..9061ed2
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NCSARequestLog.java
@@ -0,0 +1,219 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.util.TimeZone;
+
+import org.eclipse.jetty.util.RolloverFileOutputStream;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+
+/**
+ * This {@link RequestLog} implementation outputs logs in the pseudo-standard
+ * NCSA common log format. Configuration options allow a choice between the
+ * standard Common Log Format (as used in the 3 log format) and the Combined Log
+ * Format (single log format). This log format can be output by most web
+ * servers, and almost all web log analysis software can understand these
+ * formats.
+ *
+ * @deprecated use {@link CustomRequestLog} given format string {@link CustomRequestLog#EXTENDED_NCSA_FORMAT} with a {@link RequestLogWriter}
+ */
+@Deprecated
+@ManagedObject("NCSA standard format request log")
+public class NCSARequestLog extends AbstractNCSARequestLog
+{
+ private final RequestLogWriter _requestLogWriter;
+
+ /**
+ * Create request log object with default settings.
+ */
+ public NCSARequestLog()
+ {
+ this((String)null);
+ }
+
+ /**
+ * Create request log object with specified output file name.
+ *
+ * @param filename the file name for the request log.
+ * This may be in the format expected
+ * by {@link RolloverFileOutputStream}
+ */
+ public NCSARequestLog(String filename)
+ {
+ this(new RequestLogWriter(filename));
+ }
+
+ /**
+ * Create request log object given a RequestLogWriter file name.
+ *
+ * @param writer the writer which manages the output of the formatted string
+ * produced by the {@link RequestLog}
+ */
+ public NCSARequestLog(RequestLogWriter writer)
+ {
+ super(writer);
+ _requestLogWriter = writer;
+ setExtended(true);
+ }
+
+ /**
+ * Set the output file name of the request log.
+ * The file name may be in the format expected by
+ * {@link RolloverFileOutputStream}.
+ *
+ * @param filename file name of the request log
+ */
+ public void setFilename(String filename)
+ {
+ _requestLogWriter.setFilename(filename);
+ }
+
+ @Override
+ public void setLogTimeZone(String tz)
+ {
+ super.setLogTimeZone(tz);
+ _requestLogWriter.setTimeZone(tz);
+ }
+
+ /**
+ * Retrieve the output file name of the request log.
+ *
+ * @return file name of the request log
+ */
+ @ManagedAttribute("file of log")
+ public String getFilename()
+ {
+ return _requestLogWriter.getFileName();
+ }
+
+ /**
+ * Retrieve the file name of the request log with the expanded
+ * date wildcard if the output is written to the disk using
+ * {@link RolloverFileOutputStream}.
+ *
+ * @return file name of the request log, or null if not applicable
+ */
+ public String getDatedFilename()
+ {
+ return _requestLogWriter.getDatedFilename();
+ }
+
+ @Override
+ protected boolean isEnabled()
+ {
+ return _requestLogWriter.isEnabled();
+ }
+
+ /**
+ * Set the number of days before rotated log files are deleted.
+ *
+ * @param retainDays number of days to keep a log file
+ */
+ public void setRetainDays(int retainDays)
+ {
+ _requestLogWriter.setRetainDays(retainDays);
+ }
+
+ /**
+ * Retrieve the number of days before rotated log files are deleted.
+ *
+ * @return number of days to keep a log file
+ */
+ @ManagedAttribute("number of days that log files are kept")
+ public int getRetainDays()
+ {
+ return _requestLogWriter.getRetainDays();
+ }
+
+ /**
+ * Set append to log flag.
+ *
+ * @param append true - request log file will be appended after restart,
+ * false - request log file will be overwritten after restart
+ */
+ public void setAppend(boolean append)
+ {
+ _requestLogWriter.setAppend(append);
+ }
+
+ /**
+ * Retrieve append to log flag.
+ *
+ * @return value of the flag
+ */
+ @ManagedAttribute("existing log files are appends to the new one")
+ public boolean isAppend()
+ {
+ return _requestLogWriter.isAppend();
+ }
+
+ /**
+ * Set the log file name date format.
+ *
+ * @param logFileDateFormat format string that is passed to {@link RolloverFileOutputStream}
+ * @see RolloverFileOutputStream#RolloverFileOutputStream(String, boolean, int, TimeZone, String, String)
+ */
+ public void setFilenameDateFormat(String logFileDateFormat)
+ {
+ _requestLogWriter.setFilenameDateFormat(logFileDateFormat);
+ }
+
+ /**
+ * Retrieve the file name date format string.
+ *
+ * @return the log File Date Format
+ */
+ public String getFilenameDateFormat()
+ {
+ return _requestLogWriter.getFilenameDateFormat();
+ }
+
+ @Override
+ public void write(String requestEntry) throws IOException
+ {
+ _requestLogWriter.write(requestEntry);
+ }
+
+ /**
+ * Set up request logging and open log file.
+ *
+ * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
+ */
+ @Override
+ protected synchronized void doStart() throws Exception
+ {
+ super.doStart();
+ }
+
+ /**
+ * Close the log file and perform cleanup.
+ *
+ * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
+ */
+ @Override
+ protected void doStop() throws Exception
+ {
+ synchronized (this)
+ {
+ super.doStop();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NegotiatingServerConnection.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NegotiatingServerConnection.java
new file mode 100644
index 0000000..545614d
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NegotiatingServerConnection.java
@@ -0,0 +1,169 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.util.List;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+
+import org.eclipse.jetty.io.AbstractConnection;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public abstract class NegotiatingServerConnection extends AbstractConnection
+{
+ private static final Logger LOG = Log.getLogger(NegotiatingServerConnection.class);
+
+ public interface CipherDiscriminator
+ {
+ boolean isAcceptable(String protocol, String tlsProtocol, String tlsCipher);
+ }
+
+ private final Connector connector;
+ private final SSLEngine engine;
+ private final List<String> protocols;
+ private final String defaultProtocol;
+ private String protocol; // No need to be volatile: it is modified and read by the same thread
+
+ protected NegotiatingServerConnection(Connector connector, EndPoint endPoint, SSLEngine engine, List<String> protocols, String defaultProtocol)
+ {
+ super(endPoint, connector.getExecutor());
+ this.connector = connector;
+ this.protocols = protocols;
+ this.defaultProtocol = defaultProtocol;
+ this.engine = engine;
+ }
+
+ public List<String> getProtocols()
+ {
+ return protocols;
+ }
+
+ public String getDefaultProtocol()
+ {
+ return defaultProtocol;
+ }
+
+ public Connector getConnector()
+ {
+ return connector;
+ }
+
+ public SSLEngine getSSLEngine()
+ {
+ return engine;
+ }
+
+ public String getProtocol()
+ {
+ return protocol;
+ }
+
+ protected void setProtocol(String protocol)
+ {
+ this.protocol = protocol;
+ }
+
+ @Override
+ public void onOpen()
+ {
+ super.onOpen();
+ fillInterested();
+ }
+
+ @Override
+ public void onFillable()
+ {
+ int filled = fill();
+
+ if (filled == 0)
+ {
+ if (protocol == null)
+ {
+ if (engine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING)
+ {
+ // Here the SSL handshake is finished, but the protocol has not been negotiated.
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} could not negotiate protocol, SSLEngine: {}", this, engine);
+ close();
+ }
+ else
+ {
+ // Here the SSL handshake is not finished yet but we filled 0 bytes,
+ // so we need to read more.
+ fillInterested();
+ }
+ }
+ else
+ {
+ ConnectionFactory connectionFactory = connector.getConnectionFactory(protocol);
+ if (connectionFactory == null)
+ {
+ LOG.info("{} application selected protocol '{}', but no correspondent {} has been configured",
+ this, protocol, ConnectionFactory.class.getName());
+ close();
+ }
+ else
+ {
+ EndPoint endPoint = getEndPoint();
+ Connection newConnection = connectionFactory.newConnection(connector, endPoint);
+ endPoint.upgrade(newConnection);
+ }
+ }
+ }
+ else if (filled < 0)
+ {
+ // Something went bad, we need to close.
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} detected close on client side", this);
+ close();
+ }
+ else
+ {
+ // Must never happen, since we fill using an empty buffer
+ throw new IllegalStateException();
+ }
+ }
+
+ private int fill()
+ {
+ try
+ {
+ return getEndPoint().fill(BufferUtil.EMPTY_BUFFER);
+ }
+ catch (IOException x)
+ {
+ LOG.debug(x);
+ close();
+ return -1;
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ // Gentler close for SSL.
+ getEndPoint().shutdownOutput();
+ super.close();
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NegotiatingServerConnectionFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NegotiatingServerConnectionFactory.java
new file mode 100644
index 0000000..9faa681
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NegotiatingServerConnectionFactory.java
@@ -0,0 +1,118 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.net.ssl.SSLEngine;
+
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.io.AbstractConnection;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.ssl.SslConnection;
+
+public abstract class NegotiatingServerConnectionFactory extends AbstractConnectionFactory
+{
+ private final List<String> negotiatedProtocols;
+ private String defaultProtocol;
+
+ public NegotiatingServerConnectionFactory(String protocol, String... negotiatedProtocols)
+ {
+ super(protocol);
+ this.negotiatedProtocols = new ArrayList<>();
+ if (negotiatedProtocols != null)
+ {
+ // Trim the values, as they may come from XML configuration.
+ for (String p : negotiatedProtocols)
+ {
+ p = p.trim();
+ if (!p.isEmpty())
+ this.negotiatedProtocols.add(p);
+ }
+ }
+ }
+
+ public String getDefaultProtocol()
+ {
+ return defaultProtocol;
+ }
+
+ public void setDefaultProtocol(String defaultProtocol)
+ {
+ // Trim the value, as it may come from XML configuration.
+ String dft = defaultProtocol == null ? "" : defaultProtocol.trim();
+ this.defaultProtocol = dft.isEmpty() ? null : dft;
+ }
+
+ public List<String> getNegotiatedProtocols()
+ {
+ return negotiatedProtocols;
+ }
+
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endPoint)
+ {
+ List<String> negotiated = this.negotiatedProtocols;
+ if (negotiated.isEmpty())
+ {
+ // Generate list of protocols that we can negotiate.
+ negotiated = connector.getProtocols().stream()
+ .filter(p ->
+ {
+ ConnectionFactory f = connector.getConnectionFactory(p);
+ return !(f instanceof SslConnectionFactory) && !(f instanceof NegotiatingServerConnectionFactory);
+ })
+ .collect(Collectors.toList());
+ }
+
+ // If default protocol is not set, then it is
+ // either HTTP/1.1 or the first protocol given.
+ String dft = defaultProtocol;
+ if (dft == null && !negotiated.isEmpty())
+ {
+ dft = negotiated.stream()
+ .filter(HttpVersion.HTTP_1_1::is)
+ .findFirst()
+ .orElse(negotiated.get(0));
+ }
+
+ SSLEngine engine = null;
+ EndPoint ep = endPoint;
+ while (engine == null && ep != null)
+ {
+ // TODO make more generic
+ if (ep instanceof SslConnection.DecryptedEndPoint)
+ engine = ((SslConnection.DecryptedEndPoint)ep).getSslConnection().getSSLEngine();
+ else
+ ep = null;
+ }
+
+ return configure(newServerConnection(connector, endPoint, engine, negotiated, dft), connector, endPoint);
+ }
+
+ protected abstract AbstractConnection newServerConnection(Connector connector, EndPoint endPoint, SSLEngine engine, List<String> protocols, String defaultProtocol);
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{%s,%s,%s}", getClass().getSimpleName(), hashCode(), getProtocols(), getDefaultProtocol(), getNegotiatedProtocols());
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkConnector.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkConnector.java
new file mode 100644
index 0000000..60f7c46
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkConnector.java
@@ -0,0 +1,72 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * <p>A {@link Connector} for TCP/IP network connectors</p>
+ */
+public interface NetworkConnector extends Connector, Closeable
+{
+ /**
+ * <p>Performs the activities needed to open the network communication
+ * (for example, to start accepting incoming network connections).</p>
+ *
+ * @throws IOException if this connector cannot be opened
+ * @see #close()
+ */
+ void open() throws IOException;
+
+ /**
+ * <p>Performs the activities needed to close the network communication
+ * (for example, to stop accepting network connections).</p>
+ * Once a connector has been closed, it cannot be opened again without first
+ * calling {@link #stop()} and it will not be active again until a subsequent call to {@link #start()}
+ */
+ @Override
+ void close();
+
+ /**
+ * A Connector may be opened and not started (to reserve a port)
+ * or closed and running (to allow graceful shutdown of existing connections)
+ *
+ * @return True if the connector is Open.
+ */
+ boolean isOpen();
+
+ /**
+ * @return The hostname representing the interface to which
+ * this connector will bind, or null for all interfaces.
+ */
+ String getHost();
+
+ /**
+ * @return The configured port for the connector or 0 if any available
+ * port may be used.
+ */
+ int getPort();
+
+ /**
+ * @return The actual port the connector is listening on, or
+ * -1 if it has not been opened, or -2 if it has been closed.
+ */
+ int getLocalPort();
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java
new file mode 100644
index 0000000..53de36f
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java
@@ -0,0 +1,90 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.ChannelEndPoint;
+import org.eclipse.jetty.io.ManagedSelector;
+import org.eclipse.jetty.io.NetworkTrafficListener;
+import org.eclipse.jetty.io.NetworkTrafficSocketChannelEndPoint;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * <p>A specialized version of {@link ServerConnector} that supports {@link NetworkTrafficListener}s.</p>
+ * <p>{@link NetworkTrafficListener}s can be added and removed dynamically before and after this connector has
+ * been started without causing {@link java.util.ConcurrentModificationException}s.</p>
+ */
+public class NetworkTrafficServerConnector extends ServerConnector
+{
+ private final List<NetworkTrafficListener> listeners = new CopyOnWriteArrayList<>();
+
+ public NetworkTrafficServerConnector(Server server)
+ {
+ this(server, null, null, null, 0, 0, new HttpConnectionFactory());
+ }
+
+ public NetworkTrafficServerConnector(Server server, ConnectionFactory connectionFactory, SslContextFactory sslContextFactory)
+ {
+ super(server, sslContextFactory, connectionFactory);
+ }
+
+ public NetworkTrafficServerConnector(Server server, ConnectionFactory connectionFactory)
+ {
+ super(server, connectionFactory);
+ }
+
+ public NetworkTrafficServerConnector(Server server, Executor executor, Scheduler scheduler, ByteBufferPool pool, int acceptors, int selectors, ConnectionFactory... factories)
+ {
+ super(server, executor, scheduler, pool, acceptors, selectors, factories);
+ }
+
+ public NetworkTrafficServerConnector(Server server, SslContextFactory sslContextFactory)
+ {
+ super(server, sslContextFactory);
+ }
+
+ /**
+ * @param listener the listener to add
+ */
+ public void addNetworkTrafficListener(NetworkTrafficListener listener)
+ {
+ listeners.add(listener);
+ }
+
+ /**
+ * @param listener the listener to remove
+ */
+ public void removeNetworkTrafficListener(NetworkTrafficListener listener)
+ {
+ listeners.remove(listener);
+ }
+
+ @Override
+ protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key)
+ {
+ return new NetworkTrafficSocketChannelEndPoint(channel, selectSet, key, getScheduler(), getIdleTimeout(), listeners);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java
new file mode 100644
index 0000000..1f91532
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java
@@ -0,0 +1,129 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * <p>A ConnectionFactory whose connections detect whether the first bytes are
+ * TLS bytes and upgrades to either a TLS connection or to another configurable
+ * connection.</p>
+ *
+ * @deprecated Use {@link DetectorConnectionFactory} with a {@link SslConnectionFactory} instead.
+ */
+@Deprecated
+public class OptionalSslConnectionFactory extends DetectorConnectionFactory
+{
+ private static final Logger LOG = Log.getLogger(OptionalSslConnectionFactory.class);
+ private final String _nextProtocol;
+
+ /**
+ * <p>Creates a new ConnectionFactory whose connections can upgrade to TLS or another protocol.</p>
+ *
+ * @param sslConnectionFactory The {@link SslConnectionFactory} to use if the first bytes are TLS
+ * @param nextProtocol the protocol of the {@link ConnectionFactory} to use if the first bytes are not TLS,
+ * or null to explicitly handle the non-TLS case
+ */
+ public OptionalSslConnectionFactory(SslConnectionFactory sslConnectionFactory, String nextProtocol)
+ {
+ super(sslConnectionFactory);
+ _nextProtocol = nextProtocol;
+ }
+
+ /**
+ * <p>Callback method invoked when the detected bytes are not TLS.</p>
+ * <p>This typically happens when a client is trying to connect to a TLS
+ * port using the {@code http} scheme (and not the {@code https} scheme).</p>
+ *
+ * @param connector The connector object
+ * @param endPoint The connection EndPoint object
+ * @param buffer The buffer with the first bytes of the connection
+ */
+ protected void nextProtocol(Connector connector, EndPoint endPoint, ByteBuffer buffer)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("OptionalSSL TLS detection unsuccessful, attempting to upgrade to {}", _nextProtocol);
+ if (_nextProtocol != null)
+ {
+ ConnectionFactory connectionFactory = connector.getConnectionFactory(_nextProtocol);
+ if (connectionFactory == null)
+ throw new IllegalStateException("Cannot find protocol '" + _nextProtocol + "' in connector's protocol list " + connector.getProtocols() + " for " + endPoint);
+ upgradeToConnectionFactory(connectionFactory, connector, endPoint);
+ }
+ else
+ {
+ otherProtocol(buffer, endPoint);
+ }
+ }
+
+ /**
+ * <p>Legacy callback method invoked when {@code nextProtocol} is {@code null}
+ * and the first bytes are not TLS.</p>
+ * <p>This typically happens when a client is trying to connect to a TLS
+ * port using the {@code http} scheme (and not the {@code https} scheme).</p>
+ * <p>This method is kept around for backward compatibility.</p>
+ *
+ * @param buffer The buffer with the first bytes of the connection
+ * @param endPoint The connection EndPoint object
+ * @deprecated Override {@link #nextProtocol(Connector, EndPoint, ByteBuffer)} instead.
+ */
+ @Deprecated
+ protected void otherProtocol(ByteBuffer buffer, EndPoint endPoint)
+ {
+ LOG.warn("Detected non-TLS bytes, but no other protocol to upgrade to for {}", endPoint);
+
+ // There are always at least 2 bytes.
+ int byte1 = buffer.get(0) & 0xFF;
+ int byte2 = buffer.get(1) & 0xFF;
+ if (byte1 == 'G' && byte2 == 'E')
+ {
+ // Plain text HTTP to an HTTPS port,
+ // write a minimal response.
+ String body =
+ "<!DOCTYPE html>\r\n" +
+ "<html>\r\n" +
+ "<head><title>Bad Request</title></head>\r\n" +
+ "<body>" +
+ "<h1>Bad Request</h1>" +
+ "<p>HTTP request to HTTPS port</p>" +
+ "</body>\r\n" +
+ "</html>";
+ String response =
+ "HTTP/1.1 400 Bad Request\r\n" +
+ "Content-Type: text/html\r\n" +
+ "Content-Length: " + body.length() + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ body;
+ Callback.Completable completable = new Callback.Completable();
+ endPoint.write(completable, ByteBuffer.wrap(response.getBytes(StandardCharsets.US_ASCII)));
+ completable.whenComplete((r, x) -> endPoint.close());
+ }
+ else
+ {
+ endPoint.close();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java
new file mode 100644
index 0000000..71cd03c
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java
@@ -0,0 +1,939 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadPendingException;
+import java.nio.channels.WritePendingException;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jetty.io.AbstractConnection;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.AttributesMap;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * <p>ConnectionFactory for the PROXY Protocol.</p>
+ * <p>This factory can be placed in front of any other connection factory
+ * to process the proxy v1 or v2 line before the normal protocol handling</p>
+ *
+ * @see <a href="http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt">http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt</a>
+ */
+public class ProxyConnectionFactory extends DetectorConnectionFactory
+{
+ public static final String TLS_VERSION = "TLS_VERSION";
+ private static final Logger LOG = Log.getLogger(ProxyConnectionFactory.class);
+
+ public ProxyConnectionFactory()
+ {
+ this(null);
+ }
+
+ public ProxyConnectionFactory(String nextProtocol)
+ {
+ super(new ProxyV1ConnectionFactory(nextProtocol), new ProxyV2ConnectionFactory(nextProtocol));
+ }
+
+ private static ConnectionFactory findNextConnectionFactory(String nextProtocol, Connector connector, String currentProtocol, EndPoint endp)
+ {
+ currentProtocol = "[" + currentProtocol + "]";
+ if (LOG.isDebugEnabled())
+ LOG.debug("finding connection factory following {} for protocol {}", currentProtocol, nextProtocol);
+ String nextProtocolToFind = nextProtocol;
+ if (nextProtocol == null)
+ nextProtocolToFind = AbstractConnectionFactory.findNextProtocol(connector, currentProtocol);
+ if (nextProtocolToFind == null)
+ throw new IllegalStateException("Cannot find protocol following '" + currentProtocol + "' in connector's protocol list " + connector.getProtocols() + " for " + endp);
+ ConnectionFactory connectionFactory = connector.getConnectionFactory(nextProtocolToFind);
+ if (connectionFactory == null)
+ throw new IllegalStateException("Cannot find protocol '" + nextProtocol + "' in connector's protocol list " + connector.getProtocols() + " for " + endp);
+ if (LOG.isDebugEnabled())
+ LOG.debug("found next connection factory {} for protocol {}", connectionFactory, nextProtocol);
+ return connectionFactory;
+ }
+
+ public int getMaxProxyHeader()
+ {
+ ProxyV2ConnectionFactory v2 = getBean(ProxyV2ConnectionFactory.class);
+ return v2.getMaxProxyHeader();
+ }
+
+ public void setMaxProxyHeader(int maxProxyHeader)
+ {
+ ProxyV2ConnectionFactory v2 = getBean(ProxyV2ConnectionFactory.class);
+ v2.setMaxProxyHeader(maxProxyHeader);
+ }
+
+ private static class ProxyV1ConnectionFactory extends AbstractConnectionFactory implements Detecting
+ {
+ private static final byte[] SIGNATURE = "PROXY".getBytes(StandardCharsets.US_ASCII);
+
+ private final String _nextProtocol;
+
+ private ProxyV1ConnectionFactory(String nextProtocol)
+ {
+ super("proxy");
+ this._nextProtocol = nextProtocol;
+ }
+
+ @Override
+ public Detection detect(ByteBuffer buffer)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 attempting detection with {} bytes", buffer.remaining());
+ if (buffer.remaining() < SIGNATURE.length)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 detection requires more bytes");
+ return Detection.NEED_MORE_BYTES;
+ }
+
+ for (int i = 0; i < SIGNATURE.length; i++)
+ {
+ byte signatureByte = SIGNATURE[i];
+ byte byteInBuffer = buffer.get(i);
+ if (byteInBuffer != signatureByte)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 detection unsuccessful");
+ return Detection.NOT_RECOGNIZED;
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 detection succeeded");
+ return Detection.RECOGNIZED;
+ }
+
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endp)
+ {
+ ConnectionFactory nextConnectionFactory = findNextConnectionFactory(_nextProtocol, connector, getProtocol(), endp);
+ return configure(new ProxyProtocolV1Connection(endp, connector, nextConnectionFactory), connector, endp);
+ }
+
+ private static class ProxyProtocolV1Connection extends AbstractConnection implements Connection.UpgradeFrom, Connection.UpgradeTo
+ {
+ // 0 1 2 3 4 5 6
+ // 98765432109876543210987654321
+ // PROXY P R.R.R.R L.L.L.L R Lrn
+ private static final int CR_INDEX = 6;
+ private static final int LF_INDEX = 7;
+
+ private final Connector _connector;
+ private final ConnectionFactory _next;
+ private final ByteBuffer _buffer;
+ private final StringBuilder _builder = new StringBuilder();
+ private final String[] _fields = new String[6];
+ private int _index;
+ private int _length;
+
+ private ProxyProtocolV1Connection(EndPoint endp, Connector connector, ConnectionFactory next)
+ {
+ super(endp, connector.getExecutor());
+ _connector = connector;
+ _next = next;
+ _buffer = _connector.getByteBufferPool().acquire(getInputBufferSize(), true);
+ }
+
+ @Override
+ public void onFillable()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 onFillable current index = ", _index);
+ try
+ {
+ while (_index < LF_INDEX)
+ {
+ // Read data
+ int fill = getEndPoint().fill(_buffer);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 filled buffer with {} bytes", fill);
+ if (fill < 0)
+ {
+ _connector.getByteBufferPool().release(_buffer);
+ getEndPoint().shutdownOutput();
+ return;
+ }
+ if (fill == 0)
+ {
+ fillInterested();
+ return;
+ }
+
+ if (parse())
+ break;
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 onFillable parsing done, now upgrading");
+ upgrade();
+ }
+ catch (Throwable x)
+ {
+ LOG.warn("Proxy v1 error for {}", getEndPoint(), x);
+ releaseAndClose();
+ }
+ }
+
+ @Override
+ public void onOpen()
+ {
+ super.onOpen();
+
+ try
+ {
+ while (_index < LF_INDEX)
+ {
+ if (!parse())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 onOpen parsing ran out of bytes, marking as fillInterested");
+ fillInterested();
+ return;
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 onOpen parsing done, now upgrading");
+ upgrade();
+ }
+ catch (Throwable x)
+ {
+ LOG.warn("Proxy v1 error for {}", getEndPoint(), x);
+ releaseAndClose();
+ }
+ }
+
+ @Override
+ public ByteBuffer onUpgradeFrom()
+ {
+ if (_buffer.hasRemaining())
+ {
+ ByteBuffer unconsumed = ByteBuffer.allocateDirect(_buffer.remaining());
+ unconsumed.put(_buffer);
+ unconsumed.flip();
+ _connector.getByteBufferPool().release(_buffer);
+ return unconsumed;
+ }
+ return null;
+ }
+
+ @Override
+ public void onUpgradeTo(ByteBuffer buffer)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 copying unconsumed buffer {}", BufferUtil.toDetailString(buffer));
+ BufferUtil.append(_buffer, buffer);
+ }
+
+ /**
+ * @return true when parsing is done, false when more bytes are needed.
+ */
+ private boolean parse() throws IOException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 parsing {}", BufferUtil.toDetailString(_buffer));
+ _length += _buffer.remaining();
+
+ // Parse fields
+ while (_buffer.hasRemaining())
+ {
+ byte b = _buffer.get();
+ if (_index < CR_INDEX)
+ {
+ if (b == ' ' || b == '\r')
+ {
+ _fields[_index++] = _builder.toString();
+ _builder.setLength(0);
+ if (b == '\r')
+ _index = CR_INDEX;
+ }
+ else if (b < ' ')
+ {
+ throw new IOException("Proxy v1 bad character " + (b & 0xFF));
+ }
+ else
+ {
+ _builder.append((char)b);
+ }
+ }
+ else
+ {
+ if (b == '\n')
+ {
+ _index = LF_INDEX;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 parsing is done");
+ return true;
+ }
+
+ throw new IOException("Proxy v1 bad CRLF " + (b & 0xFF));
+ }
+ }
+
+ // Not enough bytes.
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 parsing requires more bytes");
+ return false;
+ }
+
+ private void releaseAndClose()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 releasing buffer and closing");
+ _connector.getByteBufferPool().release(_buffer);
+ close();
+ }
+
+ private void upgrade()
+ {
+ int proxyLineLength = _length - _buffer.remaining();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 pre-upgrade packet length (including CRLF) is {}", proxyLineLength);
+ if (proxyLineLength >= 110)
+ {
+ LOG.warn("Proxy v1 PROXY line too long {} for {}", proxyLineLength, getEndPoint());
+ releaseAndClose();
+ return;
+ }
+
+ // Check proxy
+ if (!"PROXY".equals(_fields[0]))
+ {
+ LOG.warn("Proxy v1 not PROXY protocol for {}", getEndPoint());
+ releaseAndClose();
+ return;
+ }
+
+ String srcIP = _fields[2];
+ String srcPort = _fields[4];
+ String dstIP = _fields[3];
+ String dstPort = _fields[5];
+ // If UNKNOWN, we must ignore the information sent, so use the EndPoint's.
+ boolean unknown = "UNKNOWN".equalsIgnoreCase(_fields[1]);
+ if (unknown)
+ {
+ srcIP = getEndPoint().getRemoteAddress().getAddress().getHostAddress();
+ srcPort = String.valueOf(getEndPoint().getRemoteAddress().getPort());
+ dstIP = getEndPoint().getLocalAddress().getAddress().getHostAddress();
+ dstPort = String.valueOf(getEndPoint().getLocalAddress().getPort());
+ }
+ InetSocketAddress remote = new InetSocketAddress(srcIP, Integer.parseInt(srcPort));
+ InetSocketAddress local = new InetSocketAddress(dstIP, Integer.parseInt(dstPort));
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v1 next protocol '{}' for {} r={} l={}", _next, getEndPoint(), remote, local);
+
+ EndPoint endPoint = new ProxyEndPoint(getEndPoint(), remote, local);
+ upgradeToConnectionFactory(_next, _connector, endPoint);
+ }
+ }
+ }
+
+ private static class ProxyV2ConnectionFactory extends AbstractConnectionFactory implements Detecting
+ {
+ private enum Family
+ {
+ UNSPEC, INET, INET6, UNIX
+ }
+
+ private enum Transport
+ {
+ UNSPEC, STREAM, DGRAM
+ }
+
+ private static final byte[] SIGNATURE = new byte[]
+ {
+ 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A
+ };
+ private final String _nextProtocol;
+ private int _maxProxyHeader = 1024;
+
+ private ProxyV2ConnectionFactory(String nextProtocol)
+ {
+ super("proxy");
+ this._nextProtocol = nextProtocol;
+ }
+
+ @Override
+ public Detection detect(ByteBuffer buffer)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 attempting detection with {} bytes", buffer.remaining());
+ if (buffer.remaining() < SIGNATURE.length)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 detection requires more bytes");
+ return Detection.NEED_MORE_BYTES;
+ }
+
+ for (int i = 0; i < SIGNATURE.length; i++)
+ {
+ byte signatureByte = SIGNATURE[i];
+ byte byteInBuffer = buffer.get(i);
+ if (byteInBuffer != signatureByte)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 detection unsuccessful");
+ return Detection.NOT_RECOGNIZED;
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 detection succeeded");
+ return Detection.RECOGNIZED;
+ }
+
+ public int getMaxProxyHeader()
+ {
+ return _maxProxyHeader;
+ }
+
+ public void setMaxProxyHeader(int maxProxyHeader)
+ {
+ _maxProxyHeader = maxProxyHeader;
+ }
+
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endp)
+ {
+ ConnectionFactory nextConnectionFactory = findNextConnectionFactory(_nextProtocol, connector, getProtocol(), endp);
+ return configure(new ProxyProtocolV2Connection(endp, connector, nextConnectionFactory), connector, endp);
+ }
+
+ private class ProxyProtocolV2Connection extends AbstractConnection implements Connection.UpgradeFrom, Connection.UpgradeTo
+ {
+ private static final int HEADER_LENGTH = 16;
+
+ private final Connector _connector;
+ private final ConnectionFactory _next;
+ private final ByteBuffer _buffer;
+ private boolean _local;
+ private Family _family;
+ private int _length;
+ private boolean _headerParsed;
+
+ protected ProxyProtocolV2Connection(EndPoint endp, Connector connector, ConnectionFactory next)
+ {
+ super(endp, connector.getExecutor());
+ _connector = connector;
+ _next = next;
+ _buffer = _connector.getByteBufferPool().acquire(getInputBufferSize(), true);
+ }
+
+ @Override
+ public void onUpgradeTo(ByteBuffer buffer)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 copying unconsumed buffer {}", BufferUtil.toDetailString(buffer));
+ BufferUtil.append(_buffer, buffer);
+ }
+
+ @Override
+ public void onOpen()
+ {
+ super.onOpen();
+
+ try
+ {
+ parseHeader();
+ if (_headerParsed && _buffer.remaining() >= _length)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 onOpen parsing fixed length packet part done, now upgrading");
+ parseBodyAndUpgrade();
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 onOpen parsing fixed length packet ran out of bytes, marking as fillInterested");
+ fillInterested();
+ }
+ }
+ catch (Exception x)
+ {
+ LOG.warn("Proxy v2 error for {}", getEndPoint(), x);
+ releaseAndClose();
+ }
+ }
+
+ @Override
+ public void onFillable()
+ {
+ try
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 onFillable header parsed? ", _headerParsed);
+ while (!_headerParsed)
+ {
+ // Read data
+ int fill = getEndPoint().fill(_buffer);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 filled buffer with {} bytes", fill);
+ if (fill < 0)
+ {
+ _connector.getByteBufferPool().release(_buffer);
+ getEndPoint().shutdownOutput();
+ return;
+ }
+ if (fill == 0)
+ {
+ fillInterested();
+ return;
+ }
+
+ parseHeader();
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 onFillable header parsed, length = {}, buffer = {}", _length, BufferUtil.toDetailString(_buffer));
+
+ while (_buffer.remaining() < _length)
+ {
+ // Read data
+ int fill = getEndPoint().fill(_buffer);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 filled buffer with {} bytes", fill);
+ if (fill < 0)
+ {
+ _connector.getByteBufferPool().release(_buffer);
+ getEndPoint().shutdownOutput();
+ return;
+ }
+ if (fill == 0)
+ {
+ fillInterested();
+ return;
+ }
+ }
+
+ parseBodyAndUpgrade();
+ }
+ catch (Throwable x)
+ {
+ LOG.warn("Proxy v2 error for " + getEndPoint(), x);
+ releaseAndClose();
+ }
+ }
+
+ @Override
+ public ByteBuffer onUpgradeFrom()
+ {
+ if (_buffer.hasRemaining())
+ {
+ ByteBuffer unconsumed = ByteBuffer.allocateDirect(_buffer.remaining());
+ unconsumed.put(_buffer);
+ unconsumed.flip();
+ _connector.getByteBufferPool().release(_buffer);
+ return unconsumed;
+ }
+ return null;
+ }
+
+ private void parseBodyAndUpgrade() throws IOException
+ {
+ // stop reading when bufferRemainingReserve bytes are remaining in the buffer
+ int nonProxyRemaining = _buffer.remaining() - _length;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 parsing body, length = {}, buffer = {}", _length, BufferUtil.toHexSummary(_buffer));
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 body {} from {} for {}", _next, BufferUtil.toHexSummary(_buffer), this);
+
+ // Do we need to wrap the endpoint?
+ EndPoint endPoint = getEndPoint();
+ if (!_local)
+ {
+ InetAddress src;
+ InetAddress dst;
+ int sp;
+ int dp;
+
+ switch (_family)
+ {
+ case INET:
+ {
+ byte[] addr = new byte[4];
+ _buffer.get(addr);
+ src = Inet4Address.getByAddress(addr);
+ _buffer.get(addr);
+ dst = Inet4Address.getByAddress(addr);
+ sp = _buffer.getChar();
+ dp = _buffer.getChar();
+ break;
+ }
+
+ case INET6:
+ {
+ byte[] addr = new byte[16];
+ _buffer.get(addr);
+ src = Inet6Address.getByAddress(addr);
+ _buffer.get(addr);
+ dst = Inet6Address.getByAddress(addr);
+ sp = _buffer.getChar();
+ dp = _buffer.getChar();
+ break;
+ }
+
+ default:
+ throw new IllegalStateException();
+ }
+
+ // Extract Addresses
+ InetSocketAddress remote = new InetSocketAddress(src, sp);
+ InetSocketAddress local = new InetSocketAddress(dst, dp);
+ ProxyEndPoint proxyEndPoint = new ProxyEndPoint(endPoint, remote, local);
+ endPoint = proxyEndPoint;
+
+ // Any additional info?
+ while (_buffer.remaining() > nonProxyRemaining)
+ {
+ int type = 0xff & _buffer.get();
+ int length = _buffer.getChar();
+ byte[] value = new byte[length];
+ _buffer.get(value);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug(String.format("Proxy v2 T=%x L=%d V=%s for %s", type, length, TypeUtil.toHexString(value), this));
+
+ switch (type)
+ {
+ case 0x20: // PP2_TYPE_SSL
+ {
+ int client = value[0] & 0xFF;
+ switch (client)
+ {
+ case 0x01: // PP2_CLIENT_SSL
+ {
+ int i = 5; // Index of the first sub_tlv, after verify.
+ while (i < length)
+ {
+ int subType = value[i++] & 0xFF;
+ int subLength = (value[i++] & 0xFF) * 256 + (value[i++] & 0xFF);
+ byte[] subValue = new byte[subLength];
+ System.arraycopy(value, i, subValue, 0, subLength);
+ i += subLength;
+ switch (subType)
+ {
+ case 0x21: // PP2_SUBTYPE_SSL_VERSION
+ String tlsVersion = new String(subValue, StandardCharsets.US_ASCII);
+ proxyEndPoint.setAttribute(TLS_VERSION, tlsVersion);
+ break;
+ case 0x22: // PP2_SUBTYPE_SSL_CN
+ case 0x23: // PP2_SUBTYPE_SSL_CIPHER
+ case 0x24: // PP2_SUBTYPE_SSL_SIG_ALG
+ case 0x25: // PP2_SUBTYPE_SSL_KEY_ALG
+ default:
+ break;
+ }
+ }
+ break;
+ }
+ case 0x02: // PP2_CLIENT_CERT_CONN
+ case 0x04: // PP2_CLIENT_CERT_SESS
+ default:
+ break;
+ }
+ break;
+ }
+ case 0x01: // PP2_TYPE_ALPN
+ case 0x02: // PP2_TYPE_AUTHORITY
+ case 0x03: // PP2_TYPE_CRC32C
+ case 0x04: // PP2_TYPE_NOOP
+ case 0x30: // PP2_TYPE_NETNS
+ default:
+ break;
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 {} {}", getEndPoint(), proxyEndPoint.toString());
+ }
+ else
+ {
+ _buffer.position(_buffer.position() + _length);
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 parsing dynamic packet part is now done, upgrading to {}", _nextProtocol);
+ upgradeToConnectionFactory(_next, _connector, endPoint);
+ }
+
+ private void parseHeader() throws IOException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 parsing fixed length packet part, buffer = {}", BufferUtil.toDetailString(_buffer));
+ if (_buffer.remaining() < HEADER_LENGTH)
+ return;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 header {} for {}", BufferUtil.toHexSummary(_buffer), this);
+
+ // struct proxy_hdr_v2 {
+ // uint8_t sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
+ // uint8_t ver_cmd; /* protocol version and command */
+ // uint8_t fam; /* protocol family and address */
+ // uint16_t len; /* number of following bytes part of the header */
+ // };
+ for (byte signatureByte : SIGNATURE)
+ {
+ if (_buffer.get() != signatureByte)
+ throw new IOException("Proxy v2 bad PROXY signature");
+ }
+
+ int versionAndCommand = 0xFF & _buffer.get();
+ if ((versionAndCommand & 0xF0) != 0x20)
+ throw new IOException("Proxy v2 bad PROXY version");
+ _local = (versionAndCommand & 0xF) == 0x00;
+
+ int transportAndFamily = 0xFF & _buffer.get();
+ switch (transportAndFamily >> 4)
+ {
+ case 0:
+ _family = Family.UNSPEC;
+ break;
+ case 1:
+ _family = Family.INET;
+ break;
+ case 2:
+ _family = Family.INET6;
+ break;
+ case 3:
+ _family = Family.UNIX;
+ break;
+ default:
+ throw new IOException("Proxy v2 bad PROXY family");
+ }
+
+ Transport transport;
+ switch (0xF & transportAndFamily)
+ {
+ case 0:
+ transport = Transport.UNSPEC;
+ break;
+ case 1:
+ transport = Transport.STREAM;
+ break;
+ case 2:
+ transport = Transport.DGRAM;
+ break;
+ default:
+ throw new IOException("Proxy v2 bad PROXY family");
+ }
+
+ _length = _buffer.getChar();
+
+ if (!_local && (_family == Family.UNSPEC || _family == Family.UNIX || transport != Transport.STREAM))
+ throw new IOException(String.format("Proxy v2 unsupported PROXY mode 0x%x,0x%x", versionAndCommand, transportAndFamily));
+
+ if (_length > getMaxProxyHeader())
+ throw new IOException(String.format("Proxy v2 Unsupported PROXY mode 0x%x,0x%x,0x%x", versionAndCommand, transportAndFamily, _length));
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Proxy v2 fixed length packet part is now parsed");
+ _headerParsed = true;
+ }
+
+ private void releaseAndClose()
+ {
+ _connector.getByteBufferPool().release(_buffer);
+ close();
+ }
+ }
+ }
+
+ public static class ProxyEndPoint extends AttributesMap implements EndPoint
+ {
+ private final EndPoint _endp;
+ private final InetSocketAddress _remote;
+ private final InetSocketAddress _local;
+
+ public ProxyEndPoint(EndPoint endp, InetSocketAddress remote, InetSocketAddress local)
+ {
+ _endp = endp;
+ _remote = remote;
+ _local = local;
+ }
+
+ public EndPoint unwrap()
+ {
+ return _endp;
+ }
+
+ @Override
+ public void close()
+ {
+ _endp.close();
+ }
+
+ @Override
+ public int fill(ByteBuffer buffer) throws IOException
+ {
+ return _endp.fill(buffer);
+ }
+
+ @Override
+ public void fillInterested(Callback callback) throws ReadPendingException
+ {
+ _endp.fillInterested(callback);
+ }
+
+ @Override
+ public boolean flush(ByteBuffer... buffer) throws IOException
+ {
+ return _endp.flush(buffer);
+ }
+
+ @Override
+ public Connection getConnection()
+ {
+ return _endp.getConnection();
+ }
+
+ @Override
+ public void setConnection(Connection connection)
+ {
+ _endp.setConnection(connection);
+ }
+
+ @Override
+ public long getCreatedTimeStamp()
+ {
+ return _endp.getCreatedTimeStamp();
+ }
+
+ @Override
+ public long getIdleTimeout()
+ {
+ return _endp.getIdleTimeout();
+ }
+
+ @Override
+ public void setIdleTimeout(long idleTimeout)
+ {
+ _endp.setIdleTimeout(idleTimeout);
+ }
+
+ @Override
+ public InetSocketAddress getLocalAddress()
+ {
+ return _local;
+ }
+
+ @Override
+ public InetSocketAddress getRemoteAddress()
+ {
+ return _remote;
+ }
+
+ @Override
+ public Object getTransport()
+ {
+ return _endp.getTransport();
+ }
+
+ @Override
+ public boolean isFillInterested()
+ {
+ return _endp.isFillInterested();
+ }
+
+ @Override
+ public boolean isInputShutdown()
+ {
+ return _endp.isInputShutdown();
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ return _endp.isOpen();
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return _endp.isOptimizedForDirectBuffers();
+ }
+
+ @Override
+ public boolean isOutputShutdown()
+ {
+ return _endp.isOutputShutdown();
+ }
+
+ @Override
+ public void onClose()
+ {
+ _endp.onClose();
+ }
+
+ @Override
+ public void onOpen()
+ {
+ _endp.onOpen();
+ }
+
+ @Override
+ public void shutdownOutput()
+ {
+ _endp.shutdownOutput();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x[remote=%s,local=%s,endpoint=%s]",
+ getClass().getSimpleName(),
+ hashCode(),
+ _remote,
+ _local,
+ _endp);
+ }
+
+ @Override
+ public boolean tryFillInterested(Callback callback)
+ {
+ return _endp.tryFillInterested(callback);
+ }
+
+ @Override
+ public void upgrade(Connection newConnection)
+ {
+ _endp.upgrade(newConnection);
+ }
+
+ @Override
+ public void write(Callback callback, ByteBuffer... buffers) throws WritePendingException
+ {
+ _endp.write(callback, buffers);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyCustomizer.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyCustomizer.java
new file mode 100644
index 0000000..ec9accd
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyCustomizer.java
@@ -0,0 +1,116 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.net.InetSocketAddress;
+import java.util.HashSet;
+import java.util.Set;
+import javax.servlet.ServletRequest;
+
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.Attributes;
+
+/**
+ * <p>Customizer that extracts the real local and remote address:port pairs from a {@link ProxyConnectionFactory}
+ * and sets them on the request with {@link ServletRequest#setAttribute(String, Object)}.
+ */
+public class ProxyCustomizer implements HttpConfiguration.Customizer
+{
+ /**
+ * The remote address attribute name.
+ */
+ public static final String REMOTE_ADDRESS_ATTRIBUTE_NAME = "org.eclipse.jetty.proxy.remote.address";
+
+ /**
+ * The remote port attribute name.
+ */
+ public static final String REMOTE_PORT_ATTRIBUTE_NAME = "org.eclipse.jetty.proxy.remote.port";
+
+ /**
+ * The local address attribute name.
+ */
+ public static final String LOCAL_ADDRESS_ATTRIBUTE_NAME = "org.eclipse.jetty.proxy.local.address";
+
+ /**
+ * The local port attribute name.
+ */
+ public static final String LOCAL_PORT_ATTRIBUTE_NAME = "org.eclipse.jetty.proxy.local.port";
+
+ @Override
+ public void customize(Connector connector, HttpConfiguration channelConfig, Request request)
+ {
+ EndPoint endPoint = request.getHttpChannel().getEndPoint();
+ if (endPoint instanceof ProxyConnectionFactory.ProxyEndPoint)
+ {
+ EndPoint underlyingEndpoint = ((ProxyConnectionFactory.ProxyEndPoint)endPoint).unwrap();
+ request.setAttributes(new ProxyAttributes(underlyingEndpoint.getRemoteAddress(), underlyingEndpoint.getLocalAddress(), request.getAttributes()));
+ }
+ }
+
+ private static class ProxyAttributes extends Attributes.Wrapper
+ {
+ private final String _remoteAddress;
+ private final String _localAddress;
+ private final int _remotePort;
+ private final int _localPort;
+
+ private ProxyAttributes(InetSocketAddress remoteAddress, InetSocketAddress localAddress, Attributes attributes)
+ {
+ super(attributes);
+ _remoteAddress = remoteAddress.getAddress().getHostAddress();
+ _localAddress = localAddress.getAddress().getHostAddress();
+ _remotePort = remoteAddress.getPort();
+ _localPort = localAddress.getPort();
+ }
+
+ @Override
+ public Object getAttribute(String name)
+ {
+ switch (name)
+ {
+ case REMOTE_ADDRESS_ATTRIBUTE_NAME:
+ return _remoteAddress;
+ case REMOTE_PORT_ATTRIBUTE_NAME:
+ return _remotePort;
+ case LOCAL_ADDRESS_ATTRIBUTE_NAME:
+ return _localAddress;
+ case LOCAL_PORT_ATTRIBUTE_NAME:
+ return _localPort;
+ default:
+ return super.getAttribute(name);
+ }
+ }
+
+ @Override
+ public Set<String> getAttributeNameSet()
+ {
+ Set<String> names = new HashSet<>(_attributes.getAttributeNameSet());
+ names.remove(REMOTE_ADDRESS_ATTRIBUTE_NAME);
+ names.remove(LOCAL_ADDRESS_ATTRIBUTE_NAME);
+
+ if (_remoteAddress != null)
+ names.add(REMOTE_ADDRESS_ATTRIBUTE_NAME);
+ if (_localAddress != null)
+ names.add(LOCAL_ADDRESS_ATTRIBUTE_NAME);
+ names.add(REMOTE_PORT_ATTRIBUTE_NAME);
+ names.add(LOCAL_PORT_ATTRIBUTE_NAME);
+ return names;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilder.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilder.java
new file mode 100644
index 0000000..a501262
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilder.java
@@ -0,0 +1,262 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.Set;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+/**
+ * Build a request to be pushed.
+ *
+ * <p>A PushBuilder is obtained by calling {@link
+ * Request#getPushBuilder()} (<code>Eventually HttpServletRequest.getPushBuilder()</code>).
+ * Each call to this method will
+ * return a new instance of a PushBuilder based off the current {@code
+ * HttpServletRequest}. Any mutations to the returned PushBuilder are
+ * not reflected on future returns.</p>
+ *
+ * <p>The instance is initialized as follows:</p>
+ *
+ * <ul>
+ *
+ * <li>The method is initialized to "GET"</li>
+ *
+ * <li>The existing headers of the current {@link HttpServletRequest}
+ * are added to the builder, except for:
+ *
+ * <ul>
+ * <li>Conditional headers (eg. If-Modified-Since)
+ * <li>Range headers
+ * <li>Expect headers
+ * <li>Authorization headers
+ * <li>Referrer headers
+ * </ul>
+ *
+ * </li>
+ *
+ * <li>If the request was authenticated, an Authorization header will
+ * be set with a container generated token that will result in equivalent
+ * Authorization for the pushed request.</li>
+ *
+ * <li>The {@link HttpServletRequest#getRequestedSessionId()} value,
+ * unless at the time of the call {@link
+ * HttpServletRequest#getSession(boolean)} has previously been called to
+ * create a new {@link HttpSession}, in which case the new session ID
+ * will be used as the PushBuilder's requested session ID. The source of
+ * the requested session id will be the same as for the request</li>
+ *
+ * <li>The Referer(sic) header will be set to {@link
+ * HttpServletRequest#getRequestURL()} plus any {@link
+ * HttpServletRequest#getQueryString()} </li>
+ *
+ * <li>If {@link HttpServletResponse#addCookie(Cookie)} has been called
+ * on the associated response, then a corresponding Cookie header will be added
+ * to the PushBuilder, unless the {@link Cookie#getMaxAge()} is <=0, in which
+ * case the Cookie will be removed from the builder.</li>
+ *
+ * <li>If this request has has the conditional headers If-Modified-Since
+ * or If-None-Match, then the {@link #isConditional()} header is set to
+ * true.</li>
+ *
+ * </ul>
+ *
+ * <p>The {@link #path} method must be called on the {@code PushBuilder}
+ * instance before the call to {@link #push}. Failure to do so must
+ * cause an exception to be thrown from {@link
+ * #push}, as specified in that method.</p>
+ *
+ * <p>A PushBuilder can be customized by chained calls to mutator
+ * methods before the {@link #push()} method is called to initiate an
+ * asynchronous push request with the current state of the builder.
+ * After the call to {@link #push()}, the builder may be reused for
+ * another push, however the implementation must make it so the {@link
+ * #path(String)}, {@link #etag(String)} and {@link
+ * #lastModified(String)} values are cleared before returning from
+ * {@link #push}. All other values are retained over calls to {@link
+ * #push()}.
+ *
+ * @since 4.0
+ */
+public interface PushBuilder
+{
+ /**
+ * <p>Set the method to be used for the push.</p>
+ *
+ * <p>Any non-empty String may be used for the method.</p>
+ *
+ * @param method the method to be used for the push.
+ * @return this builder.
+ * @throws NullPointerException if the argument is {@code null}
+ * @throws IllegalArgumentException if the argument is the empty String
+ */
+ PushBuilder method(String method);
+
+ /**
+ * Set the query string to be used for the push.
+ *
+ * Will be appended to any query String included in a call to {@link
+ * #path(String)}. Any duplicate parameters must be preserved. This
+ * method should be used instead of a query in {@link #path(String)}
+ * when multiple {@link #push()} calls are to be made with the same
+ * query string.
+ *
+ * @param queryString the query string to be used for the push.
+ * @return this builder.
+ */
+ PushBuilder queryString(String queryString);
+
+ /**
+ * Set the SessionID to be used for the push.
+ * The session ID will be set in the same way it was on the associated request (ie
+ * as a cookie if the associated request used a cookie, or as a url parameter if
+ * the associated request used a url parameter).
+ * Defaults to the requested session ID or any newly assigned session id from
+ * a newly created session.
+ *
+ * @param sessionId the SessionID to be used for the push.
+ * @return this builder.
+ */
+ PushBuilder sessionId(String sessionId);
+
+ /**
+ * Set if the request is to be conditional.
+ * If the request is conditional, any available values from {@link #etag(String)} or
+ * {@link #lastModified(String)} will be set in the appropriate headers. If the request
+ * is not conditional, then etag and lastModified values are ignored.
+ * Defaults to true if the associated request was conditional.
+ *
+ * @param conditional true if the push request is conditional
+ * @return this builder.
+ */
+ PushBuilder conditional(boolean conditional);
+
+ /**
+ * <p>Set a header to be used for the push. If the builder has an
+ * existing header with the same name, its value is overwritten.</p>
+ *
+ * @param name The header name to set
+ * @param value The header value to set
+ * @return this builder.
+ */
+ PushBuilder setHeader(String name, String value);
+
+ /**
+ * <p>Add a header to be used for the push.</p>
+ *
+ * @param name The header name to add
+ * @param value The header value to add
+ * @return this builder.
+ */
+ PushBuilder addHeader(String name, String value);
+
+ /**
+ * <p>Remove the named header. If the header does not exist, take
+ * no action.</p>
+ *
+ * @param name The name of the header to remove
+ * @return this builder.
+ */
+ PushBuilder removeHeader(String name);
+
+ /**
+ * Set the URI path to be used for the push. The path may start
+ * with "/" in which case it is treated as an absolute path,
+ * otherwise it is relative to the context path of the associated
+ * request. There is no path default and <code>path(String)</code> must
+ * be called before every call to {@link #push()}. If a query
+ * string is present in the argument {@code path}, its contents must
+ * be merged with the contents previously passed to {@link
+ * #queryString}, preserving duplicates.
+ *
+ * @param path the URI path to be used for the push, which may include a
+ * query string.
+ * @return this builder.
+ */
+ PushBuilder path(String path);
+
+ /**
+ * Set the etag to be used for conditional pushes.
+ * The etag will be used only if {@link #isConditional()} is true.
+ * Defaults to no etag. The value is nulled after every call to
+ * {@link #push()}
+ *
+ * @param etag the etag to be used for the push.
+ * @return this builder.
+ */
+ PushBuilder etag(String etag);
+
+ /**
+ * Set the last modified date to be used for conditional pushes.
+ * The last modified date will be used only if {@link
+ * #isConditional()} is true. Defaults to no date. The value is
+ * nulled after every call to {@link #push()}
+ *
+ * @param lastModified the last modified date to be used for the push.
+ * @return this builder.
+ */
+ PushBuilder lastModified(String lastModified);
+
+ /**
+ * Push a resource given the current state of the builder,
+ * returning immediately without blocking.
+ *
+ * <p>Push a resource based on the current state of the PushBuilder.
+ * If {@link #isConditional()} is true and an etag or lastModified
+ * value is provided, then an appropriate conditional header will be
+ * generated. If both an etag and lastModified value are provided
+ * only an If-None-Match header will be generated. If the builder
+ * has a session ID, then the pushed request will include the
+ * session ID either as a Cookie or as a URI parameter as
+ * appropriate. The builders query string is merged with any passed
+ * query string.</p>
+ *
+ * <p>Before returning from this method, the builder has its path,
+ * etag and lastModified fields nulled. All other fields are left as
+ * is for possible reuse in another push.</p>
+ *
+ * @throws IllegalArgumentException if the method set expects a
+ * request body (eg POST)
+ * @throws IllegalStateException if there was no call to {@link
+ * #path} on this instance either between its instantiation or the
+ * last call to {@code push()} that did not throw an
+ * IllegalStateException.
+ */
+ void push();
+
+ String getMethod();
+
+ String getQueryString();
+
+ String getSessionId();
+
+ boolean isConditional();
+
+ Set<String> getHeaderNames();
+
+ String getHeader(String name);
+
+ String getPath();
+
+ String getEtag();
+
+ String getLastModified();
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilderImpl.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilderImpl.java
new file mode 100644
index 0000000..ef0986d
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilderImpl.java
@@ -0,0 +1,239 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.Set;
+
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.MetaData;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ *
+ */
+public class PushBuilderImpl implements PushBuilder
+{
+ private static final Logger LOG = Log.getLogger(PushBuilderImpl.class);
+
+ private static final HttpField JettyPush = new HttpField("x-http2-push", "PushBuilder");
+
+ private final Request _request;
+ private final HttpFields _fields;
+ private String _method;
+ private String _queryString;
+ private String _sessionId;
+ private boolean _conditional;
+ private String _path;
+ private String _etag;
+ private String _lastModified;
+
+ public PushBuilderImpl(Request request, HttpFields fields, String method, String queryString, String sessionId, boolean conditional)
+ {
+ super();
+ _request = request;
+ _fields = fields;
+ _method = method;
+ _queryString = queryString;
+ _sessionId = sessionId;
+ _conditional = conditional;
+ _fields.add(JettyPush);
+ if (LOG.isDebugEnabled())
+ LOG.debug("PushBuilder({} {}?{} s={} c={})", _method, _request.getRequestURI(), _queryString, _sessionId, _conditional);
+ }
+
+ @Override
+ public String getMethod()
+ {
+ return _method;
+ }
+
+ @Override
+ public PushBuilder method(String method)
+ {
+ _method = method;
+ return this;
+ }
+
+ @Override
+ public String getQueryString()
+ {
+ return _queryString;
+ }
+
+ @Override
+ public PushBuilder queryString(String queryString)
+ {
+ _queryString = queryString;
+ return this;
+ }
+
+ @Override
+ public String getSessionId()
+ {
+ return _sessionId;
+ }
+
+ @Override
+ public PushBuilder sessionId(String sessionId)
+ {
+ _sessionId = sessionId;
+ return this;
+ }
+
+ @Override
+ public boolean isConditional()
+ {
+ return _conditional;
+ }
+
+ @Override
+ public PushBuilder conditional(boolean conditional)
+ {
+ _conditional = conditional;
+ return this;
+ }
+
+ @Override
+ public Set<String> getHeaderNames()
+ {
+ return _fields.getFieldNamesCollection();
+ }
+
+ @Override
+ public String getHeader(String name)
+ {
+ return _fields.get(name);
+ }
+
+ @Override
+ public PushBuilder setHeader(String name, String value)
+ {
+ _fields.put(name, value);
+ return this;
+ }
+
+ @Override
+ public PushBuilder addHeader(String name, String value)
+ {
+ _fields.add(name, value);
+ return this;
+ }
+
+ @Override
+ public PushBuilder removeHeader(String name)
+ {
+ _fields.remove(name);
+ return this;
+ }
+
+ @Override
+ public String getPath()
+ {
+ return _path;
+ }
+
+ @Override
+ public PushBuilder path(String path)
+ {
+ _path = path;
+ return this;
+ }
+
+ @Override
+ public String getEtag()
+ {
+ return _etag;
+ }
+
+ @Override
+ public PushBuilder etag(String etag)
+ {
+ _etag = etag;
+ return this;
+ }
+
+ @Override
+ public String getLastModified()
+ {
+ return _lastModified;
+ }
+
+ @Override
+ public PushBuilder lastModified(String lastModified)
+ {
+ _lastModified = lastModified;
+ return this;
+ }
+
+ @Override
+ public void push()
+ {
+ if (HttpMethod.POST.is(_method) || HttpMethod.PUT.is(_method))
+ throw new IllegalStateException("Bad Method " + _method);
+
+ if (_path == null || _path.length() == 0)
+ throw new IllegalStateException("Bad Path " + _path);
+
+ String path = _path;
+ String query = _queryString;
+ int q = path.indexOf('?');
+ if (q >= 0)
+ {
+ query = (query != null && query.length() > 0) ? (path.substring(q + 1) + '&' + query) : path.substring(q + 1);
+ path = path.substring(0, q);
+ }
+
+ if (!path.startsWith("/"))
+ path = URIUtil.addPaths(_request.getContextPath(), path);
+
+ String param = null;
+ if (_sessionId != null)
+ {
+ if (_request.isRequestedSessionIdFromURL())
+ param = "jsessionid=" + _sessionId;
+ // TODO else
+ // _fields.add("Cookie","JSESSIONID="+_sessionId);
+ }
+
+ if (_conditional)
+ {
+ if (_etag != null)
+ _fields.add(HttpHeader.IF_NONE_MATCH, _etag);
+ else if (_lastModified != null)
+ _fields.add(HttpHeader.IF_MODIFIED_SINCE, _lastModified);
+ }
+
+ HttpURI uri = HttpURI.createHttpURI(_request.getScheme(), _request.getServerName(), _request.getServerPort(), path, param, query, null);
+ MetaData.Request push = new MetaData.Request(_method, uri, _request.getHttpVersion(), _fields);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Push {} {} inm={} ims={}", _method, uri, _fields.get(HttpHeader.IF_NONE_MATCH), _fields.get(HttpHeader.IF_MODIFIED_SINCE));
+
+ _request.getHttpChannel().getHttpTransport().push(push);
+ _path = null;
+ _etag = null;
+ _lastModified = null;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/QuietServletException.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/QuietServletException.java
new file mode 100644
index 0000000..90c783a
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/QuietServletException.java
@@ -0,0 +1,54 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import javax.servlet.ServletException;
+
+import org.eclipse.jetty.io.QuietException;
+
+/**
+ * A ServletException that is logged less verbosely than
+ * a normal ServletException.
+ * <p>
+ * Used for container generated exceptions that need only a message rather
+ * than a stack trace.
+ * </p>
+ */
+public class QuietServletException extends ServletException implements QuietException
+{
+ public QuietServletException()
+ {
+ super();
+ }
+
+ public QuietServletException(String message, Throwable rootCause)
+ {
+ super(message, rootCause);
+ }
+
+ public QuietServletException(String message)
+ {
+ super(message);
+ }
+
+ public QuietServletException(Throwable rootCause)
+ {
+ super(rootCause);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
new file mode 100644
index 0000000..a3e06d9
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
@@ -0,0 +1,2611 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.charset.UnsupportedCharsetException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.EventListener;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import javax.servlet.AsyncContext;
+import javax.servlet.AsyncListener;
+import javax.servlet.DispatcherType;
+import javax.servlet.MultipartConfigElement;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletRequestAttributeEvent;
+import javax.servlet.ServletRequestAttributeListener;
+import javax.servlet.ServletRequestWrapper;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpUpgradeHandler;
+import javax.servlet.http.Part;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HostPortHttpField;
+import org.eclipse.jetty.http.HttpCompliance;
+import org.eclipse.jetty.http.HttpCookie;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpHeaderValue;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.http.MetaData;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.RuntimeIOException;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandler.Context;
+import org.eclipse.jetty.server.session.Session;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.util.Attributes;
+import org.eclipse.jetty.util.AttributesMap;
+import org.eclipse.jetty.util.HostPort;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.UrlEncoded;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Jetty Request.
+ * <p>
+ * Implements {@link javax.servlet.http.HttpServletRequest} from the <code>javax.servlet.http</code> package.
+ * </p>
+ * <p>
+ * The standard interface of mostly getters, is extended with setters so that the request is mutable by the handlers that it is passed to. This allows the
+ * request object to be as lightweight as possible and not actually implement any significant behavior. For example
+ * <ul>
+ *
+ * <li>The {@link Request#getContextPath()} method will return null, until the request has been passed to a {@link ContextHandler} which matches the
+ * {@link Request#getPathInfo()} with a context path and calls {@link Request#setContextPath(String)} as a result.</li>
+ *
+ * <li>the HTTP session methods will all return null sessions until such time as a request has been passed to a
+ * {@link org.eclipse.jetty.server.session.SessionHandler} which checks for session cookies and enables the ability to create new sessions.</li>
+ *
+ * <li>The {@link Request#getServletPath()} method will return null until the request has been passed to a <code>org.eclipse.jetty.servlet.ServletHandler</code>
+ * and the pathInfo matched against the servlet URL patterns and {@link Request#setServletPath(String)} called as a result.</li>
+ * </ul>
+ *
+ * <p>
+ * A request instance is created for each connection accepted by the server and recycled for each HTTP request received via that connection.
+ * An effort is made to avoid reparsing headers and cookies that are likely to be the same for requests from the same connection.
+ * </p>
+ * <p>
+ * Request instances are recycled, which combined with badly written asynchronous applications can result in calls on requests that have been reset.
+ * The code is written in a style to avoid NPE and ISE when such calls are made, as this has often proved generate exceptions that distraction
+ * from debugging such bad asynchronous applications. Instead, request methods attempt to not fail when called in an illegal state, so that hopefully
+ * the bad application will proceed to a major state event (eg calling AsyncContext.onComplete) which has better asynchronous guards, true atomic state
+ * and better failure behaviour that will assist in debugging.
+ * </p>
+ * <p>
+ * The form content that a request can process is limited to protect from Denial of Service attacks. The size in bytes is limited by
+ * {@link ContextHandler#getMaxFormContentSize()} or if there is no context then the "org.eclipse.jetty.server.Request.maxFormContentSize" {@link Server}
+ * attribute. The number of parameters keys is limited by {@link ContextHandler#getMaxFormKeys()} or if there is no context then the
+ * "org.eclipse.jetty.server.Request.maxFormKeys" {@link Server} attribute.
+ * </p>
+ * <p>If IOExceptions or timeouts occur while reading form parameters, these are thrown as unchecked Exceptions: ether {@link RuntimeIOException},
+ * {@link BadMessageException} or {@link RuntimeException} as appropriate.</p>
+ */
+public class Request implements HttpServletRequest
+{
+ public static final String MULTIPART_CONFIG_ELEMENT = "org.eclipse.jetty.multipartConfig";
+ public static final String MULTIPARTS = "org.eclipse.jetty.multiParts";
+
+ private static final Logger LOG = Log.getLogger(Request.class);
+ private static final Collection<Locale> __defaultLocale = Collections.singleton(Locale.getDefault());
+ private static final int INPUT_NONE = 0;
+ private static final int INPUT_STREAM = 1;
+ private static final int INPUT_READER = 2;
+
+ private static final MultiMap<String> NO_PARAMS = new MultiMap<>();
+
+ /**
+ * Compare inputParameters to NO_PARAMS by Reference
+ *
+ * @param inputParameters The parameters to compare to NO_PARAMS
+ * @return True if the inputParameters reference is equal to NO_PARAMS otherwise False
+ */
+ private static boolean isNoParams(MultiMap<String> inputParameters)
+ {
+ @SuppressWarnings("ReferenceEquality")
+ boolean isNoParams = (inputParameters == NO_PARAMS);
+ return isNoParams;
+ }
+
+ /**
+ * Obtain the base {@link Request} instance of a {@link ServletRequest}, by
+ * coercion, unwrapping or special attribute.
+ *
+ * @param request The request
+ * @return the base {@link Request} instance of a {@link ServletRequest}.
+ */
+ public static Request getBaseRequest(ServletRequest request)
+ {
+ if (request instanceof Request)
+ return (Request)request;
+
+ Object channel = request.getAttribute(HttpChannel.class.getName());
+ if (channel instanceof HttpChannel)
+ return ((HttpChannel)channel).getRequest();
+
+ while (request instanceof ServletRequestWrapper)
+ {
+ request = ((ServletRequestWrapper)request).getRequest();
+ }
+
+ if (request instanceof Request)
+ return (Request)request;
+
+ return null;
+ }
+
+ private final HttpChannel _channel;
+ private final List<ServletRequestAttributeListener> _requestAttributeListeners = new ArrayList<>();
+ private final HttpInput _input;
+ private MetaData.Request _metaData;
+ private String _originalURI;
+ private String _contextPath;
+ private String _servletPath;
+ private String _pathInfo;
+ private Object _asyncNotSupportedSource = null;
+ private boolean _secure;
+ private boolean _newContext;
+ private boolean _cookiesExtracted = false;
+ private boolean _handled = false;
+ private boolean _contentParamsExtracted;
+ private boolean _requestedSessionIdFromCookie = false;
+ private Attributes _attributes;
+ private Authentication _authentication;
+ private String _contentType;
+ private String _characterEncoding;
+ private ContextHandler.Context _context;
+ private ContextHandler.Context _errorContext;
+ private CookieCutter _cookies;
+ private DispatcherType _dispatcherType;
+ private int _inputState = INPUT_NONE;
+ private BufferedReader _reader;
+ private String _readerEncoding;
+ private MultiMap<String> _queryParameters;
+ private MultiMap<String> _contentParameters;
+ private MultiMap<String> _parameters;
+ private String _queryEncoding;
+ private InetSocketAddress _remote;
+ private String _requestedSessionId;
+ private UserIdentity.Scope _scope;
+ private HttpSession _session;
+ private SessionHandler _sessionHandler;
+ private long _timeStamp;
+ private MultiParts _multiParts; //if the request is a multi-part mime
+ private AsyncContextState _async;
+ private List<Session> _sessions; //list of sessions used during lifetime of request
+
+ public Request(HttpChannel channel, HttpInput input)
+ {
+ _channel = channel;
+ _input = input;
+ }
+
+ public HttpFields getHttpFields()
+ {
+ MetaData.Request metadata = _metaData;
+ return metadata == null ? null : metadata.getFields();
+ }
+
+ public HttpFields getTrailers()
+ {
+ MetaData.Request metadata = _metaData;
+ Supplier<HttpFields> trailers = metadata == null ? null : metadata.getTrailerSupplier();
+ return trailers == null ? null : trailers.get();
+ }
+
+ public HttpInput getHttpInput()
+ {
+ return _input;
+ }
+
+ public boolean isPush()
+ {
+ return Boolean.TRUE.equals(getAttribute("org.eclipse.jetty.pushed"));
+ }
+
+ public boolean isPushSupported()
+ {
+ return !isPush() && getHttpChannel().getHttpTransport().isPushSupported();
+ }
+
+ /**
+ * Get a PushBuilder associated with this request initialized as follows:<ul>
+ * <li>The method is initialized to "GET"</li>
+ * <li>The headers from this request are copied to the Builder, except for:<ul>
+ * <li>Conditional headers (eg. If-Modified-Since)
+ * <li>Range headers
+ * <li>Expect headers
+ * <li>Authorization headers
+ * <li>Referrer headers
+ * </ul></li>
+ * <li>If the request was Authenticated, an Authorization header will
+ * be set with a container generated token that will result in equivalent
+ * Authorization</li>
+ * <li>The query string from {@link #getQueryString()}
+ * <li>The {@link #getRequestedSessionId()} value, unless at the time
+ * of the call {@link #getSession(boolean)}
+ * has previously been called to create a new {@link HttpSession}, in
+ * which case the new session ID will be used as the PushBuilders
+ * requested session ID.</li>
+ * <li>The source of the requested session id will be the same as for
+ * this request</li>
+ * <li>The builders Referer header will be set to {@link #getRequestURL()}
+ * plus any {@link #getQueryString()} </li>
+ * <li>If {@link HttpServletResponse#addCookie(Cookie)} has been called
+ * on the associated response, then a corresponding Cookie header will be added
+ * to the PushBuilder, unless the {@link Cookie#getMaxAge()} is <=0, in which
+ * case the Cookie will be removed from the builder.</li>
+ * <li>If this request has has the conditional headers If-Modified-Since or
+ * If-None-Match then the {@link PushBuilderImpl#isConditional()} header is set
+ * to true.
+ * </ul>
+ *
+ * <p>Each call to getPushBuilder() will return a new instance
+ * of a PushBuilder based off this Request. Any mutations to the
+ * returned PushBuilder are not reflected on future returns.
+ *
+ * @return A new PushBuilder or null if push is not supported
+ */
+ public PushBuilder getPushBuilder()
+ {
+ if (!isPushSupported())
+ return null;
+
+ HttpFields fields = new HttpFields(getHttpFields().size() + 5);
+ boolean conditional = false;
+
+ for (HttpField field : getHttpFields())
+ {
+ HttpHeader header = field.getHeader();
+ if (header == null)
+ fields.add(field);
+ else
+ {
+ switch (header)
+ {
+ case IF_MATCH:
+ case IF_RANGE:
+ case IF_UNMODIFIED_SINCE:
+ case RANGE:
+ case EXPECT:
+ case REFERER:
+ case COOKIE:
+ continue;
+
+ case AUTHORIZATION:
+ continue;
+
+ case IF_NONE_MATCH:
+ case IF_MODIFIED_SINCE:
+ conditional = true;
+ continue;
+
+ default:
+ fields.add(field);
+ }
+ }
+ }
+
+ String id = null;
+ try
+ {
+ HttpSession session = getSession();
+ if (session != null)
+ {
+ session.getLastAccessedTime(); // checks if session is valid
+ id = session.getId();
+ }
+ else
+ id = getRequestedSessionId();
+ }
+ catch (IllegalStateException e)
+ {
+ id = getRequestedSessionId();
+ }
+
+ PushBuilder builder = new PushBuilderImpl(this, fields, getMethod(), getQueryString(), id, conditional);
+ builder.addHeader("referer", getRequestURL().toString());
+
+ // TODO process any set cookies
+ // TODO process any user_identity
+
+ return builder;
+ }
+
+ public void addEventListener(final EventListener listener)
+ {
+ if (listener instanceof ServletRequestAttributeListener)
+ _requestAttributeListeners.add((ServletRequestAttributeListener)listener);
+ if (listener instanceof AsyncListener)
+ throw new IllegalArgumentException(listener.getClass().toString());
+ }
+
+ /**
+ * Remember a session that this request has just entered.
+ *
+ * @param s the session
+ */
+ public void enterSession(HttpSession s)
+ {
+ if (!(s instanceof Session))
+ return;
+
+ if (_sessions == null)
+ _sessions = new ArrayList<>();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Request {} entering session={}", this, s);
+ _sessions.add((Session)s);
+ }
+
+ /**
+ * Complete this request's access to a session.
+ *
+ * @param session the session
+ */
+ private void leaveSession(Session session)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Request {} leaving session {}", this, session);
+ session.getSessionHandler().complete(session);
+ }
+
+ /**
+ * A response is being committed for a session,
+ * potentially write the session out before the
+ * client receives the response.
+ * @param session the session
+ */
+ private void commitSession(Session session)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Response {} committing for session {}", this, session);
+ session.getSessionHandler().commit(session);
+ }
+
+ private MultiMap<String> getParameters()
+ {
+ if (!_contentParamsExtracted)
+ {
+ // content parameters need boolean protection as they can only be read
+ // once, but may be reset to null by a reset
+ _contentParamsExtracted = true;
+
+ // Extract content parameters; these cannot be replaced by a forward()
+ // once extracted and may have already been extracted by getParts() or
+ // by a processing happening after a form-based authentication.
+ if (_contentParameters == null)
+ {
+ try
+ {
+ extractContentParameters();
+ }
+ catch (IllegalStateException | IllegalArgumentException e)
+ {
+ LOG.warn(e.toString());
+ throw new BadMessageException("Unable to parse form content", e);
+ }
+ }
+ }
+
+ // Extract query string parameters; these may be replaced by a forward()
+ // and may have already been extracted by mergeQueryParameters().
+ if (_queryParameters == null)
+ {
+ try
+ {
+ extractQueryParameters();
+ }
+ catch (IllegalStateException | IllegalArgumentException e)
+ {
+ throw new BadMessageException("Unable to parse URI query", e);
+ }
+ }
+
+ // Do parameters need to be combined?
+ if (isNoParams(_queryParameters) || _queryParameters.size() == 0)
+ _parameters = _contentParameters;
+ else if (isNoParams(_contentParameters) || _contentParameters.size() == 0)
+ _parameters = _queryParameters;
+ else if (_parameters == null)
+ {
+ _parameters = new MultiMap<>();
+ _parameters.addAllValues(_queryParameters);
+ _parameters.addAllValues(_contentParameters);
+ }
+
+ // protect against calls to recycled requests (which is illegal, but
+ // this gives better failures
+ MultiMap<String> parameters = _parameters;
+ return parameters == null ? NO_PARAMS : parameters;
+ }
+
+ private void extractQueryParameters()
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata == null || metadata.getURI() == null || !metadata.getURI().hasQuery())
+ _queryParameters = NO_PARAMS;
+ else
+ {
+ _queryParameters = new MultiMap<>();
+ if (_queryEncoding == null)
+ metadata.getURI().decodeQueryTo(_queryParameters);
+ else
+ {
+ try
+ {
+ metadata.getURI().decodeQueryTo(_queryParameters, _queryEncoding);
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.warn(e);
+ else
+ LOG.warn(e.toString());
+ }
+ }
+ }
+ }
+
+ private boolean isContentEncodingSupported()
+ {
+ String contentEncoding = getHttpFields().get(HttpHeader.CONTENT_ENCODING);
+ if (contentEncoding == null)
+ return true;
+ return HttpHeaderValue.IDENTITY.is(contentEncoding);
+ }
+
+ private void extractContentParameters()
+ {
+ String contentType = getContentType();
+ if (contentType == null || contentType.isEmpty())
+ _contentParameters = NO_PARAMS;
+ else
+ {
+ _contentParameters = new MultiMap<>();
+ int contentLength = getContentLength();
+ if (contentLength != 0 && _inputState == INPUT_NONE)
+ {
+ String baseType = HttpFields.valueParameters(contentType, null);
+ if (MimeTypes.Type.FORM_ENCODED.is(baseType) &&
+ _channel.getHttpConfiguration().isFormEncodedMethod(getMethod()))
+ {
+ if (_metaData != null && !isContentEncodingSupported())
+ {
+ throw new BadMessageException(HttpStatus.UNSUPPORTED_MEDIA_TYPE_415, "Unsupported Content-Encoding");
+ }
+
+ extractFormParameters(_contentParameters);
+ }
+ else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(baseType) &&
+ getAttribute(MULTIPART_CONFIG_ELEMENT) != null &&
+ _multiParts == null)
+ {
+ try
+ {
+ if (_metaData != null && !isContentEncodingSupported())
+ {
+ throw new BadMessageException(HttpStatus.UNSUPPORTED_MEDIA_TYPE_415, "Unsupported Content-Encoding");
+ }
+ getParts(_contentParameters);
+ }
+ catch (IOException e)
+ {
+ LOG.debug(e);
+ throw new RuntimeIOException(e);
+ }
+ }
+ }
+ }
+ }
+
+ public void extractFormParameters(MultiMap<String> params)
+ {
+ try
+ {
+ int maxFormContentSize = ContextHandler.DEFAULT_MAX_FORM_CONTENT_SIZE;
+ int maxFormKeys = ContextHandler.DEFAULT_MAX_FORM_KEYS;
+
+ if (_context != null)
+ {
+ ContextHandler contextHandler = _context.getContextHandler();
+ maxFormContentSize = contextHandler.getMaxFormContentSize();
+ maxFormKeys = contextHandler.getMaxFormKeys();
+ }
+ else
+ {
+ maxFormContentSize = lookupServerAttribute(ContextHandler.MAX_FORM_CONTENT_SIZE_KEY, maxFormContentSize);
+ maxFormKeys = lookupServerAttribute(ContextHandler.MAX_FORM_KEYS_KEY, maxFormKeys);
+ }
+
+ int contentLength = getContentLength();
+ if (maxFormContentSize >= 0 && contentLength > maxFormContentSize)
+ throw new IllegalStateException("Form is larger than max length " + maxFormContentSize);
+
+ InputStream in = getInputStream();
+ if (_input.isAsync())
+ throw new IllegalStateException("Cannot extract parameters with async IO");
+
+ UrlEncoded.decodeTo(in, params, getCharacterEncoding(), maxFormContentSize, maxFormKeys);
+ }
+ catch (IOException e)
+ {
+ LOG.debug(e);
+ throw new RuntimeIOException(e);
+ }
+ }
+
+ private int lookupServerAttribute(String key, int dftValue)
+ {
+ Object attribute = _channel.getServer().getAttribute(key);
+ if (attribute instanceof Number)
+ return ((Number)attribute).intValue();
+ else if (attribute instanceof String)
+ return Integer.parseInt((String)attribute);
+ return dftValue;
+ }
+
+ @Override
+ public AsyncContext getAsyncContext()
+ {
+ HttpChannelState state = getHttpChannelState();
+ if (_async == null || !state.isAsyncStarted())
+ throw new IllegalStateException(state.getStatusString());
+
+ return _async;
+ }
+
+ public HttpChannelState getHttpChannelState()
+ {
+ return _channel.getState();
+ }
+
+ /**
+ * Get Request Attribute.
+ * <p>
+ * Also supports jetty specific attributes to gain access to Jetty APIs:
+ * <dl>
+ * <dt>org.eclipse.jetty.server.Server</dt><dd>The Jetty Server instance</dd>
+ * <dt>org.eclipse.jetty.server.HttpChannel</dt><dd>The HttpChannel for this request</dd>
+ * <dt>org.eclipse.jetty.server.HttpConnection</dt><dd>The HttpConnection or null if another transport is used</dd>
+ * </dl>
+ * While these attributes may look like security problems, they are exposing nothing that is not already
+ * available via reflection from a Request instance.
+ *
+ * @see javax.servlet.ServletRequest#getAttribute(java.lang.String)
+ */
+ @Override
+ public Object getAttribute(String name)
+ {
+ if (name.startsWith("org.eclipse.jetty"))
+ {
+ if (Server.class.getName().equals(name))
+ return _channel.getServer();
+ if (HttpChannel.class.getName().equals(name))
+ return _channel;
+ if (HttpConnection.class.getName().equals(name) &&
+ _channel.getHttpTransport() instanceof HttpConnection)
+ return _channel.getHttpTransport();
+ }
+ return (_attributes == null) ? null : _attributes.getAttribute(name);
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getAttributeNames()
+ */
+ @Override
+ public Enumeration<String> getAttributeNames()
+ {
+ if (_attributes == null)
+ return Collections.enumeration(Collections.emptyList());
+
+ return AttributesMap.getAttributeNamesCopy(_attributes);
+ }
+
+ public Attributes getAttributes()
+ {
+ if (_attributes == null)
+ _attributes = new ServletAttributes();
+ return _attributes;
+ }
+
+ /**
+ * Get the authentication.
+ *
+ * @return the authentication
+ */
+ public Authentication getAuthentication()
+ {
+ return _authentication;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getAuthType()
+ */
+ @Override
+ public String getAuthType()
+ {
+ if (_authentication instanceof Authentication.Deferred)
+ setAuthentication(((Authentication.Deferred)_authentication).authenticate(this));
+
+ if (_authentication instanceof Authentication.User)
+ return ((Authentication.User)_authentication).getAuthMethod();
+ return null;
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getCharacterEncoding()
+ */
+ @Override
+ public String getCharacterEncoding()
+ {
+ if (_characterEncoding == null)
+ {
+ String contentType = getContentType();
+ if (contentType != null)
+ {
+ MimeTypes.Type mime = MimeTypes.CACHE.get(contentType);
+ String charset = (mime == null || mime.getCharset() == null) ? MimeTypes.getCharsetFromContentType(contentType) : mime.getCharset().toString();
+ if (charset != null)
+ _characterEncoding = charset;
+ }
+ }
+ return _characterEncoding;
+ }
+
+ /**
+ * @return Returns the connection.
+ */
+ public HttpChannel getHttpChannel()
+ {
+ return _channel;
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getContentLength()
+ */
+ @Override
+ public int getContentLength()
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata == null)
+ return -1;
+
+ long contentLength = metadata.getContentLength();
+ if (contentLength != Long.MIN_VALUE)
+ {
+ if (contentLength > Integer.MAX_VALUE)
+ {
+ // Per ServletRequest#getContentLength() javadoc this must return -1 for values exceeding Integer.MAX_VALUE
+ return -1;
+ }
+ else
+ {
+ return (int)contentLength;
+ }
+ }
+ return (int)metadata.getFields().getLongField(HttpHeader.CONTENT_LENGTH.asString());
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest.getContentLengthLong()
+ */
+ @Override
+ public long getContentLengthLong()
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata == null)
+ return -1L;
+ if (metadata.getContentLength() != Long.MIN_VALUE)
+ return metadata.getContentLength();
+ return metadata.getFields().getLongField(HttpHeader.CONTENT_LENGTH.asString());
+ }
+
+ public long getContentRead()
+ {
+ return _input.getContentConsumed();
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getContentType()
+ */
+ @Override
+ public String getContentType()
+ {
+ if (_contentType == null)
+ {
+ MetaData.Request metadata = _metaData;
+ _contentType = metadata == null ? null : metadata.getFields().get(HttpHeader.CONTENT_TYPE);
+ }
+ return _contentType;
+ }
+
+ /**
+ * @return The current {@link Context context} used for this request, or <code>null</code> if {@link #setContext} has not yet been called.
+ */
+ public Context getContext()
+ {
+ return _context;
+ }
+
+ /**
+ * @return The current {@link Context context} used for this error handling for this request. If the request is asynchronous,
+ * then it is the context that called async. Otherwise it is the last non-null context passed to #setContext
+ */
+ public Context getErrorContext()
+ {
+ if (isAsyncStarted())
+ {
+ ContextHandler handler = _channel.getState().getContextHandler();
+ if (handler != null)
+ return handler.getServletContext();
+ }
+
+ return _errorContext;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getContextPath()
+ */
+ @Override
+ public String getContextPath()
+ {
+ return _contextPath;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getCookies()
+ */
+ @Override
+ public Cookie[] getCookies()
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata == null || _cookiesExtracted)
+ {
+ if (_cookies == null || _cookies.getCookies().length == 0)
+ return null;
+
+ return _cookies.getCookies();
+ }
+
+ _cookiesExtracted = true;
+
+ for (HttpField field : metadata.getFields())
+ {
+ if (field.getHeader() == HttpHeader.COOKIE)
+ {
+ if (_cookies == null)
+ _cookies = new CookieCutter(getHttpChannel().getHttpConfiguration().getRequestCookieCompliance());
+ _cookies.addCookieField(field.getValue());
+ }
+ }
+
+ //Javadoc for Request.getCookies() stipulates null for no cookies
+ if (_cookies == null || _cookies.getCookies().length == 0)
+ return null;
+
+ return _cookies.getCookies();
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getDateHeader(java.lang.String)
+ */
+ @Override
+ public long getDateHeader(String name)
+ {
+ MetaData.Request metadata = _metaData;
+ return metadata == null ? -1 : metadata.getFields().getDateField(name);
+ }
+
+ @Override
+ public DispatcherType getDispatcherType()
+ {
+ return _dispatcherType;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getHeader(java.lang.String)
+ */
+ @Override
+ public String getHeader(String name)
+ {
+ MetaData.Request metadata = _metaData;
+ return metadata == null ? null : metadata.getFields().get(name);
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getHeaderNames()
+ */
+ @Override
+ public Enumeration<String> getHeaderNames()
+ {
+ MetaData.Request metadata = _metaData;
+ return metadata == null ? Collections.emptyEnumeration() : metadata.getFields().getFieldNames();
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getHeaders(java.lang.String)
+ */
+ @Override
+ public Enumeration<String> getHeaders(String name)
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata == null)
+ return Collections.emptyEnumeration();
+ Enumeration<String> e = metadata.getFields().getValues(name);
+ if (e == null)
+ return Collections.enumeration(Collections.emptyList());
+ return e;
+ }
+
+ /**
+ * @return Returns the inputState.
+ */
+ public int getInputState()
+ {
+ return _inputState;
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getInputStream()
+ */
+ @Override
+ public ServletInputStream getInputStream() throws IOException
+ {
+ if (_inputState != INPUT_NONE && _inputState != INPUT_STREAM)
+ throw new IllegalStateException("READER");
+ _inputState = INPUT_STREAM;
+
+ if (_channel.isExpecting100Continue())
+ _channel.continue100(_input.available());
+
+ return _input;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getIntHeader(java.lang.String)
+ */
+ @Override
+ public int getIntHeader(String name)
+ {
+ MetaData.Request metadata = _metaData;
+ return metadata == null ? -1 : (int)metadata.getFields().getLongField(name);
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getLocale()
+ */
+ @Override
+ public Locale getLocale()
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata == null)
+ return Locale.getDefault();
+
+ List<String> acceptable = metadata.getFields().getQualityCSV(HttpHeader.ACCEPT_LANGUAGE);
+
+ // handle no locale
+ if (acceptable.isEmpty())
+ return Locale.getDefault();
+
+ String language = acceptable.get(0);
+ language = HttpFields.stripParameters(language);
+ String country = "";
+ int dash = language.indexOf('-');
+ if (dash > -1)
+ {
+ country = language.substring(dash + 1).trim();
+ language = language.substring(0, dash).trim();
+ }
+ return new Locale(language, country);
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getLocales()
+ */
+ @Override
+ public Enumeration<Locale> getLocales()
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata == null)
+ return Collections.enumeration(__defaultLocale);
+
+ List<String> acceptable = metadata.getFields().getQualityCSV(HttpHeader.ACCEPT_LANGUAGE);
+
+ // handle no locale
+ if (acceptable.isEmpty())
+ return Collections.enumeration(__defaultLocale);
+
+ List<Locale> locales = acceptable.stream().map(language ->
+ {
+ language = HttpFields.stripParameters(language);
+ String country = "";
+ int dash = language.indexOf('-');
+ if (dash > -1)
+ {
+ country = language.substring(dash + 1).trim();
+ language = language.substring(0, dash).trim();
+ }
+ return new Locale(language, country);
+ }).collect(Collectors.toList());
+
+ return Collections.enumeration(locales);
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getLocalAddr()
+ */
+ @Override
+ public String getLocalAddr()
+ {
+ if (_channel == null)
+ {
+ try
+ {
+ String name = InetAddress.getLocalHost().getHostAddress();
+ if (StringUtil.ALL_INTERFACES.equals(name))
+ return null;
+ return formatAddrOrHost(name);
+ }
+ catch (UnknownHostException e)
+ {
+ LOG.ignore(e);
+ return null;
+ }
+ }
+
+ InetSocketAddress local = _channel.getLocalAddress();
+ if (local == null)
+ return "";
+ InetAddress address = local.getAddress();
+ String result = address == null
+ ? local.getHostString()
+ : address.getHostAddress();
+ return formatAddrOrHost(result);
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getLocalName()
+ */
+ @Override
+ public String getLocalName()
+ {
+ if (_channel != null)
+ {
+ InetSocketAddress local = _channel.getLocalAddress();
+ if (local != null)
+ return formatAddrOrHost(local.getHostString());
+ }
+
+ try
+ {
+ String name = InetAddress.getLocalHost().getHostName();
+ if (StringUtil.ALL_INTERFACES.equals(name))
+ return null;
+ return formatAddrOrHost(name);
+ }
+ catch (UnknownHostException e)
+ {
+ LOG.ignore(e);
+ }
+ return null;
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getLocalPort()
+ */
+ @Override
+ public int getLocalPort()
+ {
+ if (_channel == null)
+ return 0;
+ InetSocketAddress local = _channel.getLocalAddress();
+ return local == null ? 0 : local.getPort();
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getMethod()
+ */
+ @Override
+ public String getMethod()
+ {
+ MetaData.Request metadata = _metaData;
+ return metadata == null ? null : metadata.getMethod();
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
+ */
+ @Override
+ public String getParameter(String name)
+ {
+ return getParameters().getValue(name, 0);
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getParameterMap()
+ */
+ @Override
+ public Map<String, String[]> getParameterMap()
+ {
+ return Collections.unmodifiableMap(getParameters().toStringArrayMap());
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getParameterNames()
+ */
+ @Override
+ public Enumeration<String> getParameterNames()
+ {
+ return Collections.enumeration(getParameters().keySet());
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
+ */
+ @Override
+ public String[] getParameterValues(String name)
+ {
+ List<String> vals = getParameters().getValues(name);
+ if (vals == null)
+ return null;
+ return vals.toArray(new String[vals.size()]);
+ }
+
+ public MultiMap<String> getQueryParameters()
+ {
+ return _queryParameters;
+ }
+
+ public void setQueryParameters(MultiMap<String> queryParameters)
+ {
+ _queryParameters = queryParameters;
+ }
+
+ public void setContentParameters(MultiMap<String> contentParameters)
+ {
+ _contentParameters = contentParameters;
+ }
+
+ public void resetParameters()
+ {
+ _parameters = null;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getPathInfo()
+ */
+ @Override
+ public String getPathInfo()
+ {
+ return _pathInfo;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getPathTranslated()
+ */
+ @Override
+ public String getPathTranslated()
+ {
+ if (_pathInfo == null || _context == null)
+ return null;
+ return _context.getRealPath(_pathInfo);
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getProtocol()
+ */
+ @Override
+ public String getProtocol()
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata == null)
+ return null;
+ HttpVersion version = metadata.getHttpVersion();
+ if (version == null)
+ return null;
+ return version.toString();
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getProtocol()
+ */
+ public HttpVersion getHttpVersion()
+ {
+ MetaData.Request metadata = _metaData;
+ return metadata == null ? null : metadata.getHttpVersion();
+ }
+
+ public String getQueryEncoding()
+ {
+ return _queryEncoding;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getQueryString()
+ */
+ @Override
+ public String getQueryString()
+ {
+ MetaData.Request metadata = _metaData;
+ return metadata == null ? null : metadata.getURI().getQuery();
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getReader()
+ */
+ @Override
+ public BufferedReader getReader() throws IOException
+ {
+ if (_inputState != INPUT_NONE && _inputState != INPUT_READER)
+ throw new IllegalStateException("STREAMED");
+
+ if (_inputState == INPUT_READER)
+ return _reader;
+
+ String encoding = getCharacterEncoding();
+ if (encoding == null)
+ encoding = StringUtil.__ISO_8859_1;
+
+ if (_reader == null || !encoding.equalsIgnoreCase(_readerEncoding))
+ {
+ final ServletInputStream in = getInputStream();
+ _readerEncoding = encoding;
+ _reader = new BufferedReader(new InputStreamReader(in, encoding))
+ {
+ @Override
+ public void close() throws IOException
+ {
+ in.close();
+ }
+ };
+ }
+ _inputState = INPUT_READER;
+ return _reader;
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getRealPath(java.lang.String)
+ */
+ @Override
+ public String getRealPath(String path)
+ {
+ if (_context == null)
+ return null;
+ return _context.getRealPath(path);
+ }
+
+ /**
+ * Access the underlying Remote {@link InetSocketAddress} for this request.
+ *
+ * @return the remote {@link InetSocketAddress} for this request, or null if the request has no remote (see {@link ServletRequest#getRemoteAddr()} for
+ * conditions that result in no remote address)
+ */
+ public InetSocketAddress getRemoteInetSocketAddress()
+ {
+ InetSocketAddress remote = _remote;
+ if (remote == null)
+ remote = _channel.getRemoteAddress();
+
+ return remote;
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getRemoteAddr()
+ */
+ @Override
+ public String getRemoteAddr()
+ {
+ InetSocketAddress remote = _remote;
+ if (remote == null)
+ remote = _channel.getRemoteAddress();
+ if (remote == null)
+ return "";
+
+ InetAddress address = remote.getAddress();
+ String result = address == null
+ ? remote.getHostString()
+ : address.getHostAddress();
+
+ return formatAddrOrHost(result);
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getRemoteHost()
+ */
+ @Override
+ public String getRemoteHost()
+ {
+ InetSocketAddress remote = _remote;
+ if (remote == null)
+ remote = _channel.getRemoteAddress();
+ if (remote == null)
+ return "";
+
+ // We want the URI host, so add IPv6 brackets if necessary.
+ return formatAddrOrHost(remote.getHostString());
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getRemotePort()
+ */
+ @Override
+ public int getRemotePort()
+ {
+ InetSocketAddress remote = _remote;
+ if (remote == null)
+ remote = _channel.getRemoteAddress();
+ return remote == null ? 0 : remote.getPort();
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getRemoteUser()
+ */
+ @Override
+ public String getRemoteUser()
+ {
+ Principal p = getUserPrincipal();
+ if (p == null)
+ return null;
+ return p.getName();
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getRequestDispatcher(java.lang.String)
+ */
+ @Override
+ public RequestDispatcher getRequestDispatcher(String path)
+ {
+ // path is encoded, potentially with query
+
+ path = URIUtil.compactPath(path);
+
+ if (path == null || _context == null)
+ return null;
+
+ // handle relative path
+ if (!path.startsWith("/"))
+ {
+ String relTo = URIUtil.addPaths(_servletPath, _pathInfo);
+ int slash = relTo.lastIndexOf("/");
+ if (slash > 1)
+ relTo = relTo.substring(0, slash + 1);
+ else
+ relTo = "/";
+ path = URIUtil.addPaths(relTo, path);
+ }
+
+ return _context.getRequestDispatcher(path);
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getRequestedSessionId()
+ */
+ @Override
+ public String getRequestedSessionId()
+ {
+ return _requestedSessionId;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getRequestURI()
+ */
+ @Override
+ public String getRequestURI()
+ {
+ MetaData.Request metadata = _metaData;
+ return (metadata == null) ? null : metadata.getURI().getPath();
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getRequestURL()
+ */
+ @Override
+ public StringBuffer getRequestURL()
+ {
+ final StringBuffer url = new StringBuffer(128);
+ URIUtil.appendSchemeHostPort(url, getScheme(), getServerName(), getServerPort());
+ url.append(getRequestURI());
+ return url;
+ }
+
+ public Response getResponse()
+ {
+ return _channel.getResponse();
+ }
+
+ /**
+ * Reconstructs the URL the client used to make the request. The returned URL contains a protocol, server name, port number, and, but it does not include a
+ * path.
+ * <p>
+ * Because this method returns a <code>StringBuffer</code>, not a string, you can modify the URL easily, for example, to append path and query parameters.
+ *
+ * This method is useful for creating redirect messages and for reporting errors.
+ *
+ * @return "scheme://host:port"
+ */
+ public StringBuilder getRootURL()
+ {
+ StringBuilder url = new StringBuilder(128);
+ URIUtil.appendSchemeHostPort(url, getScheme(), getServerName(), getServerPort());
+ return url;
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getScheme()
+ */
+ @Override
+ public String getScheme()
+ {
+ MetaData.Request metadata = _metaData;
+ String scheme = metadata == null ? null : metadata.getURI().getScheme();
+ return scheme == null ? HttpScheme.HTTP.asString() : scheme;
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getServerName()
+ */
+ @Override
+ public String getServerName()
+ {
+ MetaData.Request metadata = _metaData;
+ String name = metadata == null ? null : formatAddrOrHost(metadata.getURI().getHost());
+
+ // Return already determined host
+ if (name != null)
+ return name;
+
+ return findServerName();
+ }
+
+ private String findServerName()
+ {
+ MetaData.Request metadata = _metaData;
+ // Return host from header field
+ HttpField host = metadata == null ? null : metadata.getFields().getField(HttpHeader.HOST);
+ if (host != null)
+ {
+ if (!(host instanceof HostPortHttpField) && host.getValue() != null && !host.getValue().isEmpty())
+ host = new HostPortHttpField(host.getValue());
+ if (host instanceof HostPortHttpField)
+ {
+ HostPortHttpField authority = (HostPortHttpField)host;
+ metadata.getURI().setAuthority(authority.getHost(), authority.getPort());
+ return formatAddrOrHost(authority.getHost());
+ }
+ }
+
+ // Return host from connection
+ String name = getLocalName();
+ if (name != null)
+ return formatAddrOrHost(name);
+
+ // Return the local host
+ try
+ {
+ return formatAddrOrHost(InetAddress.getLocalHost().getHostAddress());
+ }
+ catch (UnknownHostException e)
+ {
+ LOG.ignore(e);
+ }
+ return null;
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getServerPort()
+ */
+ @Override
+ public int getServerPort()
+ {
+ MetaData.Request metadata = _metaData;
+ HttpURI uri = metadata == null ? null : metadata.getURI();
+ int port = (uri == null || uri.getHost() == null) ? findServerPort() : uri.getPort();
+
+ // If no port specified, return the default port for the scheme
+ if (port <= 0)
+ {
+ if (getScheme().equalsIgnoreCase(URIUtil.HTTPS))
+ return 443;
+ return 80;
+ }
+
+ // return a specific port
+ return port;
+ }
+
+ private int findServerPort()
+ {
+ MetaData.Request metadata = _metaData;
+ // Return host from header field
+ HttpField host = metadata == null ? null : metadata.getFields().getField(HttpHeader.HOST);
+ if (host != null)
+ {
+ // TODO is this needed now?
+ HostPortHttpField authority = (host instanceof HostPortHttpField)
+ ? ((HostPortHttpField)host)
+ : new HostPortHttpField(host.getValue());
+ metadata.getURI().setAuthority(authority.getHost(), authority.getPort());
+ return authority.getPort();
+ }
+
+ // Return host from connection
+ if (_channel != null)
+ return getLocalPort();
+
+ return -1;
+ }
+
+ @Override
+ public ServletContext getServletContext()
+ {
+ return _context;
+ }
+
+ public String getServletName()
+ {
+ if (_scope != null)
+ return _scope.getName();
+ return null;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getServletPath()
+ */
+ @Override
+ public String getServletPath()
+ {
+ if (_servletPath == null)
+ _servletPath = "";
+ return _servletPath;
+ }
+
+ public ServletResponse getServletResponse()
+ {
+ return _channel.getResponse();
+ }
+
+ @Override
+ public String changeSessionId()
+ {
+ HttpSession session = getSession(false);
+ if (session == null)
+ throw new IllegalStateException("No session");
+
+ if (session instanceof Session)
+ {
+ Session s = ((Session)session);
+ s.renewId(this);
+ if (getRemoteUser() != null)
+ s.setAttribute(Session.SESSION_CREATED_SECURE, Boolean.TRUE);
+ if (s.isIdChanged() && _sessionHandler.isUsingCookies())
+ _channel.getResponse().replaceCookie(_sessionHandler.getSessionCookie(s, getContextPath(), isSecure()));
+ }
+
+ return session.getId();
+ }
+
+ /**
+ * Called when the request is fully finished being handled.
+ * For every session in any context that the session has
+ * accessed, ensure that the session is completed.
+ */
+ public void onCompleted()
+ {
+ if (_sessions != null)
+ {
+ for (Session s:_sessions)
+ leaveSession(s);
+ }
+ }
+
+ /**
+ * Called when a response is about to be committed, ie sent
+ * back to the client
+ */
+ public void onResponseCommit()
+ {
+ if (_sessions != null)
+ {
+ for (Session s:_sessions)
+ commitSession(s);
+ }
+ }
+
+ /**
+ * Find a session that this request has already entered for the
+ * given SessionHandler
+ *
+ * @param sessionHandler the SessionHandler (ie context) to check
+ * @return
+ */
+ public HttpSession getSession(SessionHandler sessionHandler)
+ {
+ if (_sessions == null || _sessions.size() == 0 || sessionHandler == null)
+ return null;
+
+ HttpSession session = null;
+
+ for (HttpSession s:_sessions)
+ {
+ Session ss = Session.class.cast(s);
+ if (sessionHandler == ss.getSessionHandler())
+ {
+ session = s;
+ if (ss.isValid())
+ return session;
+ }
+ }
+ return session;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getSession()
+ */
+ @Override
+ public HttpSession getSession()
+ {
+ return getSession(true);
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getSession(boolean)
+ */
+ @Override
+ public HttpSession getSession(boolean create)
+ {
+ if (_session != null)
+ {
+ if (_sessionHandler != null && !_sessionHandler.isValid(_session))
+ _session = null;
+ else
+ return _session;
+ }
+
+ if (!create)
+ return null;
+
+ if (getResponse().isCommitted())
+ throw new IllegalStateException("Response is committed");
+
+ if (_sessionHandler == null)
+ throw new IllegalStateException("No SessionManager");
+
+ _session = _sessionHandler.newHttpSession(this);
+ if (_session == null)
+ throw new IllegalStateException("Create session failed");
+
+ HttpCookie cookie = _sessionHandler.getSessionCookie(_session, getContextPath(), isSecure());
+ if (cookie != null)
+ _channel.getResponse().replaceCookie(cookie);
+
+ return _session;
+ }
+
+ /**
+ * @return Returns the sessionManager.
+ */
+ public SessionHandler getSessionHandler()
+ {
+ return _sessionHandler;
+ }
+
+ /**
+ * Get Request TimeStamp
+ *
+ * @return The time that the request was received.
+ */
+ public long getTimeStamp()
+ {
+ return _timeStamp;
+ }
+
+ /**
+ * @return Returns the uri.
+ */
+ public HttpURI getHttpURI()
+ {
+ MetaData.Request metadata = _metaData;
+ return metadata == null ? null : metadata.getURI();
+ }
+
+ /**
+ * @return Returns the original uri passed in metadata before customization/rewrite
+ */
+ public String getOriginalURI()
+ {
+ return _originalURI;
+ }
+
+ /**
+ * @param uri the URI to set
+ */
+ public void setHttpURI(HttpURI uri)
+ {
+ MetaData.Request metadata = _metaData;
+ metadata.setURI(uri);
+ }
+
+ public UserIdentity getUserIdentity()
+ {
+ if (_authentication instanceof Authentication.Deferred)
+ setAuthentication(((Authentication.Deferred)_authentication).authenticate(this));
+
+ if (_authentication instanceof Authentication.User)
+ return ((Authentication.User)_authentication).getUserIdentity();
+ return null;
+ }
+
+ /**
+ * @return The resolved user Identity, which may be null if the {@link Authentication} is not {@link Authentication.User} (eg.
+ * {@link Authentication.Deferred}).
+ */
+ public UserIdentity getResolvedUserIdentity()
+ {
+ if (_authentication instanceof Authentication.User)
+ return ((Authentication.User)_authentication).getUserIdentity();
+ return null;
+ }
+
+ public UserIdentity.Scope getUserIdentityScope()
+ {
+ return _scope;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#getUserPrincipal()
+ */
+ @Override
+ public Principal getUserPrincipal()
+ {
+ if (_authentication instanceof Authentication.Deferred)
+ setAuthentication(((Authentication.Deferred)_authentication).authenticate(this));
+
+ if (_authentication instanceof Authentication.User)
+ {
+ UserIdentity user = ((Authentication.User)_authentication).getUserIdentity();
+ return user.getUserPrincipal();
+ }
+
+ return null;
+ }
+
+ public boolean isHandled()
+ {
+ return _handled;
+ }
+
+ @Override
+ public boolean isAsyncStarted()
+ {
+ return getHttpChannelState().isAsyncStarted();
+ }
+
+ @Override
+ public boolean isAsyncSupported()
+ {
+ return _asyncNotSupportedSource == null;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromCookie()
+ */
+ @Override
+ public boolean isRequestedSessionIdFromCookie()
+ {
+ return _requestedSessionId != null && _requestedSessionIdFromCookie;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromUrl()
+ */
+ @Override
+ public boolean isRequestedSessionIdFromUrl()
+ {
+ return _requestedSessionId != null && !_requestedSessionIdFromCookie;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromURL()
+ */
+ @Override
+ public boolean isRequestedSessionIdFromURL()
+ {
+ return _requestedSessionId != null && !_requestedSessionIdFromCookie;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdValid()
+ */
+ @Override
+ public boolean isRequestedSessionIdValid()
+ {
+ if (_requestedSessionId == null)
+ return false;
+
+ HttpSession session = getSession(false);
+ return (session != null && _sessionHandler.getSessionIdManager().getId(_requestedSessionId).equals(_sessionHandler.getId(session)));
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#isSecure()
+ */
+ @Override
+ public boolean isSecure()
+ {
+ return _secure;
+ }
+
+ public void setSecure(boolean secure)
+ {
+ _secure = secure;
+ }
+
+ /*
+ * @see javax.servlet.http.HttpServletRequest#isUserInRole(java.lang.String)
+ */
+ @Override
+ public boolean isUserInRole(String role)
+ {
+ if (_authentication instanceof Authentication.Deferred)
+ setAuthentication(((Authentication.Deferred)_authentication).authenticate(this));
+
+ if (_authentication instanceof Authentication.User)
+ return ((Authentication.User)_authentication).isUserInRole(_scope, role);
+ return false;
+ }
+
+ /**
+ * @param request the Request metadata
+ */
+ public void setMetaData(org.eclipse.jetty.http.MetaData.Request request)
+ {
+ if (_metaData == null && _input != null && _channel != null)
+ {
+ _input.recycle();
+ _channel.getResponse().getHttpOutput().reopen();
+ }
+ _metaData = request;
+
+ setMethod(request.getMethod());
+ HttpURI uri = request.getURI();
+ if (uri.hasViolations())
+ {
+ // Replaced in jetty-10 with URICompliance from the HttpConfiguration.
+ Connection connection = _channel == null ? null : _channel.getConnection();
+ HttpCompliance compliance = connection instanceof HttpConnection
+ ? ((HttpConnection)connection).getHttpCompliance()
+ : _channel != null ? _channel.getConnector().getBean(HttpCompliance.class) : null;
+
+ String badMessage = HttpCompliance.checkUriCompliance(compliance, uri);
+ if (badMessage != null)
+ throw new BadMessageException(badMessage);
+ }
+
+ _originalURI = uri.isAbsolute() && request.getHttpVersion() != HttpVersion.HTTP_2 ? uri.toString() : uri.getPathQuery();
+
+ String encoded = uri.getPath();
+ String path;
+ if (encoded == null)
+ {
+ path = uri.isAbsolute() ? "/" : null;
+ uri.setPath(path);
+ }
+ else if (encoded.startsWith("/"))
+ {
+ path = (encoded.length() == 1) ? "/" : uri.getDecodedPath();
+ }
+ else if ("*".equals(encoded) || HttpMethod.CONNECT.is(getMethod()))
+ {
+ path = encoded;
+ }
+ else
+ {
+ path = null;
+ }
+
+ if (path == null || path.isEmpty())
+ {
+ setPathInfo(encoded == null ? "" : encoded);
+ throw new BadMessageException(400, "Bad URI");
+ }
+ setPathInfo(path);
+ }
+
+ public org.eclipse.jetty.http.MetaData.Request getMetaData()
+ {
+ return _metaData;
+ }
+
+ public boolean hasMetaData()
+ {
+ return _metaData != null;
+ }
+
+ protected void recycle()
+ {
+ if (_context != null)
+ throw new IllegalStateException("Request in context!");
+ if (_reader != null && _inputState == INPUT_READER)
+ {
+ try
+ {
+ int r = _reader.read();
+ while (r != -1)
+ {
+ r = _reader.read();
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ _reader = null;
+ _readerEncoding = null;
+ }
+ }
+
+ getHttpChannelState().recycle();
+ _requestAttributeListeners.clear();
+ // Defer _input.recycle() until setMetaData on next request, TODO replace with recycle and reopen in 10
+ _metaData = null;
+ _originalURI = null;
+ _contextPath = null;
+ _servletPath = null;
+ _pathInfo = null;
+ _asyncNotSupportedSource = null;
+ _secure = false;
+ _newContext = false;
+ _cookiesExtracted = false;
+ _handled = false;
+ _contentParamsExtracted = false;
+ _requestedSessionIdFromCookie = false;
+ _attributes = Attributes.unwrap(_attributes);
+ if (_attributes != null)
+ {
+ if (ServletAttributes.class.equals(_attributes.getClass()))
+ _attributes.clearAttributes();
+ else
+ _attributes = null;
+ }
+ setAuthentication(Authentication.NOT_CHECKED);
+ _contentType = null;
+ _characterEncoding = null;
+ _context = null;
+ _errorContext = null;
+ if (_cookies != null)
+ _cookies.reset();
+ _dispatcherType = null;
+ _inputState = INPUT_NONE;
+ // _reader can be reused
+ _queryParameters = null;
+ _contentParameters = null;
+ _parameters = null;
+ _queryEncoding = null;
+ _remote = null;
+ _requestedSessionId = null;
+ _scope = null;
+ _session = null;
+ _sessionHandler = null;
+ _timeStamp = 0;
+ _multiParts = null;
+ if (_async != null)
+ _async.reset();
+ _async = null;
+ _sessions = null;
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#removeAttribute(java.lang.String)
+ */
+ @Override
+ public void removeAttribute(String name)
+ {
+ Object oldValue = _attributes == null ? null : _attributes.getAttribute(name);
+
+ if (_attributes != null)
+ _attributes.removeAttribute(name);
+
+ if (oldValue != null && !_requestAttributeListeners.isEmpty())
+ {
+ final ServletRequestAttributeEvent event = new ServletRequestAttributeEvent(_context, this, name, oldValue);
+ for (ServletRequestAttributeListener listener : _requestAttributeListeners)
+ {
+ listener.attributeRemoved(event);
+ }
+ }
+ }
+
+ public void removeEventListener(final EventListener listener)
+ {
+ _requestAttributeListeners.remove(listener);
+ }
+
+ public void setAsyncSupported(boolean supported, Object source)
+ {
+ _asyncNotSupportedSource = supported ? null : (source == null ? "unknown" : source);
+ }
+
+ /*
+ * Set a request attribute. if the attribute name is "org.eclipse.jetty.server.server.Request.queryEncoding" then the value is also passed in a call to
+ * {@link #setQueryEncoding}.
+ *
+ * @see javax.servlet.ServletRequest#setAttribute(java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void setAttribute(String name, Object value)
+ {
+ Object oldValue = _attributes == null ? null : _attributes.getAttribute(name);
+
+ if ("org.eclipse.jetty.server.Request.queryEncoding".equals(name))
+ setQueryEncoding(value == null ? null : value.toString());
+ else if ("org.eclipse.jetty.server.sendContent".equals(name))
+ LOG.warn("Deprecated: org.eclipse.jetty.server.sendContent");
+
+ if (_attributes == null)
+ _attributes = new ServletAttributes();
+ _attributes.setAttribute(name, value);
+
+ if (!_requestAttributeListeners.isEmpty())
+ {
+ final ServletRequestAttributeEvent event = new ServletRequestAttributeEvent(_context, this, name, oldValue == null ? value : oldValue);
+ for (ServletRequestAttributeListener l : _requestAttributeListeners)
+ {
+ if (oldValue == null)
+ l.attributeAdded(event);
+ else if (value == null)
+ l.attributeRemoved(event);
+ else
+ l.attributeReplaced(event);
+ }
+ }
+ }
+
+ public void setAttributes(Attributes attributes)
+ {
+ _attributes = attributes;
+ }
+
+ public void setAsyncAttributes()
+ {
+ // Return if we have been async dispatched before.
+ if (getAttribute(AsyncContext.ASYNC_REQUEST_URI) != null)
+ return;
+
+ String requestURI;
+ String contextPath;
+ String servletPath;
+ String pathInfo;
+ String queryString;
+
+ // Have we been forwarded before?
+ requestURI = (String)getAttribute(RequestDispatcher.FORWARD_REQUEST_URI);
+ if (requestURI != null)
+ {
+ contextPath = (String)getAttribute(RequestDispatcher.FORWARD_CONTEXT_PATH);
+ servletPath = (String)getAttribute(RequestDispatcher.FORWARD_SERVLET_PATH);
+ pathInfo = (String)getAttribute(RequestDispatcher.FORWARD_PATH_INFO);
+ queryString = (String)getAttribute(RequestDispatcher.FORWARD_QUERY_STRING);
+ }
+ else
+ {
+ requestURI = getRequestURI();
+ contextPath = getContextPath();
+ servletPath = getServletPath();
+ pathInfo = getPathInfo();
+ queryString = getQueryString();
+ }
+
+ // Unwrap the _attributes to get the base attributes instance.
+ Attributes baseAttributes;
+ if (_attributes == null)
+ _attributes = baseAttributes = new ServletAttributes();
+ else
+ baseAttributes = Attributes.unwrap(_attributes);
+
+ if (baseAttributes instanceof ServletAttributes)
+ {
+ // Set the AsyncAttributes on the ServletAttributes.
+ ServletAttributes servletAttributes = (ServletAttributes)baseAttributes;
+ servletAttributes.setAsyncAttributes(requestURI, contextPath, servletPath, pathInfo, queryString);
+ }
+ else
+ {
+ // If ServletAttributes has been replaced just set them on the top level Attributes.
+ AsyncAttributes.applyAsyncAttributes(_attributes, requestURI, contextPath, servletPath, pathInfo, queryString);
+ }
+ }
+
+ /**
+ * Set the authentication.
+ *
+ * @param authentication the authentication to set
+ */
+ public void setAuthentication(Authentication authentication)
+ {
+ _authentication = authentication;
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
+ */
+ @Override
+ public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException
+ {
+ if (_inputState != INPUT_NONE)
+ return;
+
+ _characterEncoding = encoding;
+
+ // check encoding is supported
+ if (!StringUtil.isUTF8(encoding))
+ {
+ try
+ {
+ Charset.forName(encoding);
+ }
+ catch (UnsupportedCharsetException e)
+ {
+ throw new UnsupportedEncodingException(e.getMessage());
+ }
+ }
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
+ */
+ public void setCharacterEncodingUnchecked(String encoding)
+ {
+ _characterEncoding = encoding;
+ }
+
+ /*
+ * @see javax.servlet.ServletRequest#getContentType()
+ */
+ public void setContentType(String contentType)
+ {
+ _contentType = contentType;
+ }
+
+ /**
+ * Set request context
+ *
+ * @param context context object
+ */
+ public void setContext(Context context)
+ {
+ _newContext = _context != context;
+ if (context == null)
+ _context = null;
+ else
+ {
+ _context = context;
+ _errorContext = context;
+ }
+ }
+
+ /**
+ * @return True if this is the first call of <code>takeNewContext()</code> since the last
+ * {@link #setContext(org.eclipse.jetty.server.handler.ContextHandler.Context)} call.
+ */
+ public boolean takeNewContext()
+ {
+ boolean nc = _newContext;
+ _newContext = false;
+ return nc;
+ }
+
+ /**
+ * Sets the "context path" for this request
+ *
+ * @param contextPath the context path for this request
+ * @see HttpServletRequest#getContextPath()
+ */
+ public void setContextPath(String contextPath)
+ {
+ _contextPath = contextPath;
+ }
+
+ /**
+ * @param cookies The cookies to set.
+ */
+ public void setCookies(Cookie[] cookies)
+ {
+ if (_cookies == null)
+ _cookies = new CookieCutter(getHttpChannel().getHttpConfiguration().getRequestCookieCompliance());
+ _cookies.setCookies(cookies);
+ }
+
+ public void setDispatcherType(DispatcherType type)
+ {
+ _dispatcherType = type;
+ }
+
+ public void setHandled(boolean h)
+ {
+ _handled = h;
+ }
+
+ /**
+ * @param method The method to set.
+ */
+ public void setMethod(String method)
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata != null)
+ metadata.setMethod(method);
+ }
+
+ public void setHttpVersion(HttpVersion version)
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata != null)
+ metadata.setHttpVersion(version);
+ }
+
+ public boolean isHead()
+ {
+ MetaData.Request metadata = _metaData;
+ return metadata != null && HttpMethod.HEAD.is(metadata.getMethod());
+ }
+
+ /**
+ * @param pathInfo The pathInfo to set.
+ */
+ public void setPathInfo(String pathInfo)
+ {
+ _pathInfo = pathInfo;
+ }
+
+ /**
+ * Set the character encoding used for the query string. This call will effect the return of getQueryString and getParamaters. It must be called before any
+ * getParameter methods.
+ *
+ * The request attribute "org.eclipse.jetty.server.Request.queryEncoding" may be set as an alternate method of calling setQueryEncoding.
+ *
+ * @param queryEncoding the URI query character encoding
+ */
+ public void setQueryEncoding(String queryEncoding)
+ {
+ _queryEncoding = queryEncoding;
+ }
+
+ /**
+ * @param queryString The queryString to set.
+ */
+ public void setQueryString(String queryString)
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata != null)
+ metadata.getURI().setQuery(queryString);
+ _queryEncoding = null; //assume utf-8
+ }
+
+ /**
+ * @param addr The address to set.
+ */
+ public void setRemoteAddr(InetSocketAddress addr)
+ {
+ _remote = addr;
+ }
+
+ /**
+ * @param requestedSessionId The requestedSessionId to set.
+ */
+ public void setRequestedSessionId(String requestedSessionId)
+ {
+ _requestedSessionId = requestedSessionId;
+ }
+
+ /**
+ * @param requestedSessionIdCookie The requestedSessionIdCookie to set.
+ */
+ public void setRequestedSessionIdFromCookie(boolean requestedSessionIdCookie)
+ {
+ _requestedSessionIdFromCookie = requestedSessionIdCookie;
+ }
+
+ public void setURIPathQuery(String requestURI)
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata != null)
+ metadata.getURI().setPathQuery(requestURI);
+ }
+
+ /**
+ * @param scheme The scheme to set.
+ */
+ public void setScheme(String scheme)
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata != null)
+ metadata.getURI().setScheme(scheme);
+ }
+
+ /**
+ * @param host The host to set.
+ * @param port the port to set
+ */
+ public void setAuthority(String host, int port)
+ {
+ MetaData.Request metadata = _metaData;
+ if (metadata != null)
+ metadata.getURI().setAuthority(host, port);
+ }
+
+ /**
+ * @param servletPath The servletPath to set.
+ */
+ public void setServletPath(String servletPath)
+ {
+ _servletPath = servletPath;
+ }
+
+ /**
+ * @param session The session to set.
+ */
+ public void setSession(HttpSession session)
+ {
+ _session = session;
+ }
+
+ /**
+ * @param sessionHandler The SessionHandler to set.
+ */
+ public void setSessionHandler(SessionHandler sessionHandler)
+ {
+ _sessionHandler = sessionHandler;
+ }
+
+ public void setTimeStamp(long ts)
+ {
+ _timeStamp = ts;
+ }
+
+ public void setUserIdentityScope(UserIdentity.Scope scope)
+ {
+ _scope = scope;
+ }
+
+ @Override
+ public AsyncContext startAsync() throws IllegalStateException
+ {
+ if (_asyncNotSupportedSource != null)
+ throw new IllegalStateException("!asyncSupported: " + _asyncNotSupportedSource);
+ HttpChannelState state = getHttpChannelState();
+ if (_async == null)
+ _async = new AsyncContextState(state);
+ AsyncContextEvent event = new AsyncContextEvent(_context, _async, state, this, this, getResponse());
+ state.startAsync(event);
+ return _async;
+ }
+
+ @Override
+ public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException
+ {
+ if (_asyncNotSupportedSource != null)
+ throw new IllegalStateException("!asyncSupported: " + _asyncNotSupportedSource);
+ HttpChannelState state = getHttpChannelState();
+ if (_async == null)
+ _async = new AsyncContextState(state);
+ AsyncContextEvent event = new AsyncContextEvent(_context, _async, state, this, servletRequest, servletResponse);
+ event.setDispatchContext(getServletContext());
+
+ String uri = unwrap(servletRequest).getRequestURI();
+ if (_contextPath != null && uri.startsWith(_contextPath))
+ uri = uri.substring(_contextPath.length());
+ else
+ // TODO probably need to strip encoded context from requestURI, but will do this for now:
+ uri = URIUtil.encodePath(URIUtil.addPaths(getServletPath(), getPathInfo()));
+
+ event.setDispatchPath(uri);
+ state.startAsync(event);
+ return _async;
+ }
+
+ public static HttpServletRequest unwrap(ServletRequest servletRequest)
+ {
+ if (servletRequest instanceof HttpServletRequestWrapper)
+ {
+ return (HttpServletRequestWrapper)servletRequest;
+ }
+ if (servletRequest instanceof ServletRequestWrapper)
+ {
+ return unwrap(((ServletRequestWrapper)servletRequest).getRequest());
+ }
+ return ((HttpServletRequest)servletRequest);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s%s%s %s%s@%x",
+ getClass().getSimpleName(),
+ _handled ? "[" : "(",
+ getMethod(),
+ getHttpURI(),
+ _handled ? "]" : ")",
+ hashCode());
+ }
+
+ @Override
+ public boolean authenticate(HttpServletResponse response) throws IOException, ServletException
+ {
+ if (_authentication instanceof Authentication.Deferred)
+ {
+ setAuthentication(((Authentication.Deferred)_authentication).authenticate(this, response));
+ return !(_authentication instanceof Authentication.ResponseSent);
+ }
+ response.sendError(HttpStatus.UNAUTHORIZED_401);
+ return false;
+ }
+
+ @Override
+ public Part getPart(String name) throws IOException, ServletException
+ {
+ getParts();
+ return _multiParts.getPart(name);
+ }
+
+ @Override
+ public Collection<Part> getParts() throws IOException, ServletException
+ {
+ String contentType = getContentType();
+ if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpFields.valueParameters(contentType, null)))
+ throw new ServletException("Unsupported Content-Type [" + contentType + "], expected [multipart/form-data]");
+ return getParts(null);
+ }
+
+ private Collection<Part> getParts(MultiMap<String> params) throws IOException
+ {
+ if (_multiParts == null)
+ _multiParts = (MultiParts)getAttribute(MULTIPARTS);
+
+ if (_multiParts == null)
+ {
+ MultipartConfigElement config = (MultipartConfigElement)getAttribute(MULTIPART_CONFIG_ELEMENT);
+ if (config == null)
+ throw new IllegalStateException("No multipart config for servlet");
+
+ _multiParts = newMultiParts(config);
+ setAttribute(MULTIPARTS, _multiParts);
+ Collection<Part> parts = _multiParts.getParts();
+
+ String formCharset = null;
+ Part charsetPart = _multiParts.getPart("_charset_");
+ if (charsetPart != null)
+ {
+ try (InputStream is = charsetPart.getInputStream())
+ {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ IO.copy(is, os);
+ formCharset = new String(os.toByteArray(), StandardCharsets.UTF_8);
+ }
+ }
+
+ /*
+ Select Charset to use for this part. (NOTE: charset behavior is for the part value only and not the part header/field names)
+ 1. Use the part specific charset as provided in that part's Content-Type header; else
+ 2. Use the overall default charset. Determined by:
+ a. if part name _charset_ exists, use that part's value.
+ b. if the request.getCharacterEncoding() returns a value, use that.
+ (note, this can be either from the charset field on the request Content-Type
+ header, or from a manual call to request.setCharacterEncoding())
+ c. use utf-8.
+ */
+ Charset defaultCharset;
+ if (formCharset != null)
+ defaultCharset = Charset.forName(formCharset);
+ else if (getCharacterEncoding() != null)
+ defaultCharset = Charset.forName(getCharacterEncoding());
+ else
+ defaultCharset = StandardCharsets.UTF_8;
+
+ ByteArrayOutputStream os = null;
+ for (Part p : parts)
+ {
+ if (p.getSubmittedFileName() == null)
+ {
+ // Servlet Spec 3.0 pg 23, parts without filename must be put into params.
+ String charset = null;
+ if (p.getContentType() != null)
+ charset = MimeTypes.getCharsetFromContentType(p.getContentType());
+
+ try (InputStream is = p.getInputStream())
+ {
+ if (os == null)
+ os = new ByteArrayOutputStream();
+ IO.copy(is, os);
+
+ String content = new String(os.toByteArray(), charset == null ? defaultCharset : Charset.forName(charset));
+ if (_contentParameters == null)
+ _contentParameters = params == null ? new MultiMap<>() : params;
+ _contentParameters.add(p.getName(), content);
+ }
+ os.reset();
+ }
+ }
+ }
+
+ return _multiParts.getParts();
+ }
+
+ private MultiParts newMultiParts(MultipartConfigElement config) throws IOException
+ {
+ MultiPartFormDataCompliance compliance = getHttpChannel().getHttpConfiguration().getMultipartFormDataCompliance();
+ if (LOG.isDebugEnabled())
+ LOG.debug("newMultiParts {} {}", compliance, this);
+
+ switch (compliance)
+ {
+ case RFC7578:
+ return new MultiParts.MultiPartsHttpParser(getInputStream(), getContentType(), config,
+ (_context != null ? (File)_context.getAttribute("javax.servlet.context.tempdir") : null), this);
+
+ case LEGACY:
+ default:
+ return new MultiParts.MultiPartsUtilParser(getInputStream(), getContentType(), config,
+ (_context != null ? (File)_context.getAttribute("javax.servlet.context.tempdir") : null), this);
+ }
+ }
+
+ @Override
+ public void login(String username, String password) throws ServletException
+ {
+ if (_authentication instanceof Authentication.LoginAuthentication)
+ {
+ Authentication auth = ((Authentication.LoginAuthentication)_authentication).login(username, password, this);
+ if (auth == null)
+ throw new Authentication.Failed("Authentication failed for username '" + username + "'");
+ else
+ _authentication = auth;
+ }
+ else
+ {
+ throw new Authentication.Failed("Authenticated failed for username '" + username + "'. Already authenticated as " + _authentication);
+ }
+ }
+
+ @Override
+ public void logout() throws ServletException
+ {
+ if (_authentication instanceof Authentication.LogoutAuthentication)
+ _authentication = ((Authentication.LogoutAuthentication)_authentication).logout(this);
+ }
+
+ public void mergeQueryParameters(String oldQuery, String newQuery, boolean updateQueryString)
+ {
+ MultiMap<String> newQueryParams = null;
+ // Have to assume ENCODING because we can't know otherwise.
+ if (newQuery != null)
+ {
+ newQueryParams = new MultiMap<>();
+ UrlEncoded.decodeTo(newQuery, newQueryParams, UrlEncoded.ENCODING);
+ }
+
+ MultiMap<String> oldQueryParams = _queryParameters;
+ if (oldQueryParams == null && oldQuery != null)
+ {
+ oldQueryParams = new MultiMap<>();
+ try
+ {
+ UrlEncoded.decodeTo(oldQuery, oldQueryParams, getQueryEncoding());
+ }
+ catch (Throwable ex)
+ {
+ throw new BadMessageException(400, "Bad query encoding", ex);
+ }
+ }
+
+ MultiMap<String> mergedQueryParams;
+ if (newQueryParams == null || newQueryParams.size() == 0)
+ mergedQueryParams = oldQueryParams == null ? NO_PARAMS : oldQueryParams;
+ else if (oldQueryParams == null || oldQueryParams.size() == 0)
+ mergedQueryParams = newQueryParams == null ? NO_PARAMS : newQueryParams;
+ else
+ {
+ // Parameters values are accumulated.
+ mergedQueryParams = new MultiMap<>(newQueryParams);
+ mergedQueryParams.addAllValues(oldQueryParams);
+ }
+
+ setQueryParameters(mergedQueryParams);
+ resetParameters();
+
+ if (updateQueryString)
+ {
+ if (newQuery == null)
+ setQueryString(oldQuery);
+ else if (oldQuery == null)
+ setQueryString(newQuery);
+ else
+ {
+ // Build the new merged query string, parameters in the
+ // new query string hide parameters in the old query string.
+ StringBuilder mergedQuery = new StringBuilder();
+ if (newQuery != null)
+ mergedQuery.append(newQuery);
+ for (Map.Entry<String, List<String>> entry : mergedQueryParams.entrySet())
+ {
+ if (newQueryParams != null && newQueryParams.containsKey(entry.getKey()))
+ continue;
+ for (String value : entry.getValue())
+ {
+ if (mergedQuery.length() > 0)
+ mergedQuery.append("&");
+ URIUtil.encodePath(mergedQuery, entry.getKey());
+ mergedQuery.append('=');
+ URIUtil.encodePath(mergedQuery, value);
+ }
+ }
+ setQueryString(mergedQuery.toString());
+ }
+ }
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServletRequest#upgrade(java.lang.Class)
+ */
+ @Override
+ public <T extends HttpUpgradeHandler> T upgrade(Class<T> handlerClass) throws IOException, ServletException
+ {
+ throw new ServletException("HttpServletRequest.upgrade() not supported in Jetty");
+ }
+
+ private String formatAddrOrHost(String name)
+ {
+ return _channel == null ? HostPort.normalizeHost(name) : _channel.formatAddrOrHost(name);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/RequestLog.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/RequestLog.java
new file mode 100644
index 0000000..658728e
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/RequestLog.java
@@ -0,0 +1,70 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+
+import org.eclipse.jetty.server.handler.RequestLogHandler;
+
+/**
+ * A <code>RequestLog</code> can be attached to a {@link org.eclipse.jetty.server.handler.RequestLogHandler} to enable
+ * logging of requests/responses.
+ *
+ * @see RequestLogHandler#setRequestLog(RequestLog)
+ * @see Server#setRequestLog(RequestLog)
+ */
+public interface RequestLog
+{
+ /**
+ * @param request The request to log.
+ * @param response The response to log. Note that for some requests
+ * the response instance may not have been fully populated (Eg 400 bad request
+ * responses are sent without a servlet response object). Thus for basic
+ * log information it is best to consult {@link Response#getCommittedMetaData()}
+ * and {@link Response#getHttpChannel()} directly.
+ */
+ void log(Request request, Response response);
+
+ /**
+ * Writes the generated log string to a log sink
+ */
+ interface Writer
+ {
+ void write(String requestEntry) throws IOException;
+ }
+
+ class Collection implements RequestLog
+ {
+ private final RequestLog[] _logs;
+
+ public Collection(RequestLog... logs)
+ {
+ _logs = logs;
+ }
+
+ @Override
+ public void log(Request request, Response response)
+ {
+ for (RequestLog log : _logs)
+ {
+ log.log(request, response);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/RequestLogCollection.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/RequestLogCollection.java
new file mode 100644
index 0000000..f6bc835
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/RequestLogCollection.java
@@ -0,0 +1,48 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.ArrayList;
+
+import static java.util.Arrays.asList;
+
+class RequestLogCollection
+ implements RequestLog
+{
+ private final ArrayList<RequestLog> delegates;
+
+ public RequestLogCollection(RequestLog... requestLogs)
+ {
+ delegates = new ArrayList<>(asList(requestLogs));
+ }
+
+ public void add(RequestLog requestLog)
+ {
+ delegates.add(requestLog);
+ }
+
+ @Override
+ public void log(Request request, Response response)
+ {
+ for (RequestLog delegate : delegates)
+ {
+ delegate.log(request, response);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/RequestLogWriter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/RequestLogWriter.java
new file mode 100644
index 0000000..9a4fb74
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/RequestLogWriter.java
@@ -0,0 +1,257 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.TimeZone;
+
+import org.eclipse.jetty.util.RolloverFileOutputStream;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Writer which outputs pre-formatted request log strings to a file using {@link RolloverFileOutputStream}.
+ */
+@ManagedObject("Request Log writer which writes to file")
+public class RequestLogWriter extends AbstractLifeCycle implements RequestLog.Writer
+{
+ private static final Logger LOG = Log.getLogger(RequestLogWriter.class);
+
+ private String _filename;
+ private boolean _append;
+ private int _retainDays;
+ private boolean _closeOut;
+ private String _timeZone = "GMT";
+ private String _filenameDateFormat = null;
+ private transient OutputStream _out;
+ private transient OutputStream _fileOut;
+ private transient Writer _writer;
+
+ public RequestLogWriter()
+ {
+ this(null);
+ }
+
+ public RequestLogWriter(String filename)
+ {
+ setAppend(true);
+ setRetainDays(31);
+
+ if (filename != null)
+ setFilename(filename);
+ }
+
+ /**
+ * Set the output file name of the request log.
+ * The file name may be in the format expected by
+ * {@link RolloverFileOutputStream}.
+ *
+ * @param filename file name of the request log
+ */
+ public void setFilename(String filename)
+ {
+ if (filename != null)
+ {
+ filename = filename.trim();
+ if (filename.length() == 0)
+ filename = null;
+ }
+ _filename = filename;
+ }
+
+ /**
+ * Retrieve the output file name of the request log.
+ *
+ * @return file name of the request log
+ */
+ @ManagedAttribute("filename")
+ public String getFileName()
+ {
+ return _filename;
+ }
+
+ /**
+ * Retrieve the file name of the request log with the expanded
+ * date wildcard if the output is written to the disk using
+ * {@link RolloverFileOutputStream}.
+ *
+ * @return file name of the request log, or null if not applicable
+ */
+ @ManagedAttribute("dated filename")
+ public String getDatedFilename()
+ {
+ if (_fileOut instanceof RolloverFileOutputStream)
+ return ((RolloverFileOutputStream)_fileOut).getDatedFilename();
+ return null;
+ }
+
+ @Deprecated
+ protected boolean isEnabled()
+ {
+ return (_fileOut != null);
+ }
+
+ /**
+ * Set the number of days before rotated log files are deleted.
+ *
+ * @param retainDays number of days to keep a log file
+ */
+ public void setRetainDays(int retainDays)
+ {
+ _retainDays = retainDays;
+ }
+
+ /**
+ * Retrieve the number of days before rotated log files are deleted.
+ *
+ * @return number of days to keep a log file
+ */
+ @ManagedAttribute("number of days to keep a log file")
+ public int getRetainDays()
+ {
+ return _retainDays;
+ }
+
+ /**
+ * Set append to log flag.
+ *
+ * @param append true - request log file will be appended after restart,
+ * false - request log file will be overwritten after restart
+ */
+ public void setAppend(boolean append)
+ {
+ _append = append;
+ }
+
+ /**
+ * Retrieve append to log flag.
+ *
+ * @return value of the flag
+ */
+ @ManagedAttribute("if request log file will be appended after restart")
+ public boolean isAppend()
+ {
+ return _append;
+ }
+
+ /**
+ * Set the log file name date format.
+ *
+ * @param logFileDateFormat format string that is passed to {@link RolloverFileOutputStream}
+ * @see RolloverFileOutputStream#RolloverFileOutputStream(String, boolean, int, TimeZone, String, String)
+ */
+ public void setFilenameDateFormat(String logFileDateFormat)
+ {
+ _filenameDateFormat = logFileDateFormat;
+ }
+
+ /**
+ * Retrieve the file name date format string.
+ *
+ * @return the log File Date Format
+ */
+ @ManagedAttribute("log file name date format")
+ public String getFilenameDateFormat()
+ {
+ return _filenameDateFormat;
+ }
+
+ @Override
+ public void write(String requestEntry) throws IOException
+ {
+ synchronized (this)
+ {
+ if (_writer == null)
+ return;
+ _writer.write(requestEntry);
+ _writer.write(System.lineSeparator());
+ _writer.flush();
+ }
+ }
+
+ @Override
+ protected synchronized void doStart() throws Exception
+ {
+ if (_filename != null)
+ {
+ _fileOut = new RolloverFileOutputStream(_filename, _append, _retainDays, TimeZone.getTimeZone(getTimeZone()), _filenameDateFormat, null);
+ _closeOut = true;
+ LOG.info("Opened " + getDatedFilename());
+ }
+ else
+ _fileOut = System.err;
+
+ _out = _fileOut;
+
+ synchronized (this)
+ {
+ _writer = new OutputStreamWriter(_out);
+ }
+ super.doStart();
+ }
+
+ public void setTimeZone(String timeZone)
+ {
+ _timeZone = timeZone;
+ }
+
+ @ManagedAttribute("timezone of the log")
+ public String getTimeZone()
+ {
+ return _timeZone;
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ synchronized (this)
+ {
+ super.doStop();
+ try
+ {
+ if (_writer != null)
+ _writer.flush();
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ }
+ if (_out != null && _closeOut)
+ try
+ {
+ _out.close();
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ }
+
+ _out = null;
+ _fileOut = null;
+ _closeOut = false;
+ _writer = null;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java
new file mode 100644
index 0000000..e66acb1
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java
@@ -0,0 +1,105 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.nio.file.InvalidPathException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jetty.http.CompressedContentFormat;
+import org.eclipse.jetty.http.HttpContent;
+import org.eclipse.jetty.http.HttpContent.ContentFactory;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.http.ResourceHttpContent;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+
+/**
+ * An HttpContent.Factory for transient content (not cached). The HttpContent's created by
+ * this factory are not intended to be cached, so memory limits for individual
+ * HttpOutput streams are enforced.
+ */
+public class ResourceContentFactory implements ContentFactory
+{
+ private final ResourceFactory _factory;
+ private final MimeTypes _mimeTypes;
+ private final CompressedContentFormat[] _precompressedFormats;
+
+ public ResourceContentFactory(ResourceFactory factory, MimeTypes mimeTypes, CompressedContentFormat[] precompressedFormats)
+ {
+ _factory = factory;
+ _mimeTypes = mimeTypes;
+ _precompressedFormats = precompressedFormats;
+ }
+
+ @Override
+ public HttpContent getContent(String pathInContext, int maxBufferSize)
+ throws IOException
+ {
+ try
+ {
+ // try loading the content from our factory.
+ Resource resource = _factory.getResource(pathInContext);
+ HttpContent loaded = load(pathInContext, resource, maxBufferSize);
+ return loaded;
+ }
+ catch (Throwable t)
+ {
+ // Any error has potential to reveal fully qualified path
+ throw (InvalidPathException)new InvalidPathException(pathInContext, "Invalid PathInContext").initCause(t);
+ }
+ }
+
+ private HttpContent load(String pathInContext, Resource resource, int maxBufferSize)
+ throws IOException
+ {
+ if (resource == null || !resource.exists())
+ return null;
+
+ if (resource.isDirectory())
+ return new ResourceHttpContent(resource, _mimeTypes.getMimeByExtension(resource.toString()), maxBufferSize);
+
+ // Look for a precompressed resource or content
+ String mt = _mimeTypes.getMimeByExtension(pathInContext);
+ if (_precompressedFormats.length > 0)
+ {
+ // Is there a compressed resource?
+ Map<CompressedContentFormat, HttpContent> compressedContents = new HashMap<>(_precompressedFormats.length);
+ for (CompressedContentFormat format : _precompressedFormats)
+ {
+ String compressedPathInContext = pathInContext + format.getExtension();
+ Resource compressedResource = _factory.getResource(compressedPathInContext);
+ if (compressedResource != null && compressedResource.exists() && compressedResource.lastModified() >= resource.lastModified() &&
+ compressedResource.length() < resource.length())
+ compressedContents.put(format,
+ new ResourceHttpContent(compressedResource, _mimeTypes.getMimeByExtension(compressedPathInContext), maxBufferSize));
+ }
+ if (!compressedContents.isEmpty())
+ return new ResourceHttpContent(resource, mt, maxBufferSize, compressedContents);
+ }
+ return new ResourceHttpContent(resource, mt, maxBufferSize);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ResourceContentFactory[" + _factory + "]@" + hashCode();
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java
new file mode 100644
index 0000000..f51b827
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java
@@ -0,0 +1,869 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+import javax.servlet.AsyncContext;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.CompressedContentFormat;
+import org.eclipse.jetty.http.DateParser;
+import org.eclipse.jetty.http.HttpContent;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.PreEncodedHttpField;
+import org.eclipse.jetty.http.QuotedCSV;
+import org.eclipse.jetty.http.QuotedQualityCSV;
+import org.eclipse.jetty.io.WriterOutputStream;
+import org.eclipse.jetty.server.resource.HttpContentRangeWriter;
+import org.eclipse.jetty.server.resource.RangeWriter;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.MultiPartOutputStream;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+
+import static java.util.Arrays.stream;
+import static java.util.Collections.emptyList;
+import static org.eclipse.jetty.http.HttpHeaderValue.IDENTITY;
+
+/**
+ * Abstract resource service, used by DefaultServlet and ResourceHandler
+ */
+public class ResourceService
+{
+ private static final Logger LOG = Log.getLogger(ResourceService.class);
+
+ private static final PreEncodedHttpField ACCEPT_RANGES = new PreEncodedHttpField(HttpHeader.ACCEPT_RANGES, "bytes");
+
+ private HttpContent.ContentFactory _contentFactory;
+ private WelcomeFactory _welcomeFactory;
+ private boolean _acceptRanges = true;
+ private boolean _dirAllowed = true;
+ private boolean _redirectWelcome = false;
+ private CompressedContentFormat[] _precompressedFormats = new CompressedContentFormat[0];
+ private String[] _preferredEncodingOrder = new String[0];
+ private final Map<String, List<String>> _preferredEncodingOrderCache = new ConcurrentHashMap<>();
+ private int _encodingCacheSize = 100;
+ private boolean _pathInfoOnly = false;
+ private boolean _etags = false;
+ private HttpField _cacheControl;
+ private List<String> _gzipEquivalentFileExtensions;
+
+ public HttpContent.ContentFactory getContentFactory()
+ {
+ return _contentFactory;
+ }
+
+ public void setContentFactory(HttpContent.ContentFactory contentFactory)
+ {
+ _contentFactory = contentFactory;
+ }
+
+ public WelcomeFactory getWelcomeFactory()
+ {
+ return _welcomeFactory;
+ }
+
+ public void setWelcomeFactory(WelcomeFactory welcomeFactory)
+ {
+ _welcomeFactory = welcomeFactory;
+ }
+
+ public boolean isAcceptRanges()
+ {
+ return _acceptRanges;
+ }
+
+ public void setAcceptRanges(boolean acceptRanges)
+ {
+ _acceptRanges = acceptRanges;
+ }
+
+ public boolean isDirAllowed()
+ {
+ return _dirAllowed;
+ }
+
+ public void setDirAllowed(boolean dirAllowed)
+ {
+ _dirAllowed = dirAllowed;
+ }
+
+ public boolean isRedirectWelcome()
+ {
+ return _redirectWelcome;
+ }
+
+ public void setRedirectWelcome(boolean redirectWelcome)
+ {
+ _redirectWelcome = redirectWelcome;
+ }
+
+ public CompressedContentFormat[] getPrecompressedFormats()
+ {
+ return _precompressedFormats;
+ }
+
+ public void setPrecompressedFormats(CompressedContentFormat[] precompressedFormats)
+ {
+ _precompressedFormats = precompressedFormats;
+ _preferredEncodingOrder = stream(_precompressedFormats).map(f -> f.getEncoding()).toArray(String[]::new);
+ }
+
+ public void setEncodingCacheSize(int encodingCacheSize)
+ {
+ _encodingCacheSize = encodingCacheSize;
+ }
+
+ public int getEncodingCacheSize()
+ {
+ return _encodingCacheSize;
+ }
+
+ public boolean isPathInfoOnly()
+ {
+ return _pathInfoOnly;
+ }
+
+ public void setPathInfoOnly(boolean pathInfoOnly)
+ {
+ _pathInfoOnly = pathInfoOnly;
+ }
+
+ public boolean isEtags()
+ {
+ return _etags;
+ }
+
+ public void setEtags(boolean etags)
+ {
+ _etags = etags;
+ }
+
+ public HttpField getCacheControl()
+ {
+ return _cacheControl;
+ }
+
+ public void setCacheControl(HttpField cacheControl)
+ {
+ if (cacheControl == null)
+ _cacheControl = null;
+ if (cacheControl.getHeader() != HttpHeader.CACHE_CONTROL)
+ throw new IllegalArgumentException("!Cache-Control");
+ _cacheControl = cacheControl instanceof PreEncodedHttpField
+ ? cacheControl
+ : new PreEncodedHttpField(cacheControl.getHeader(), cacheControl.getValue());
+ }
+
+ public List<String> getGzipEquivalentFileExtensions()
+ {
+ return _gzipEquivalentFileExtensions;
+ }
+
+ public void setGzipEquivalentFileExtensions(List<String> gzipEquivalentFileExtensions)
+ {
+ _gzipEquivalentFileExtensions = gzipEquivalentFileExtensions;
+ }
+
+ public boolean doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException
+ {
+ String servletPath = null;
+ String pathInfo = null;
+ Enumeration<String> reqRanges = null;
+ boolean included = request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
+ if (included)
+ {
+ servletPath = _pathInfoOnly ? "/" : (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
+ pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
+ if (servletPath == null)
+ {
+ servletPath = request.getServletPath();
+ pathInfo = request.getPathInfo();
+ }
+ }
+ else
+ {
+ servletPath = _pathInfoOnly ? "/" : request.getServletPath();
+ pathInfo = request.getPathInfo();
+
+ // Is this a Range request?
+ reqRanges = request.getHeaders(HttpHeader.RANGE.asString());
+ if (!hasDefinedRange(reqRanges))
+ reqRanges = null;
+ }
+
+ String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
+
+ boolean endsWithSlash = (pathInfo == null ? (_pathInfoOnly ? "" : servletPath) : pathInfo).endsWith(URIUtil.SLASH);
+ boolean checkPrecompressedVariants = _precompressedFormats.length > 0 && !endsWithSlash && !included && reqRanges == null;
+
+ HttpContent content = null;
+ boolean releaseContent = true;
+ try
+ {
+ // Find the content
+ content = _contentFactory.getContent(pathInContext, response.getBufferSize());
+ if (LOG.isDebugEnabled())
+ LOG.debug("content={}", content);
+
+ // Not found?
+ if (content == null || !content.getResource().exists())
+ {
+ if (included)
+ throw new FileNotFoundException("!" + pathInContext);
+ notFound(request, response);
+ return response.isCommitted();
+ }
+
+ // Directory?
+ if (content.getResource().isDirectory())
+ {
+ sendWelcome(content, pathInContext, endsWithSlash, included, request, response);
+ return true;
+ }
+
+ // Strip slash?
+ if (!included && endsWithSlash && pathInContext.length() > 1)
+ {
+ String q = request.getQueryString();
+ pathInContext = pathInContext.substring(0, pathInContext.length() - 1);
+ if (q != null && q.length() != 0)
+ pathInContext += "?" + q;
+ response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), pathInContext)));
+ return true;
+ }
+
+ // Conditional response?
+ if (!included && !passConditionalHeaders(request, response, content))
+ return true;
+
+ // Precompressed variant available?
+ Map<CompressedContentFormat, ? extends HttpContent> precompressedContents = checkPrecompressedVariants ? content.getPrecompressedContents() : null;
+ if (precompressedContents != null && precompressedContents.size() > 0)
+ {
+ // Tell caches that response may vary by accept-encoding
+ response.addHeader(HttpHeader.VARY.asString(), HttpHeader.ACCEPT_ENCODING.asString());
+
+ List<String> preferredEncodings = getPreferredEncodingOrder(request);
+ CompressedContentFormat precompressedContentEncoding = getBestPrecompressedContent(preferredEncodings, precompressedContents.keySet());
+ if (precompressedContentEncoding != null)
+ {
+ HttpContent precompressedContent = precompressedContents.get(precompressedContentEncoding);
+ if (LOG.isDebugEnabled())
+ LOG.debug("precompressed={}", precompressedContent);
+ content = precompressedContent;
+ response.setHeader(HttpHeader.CONTENT_ENCODING.asString(), precompressedContentEncoding.getEncoding());
+ }
+ }
+
+ // TODO this should be done by HttpContent#getContentEncoding
+ if (isGzippedContent(pathInContext))
+ response.setHeader(HttpHeader.CONTENT_ENCODING.asString(), "gzip");
+
+ // Send the data
+ releaseContent = sendData(request, response, included, content, reqRanges);
+ }
+ catch (IllegalArgumentException e)
+ {
+ LOG.warn(Log.EXCEPTION, e);
+ if (!response.isCommitted())
+ response.sendError(500, e.getMessage());
+ }
+ finally
+ {
+ if (releaseContent)
+ {
+ if (content != null)
+ content.release();
+ }
+ }
+
+ return true;
+ }
+
+ private List<String> getPreferredEncodingOrder(HttpServletRequest request)
+ {
+ Enumeration<String> headers = request.getHeaders(HttpHeader.ACCEPT_ENCODING.asString());
+ if (!headers.hasMoreElements())
+ return emptyList();
+
+ String key = headers.nextElement();
+ if (headers.hasMoreElements())
+ {
+ StringBuilder sb = new StringBuilder(key.length() * 2);
+ do
+ {
+ sb.append(',').append(headers.nextElement());
+ }
+ while (headers.hasMoreElements());
+ key = sb.toString();
+ }
+
+ List<String> values = _preferredEncodingOrderCache.get(key);
+ if (values == null)
+ {
+ QuotedQualityCSV encodingQualityCSV = new QuotedQualityCSV(_preferredEncodingOrder);
+ encodingQualityCSV.addValue(key);
+ values = encodingQualityCSV.getValues();
+
+ // keep cache size in check even if we get strange/malicious input
+ if (_preferredEncodingOrderCache.size() > _encodingCacheSize)
+ _preferredEncodingOrderCache.clear();
+
+ _preferredEncodingOrderCache.put(key, values);
+ }
+
+ return values;
+ }
+
+ private CompressedContentFormat getBestPrecompressedContent(List<String> preferredEncodings, Collection<CompressedContentFormat> availableFormats)
+ {
+ if (availableFormats.isEmpty())
+ return null;
+
+ for (String encoding : preferredEncodings)
+ {
+ for (CompressedContentFormat format : availableFormats)
+ {
+ if (format.getEncoding().equals(encoding))
+ return format;
+ }
+
+ if ("*".equals(encoding))
+ return availableFormats.iterator().next();
+
+ if (IDENTITY.asString().equals(encoding))
+ return null;
+ }
+ return null;
+ }
+
+ protected void sendWelcome(HttpContent content, String pathInContext, boolean endsWithSlash, boolean included, HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException
+ {
+ // Redirect to directory
+ if (!endsWithSlash)
+ {
+ StringBuilder buf = new StringBuilder(request.getRequestURI());
+ int param = buf.lastIndexOf(";");
+ if (param < 0 || buf.lastIndexOf("/", param) > 0)
+ buf.append('/');
+ else
+ buf.insert(param, '/');
+ String q = request.getQueryString();
+ if (q != null && q.length() != 0)
+ {
+ buf.append('?');
+ buf.append(q);
+ }
+ response.setContentLength(0);
+ response.sendRedirect(response.encodeRedirectURL(buf.toString()));
+ return;
+ }
+
+ // look for a welcome file
+ String welcome = _welcomeFactory == null ? null : _welcomeFactory.getWelcomeFile(pathInContext);
+
+ if (welcome != null)
+ {
+ String servletPath = included ? (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH)
+ : request.getServletPath();
+
+ if (_pathInfoOnly)
+ welcome = URIUtil.addPaths(servletPath, welcome);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("welcome={}", welcome);
+
+ ServletContext context = request.getServletContext();
+
+ if (_redirectWelcome || context == null)
+ {
+ // Redirect to the index
+ response.setContentLength(0);
+
+ String uri = URIUtil.encodePath(URIUtil.addPaths(request.getContextPath(), welcome));
+ String q = request.getQueryString();
+ if (q != null && !q.isEmpty())
+ uri += "?" + q;
+
+ response.sendRedirect(response.encodeRedirectURL(uri));
+ return;
+ }
+
+ RequestDispatcher dispatcher = context.getRequestDispatcher(URIUtil.encodePath(welcome));
+ if (dispatcher != null)
+ {
+ // Forward to the index
+ if (included)
+ dispatcher.include(request, response);
+ else
+ {
+ request.setAttribute("org.eclipse.jetty.server.welcome", welcome);
+ dispatcher.forward(request, response);
+ }
+ }
+ return;
+ }
+
+ if (included || passConditionalHeaders(request, response, content))
+ sendDirectory(request, response, content.getResource(), pathInContext);
+ }
+
+ protected boolean isGzippedContent(String path)
+ {
+ if (path == null || _gzipEquivalentFileExtensions == null)
+ return false;
+
+ for (String suffix : _gzipEquivalentFileExtensions)
+ {
+ if (path.endsWith(suffix))
+ return true;
+ }
+ return false;
+ }
+
+ private boolean hasDefinedRange(Enumeration<String> reqRanges)
+ {
+ return (reqRanges != null && reqRanges.hasMoreElements());
+ }
+
+ protected void notFound(HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ protected void sendStatus(HttpServletResponse response, int status, Supplier<String> etag) throws IOException
+ {
+ response.setStatus(status);
+ if (_etags && etag != null)
+ response.setHeader(HttpHeader.ETAG.asString(), etag.get());
+ response.flushBuffer();
+ }
+
+ /* Check modification date headers.
+ */
+ protected boolean passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, HttpContent content)
+ throws IOException
+ {
+ try
+ {
+ String ifm = null;
+ String ifnm = null;
+ String ifms = null;
+ long ifums = -1;
+
+ if (request instanceof Request)
+ {
+ // Find multiple fields by iteration as an optimization
+ HttpFields fields = ((Request)request).getHttpFields();
+ for (int i = fields.size(); i-- > 0; )
+ {
+ HttpField field = fields.getField(i);
+ if (field.getHeader() != null)
+ {
+ switch (field.getHeader())
+ {
+ case IF_MATCH:
+ ifm = field.getValue();
+ break;
+ case IF_NONE_MATCH:
+ ifnm = field.getValue();
+ break;
+ case IF_MODIFIED_SINCE:
+ ifms = field.getValue();
+ break;
+ case IF_UNMODIFIED_SINCE:
+ ifums = DateParser.parseDate(field.getValue());
+ break;
+ default:
+ }
+ }
+ }
+ }
+ else
+ {
+ ifm = request.getHeader(HttpHeader.IF_MATCH.asString());
+ ifnm = request.getHeader(HttpHeader.IF_NONE_MATCH.asString());
+ ifms = request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
+ ifums = request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString());
+ }
+
+ if (_etags)
+ {
+ String etag = content.getETagValue();
+ if (ifm != null)
+ {
+ boolean match = false;
+ if (etag != null)
+ {
+ QuotedCSV quoted = new QuotedCSV(true, ifm);
+ for (String etagWithSuffix : quoted)
+ {
+ if (CompressedContentFormat.tagEquals(etag, etagWithSuffix))
+ {
+ match = true;
+ break;
+ }
+ }
+ }
+
+ if (!match)
+ {
+ sendStatus(response, HttpServletResponse.SC_PRECONDITION_FAILED, null);
+ return false;
+ }
+ }
+
+ if (ifnm != null && etag != null)
+ {
+ // Handle special case of exact match OR gzip exact match
+ if (CompressedContentFormat.tagEquals(etag, ifnm) && ifnm.indexOf(',') < 0)
+ {
+ sendStatus(response, HttpServletResponse.SC_NOT_MODIFIED, ifnm::toString);
+ return false;
+ }
+
+ // Handle list of tags
+ QuotedCSV quoted = new QuotedCSV(true, ifnm);
+ for (String tag : quoted)
+ {
+ if (CompressedContentFormat.tagEquals(etag, tag))
+ {
+ sendStatus(response, HttpServletResponse.SC_NOT_MODIFIED, tag::toString);
+ return false;
+ }
+ }
+
+ // If etag requires content to be served, then do not check if-modified-since
+ return true;
+ }
+ }
+
+ // Handle if modified since
+ if (ifms != null)
+ {
+ //Get jetty's Response impl
+ String mdlm = content.getLastModifiedValue();
+ if (ifms.equals(mdlm))
+ {
+ sendStatus(response, HttpServletResponse.SC_NOT_MODIFIED, content::getETagValue);
+ return false;
+ }
+
+ long ifmsl = request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
+ if (ifmsl != -1 && content.getResource().lastModified() / 1000 <= ifmsl / 1000)
+ {
+ sendStatus(response, HttpServletResponse.SC_NOT_MODIFIED, content::getETagValue);
+ return false;
+ }
+ }
+
+ // Parse the if[un]modified dates and compare to resource
+ if (ifums != -1 && content.getResource().lastModified() / 1000 > ifums / 1000)
+ {
+ response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+ return false;
+ }
+ }
+ catch (IllegalArgumentException iae)
+ {
+ if (!response.isCommitted())
+ response.sendError(400, iae.getMessage());
+ throw iae;
+ }
+
+ return true;
+ }
+
+ protected void sendDirectory(HttpServletRequest request,
+ HttpServletResponse response,
+ Resource resource,
+ String pathInContext)
+ throws IOException
+ {
+ if (!_dirAllowed)
+ {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ byte[] data = null;
+ String base = URIUtil.addEncodedPaths(request.getRequestURI(), URIUtil.SLASH);
+ String dir = resource.getListHTML(base, pathInContext.length() > 1, request.getQueryString());
+ if (dir == null)
+ {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN,
+ "No directory");
+ return;
+ }
+
+ data = dir.getBytes(StandardCharsets.UTF_8);
+ response.setContentType("text/html;charset=utf-8");
+ response.setContentLength(data.length);
+ response.getOutputStream().write(data);
+ }
+
+ protected boolean sendData(HttpServletRequest request,
+ HttpServletResponse response,
+ boolean include,
+ final HttpContent content,
+ Enumeration<String> reqRanges)
+ throws IOException
+ {
+ final long content_length = content.getContentLengthValue();
+
+ // Get the output stream (or writer)
+ OutputStream out;
+ boolean written;
+ try
+ {
+ out = response.getOutputStream();
+
+ // has something already written to the response?
+ written = !(out instanceof HttpOutput) || ((HttpOutput)out).isWritten();
+ }
+ catch (IllegalStateException e)
+ {
+ out = new WriterOutputStream(response.getWriter());
+ written = true; // there may be data in writer buffer, so assume written
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug(String.format("sendData content=%s out=%s async=%b", content, out, request.isAsyncSupported()));
+
+ if (reqRanges == null || !reqRanges.hasMoreElements() || content_length < 0)
+ {
+ // if there were no ranges, send entire entity
+ if (include)
+ {
+ // write without headers
+ content.getResource().writeTo(out, 0, content_length);
+ }
+ // else if we can't do a bypass write because of wrapping
+ else if (written)
+ {
+ // write normally
+ putHeaders(response, content, Response.NO_CONTENT_LENGTH);
+ ByteBuffer buffer = content.getIndirectBuffer();
+ if (buffer != null)
+ BufferUtil.writeTo(buffer, out);
+ else
+ content.getResource().writeTo(out, 0, content_length);
+ }
+ // else do a bypass write
+ else
+ {
+ // write the headers
+ putHeaders(response, content, Response.USE_KNOWN_CONTENT_LENGTH);
+
+ // write the content asynchronously if supported
+ if (request.isAsyncSupported())
+ {
+ final AsyncContext context = request.startAsync();
+ context.setTimeout(0);
+
+ ((HttpOutput)out).sendContent(content, new Callback()
+ {
+ @Override
+ public void succeeded()
+ {
+ context.complete();
+ content.release();
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ if (x instanceof IOException)
+ LOG.debug(x);
+ else
+ LOG.warn(x);
+ context.complete();
+ content.release();
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return InvocationType.NON_BLOCKING;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("ResourceService@%x$CB", ResourceService.this.hashCode());
+ }
+ });
+ return false;
+ }
+ // otherwise write content blocking
+ ((HttpOutput)out).sendContent(content);
+ }
+ }
+ else
+ {
+ // Parse the satisfiable ranges
+ List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(reqRanges, content_length);
+
+ // if there are no satisfiable ranges, send 416 response
+ if (ranges == null || ranges.size() == 0)
+ {
+ putHeaders(response, content, Response.USE_KNOWN_CONTENT_LENGTH);
+ response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
+ InclusiveByteRange.to416HeaderRangeString(content_length));
+ sendStatus(response, HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE, null);
+ return true;
+ }
+
+ // if there is only a single valid range (must be satisfiable
+ // since were here now), send that range with a 216 response
+ if (ranges.size() == 1)
+ {
+ InclusiveByteRange singleSatisfiableRange = ranges.iterator().next();
+ long singleLength = singleSatisfiableRange.getSize();
+ putHeaders(response, content, singleLength);
+ response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
+ if (!response.containsHeader(HttpHeader.DATE.asString()))
+ response.addDateHeader(HttpHeader.DATE.asString(), System.currentTimeMillis());
+ response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
+ singleSatisfiableRange.toHeaderRangeString(content_length));
+ content.getResource().writeTo(out, singleSatisfiableRange.getFirst(), singleLength);
+ return true;
+ }
+
+ // multiple non-overlapping valid ranges cause a multipart
+ // 216 response which does not require an overall
+ // content-length header
+ //
+ putHeaders(response, content, Response.NO_CONTENT_LENGTH);
+ String mimetype = content.getContentTypeValue();
+ if (mimetype == null)
+ LOG.warn("Unknown mimetype for " + request.getRequestURI());
+ MultiPartOutputStream multi = new MultiPartOutputStream(out);
+ response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
+ if (!response.containsHeader(HttpHeader.DATE.asString()))
+ response.addDateHeader(HttpHeader.DATE.asString(), System.currentTimeMillis());
+
+ // If the request has a "Request-Range" header then we need to
+ // send an old style multipart/x-byteranges Content-Type. This
+ // keeps Netscape and acrobat happy. This is what Apache does.
+ String ctp;
+ if (request.getHeader(HttpHeader.REQUEST_RANGE.asString()) != null)
+ ctp = "multipart/x-byteranges; boundary=";
+ else
+ ctp = "multipart/byteranges; boundary=";
+ response.setContentType(ctp + multi.getBoundary());
+
+ // calculate the content-length
+ int length = 0;
+ String[] header = new String[ranges.size()];
+ int i = 0;
+ final int CRLF = "\r\n".length();
+ final int DASHDASH = "--".length();
+ final int BOUNDARY = multi.getBoundary().length();
+ final int FIELD_SEP = ": ".length();
+ for (InclusiveByteRange ibr : ranges)
+ {
+ header[i] = ibr.toHeaderRangeString(content_length);
+ if (i > 0) // in-part
+ length += CRLF;
+ length += DASHDASH + BOUNDARY + CRLF;
+ if (mimetype != null)
+ length += HttpHeader.CONTENT_TYPE.asString().length() + FIELD_SEP + mimetype.length() + CRLF;
+ length += HttpHeader.CONTENT_RANGE.asString().length() + FIELD_SEP + header[i].length() + CRLF;
+ length += CRLF;
+ length += ibr.getSize();
+ i++;
+ }
+ length += CRLF + DASHDASH + BOUNDARY + DASHDASH + CRLF;
+ response.setContentLength(length);
+
+ try (RangeWriter rangeWriter = HttpContentRangeWriter.newRangeWriter(content))
+ {
+ i = 0;
+ for (InclusiveByteRange ibr : ranges)
+ {
+ multi.startPart(mimetype, new String[]{HttpHeader.CONTENT_RANGE + ": " + header[i]});
+ rangeWriter.writeTo(multi, ibr.getFirst(), ibr.getSize());
+ i++;
+ }
+ }
+
+ multi.close();
+ }
+ return true;
+ }
+
+ protected void putHeaders(HttpServletResponse response, HttpContent content, long contentLength)
+ {
+ if (response instanceof Response)
+ {
+ Response r = (Response)response;
+ r.putHeaders(content, contentLength, _etags);
+ HttpFields fields = r.getHttpFields();
+ if (_acceptRanges && !fields.contains(HttpHeader.ACCEPT_RANGES))
+ fields.add(ACCEPT_RANGES);
+
+ if (_cacheControl != null && !fields.contains(HttpHeader.CACHE_CONTROL))
+ fields.add(_cacheControl);
+ }
+ else
+ {
+ Response.putHeaders(response, content, contentLength, _etags);
+ if (_acceptRanges && !response.containsHeader(HttpHeader.ACCEPT_RANGES.asString()))
+ response.setHeader(ACCEPT_RANGES.getName(), ACCEPT_RANGES.getValue());
+
+ if (_cacheControl != null && !response.containsHeader(HttpHeader.CACHE_CONTROL.asString()))
+ response.setHeader(_cacheControl.getName(), _cacheControl.getValue());
+ }
+ }
+
+ public interface WelcomeFactory
+ {
+
+ /**
+ * Finds a matching welcome file for the supplied {@link Resource}.
+ *
+ * @param pathInContext the path of the request
+ * @return The path of the matching welcome file in context or null.
+ */
+ String getWelcomeFile(String pathInContext);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java
new file mode 100644
index 0000000..97610f3
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java
@@ -0,0 +1,1413 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.channels.IllegalSelectorException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Iterator;
+import java.util.ListIterator;
+import java.util.Locale;
+import java.util.function.Supplier;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletResponse;
+import javax.servlet.ServletResponseWrapper;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+import javax.servlet.http.HttpSession;
+
+import org.eclipse.jetty.http.CookieCompliance;
+import org.eclipse.jetty.http.DateGenerator;
+import org.eclipse.jetty.http.HttpContent;
+import org.eclipse.jetty.http.HttpCookie;
+import org.eclipse.jetty.http.HttpCookie.SameSite;
+import org.eclipse.jetty.http.HttpCookie.SetCookieHttpField;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpGenerator;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpHeaderValue;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.http.MetaData;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.http.PreEncodedHttpField;
+import org.eclipse.jetty.io.RuntimeIOException;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.util.AtomicBiInteger;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * <p>{@link Response} provides the implementation for {@link HttpServletResponse}.</p>
+ */
+public class Response implements HttpServletResponse
+{
+ private static final Logger LOG = Log.getLogger(Response.class);
+ private static final int __MIN_BUFFER_SIZE = 1;
+ private static final HttpField __EXPIRES_01JAN1970 = new PreEncodedHttpField(HttpHeader.EXPIRES, DateGenerator.__01Jan1970);
+ public static final int NO_CONTENT_LENGTH = -1;
+ public static final int USE_KNOWN_CONTENT_LENGTH = -2;
+
+ public enum OutputType
+ {
+ NONE, STREAM, WRITER
+ }
+
+ /**
+ * If a header name starts with this string, the header (stripped of the prefix)
+ * can be set during include using only {@link #setHeader(String, String)} or
+ * {@link #addHeader(String, String)}.
+ */
+ public static final String SET_INCLUDE_HEADER_PREFIX = "org.eclipse.jetty.server.include.";
+
+ private final HttpChannel _channel;
+ private final HttpFields _fields = new HttpFields();
+ private final AtomicBiInteger _errorSentAndIncludes = new AtomicBiInteger(); // hi is errorSent flag, lo is include count
+ private final HttpOutput _out;
+ private int _status = HttpStatus.OK_200;
+ private String _reason;
+ private Locale _locale;
+ private MimeTypes.Type _mimeType;
+ private String _characterEncoding;
+ private EncodingFrom _encodingFrom = EncodingFrom.NOT_SET;
+ private String _contentType;
+ private OutputType _outputType = OutputType.NONE;
+ private ResponseWriter _writer;
+ private long _contentLength = -1;
+ private Supplier<HttpFields> _trailers;
+
+ private enum EncodingFrom
+ {
+ NOT_SET, INFERRED, SET_LOCALE, SET_CONTENT_TYPE, SET_CHARACTER_ENCODING
+ }
+
+ private static final EnumSet<EncodingFrom> __localeOverride = EnumSet.of(EncodingFrom.NOT_SET, EncodingFrom.INFERRED);
+ private static final EnumSet<EncodingFrom> __explicitCharset = EnumSet.of(EncodingFrom.SET_LOCALE, EncodingFrom.SET_CHARACTER_ENCODING);
+
+ public Response(HttpChannel channel, HttpOutput out)
+ {
+ _channel = channel;
+ _out = out;
+ }
+
+ public HttpChannel getHttpChannel()
+ {
+ return _channel;
+ }
+
+ protected void recycle()
+ {
+ // _channel need not be recycled
+ _fields.clear();
+ _errorSentAndIncludes.set(0);
+ _out.recycle();
+ _status = HttpStatus.OK_200;
+ _reason = null;
+ _locale = null;
+ _mimeType = null;
+ _characterEncoding = null;
+ _encodingFrom = EncodingFrom.NOT_SET;
+ _contentType = null;
+ _outputType = OutputType.NONE;
+ // _writer does not need to be recycled
+ _contentLength = -1;
+ _trailers = null;
+ }
+
+ public HttpOutput getHttpOutput()
+ {
+ return _out;
+ }
+
+ public void reopen()
+ {
+ // Make the response mutable and reopen output.
+ setErrorSent(false);
+ _out.reopen();
+ }
+
+ public void errorClose()
+ {
+ // Make the response immutable and soft close the output.
+ setErrorSent(true);
+ _out.softClose();
+ }
+
+ /**
+ * @return true if the response is mutable, ie not errorSent and not included
+ */
+ private boolean isMutable()
+ {
+ return _errorSentAndIncludes.get() == 0;
+ }
+
+ private void setErrorSent(boolean errorSent)
+ {
+ _errorSentAndIncludes.getAndSetHi(errorSent ? 1 : 0);
+ }
+
+ public boolean isIncluding()
+ {
+ return _errorSentAndIncludes.getLo() > 0;
+ }
+
+ public void include()
+ {
+ _errorSentAndIncludes.add(0, 1);
+ }
+
+ public void included()
+ {
+ _errorSentAndIncludes.add(0, -1);
+ if (_outputType == OutputType.WRITER)
+ {
+ _writer.reopen();
+ }
+ _out.reopen();
+ }
+
+ public void addCookie(HttpCookie cookie)
+ {
+ if (StringUtil.isBlank(cookie.getName()))
+ throw new IllegalArgumentException("Cookie.name cannot be blank/null");
+
+ // add the set cookie
+ _fields.add(new SetCookieHttpField(checkSameSite(cookie), getHttpChannel().getHttpConfiguration().getResponseCookieCompliance()));
+
+ // Expire responses with set-cookie headers so they do not get cached.
+ _fields.put(__EXPIRES_01JAN1970);
+ }
+
+ /**
+ * Check that samesite is set on the cookie. If not, use a
+ * context default value, if one has been set.
+ *
+ * @param cookie the cookie to check
+ * @return either the original cookie, or a new one that has the samesit default set
+ */
+ private HttpCookie checkSameSite(HttpCookie cookie)
+ {
+ if (cookie == null || cookie.getSameSite() != null)
+ return cookie;
+
+ //sameSite is not set, use the default configured for the context, if one exists
+ SameSite contextDefault = HttpCookie.getSameSiteDefault(_channel.getRequest().getServletContext());
+ if (contextDefault == null)
+ return cookie; //no default set
+
+ return new HttpCookie(cookie.getName(),
+ cookie.getValue(),
+ cookie.getDomain(),
+ cookie.getPath(),
+ cookie.getMaxAge(),
+ cookie.isHttpOnly(),
+ cookie.isSecure(),
+ cookie.getComment(),
+ cookie.getVersion(),
+ contextDefault);
+ }
+
+ /**
+ * Replace (or add) a cookie.
+ * Using name, path and domain, look for a matching set-cookie header and replace it.
+ *
+ * @param cookie The cookie to add/replace
+ */
+ public void replaceCookie(HttpCookie cookie)
+ {
+ for (ListIterator<HttpField> i = _fields.listIterator(); i.hasNext(); )
+ {
+ HttpField field = i.next();
+
+ if (field.getHeader() == HttpHeader.SET_COOKIE)
+ {
+ CookieCompliance compliance = getHttpChannel().getHttpConfiguration().getResponseCookieCompliance();
+
+ HttpCookie oldCookie;
+ if (field instanceof SetCookieHttpField)
+ oldCookie = ((SetCookieHttpField)field).getHttpCookie();
+ else
+ oldCookie = new HttpCookie(field.getValue());
+
+ if (!cookie.getName().equals(oldCookie.getName()))
+ continue;
+
+ if (cookie.getDomain() == null)
+ {
+ if (oldCookie.getDomain() != null)
+ continue;
+ }
+ else if (!cookie.getDomain().equalsIgnoreCase(oldCookie.getDomain()))
+ continue;
+
+ if (cookie.getPath() == null)
+ {
+ if (oldCookie.getPath() != null)
+ continue;
+ }
+ else if (!cookie.getPath().equals(oldCookie.getPath()))
+ continue;
+
+ i.set(new SetCookieHttpField(checkSameSite(cookie), compliance));
+ return;
+ }
+ }
+
+ // Not replaced, so add normally
+ addCookie(cookie);
+ }
+
+ @Override
+ public void addCookie(Cookie cookie)
+ {
+ //Servlet Spec 9.3 Include method: cannot set a cookie if handling an include
+ if (isMutable())
+ {
+ if (StringUtil.isBlank(cookie.getName()))
+ throw new IllegalArgumentException("Cookie.name cannot be blank/null");
+
+ String comment = cookie.getComment();
+ // HttpOnly was supported as a comment in cookie flags before the java.net.HttpCookie implementation so need to check that
+ boolean httpOnly = cookie.isHttpOnly() || HttpCookie.isHttpOnlyInComment(comment);
+ SameSite sameSite = HttpCookie.getSameSiteFromComment(comment);
+ comment = HttpCookie.getCommentWithoutAttributes(comment);
+
+ addCookie(new HttpCookie(
+ cookie.getName(),
+ cookie.getValue(),
+ cookie.getDomain(),
+ cookie.getPath(),
+ (long)cookie.getMaxAge(),
+ httpOnly,
+ cookie.getSecure(),
+ comment,
+ cookie.getVersion(),
+ sameSite));
+ }
+ }
+
+ @Override
+ public boolean containsHeader(String name)
+ {
+ return _fields.containsKey(name);
+ }
+
+ @Override
+ public String encodeURL(String url)
+ {
+ final Request request = _channel.getRequest();
+ SessionHandler sessionManager = request.getSessionHandler();
+
+ if (sessionManager == null)
+ return url;
+
+ HttpURI uri = null;
+ if (sessionManager.isCheckingRemoteSessionIdEncoding() && URIUtil.hasScheme(url))
+ {
+ uri = new HttpURI(url);
+ String path = uri.getPath();
+ path = (path == null ? "" : path);
+ int port = uri.getPort();
+ if (port < 0)
+ port = HttpScheme.HTTPS.asString().equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
+
+ // Is it the same server?
+ if (!request.getServerName().equalsIgnoreCase(uri.getHost()))
+ return url;
+ if (request.getServerPort() != port)
+ return url;
+ if (!path.startsWith(request.getContextPath())) //TODO the root context path is "", with which every non null string starts
+ return url;
+ }
+
+ String sessionURLPrefix = sessionManager.getSessionIdPathParameterNamePrefix();
+ if (sessionURLPrefix == null)
+ return url;
+
+ if (url == null)
+ return null;
+
+ // should not encode if cookies in evidence
+ if ((sessionManager.isUsingCookies() && request.isRequestedSessionIdFromCookie()) || !sessionManager.isUsingURLs())
+ {
+ int prefix = url.indexOf(sessionURLPrefix);
+ if (prefix != -1)
+ {
+ int suffix = url.indexOf("?", prefix);
+ if (suffix < 0)
+ suffix = url.indexOf("#", prefix);
+
+ if (suffix <= prefix)
+ return url.substring(0, prefix);
+ return url.substring(0, prefix) + url.substring(suffix);
+ }
+ return url;
+ }
+
+ // get session;
+ HttpSession session = request.getSession(false);
+
+ // no session
+ if (session == null)
+ return url;
+
+ // invalid session
+ if (!sessionManager.isValid(session))
+ return url;
+
+ String id = sessionManager.getExtendedId(session);
+
+ if (uri == null)
+ uri = new HttpURI(url);
+
+ // Already encoded
+ int prefix = url.indexOf(sessionURLPrefix);
+ if (prefix != -1)
+ {
+ int suffix = url.indexOf("?", prefix);
+ if (suffix < 0)
+ suffix = url.indexOf("#", prefix);
+
+ if (suffix <= prefix)
+ return url.substring(0, prefix + sessionURLPrefix.length()) + id;
+ return url.substring(0, prefix + sessionURLPrefix.length()) + id +
+ url.substring(suffix);
+ }
+
+ // edit the session
+ int suffix = url.indexOf('?');
+ if (suffix < 0)
+ suffix = url.indexOf('#');
+ if (suffix < 0)
+ {
+ return url +
+ ((HttpScheme.HTTPS.is(uri.getScheme()) || HttpScheme.HTTP.is(uri.getScheme())) && uri.getPath() == null ? "/" : "") + //if no path, insert the root path
+ sessionURLPrefix + id;
+ }
+
+ return url.substring(0, suffix) +
+ ((HttpScheme.HTTPS.is(uri.getScheme()) || HttpScheme.HTTP.is(uri.getScheme())) && uri.getPath() == null ? "/" : "") + //if no path so insert the root path
+ sessionURLPrefix + id + url.substring(suffix);
+ }
+
+ @Override
+ public String encodeRedirectURL(String url)
+ {
+ return encodeURL(url);
+ }
+
+ @Override
+ @Deprecated
+ public String encodeUrl(String url)
+ {
+ return encodeURL(url);
+ }
+
+ @Override
+ @Deprecated
+ public String encodeRedirectUrl(String url)
+ {
+ return encodeRedirectURL(url);
+ }
+
+ @Override
+ public void sendError(int sc) throws IOException
+ {
+ sendError(sc, null);
+ }
+
+ /**
+ * Send an error response.
+ * <p>In addition to the servlet standard handling, this method supports some additional codes:</p>
+ * <dl>
+ * <dt>102</dt><dd>Send a partial PROCESSING response and allow additional responses</dd>
+ * <dt>-1</dt><dd>Abort the HttpChannel and close the connection/stream</dd>
+ * </dl>
+ * @param code The error code
+ * @param message The message
+ * @throws IOException If an IO problem occurred sending the error response.
+ */
+ @Override
+ public void sendError(int code, String message) throws IOException
+ {
+ if (isIncluding())
+ return;
+
+ switch (code)
+ {
+ case -1:
+ _channel.abort(new IOException(message));
+ break;
+ case HttpStatus.PROCESSING_102:
+ sendProcessing();
+ break;
+ default:
+ _channel.getState().sendError(code, message);
+ break;
+ }
+ }
+
+ /**
+ * Sends a 102-Processing response.
+ * If the connection is an HTTP connection, the version is 1.1 and the
+ * request has a Expect header starting with 102, then a 102 response is
+ * sent. This indicates that the request still be processed and real response
+ * can still be sent. This method is called by sendError if it is passed 102.
+ *
+ * @throws IOException if unable to send the 102 response
+ * @see javax.servlet.http.HttpServletResponse#sendError(int)
+ */
+ public void sendProcessing() throws IOException
+ {
+ if (_channel.isExpecting102Processing() && !isCommitted())
+ {
+ _channel.sendResponse(HttpGenerator.PROGRESS_102_INFO, null, true);
+ }
+ }
+
+ /**
+ * Sends a response with one of the 300 series redirection codes.
+ *
+ * @param code the redirect status code
+ * @param location the location to send in {@code Location} headers
+ * @throws IOException if unable to send the redirect
+ */
+ public void sendRedirect(int code, String location) throws IOException
+ {
+ sendRedirect(code, location, false);
+ }
+
+ /**
+ * Sends a response with a HTTP version appropriate 30x redirection.
+ *
+ * @param location the location to send in {@code Location} headers
+ * @param consumeAll if True, consume any HTTP/1 request input before doing the redirection. If the input cannot
+ * be consumed without blocking, then add a `Connection: close` header to the response.
+ * @throws IOException if unable to send the redirect
+ */
+ public void sendRedirect(String location, boolean consumeAll) throws IOException
+ {
+ sendRedirect(getHttpChannel().getRequest().getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion()
+ ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER, location, consumeAll);
+ }
+
+ /**
+ * Sends a response with a given redirection code.
+ *
+ * @param code the redirect status code
+ * @param location the location to send in {@code Location} headers
+ * @param consumeAll if True, consume any HTTP/1 request input before doing the redirection. If the input cannot
+ * be consumed without blocking, then add a `Connection: close` header to the response.
+ * @throws IOException if unable to send the redirect
+ */
+ public void sendRedirect(int code, String location, boolean consumeAll) throws IOException
+ {
+ if (consumeAll)
+ getHttpChannel().ensureConsumeAllOrNotPersistent();
+ if (!HttpStatus.isRedirection(code))
+ throw new IllegalArgumentException("Not a 3xx redirect code");
+
+ if (!isMutable())
+ return;
+
+ if (location == null)
+ throw new IllegalArgumentException();
+
+ if (!URIUtil.hasScheme(location))
+ {
+ StringBuilder buf = _channel.getHttpConfiguration().isRelativeRedirectAllowed()
+ ? new StringBuilder()
+ : _channel.getRequest().getRootURL();
+ if (location.startsWith("/"))
+ {
+ // absolute in context
+ location = URIUtil.canonicalURI(location);
+ }
+ else
+ {
+ // relative to request
+ String path = _channel.getRequest().getRequestURI();
+ String parent = (path.endsWith("/")) ? path : URIUtil.parentPath(path);
+ location = URIUtil.canonicalURI(URIUtil.addEncodedPaths(parent, location));
+ if (location != null && !location.startsWith("/"))
+ buf.append('/');
+ }
+
+ if (location == null)
+ throw new IllegalStateException("path cannot be above root");
+ buf.append(location);
+
+ location = buf.toString();
+ }
+
+ resetBuffer();
+ setHeader(HttpHeader.LOCATION, location);
+ setStatus(code);
+ closeOutput();
+ }
+
+ @Override
+ public void sendRedirect(String location) throws IOException
+ {
+ sendRedirect(HttpServletResponse.SC_MOVED_TEMPORARILY, location);
+ }
+
+ @Override
+ public void setDateHeader(String name, long date)
+ {
+ if (isMutable())
+ _fields.putDateField(name, date);
+ }
+
+ @Override
+ public void addDateHeader(String name, long date)
+ {
+ if (isMutable())
+ _fields.addDateField(name, date);
+ }
+
+ public void setHeader(HttpHeader name, String value)
+ {
+ if (isMutable())
+ {
+ if (HttpHeader.CONTENT_TYPE == name)
+ setContentType(value);
+ else
+ {
+ _fields.put(name, value);
+
+ if (HttpHeader.CONTENT_LENGTH == name)
+ {
+ if (value == null)
+ _contentLength = -1L;
+ else
+ _contentLength = Long.parseLong(value);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void setHeader(String name, String value)
+ {
+ long biInt = _errorSentAndIncludes.get();
+ if (biInt != 0)
+ {
+ boolean errorSent = AtomicBiInteger.getHi(biInt) != 0;
+ boolean including = AtomicBiInteger.getLo(biInt) > 0;
+ if (!errorSent && including && name.startsWith(SET_INCLUDE_HEADER_PREFIX))
+ name = name.substring(SET_INCLUDE_HEADER_PREFIX.length());
+ else
+ return;
+ }
+
+ if (HttpHeader.CONTENT_TYPE.is(name))
+ setContentType(value);
+ else
+ {
+ _fields.put(name, value);
+ if (HttpHeader.CONTENT_LENGTH.is(name))
+ {
+ if (value == null)
+ _contentLength = -1L;
+ else
+ _contentLength = Long.parseLong(value);
+ }
+ }
+ }
+
+ @Override
+ public Collection<String> getHeaderNames()
+ {
+ return _fields.getFieldNamesCollection();
+ }
+
+ @Override
+ public String getHeader(String name)
+ {
+ return _fields.get(name);
+ }
+
+ @Override
+ public Collection<String> getHeaders(String name)
+ {
+ Collection<String> i = _fields.getValuesList(name);
+ if (i == null)
+ return Collections.emptyList();
+ return i;
+ }
+
+ @Override
+ public void addHeader(String name, String value)
+ {
+ long biInt = _errorSentAndIncludes.get();
+ if (biInt != 0)
+ {
+ boolean errorSent = AtomicBiInteger.getHi(biInt) != 0;
+ boolean including = AtomicBiInteger.getLo(biInt) > 0;
+ if (!errorSent && including && name.startsWith(SET_INCLUDE_HEADER_PREFIX))
+ name = name.substring(SET_INCLUDE_HEADER_PREFIX.length());
+ else
+ return;
+ }
+
+ if (HttpHeader.CONTENT_TYPE.is(name))
+ {
+ setContentType(value);
+ return;
+ }
+
+ if (HttpHeader.CONTENT_LENGTH.is(name))
+ {
+ setHeader(name, value);
+ return;
+ }
+
+ _fields.add(name, value);
+ }
+
+ @Override
+ public void setIntHeader(String name, int value)
+ {
+ if (isMutable())
+ {
+ _fields.putLongField(name, value);
+ if (HttpHeader.CONTENT_LENGTH.is(name))
+ _contentLength = value;
+ }
+ }
+
+ @Override
+ public void addIntHeader(String name, int value)
+ {
+ if (isMutable())
+ {
+ _fields.add(name, Integer.toString(value));
+ if (HttpHeader.CONTENT_LENGTH.is(name))
+ _contentLength = value;
+ }
+ }
+
+ @Override
+ public void setStatus(int sc)
+ {
+ if (sc <= 0)
+ throw new IllegalArgumentException();
+ if (isMutable())
+ {
+ // Null the reason only if the status is different. This allows
+ // a specific reason to be sent with setStatusWithReason followed by sendError.
+ if (_status != sc)
+ _reason = null;
+ _status = sc;
+ }
+ }
+
+ @Override
+ @Deprecated
+ public void setStatus(int sc, String sm)
+ {
+ setStatusWithReason(sc, sm);
+ }
+
+ public void setStatusWithReason(int sc, String sm)
+ {
+ if (sc <= 0)
+ throw new IllegalArgumentException();
+ if (isMutable())
+ {
+ _status = sc;
+ _reason = sm;
+ }
+ }
+
+ @Override
+ public String getCharacterEncoding()
+ {
+ if (_characterEncoding == null)
+ {
+ String encoding = MimeTypes.getCharsetAssumedFromContentType(_contentType);
+ if (encoding != null)
+ return encoding;
+ encoding = MimeTypes.getCharsetInferredFromContentType(_contentType);
+ if (encoding != null)
+ return encoding;
+ return StringUtil.__ISO_8859_1;
+ }
+ return _characterEncoding;
+ }
+
+ @Override
+ public String getContentType()
+ {
+ return _contentType;
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() throws IOException
+ {
+ if (_outputType == OutputType.WRITER)
+ throw new IllegalStateException("WRITER");
+ _outputType = OutputType.STREAM;
+ return _out;
+ }
+
+ public boolean isWriting()
+ {
+ return _outputType == OutputType.WRITER;
+ }
+
+ public boolean isStreaming()
+ {
+ return _outputType == OutputType.STREAM;
+ }
+
+ public boolean isWritingOrStreaming()
+ {
+ return isWriting() || isStreaming();
+ }
+
+ @Override
+ public PrintWriter getWriter() throws IOException
+ {
+ if (_outputType == OutputType.STREAM)
+ throw new IllegalStateException("STREAM");
+
+ if (_outputType == OutputType.NONE)
+ {
+ /* get encoding from Content-Type header */
+ String encoding = _characterEncoding;
+ if (encoding == null)
+ {
+ if (_mimeType != null && _mimeType.isCharsetAssumed())
+ encoding = _mimeType.getCharsetString();
+ else
+ {
+ encoding = MimeTypes.getCharsetAssumedFromContentType(_contentType);
+ if (encoding == null)
+ {
+ encoding = MimeTypes.getCharsetInferredFromContentType(_contentType);
+ if (encoding == null)
+ encoding = StringUtil.__ISO_8859_1;
+ setCharacterEncoding(encoding, EncodingFrom.INFERRED);
+ }
+ }
+ }
+
+ Locale locale = getLocale();
+
+ if (_writer != null && _writer.isFor(locale, encoding))
+ _writer.reopen();
+ else
+ {
+ if (StringUtil.__ISO_8859_1.equalsIgnoreCase(encoding))
+ _writer = new ResponseWriter(new Iso88591HttpWriter(_out), locale, encoding);
+ else if (StringUtil.__UTF8.equalsIgnoreCase(encoding))
+ _writer = new ResponseWriter(new Utf8HttpWriter(_out), locale, encoding);
+ else
+ _writer = new ResponseWriter(new EncodingHttpWriter(_out, encoding), locale, encoding);
+ }
+
+ // Set the output type at the end, because setCharacterEncoding() checks for it
+ _outputType = OutputType.WRITER;
+ }
+ return _writer;
+ }
+
+ @Override
+ public void setContentLength(int len)
+ {
+ // Protect from setting after committed as default handling
+ // of a servlet HEAD request ALWAYS sets _content length, even
+ // if the getHandling committed the response!
+ if (isCommitted() || !isMutable())
+ return;
+
+ if (len > 0)
+ {
+ long written = _out.getWritten();
+ if (written > len)
+ throw new IllegalArgumentException("setContentLength(" + len + ") when already written " + written);
+
+ _contentLength = len;
+ _fields.putLongField(HttpHeader.CONTENT_LENGTH, len);
+ if (isAllContentWritten(written))
+ {
+ try
+ {
+ closeOutput();
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeIOException(e);
+ }
+ }
+ }
+ else if (len == 0)
+ {
+ long written = _out.getWritten();
+ if (written > 0)
+ throw new IllegalArgumentException("setContentLength(0) when already written " + written);
+ _contentLength = len;
+ _fields.put(HttpHeader.CONTENT_LENGTH, "0");
+ }
+ else
+ {
+ _contentLength = len;
+ _fields.remove(HttpHeader.CONTENT_LENGTH);
+ }
+ }
+
+ public long getContentLength()
+ {
+ return _contentLength;
+ }
+
+ public boolean isAllContentWritten(long written)
+ {
+ return (_contentLength >= 0 && written >= _contentLength);
+ }
+
+ public boolean isContentComplete(long written)
+ {
+ return (_contentLength < 0 || written >= _contentLength);
+ }
+
+ public void closeOutput() throws IOException
+ {
+ if (_outputType == OutputType.WRITER)
+ _writer.close();
+ else
+ _out.close();
+ }
+
+ /**
+ * close the output
+ *
+ * @deprecated Use {@link #closeOutput()}
+ */
+ @Deprecated
+ public void completeOutput() throws IOException
+ {
+ closeOutput();
+ }
+
+ public void completeOutput(Callback callback)
+ {
+ if (_outputType == OutputType.WRITER)
+ _writer.complete(callback);
+ else
+ _out.complete(callback);
+ }
+
+ public long getLongContentLength()
+ {
+ return _contentLength;
+ }
+
+ public void setLongContentLength(long len)
+ {
+ // Protect from setting after committed as default handling
+ // of a servlet HEAD request ALWAYS sets _content length, even
+ // if the getHandling committed the response!
+ if (isCommitted() || !isMutable())
+ return;
+ _contentLength = len;
+ _fields.putLongField(HttpHeader.CONTENT_LENGTH.toString(), len);
+ }
+
+ @Override
+ public void setContentLengthLong(long length)
+ {
+ setLongContentLength(length);
+ }
+
+ @Override
+ public void setCharacterEncoding(String encoding)
+ {
+ setCharacterEncoding(encoding, EncodingFrom.SET_CHARACTER_ENCODING);
+ }
+
+ private void setCharacterEncoding(String encoding, EncodingFrom from)
+ {
+ if (!isMutable() || isWriting())
+ return;
+
+ if (_outputType != OutputType.WRITER && !isCommitted())
+ {
+ if (encoding == null)
+ {
+ _encodingFrom = EncodingFrom.NOT_SET;
+
+ // Clear any encoding.
+ if (_characterEncoding != null)
+ {
+ _characterEncoding = null;
+
+ if (_mimeType != null)
+ {
+ _mimeType = _mimeType.getBaseType();
+ _contentType = _mimeType.asString();
+ _fields.put(_mimeType.getContentTypeField());
+ }
+ else if (_contentType != null)
+ {
+ _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType);
+ _fields.put(HttpHeader.CONTENT_TYPE, _contentType);
+ }
+ }
+ }
+ else
+ {
+ // No, so just add this one to the mimetype
+ _encodingFrom = from;
+ _characterEncoding = HttpGenerator.__STRICT ? encoding : StringUtil.normalizeCharset(encoding);
+ if (_mimeType != null)
+ {
+ _contentType = _mimeType.getBaseType().asString() + ";charset=" + _characterEncoding;
+ _mimeType = MimeTypes.CACHE.get(_contentType);
+ if (_mimeType == null || HttpGenerator.__STRICT)
+ _fields.put(HttpHeader.CONTENT_TYPE, _contentType);
+ else
+ _fields.put(_mimeType.getContentTypeField());
+ }
+ else if (_contentType != null)
+ {
+ _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType) + ";charset=" + _characterEncoding;
+ _fields.put(HttpHeader.CONTENT_TYPE, _contentType);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void setContentType(String contentType)
+ {
+ if (isCommitted() || !isMutable())
+ return;
+
+ if (contentType == null)
+ {
+ if (isWriting() && _characterEncoding != null)
+ throw new IllegalSelectorException();
+
+ if (_locale == null)
+ _characterEncoding = null;
+ _mimeType = null;
+ _contentType = null;
+ _fields.remove(HttpHeader.CONTENT_TYPE);
+ }
+ else
+ {
+ _contentType = contentType;
+ _mimeType = MimeTypes.CACHE.get(contentType);
+
+ String charset;
+ if (_mimeType != null && _mimeType.getCharset() != null && !_mimeType.isCharsetAssumed())
+ charset = _mimeType.getCharsetString();
+ else
+ charset = MimeTypes.getCharsetFromContentType(contentType);
+
+ if (charset == null)
+ {
+ switch (_encodingFrom)
+ {
+ case NOT_SET:
+ break;
+ case INFERRED:
+ case SET_CONTENT_TYPE:
+ if (isWriting())
+ {
+ _mimeType = null;
+ _contentType = _contentType + ";charset=" + _characterEncoding;
+ }
+ else
+ {
+ _encodingFrom = EncodingFrom.NOT_SET;
+ _characterEncoding = null;
+ }
+ break;
+ case SET_LOCALE:
+ case SET_CHARACTER_ENCODING:
+ {
+ _contentType = contentType + ";charset=" + _characterEncoding;
+ _mimeType = null;
+ }
+ }
+ }
+ else if (isWriting() && !charset.equalsIgnoreCase(_characterEncoding))
+ {
+ // too late to change the character encoding;
+ _mimeType = null;
+ _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType);
+ if (_characterEncoding != null)
+ _contentType = _contentType + ";charset=" + _characterEncoding;
+ }
+ else
+ {
+ _characterEncoding = charset;
+ _encodingFrom = EncodingFrom.SET_CONTENT_TYPE;
+ }
+
+ if (HttpGenerator.__STRICT || _mimeType == null)
+ _fields.put(HttpHeader.CONTENT_TYPE, _contentType);
+ else
+ {
+ _contentType = _mimeType.asString();
+ _fields.put(_mimeType.getContentTypeField());
+ }
+ }
+ }
+
+ @Override
+ public void setBufferSize(int size)
+ {
+ if (isCommitted())
+ throw new IllegalStateException("cannot set buffer size after response is in committed state");
+ if (getContentCount() > 0)
+ throw new IllegalStateException("cannot set buffer size after response has " + getContentCount() + " bytes already written");
+ if (size < __MIN_BUFFER_SIZE)
+ size = __MIN_BUFFER_SIZE;
+ _out.setBufferSize(size);
+ }
+
+ @Override
+ public int getBufferSize()
+ {
+ return _out.getBufferSize();
+ }
+
+ @Override
+ public void flushBuffer() throws IOException
+ {
+ if (!_out.isClosed())
+ _out.flush();
+ }
+
+ @Override
+ public void reset()
+ {
+ _status = 200;
+ _reason = null;
+ _out.resetBuffer();
+ _outputType = OutputType.NONE;
+ _contentLength = -1;
+ _contentType = null;
+ _mimeType = null;
+ _characterEncoding = null;
+ _encodingFrom = EncodingFrom.NOT_SET;
+ _trailers = null;
+
+ // Clear all response headers
+ _fields.clear();
+
+ // recreate necessary connection related fields
+ for (String value : _channel.getRequest().getHttpFields().getCSV(HttpHeader.CONNECTION, false))
+ {
+ HttpHeaderValue cb = HttpHeaderValue.CACHE.get(value);
+ if (cb != null)
+ {
+ switch (cb)
+ {
+ case CLOSE:
+ _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.toString());
+ break;
+ case KEEP_ALIVE:
+ if (HttpVersion.HTTP_1_0.is(_channel.getRequest().getProtocol()))
+ _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.toString());
+ break;
+ case TE:
+ _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.TE.toString());
+ break;
+ default:
+ }
+ }
+ }
+
+ // recreate session cookies
+ Request request = getHttpChannel().getRequest();
+ HttpSession session = request.getSession(false);
+ if (session != null && session.isNew())
+ {
+ SessionHandler sh = request.getSessionHandler();
+ if (sh != null)
+ {
+ HttpCookie c = sh.getSessionCookie(session, request.getContextPath(), request.isSecure());
+ if (c != null)
+ addCookie(c);
+ }
+ }
+ }
+
+ public void resetContent()
+ {
+ _out.resetBuffer();
+ _outputType = OutputType.NONE;
+ _contentLength = -1;
+ _contentType = null;
+ _mimeType = null;
+ _characterEncoding = null;
+ _encodingFrom = EncodingFrom.NOT_SET;
+
+ // remove the content related response headers and keep all others
+ for (Iterator<HttpField> i = getHttpFields().iterator(); i.hasNext(); )
+ {
+ HttpField field = i.next();
+ if (field.getHeader() == null)
+ continue;
+
+ switch (field.getHeader())
+ {
+ case CONTENT_TYPE:
+ case CONTENT_LENGTH:
+ case CONTENT_ENCODING:
+ case CONTENT_LANGUAGE:
+ case CONTENT_RANGE:
+ case CONTENT_MD5:
+ case CONTENT_LOCATION:
+ case TRANSFER_ENCODING:
+ case CACHE_CONTROL:
+ case LAST_MODIFIED:
+ case EXPIRES:
+ case ETAG:
+ case DATE:
+ case VARY:
+ i.remove();
+ continue;
+ default:
+ }
+ }
+ }
+
+ public void resetForForward()
+ {
+ resetBuffer();
+ _outputType = OutputType.NONE;
+ }
+
+ @Override
+ public void resetBuffer()
+ {
+ _out.resetBuffer();
+ _out.reopen();
+ }
+
+ public void setTrailers(Supplier<HttpFields> trailers)
+ {
+ this._trailers = trailers;
+ }
+
+ public Supplier<HttpFields> getTrailers()
+ {
+ return _trailers;
+ }
+
+ protected MetaData.Response newResponseMetaData()
+ {
+ MetaData.Response info = new MetaData.Response(_channel.getRequest().getHttpVersion(), getStatus(), getReason(), _fields, getLongContentLength());
+ info.setTrailerSupplier(getTrailers());
+ return info;
+ }
+
+ /**
+ * Get the MetaData.Response committed for this response.
+ * This may differ from the meta data in this response for
+ * exceptional responses (eg 4xx and 5xx responses generated
+ * by the container) and the committedMetaData should be used
+ * for logging purposes.
+ *
+ * @return The committed MetaData or a {@link #newResponseMetaData()}
+ * if not yet committed.
+ */
+ public MetaData.Response getCommittedMetaData()
+ {
+ MetaData.Response meta = _channel.getCommittedMetaData();
+ if (meta == null)
+ return newResponseMetaData();
+ return meta;
+ }
+
+ @Override
+ public boolean isCommitted()
+ {
+ // If we are in sendError state, we pretend to be committed
+ if (_channel.isSendError())
+ return true;
+ return _channel.isCommitted();
+ }
+
+ @Override
+ public void setLocale(Locale locale)
+ {
+ if (locale == null || isCommitted() || !isMutable())
+ return;
+
+ _locale = locale;
+ _fields.put(HttpHeader.CONTENT_LANGUAGE, StringUtil.replace(locale.toString(), '_', '-'));
+
+ if (_outputType != OutputType.NONE)
+ return;
+
+ if (_channel.getRequest().getContext() == null)
+ return;
+
+ String charset = _channel.getRequest().getContext().getContextHandler().getLocaleEncoding(locale);
+
+ if (charset != null && charset.length() > 0 && __localeOverride.contains(_encodingFrom))
+ setCharacterEncoding(charset, EncodingFrom.SET_LOCALE);
+ }
+
+ @Override
+ public Locale getLocale()
+ {
+ if (_locale == null)
+ return Locale.getDefault();
+ return _locale;
+ }
+
+ @Override
+ public int getStatus()
+ {
+ return _status;
+ }
+
+ public String getReason()
+ {
+ return _reason;
+ }
+
+ public HttpFields getHttpFields()
+ {
+ return _fields;
+ }
+
+ public long getContentCount()
+ {
+ return _out.getWritten();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s %d %s%n%s", _channel.getRequest().getHttpVersion(), _status, _reason == null ? "" : _reason, _fields);
+ }
+
+ public void putHeaders(HttpContent content, long contentLength, boolean etag)
+ {
+ HttpField lm = content.getLastModified();
+ if (lm != null)
+ _fields.put(lm);
+
+ if (contentLength == USE_KNOWN_CONTENT_LENGTH)
+ {
+ _fields.put(content.getContentLength());
+ _contentLength = content.getContentLengthValue();
+ }
+ else if (contentLength > NO_CONTENT_LENGTH)
+ {
+ _fields.putLongField(HttpHeader.CONTENT_LENGTH, contentLength);
+ _contentLength = contentLength;
+ }
+
+ HttpField ct = content.getContentType();
+ if (ct != null)
+ {
+ if (_characterEncoding != null &&
+ content.getCharacterEncoding() == null &&
+ content.getContentTypeValue() != null &&
+ __explicitCharset.contains(_encodingFrom))
+ {
+ setContentType(MimeTypes.getContentTypeWithoutCharset(content.getContentTypeValue()));
+ }
+ else
+ {
+ _fields.put(ct);
+ _contentType = ct.getValue();
+ _characterEncoding = content.getCharacterEncoding();
+ _mimeType = content.getMimeType();
+ }
+ }
+
+ HttpField ce = content.getContentEncoding();
+ if (ce != null)
+ _fields.put(ce);
+
+ if (etag)
+ {
+ HttpField et = content.getETag();
+ if (et != null)
+ _fields.put(et);
+ }
+ }
+
+ public static void putHeaders(HttpServletResponse response, HttpContent content, long contentLength, boolean etag)
+ {
+ long lml = content.getResource().lastModified();
+ if (lml >= 0)
+ response.setDateHeader(HttpHeader.LAST_MODIFIED.asString(), lml);
+
+ if (contentLength == USE_KNOWN_CONTENT_LENGTH)
+ contentLength = content.getContentLengthValue();
+ if (contentLength > NO_CONTENT_LENGTH)
+ {
+ if (contentLength < Integer.MAX_VALUE)
+ response.setContentLength((int)contentLength);
+ else
+ response.setHeader(HttpHeader.CONTENT_LENGTH.asString(), Long.toString(contentLength));
+ }
+
+ String ct = content.getContentTypeValue();
+ if (ct != null && response.getContentType() == null)
+ response.setContentType(ct);
+
+ String ce = content.getContentEncodingValue();
+ if (ce != null)
+ response.setHeader(HttpHeader.CONTENT_ENCODING.asString(), ce);
+
+ if (etag)
+ {
+ String et = content.getETagValue();
+ if (et != null)
+ response.setHeader(HttpHeader.ETAG.asString(), et);
+ }
+ }
+
+ public static HttpServletResponse unwrap(ServletResponse servletResponse)
+ {
+ if (servletResponse instanceof HttpServletResponseWrapper)
+ {
+ return (HttpServletResponseWrapper)servletResponse;
+ }
+ if (servletResponse instanceof ServletResponseWrapper)
+ {
+ return unwrap(((ServletResponseWrapper)servletResponse).getResponse());
+ }
+ return (HttpServletResponse)servletResponse;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ResponseWriter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ResponseWriter.java
new file mode 100644
index 0000000..39866e3
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ResponseWriter.java
@@ -0,0 +1,503 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.PrintWriter;
+import java.util.Formatter;
+import java.util.Locale;
+import javax.servlet.ServletResponse;
+
+import org.eclipse.jetty.io.EofException;
+import org.eclipse.jetty.io.RuntimeIOException;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Specialized PrintWriter for servlet Responses
+ * <p>An instance of ResponseWriter is the {@link PrintWriter} subclass returned by {@link Response#getWriter()}.
+ * It differs from the standard {@link PrintWriter} in that:<ul>
+ * <li>It does not support autoflush</li>
+ * <li>The default Locale for {@link #format(String, Object...)} is the locale obtained by {@link ServletResponse#getLocale()}</li>
+ * <li>If a write or print method is called while {@link #checkError()} returns true, then a {@link RuntimeIOException} is thrown to stop needless iterations.</li>
+ * <li>The writer may be reopen to allow for recycling</li>
+ * </ul>
+ */
+public class ResponseWriter extends PrintWriter
+{
+ private static final Logger LOG = Log.getLogger(ResponseWriter.class);
+ private static final String __lineSeparator = System.getProperty("line.separator");
+ private static final String __trueln = "true" + __lineSeparator;
+ private static final String __falseln = "false" + __lineSeparator;
+
+ private final HttpWriter _httpWriter;
+ private final Locale _locale;
+ private final String _encoding;
+ private IOException _ioException;
+ private boolean _isClosed = false;
+ private Formatter _formatter;
+
+ public ResponseWriter(HttpWriter httpWriter, Locale locale, String encoding)
+ {
+ super(httpWriter, false);
+ _httpWriter = httpWriter;
+ _locale = locale;
+ _encoding = encoding;
+ }
+
+ public boolean isFor(Locale locale, String encoding)
+ {
+ if (_locale == null && locale != null)
+ return false;
+ if (_encoding == null && encoding != null)
+ return false;
+ return _encoding.equalsIgnoreCase(encoding) && _locale.equals(locale);
+ }
+
+ protected void reopen()
+ {
+ synchronized (lock)
+ {
+ _isClosed = false;
+ clearError();
+ out = _httpWriter;
+ }
+ }
+
+ @Override
+ protected void clearError()
+ {
+ synchronized (lock)
+ {
+ _ioException = null;
+ super.clearError();
+ }
+ }
+
+ @Override
+ public boolean checkError()
+ {
+ synchronized (lock)
+ {
+ return _ioException != null || super.checkError();
+ }
+ }
+
+ private void setError(Throwable th)
+ {
+ super.setError();
+
+ if (th instanceof IOException)
+ _ioException = (IOException)th;
+ else
+ {
+ _ioException = new IOException(String.valueOf(th));
+ _ioException.initCause(th);
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug(th);
+ }
+
+ @Override
+ protected void setError()
+ {
+ setError(new IOException());
+ }
+
+ /**
+ * Check to make sure that the stream has not been closed
+ */
+ private void isOpen() throws IOException
+ {
+ if (_ioException != null)
+ throw _ioException;
+
+ if (_isClosed)
+ {
+ _ioException = new EofException("Stream closed");
+ throw _ioException;
+ }
+ }
+
+ @Override
+ public void flush()
+ {
+ try
+ {
+ synchronized (lock)
+ {
+ isOpen();
+ out.flush();
+ }
+ }
+ catch (Throwable ex)
+ {
+ setError(ex);
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ try
+ {
+ synchronized (lock)
+ {
+ out.close();
+ _isClosed = true;
+ }
+ }
+ catch (IOException ex)
+ {
+ setError(ex);
+ }
+ }
+
+ public void complete(Callback callback)
+ {
+ synchronized (lock)
+ {
+ _isClosed = true;
+ }
+ _httpWriter.complete(callback);
+ }
+
+ @Override
+ public void write(int c)
+ {
+ try
+ {
+ synchronized (lock)
+ {
+ isOpen();
+ out.write(c);
+ }
+ }
+ catch (InterruptedIOException ex)
+ {
+ LOG.debug(ex);
+ Thread.currentThread().interrupt();
+ }
+ catch (IOException ex)
+ {
+ setError(ex);
+ }
+ }
+
+ @Override
+ public void write(char[] buf, int off, int len)
+ {
+ try
+ {
+ synchronized (lock)
+ {
+ isOpen();
+ out.write(buf, off, len);
+ }
+ }
+ catch (InterruptedIOException ex)
+ {
+ LOG.debug(ex);
+ Thread.currentThread().interrupt();
+ }
+ catch (IOException ex)
+ {
+ setError(ex);
+ }
+ }
+
+ @Override
+ public void write(char[] buf)
+ {
+ this.write(buf, 0, buf.length);
+ }
+
+ @Override
+ public void write(String s, int off, int len)
+ {
+ try
+ {
+ synchronized (lock)
+ {
+ isOpen();
+ out.write(s, off, len);
+ }
+ }
+ catch (InterruptedIOException ex)
+ {
+ LOG.debug(ex);
+ Thread.currentThread().interrupt();
+ }
+ catch (IOException ex)
+ {
+ setError(ex);
+ }
+ }
+
+ @Override
+ public void write(String s)
+ {
+ this.write(s, 0, s.length());
+ }
+
+ @Override
+ public void print(boolean b)
+ {
+ this.write(b ? "true" : "false");
+ }
+
+ @Override
+ public void print(char c)
+ {
+ this.write(c);
+ }
+
+ @Override
+ public void print(int i)
+ {
+ this.write(String.valueOf(i));
+ }
+
+ @Override
+ public void print(long l)
+ {
+ this.write(String.valueOf(l));
+ }
+
+ @Override
+ public void print(float f)
+ {
+ this.write(String.valueOf(f));
+ }
+
+ @Override
+ public void print(double d)
+ {
+ this.write(String.valueOf(d));
+ }
+
+ @Override
+ public void print(char[] s)
+ {
+ this.write(s);
+ }
+
+ @Override
+ public void print(String s)
+ {
+ if (s == null)
+ s = "null";
+ this.write(s);
+ }
+
+ @Override
+ public void print(Object obj)
+ {
+ this.write(String.valueOf(obj));
+ }
+
+ @Override
+ public void println()
+ {
+ try
+ {
+ synchronized (lock)
+ {
+ isOpen();
+ out.write(__lineSeparator);
+ }
+ }
+ catch (InterruptedIOException ex)
+ {
+ LOG.debug(ex);
+ Thread.currentThread().interrupt();
+ }
+ catch (IOException ex)
+ {
+ setError(ex);
+ }
+ }
+
+ @Override
+ public void println(boolean b)
+ {
+ println(b ? __trueln : __falseln);
+ }
+
+ @Override
+ public void println(char c)
+ {
+ try
+ {
+ synchronized (lock)
+ {
+ isOpen();
+ out.write(c);
+ }
+ }
+ catch (InterruptedIOException ex)
+ {
+ LOG.debug(ex);
+ Thread.currentThread().interrupt();
+ }
+ catch (IOException ex)
+ {
+ setError(ex);
+ }
+ }
+
+ @Override
+ public void println(int x)
+ {
+ this.println(String.valueOf(x));
+ }
+
+ @Override
+ public void println(long x)
+ {
+ this.println(String.valueOf(x));
+ }
+
+ @Override
+ public void println(float x)
+ {
+ this.println(String.valueOf(x));
+ }
+
+ @Override
+ public void println(double x)
+ {
+ this.println(String.valueOf(x));
+ }
+
+ @Override
+ public void println(char[] s)
+ {
+ try
+ {
+ synchronized (lock)
+ {
+ isOpen();
+ out.write(s, 0, s.length);
+ out.write(__lineSeparator);
+ }
+ }
+ catch (InterruptedIOException ex)
+ {
+ LOG.debug(ex);
+ Thread.currentThread().interrupt();
+ }
+ catch (IOException ex)
+ {
+ setError(ex);
+ }
+ }
+
+ @Override
+ public void println(String s)
+ {
+ if (s == null)
+ s = "null";
+
+ try
+ {
+ synchronized (lock)
+ {
+ isOpen();
+ out.write(s, 0, s.length());
+ out.write(__lineSeparator);
+ }
+ }
+ catch (InterruptedIOException ex)
+ {
+ LOG.debug(ex);
+ Thread.currentThread().interrupt();
+ }
+ catch (IOException ex)
+ {
+ setError(ex);
+ }
+ }
+
+ @Override
+ public void println(Object x)
+ {
+ this.println(String.valueOf(x));
+ }
+
+ @Override
+ public PrintWriter printf(String format, Object... args)
+ {
+ return format(_locale, format, args);
+ }
+
+ @Override
+ public PrintWriter printf(Locale l, String format, Object... args)
+ {
+ return format(l, format, args);
+ }
+
+ @Override
+ public PrintWriter format(String format, Object... args)
+ {
+ return format(_locale, format, args);
+ }
+
+ @Override
+ public PrintWriter format(Locale locale, String format, Object... args)
+ {
+ try
+ {
+
+ /* If the passed locale is null then
+ use any locale set on the response as the default. */
+ if (locale == null)
+ locale = _locale;
+
+ synchronized (lock)
+ {
+ isOpen();
+
+ if (_formatter == null)
+ {
+ _formatter = new Formatter(this, locale);
+ }
+ else if (!_formatter.locale().equals(locale))
+ {
+ _formatter = new Formatter(this, locale);
+ }
+
+ _formatter.format(locale, format, args);
+ }
+ }
+ catch (InterruptedIOException ex)
+ {
+ LOG.debug(ex);
+ Thread.currentThread().interrupt();
+ }
+ catch (IOException ex)
+ {
+ setError(ex);
+ }
+ return this;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SameFileAliasChecker.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SameFileAliasChecker.java
new file mode 100644
index 0000000..5f7009e
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SameFileAliasChecker.java
@@ -0,0 +1,78 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.jetty.server.handler.ContextHandler.AliasCheck;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.PathResource;
+import org.eclipse.jetty.util.resource.Resource;
+
+/**
+ * Alias checking for working with FileSystems that normalize access to the
+ * File System.
+ * <p>
+ * The Java {@link Files#isSameFile(Path, Path)} method is used to determine
+ * if the requested file is the same as the alias file.
+ * </p>
+ * <p>
+ * For File Systems that are case insensitive (eg: Microsoft Windows FAT32 and NTFS),
+ * the access to the file can be in any combination or style of upper and lowercase.
+ * </p>
+ * <p>
+ * For File Systems that normalize UTF-8 access (eg: Mac OSX on HFS+ or APFS,
+ * or Linux on XFS) the the actual file could be stored using UTF-16,
+ * but be accessed using NFD UTF-8 or NFC UTF-8 for the same file.
+ * </p>
+ */
+public class SameFileAliasChecker implements AliasCheck
+{
+ private static final Logger LOG = Log.getLogger(SameFileAliasChecker.class);
+
+ @Override
+ public boolean check(String uri, Resource resource)
+ {
+ // Only support PathResource alias checking
+ if (!(resource instanceof PathResource))
+ return false;
+
+ try
+ {
+ PathResource pathResource = (PathResource)resource;
+ Path path = pathResource.getPath();
+ Path alias = pathResource.getAliasPath();
+
+ if (Files.isSameFile(path, alias))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Allow alias to same file {} --> {}", path, alias);
+ return true;
+ }
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ }
+ return false;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java
new file mode 100644
index 0000000..8d2242a
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java
@@ -0,0 +1,450 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.security.cert.X509Certificate;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLSession;
+import javax.servlet.ServletRequest;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.http.PreEncodedHttpField;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.ssl.SslConnection;
+import org.eclipse.jetty.io.ssl.SslConnection.DecryptedEndPoint;
+import org.eclipse.jetty.util.Attributes;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.util.annotation.Name;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.ssl.SniX509ExtendedKeyManager;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.ssl.X509;
+
+/**
+ * <p>Customizer that extracts the attribute from an {@link SSLContext}
+ * and sets them on the request with {@link ServletRequest#setAttribute(String, Object)}
+ * according to Servlet Specification Requirements.</p>
+ */
+public class SecureRequestCustomizer implements HttpConfiguration.Customizer
+{
+ private static final Logger LOG = Log.getLogger(SecureRequestCustomizer.class);
+ public static final String JAVAX_SERVLET_REQUEST_X_509_CERTIFICATE = "javax.servlet.request.X509Certificate";
+ public static final String JAVAX_SERVLET_REQUEST_CIPHER_SUITE = "javax.servlet.request.cipher_suite";
+ public static final String JAVAX_SERVLET_REQUEST_KEY_SIZE = "javax.servlet.request.key_size";
+ public static final String JAVAX_SERVLET_REQUEST_SSL_SESSION_ID = "javax.servlet.request.ssl_session_id";
+
+ private String sslSessionAttribute = "org.eclipse.jetty.servlet.request.ssl_session";
+
+ private boolean _sniRequired;
+ private boolean _sniHostCheck;
+ private long _stsMaxAge = -1;
+ private boolean _stsIncludeSubDomains;
+ private HttpField _stsField;
+
+ public SecureRequestCustomizer()
+ {
+ this(true);
+ }
+
+ public SecureRequestCustomizer(@Name("sniHostCheck") boolean sniHostCheck)
+ {
+ this(sniHostCheck, -1, false);
+ }
+
+ /**
+ * @param sniHostCheck True if the SNI Host name must match.
+ * @param stsMaxAgeSeconds The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent.
+ * @param stsIncludeSubdomains If true, a include subdomain property is sent with any Strict-Transport-Security header
+ */
+ public SecureRequestCustomizer(
+ @Name("sniHostCheck") boolean sniHostCheck,
+ @Name("stsMaxAgeSeconds") long stsMaxAgeSeconds,
+ @Name("stsIncludeSubdomains") boolean stsIncludeSubdomains)
+ {
+ this(false, sniHostCheck, stsMaxAgeSeconds, stsIncludeSubdomains);
+ }
+
+ /**
+ * @param sniRequired True if a SNI certificate is required.
+ * @param sniHostCheck True if the SNI Host name must match.
+ * @param stsMaxAgeSeconds The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent.
+ * @param stsIncludeSubdomains If true, a include subdomain property is sent with any Strict-Transport-Security header
+ */
+ public SecureRequestCustomizer(
+ @Name("sniRequired") boolean sniRequired,
+ @Name("sniHostCheck") boolean sniHostCheck,
+ @Name("stsMaxAgeSeconds") long stsMaxAgeSeconds,
+ @Name("stsIncludeSubdomains") boolean stsIncludeSubdomains)
+ {
+ _sniRequired = sniRequired;
+ _sniHostCheck = sniHostCheck;
+ _stsMaxAge = stsMaxAgeSeconds;
+ _stsIncludeSubDomains = stsIncludeSubdomains;
+ formatSTS();
+ }
+
+ /**
+ * @return True if the SNI Host name must match when there is an SNI certificate.
+ */
+ public boolean isSniHostCheck()
+ {
+ return _sniHostCheck;
+ }
+
+ /**
+ * @param sniHostCheck True if the SNI Host name must match when there is an SNI certificate.
+ */
+ public void setSniHostCheck(boolean sniHostCheck)
+ {
+ _sniHostCheck = sniHostCheck;
+ }
+
+ /**
+ * @return True if SNI is required, else requests will be rejected with 400 response.
+ * @see SslContextFactory.Server#isSniRequired()
+ */
+ public boolean isSniRequired()
+ {
+ return _sniRequired;
+ }
+
+ /**
+ * @param sniRequired True if SNI is required, else requests will be rejected with 400 response.
+ * @see SslContextFactory.Server#setSniRequired(boolean)
+ */
+ public void setSniRequired(boolean sniRequired)
+ {
+ _sniRequired = sniRequired;
+ }
+
+ /**
+ * @return The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent.
+ */
+ public long getStsMaxAge()
+ {
+ return _stsMaxAge;
+ }
+
+ /**
+ * Set the Strict-Transport-Security max age.
+ *
+ * @param stsMaxAgeSeconds The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent.
+ */
+ public void setStsMaxAge(long stsMaxAgeSeconds)
+ {
+ _stsMaxAge = stsMaxAgeSeconds;
+ formatSTS();
+ }
+
+ /**
+ * Convenience method to call {@link #setStsMaxAge(long)}
+ *
+ * @param period The period in units
+ * @param units The {@link TimeUnit} of the period
+ */
+ public void setStsMaxAge(long period, TimeUnit units)
+ {
+ _stsMaxAge = units.toSeconds(period);
+ formatSTS();
+ }
+
+ /**
+ * @return true if a include subdomain property is sent with any Strict-Transport-Security header
+ */
+ public boolean isStsIncludeSubDomains()
+ {
+ return _stsIncludeSubDomains;
+ }
+
+ /**
+ * @param stsIncludeSubDomains If true, a include subdomain property is sent with any Strict-Transport-Security header
+ */
+ public void setStsIncludeSubDomains(boolean stsIncludeSubDomains)
+ {
+ _stsIncludeSubDomains = stsIncludeSubDomains;
+ formatSTS();
+ }
+
+ private void formatSTS()
+ {
+ if (_stsMaxAge < 0)
+ _stsField = null;
+ else
+ _stsField = new PreEncodedHttpField(HttpHeader.STRICT_TRANSPORT_SECURITY, String.format("max-age=%d%s", _stsMaxAge, _stsIncludeSubDomains ? "; includeSubDomains" : ""));
+ }
+
+ @Override
+ public void customize(Connector connector, HttpConfiguration channelConfig, Request request)
+ {
+ EndPoint endp = request.getHttpChannel().getEndPoint();
+ if (endp instanceof DecryptedEndPoint)
+ {
+ SslConnection.DecryptedEndPoint sslEndp = (DecryptedEndPoint)endp;
+ SslConnection sslConnection = sslEndp.getSslConnection();
+ SSLEngine sslEngine = sslConnection.getSSLEngine();
+ customize(sslEngine, request);
+
+ if (request.getHttpURI().getScheme() == null)
+ request.setScheme(HttpScheme.HTTPS.asString());
+ }
+ else if (endp instanceof ProxyConnectionFactory.ProxyEndPoint)
+ {
+ ProxyConnectionFactory.ProxyEndPoint proxy = (ProxyConnectionFactory.ProxyEndPoint)endp;
+ if (request.getHttpURI().getScheme() == null && proxy.getAttribute(ProxyConnectionFactory.TLS_VERSION) != null)
+ request.setScheme(HttpScheme.HTTPS.asString());
+ }
+
+ if (HttpScheme.HTTPS.is(request.getScheme()))
+ customizeSecure(request);
+ }
+
+ /**
+ * Customizes the request attributes for general secure settings.
+ * The default impl calls {@link Request#setSecure(boolean)} with true
+ * and sets a response header if the Strict-Transport-Security options
+ * are set.
+ *
+ * @param request the request being customized
+ */
+ protected void customizeSecure(Request request)
+ {
+ request.setSecure(true);
+
+ if (_stsField != null)
+ request.getResponse().getHttpFields().add(_stsField);
+ }
+
+ /**
+ * <p>
+ * Customizes the request attributes to be set for SSL requests.
+ * </p>
+ * <p>
+ * The requirements of the Servlet specs are:
+ * </p>
+ * <ul>
+ * <li>an attribute named "javax.servlet.request.ssl_session_id" of type String (since Servlet Spec 3.0).</li>
+ * <li>an attribute named "javax.servlet.request.cipher_suite" of type String.</li>
+ * <li>an attribute named "javax.servlet.request.key_size" of type Integer.</li>
+ * <li>an attribute named "javax.servlet.request.X509Certificate" of type java.security.cert.X509Certificate[]. This
+ * is an array of objects of type X509Certificate, the order of this array is defined as being in ascending order of
+ * trust. The first certificate in the chain is the one set by the client, the next is the one used to authenticate
+ * the first, and so on.</li>
+ * </ul>
+ *
+ * @param sslEngine the sslEngine to be customized.
+ * @param request HttpRequest to be customized.
+ */
+ protected void customize(SSLEngine sslEngine, Request request)
+ {
+ SSLSession sslSession = sslEngine.getSession();
+
+ if (_sniHostCheck || _sniRequired)
+ {
+ X509 x509 = (X509)sslSession.getValue(SniX509ExtendedKeyManager.SNI_X509);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Host {} with SNI {}", request.getServerName(), x509);
+
+ if (x509 == null)
+ {
+ if (_sniRequired)
+ throw new BadMessageException(400, "SNI required");
+ }
+ else if (_sniHostCheck && !x509.matches(request.getServerName()))
+ {
+ throw new BadMessageException(400, "Host does not match SNI");
+ }
+ }
+
+ request.setAttributes(new SslAttributes(request, sslSession, request.getAttributes()));
+ }
+
+ private X509Certificate[] getCertChain(Connector connector, SSLSession sslSession)
+ {
+ // The in-use SslContextFactory should be present in the Connector's SslConnectionFactory
+ SslConnectionFactory sslConnectionFactory = connector.getConnectionFactory(SslConnectionFactory.class);
+ if (sslConnectionFactory != null)
+ {
+ SslContextFactory sslContextFactory = sslConnectionFactory.getSslContextFactory();
+ if (sslContextFactory != null)
+ return sslContextFactory.getX509CertChain(sslSession);
+ }
+
+ // Fallback, either no SslConnectionFactory or no SslContextFactory instance found
+ return SslContextFactory.getCertChain(sslSession);
+ }
+
+ public void setSslSessionAttribute(String attribute)
+ {
+ this.sslSessionAttribute = attribute;
+ }
+
+ public String getSslSessionAttribute()
+ {
+ return sslSessionAttribute;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x", this.getClass().getSimpleName(), hashCode());
+ }
+
+ private class SslAttributes extends Attributes.Wrapper
+ {
+ private final Request _request;
+ private final SSLSession _session;
+
+ private X509Certificate[] _certs;
+ private String _cipherSuite;
+ private Integer _keySize;
+ private String _sessionId;
+ private String _sessionAttribute;
+
+ public SslAttributes(Request request, SSLSession sslSession, Attributes attributes)
+ {
+ super(attributes);
+ this._request = request;
+ this._session = sslSession;
+
+ try
+ {
+ SslSessionData sslSessionData = getSslSessionData();
+ _certs = sslSessionData.getCerts();
+ _cipherSuite = _session.getCipherSuite();
+ _keySize = sslSessionData.getKeySize();
+ _sessionId = sslSessionData.getIdStr();
+ _sessionAttribute = getSslSessionAttribute();
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Unable to get secure details ", e);
+ }
+ }
+
+ @Override
+ public Object getAttribute(String name)
+ {
+ switch (name)
+ {
+ case JAVAX_SERVLET_REQUEST_X_509_CERTIFICATE:
+ return _certs;
+ case JAVAX_SERVLET_REQUEST_CIPHER_SUITE:
+ return _cipherSuite;
+ case JAVAX_SERVLET_REQUEST_KEY_SIZE:
+ return _keySize;
+ case JAVAX_SERVLET_REQUEST_SSL_SESSION_ID:
+ return _sessionId;
+ default:
+ if (!StringUtil.isEmpty(_sessionAttribute) && _sessionAttribute.equals(name))
+ return _session;
+ }
+
+ return _attributes.getAttribute(name);
+ }
+
+ /**
+ * Get data belonging to the {@link SSLSession}.
+ *
+ * @return the SslSessionData
+ */
+ private SslSessionData getSslSessionData()
+ {
+ String key = SslSessionData.class.getName();
+ SslSessionData sslSessionData = (SslSessionData)_session.getValue(key);
+ if (sslSessionData == null)
+ {
+ String cipherSuite = _session.getCipherSuite();
+ int keySize = SslContextFactory.deduceKeyLength(cipherSuite);
+
+ X509Certificate[] certs = getCertChain(_request.getHttpChannel().getConnector(), _session);
+
+ byte[] bytes = _session.getId();
+ String idStr = TypeUtil.toHexString(bytes);
+
+ sslSessionData = new SslSessionData(keySize, certs, idStr);
+ _session.putValue(key, sslSessionData);
+ }
+ return sslSessionData;
+ }
+
+ @Override
+ public Set<String> getAttributeNameSet()
+ {
+ Set<String> names = new HashSet<>(_attributes.getAttributeNameSet());
+ names.remove(JAVAX_SERVLET_REQUEST_X_509_CERTIFICATE);
+ names.remove(JAVAX_SERVLET_REQUEST_CIPHER_SUITE);
+ names.remove(JAVAX_SERVLET_REQUEST_KEY_SIZE);
+ names.remove(JAVAX_SERVLET_REQUEST_SSL_SESSION_ID);
+
+ if (_certs != null)
+ names.add(JAVAX_SERVLET_REQUEST_X_509_CERTIFICATE);
+ if (_cipherSuite != null)
+ names.add(JAVAX_SERVLET_REQUEST_CIPHER_SUITE);
+ if (_keySize != null)
+ names.add(JAVAX_SERVLET_REQUEST_KEY_SIZE);
+ if (_sessionId != null)
+ names.add(JAVAX_SERVLET_REQUEST_SSL_SESSION_ID);
+ if (!StringUtil.isEmpty(_sessionAttribute))
+ names.add(_sessionAttribute);
+
+ return names;
+ }
+ }
+
+ /**
+ * Simple bundle of data that is cached in the SSLSession.
+ */
+ private static class SslSessionData
+ {
+ private final Integer _keySize;
+ private final X509Certificate[] _certs;
+ private final String _idStr;
+
+ private SslSessionData(Integer keySize, X509Certificate[] certs, String idStr)
+ {
+ this._keySize = keySize;
+ this._certs = certs;
+ this._idStr = idStr;
+ }
+
+ private Integer getKeySize()
+ {
+ return _keySize;
+ }
+
+ private X509Certificate[] getCerts()
+ {
+ return _certs;
+ }
+
+ private String getIdStr()
+ {
+ return _idStr;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java
new file mode 100644
index 0000000..88f802e
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java
@@ -0,0 +1,711 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Future;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.DateGenerator;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpGenerator;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.PreEncodedHttpField;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.server.handler.HandlerWrapper;
+import org.eclipse.jetty.server.handler.StatisticsHandler;
+import org.eclipse.jetty.util.Attributes;
+import org.eclipse.jetty.util.Jetty;
+import org.eclipse.jetty.util.MultiException;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.Uptime;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.Name;
+import org.eclipse.jetty.util.component.AttributeContainerMap;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Locker;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.ShutdownThread;
+import org.eclipse.jetty.util.thread.ThreadPool;
+
+/**
+ * Jetty HTTP Servlet Server.
+ * This class is the main class for the Jetty HTTP Servlet server.
+ * It aggregates Connectors (HTTP request receivers) and request Handlers.
+ * The server is itself a handler and a ThreadPool. Connectors use the ThreadPool methods
+ * to run jobs that will eventually call the handle method.
+ */
+@ManagedObject(value = "Jetty HTTP Servlet server")
+public class Server extends HandlerWrapper implements Attributes
+{
+ private static final Logger LOG = Log.getLogger(Server.class);
+
+ private final AttributeContainerMap _attributes = new AttributeContainerMap();
+ private final ThreadPool _threadPool;
+ private final List<Connector> _connectors = new CopyOnWriteArrayList<>();
+ private SessionIdManager _sessionIdManager;
+ private boolean _stopAtShutdown;
+ private boolean _dumpAfterStart = false;
+ private boolean _dumpBeforeStop = false;
+ private ErrorHandler _errorHandler;
+ private RequestLog _requestLog;
+
+ private final Locker _dateLocker = new Locker();
+ private volatile DateField _dateField;
+
+ public Server()
+ {
+ this((ThreadPool)null);
+ }
+
+ /**
+ * Convenience constructor
+ * Creates server and a {@link ServerConnector} at the passed port.
+ *
+ * @param port The port of a network HTTP connector (or 0 for a randomly allocated port).
+ * @see NetworkConnector#getLocalPort()
+ */
+ public Server(@Name("port") int port)
+ {
+ this((ThreadPool)null);
+ ServerConnector connector = new ServerConnector(this);
+ connector.setPort(port);
+ setConnectors(new Connector[]{connector});
+ addBean(_attributes);
+ }
+
+ /**
+ * Convenience constructor
+ * <p>
+ * Creates server and a {@link ServerConnector} at the passed address.
+ *
+ * @param addr the inet socket address to create the connector from
+ */
+ public Server(@Name("address") InetSocketAddress addr)
+ {
+ this((ThreadPool)null);
+ ServerConnector connector = new ServerConnector(this);
+ connector.setHost(addr.getHostName());
+ connector.setPort(addr.getPort());
+ setConnectors(new Connector[]{connector});
+ }
+
+ public Server(@Name("threadpool") ThreadPool pool)
+ {
+ _threadPool = pool != null ? pool : new QueuedThreadPool();
+ addBean(_threadPool);
+ setServer(this);
+ }
+
+ public RequestLog getRequestLog()
+ {
+ return _requestLog;
+ }
+
+ public ErrorHandler getErrorHandler()
+ {
+ return _errorHandler;
+ }
+
+ public void setRequestLog(RequestLog requestLog)
+ {
+ updateBean(_requestLog, requestLog);
+ _requestLog = requestLog;
+ }
+
+ public void setErrorHandler(ErrorHandler errorHandler)
+ {
+ if (errorHandler instanceof ErrorHandler.ErrorPageMapper)
+ throw new IllegalArgumentException("ErrorPageMapper is applicable only to ContextHandler");
+ updateBean(_errorHandler, errorHandler);
+ _errorHandler = errorHandler;
+ if (errorHandler != null)
+ errorHandler.setServer(this);
+ }
+
+ @ManagedAttribute("version of this server")
+ public static String getVersion()
+ {
+ return Jetty.VERSION;
+ }
+
+ public boolean getStopAtShutdown()
+ {
+ return _stopAtShutdown;
+ }
+
+ /**
+ * Set a graceful stop time.
+ * The {@link StatisticsHandler} must be configured so that open connections can
+ * be tracked for a graceful shutdown.
+ *
+ * @see org.eclipse.jetty.util.component.ContainerLifeCycle#setStopTimeout(long)
+ */
+ @Override
+ public void setStopTimeout(long stopTimeout)
+ {
+ super.setStopTimeout(stopTimeout);
+ }
+
+ /**
+ * Set stop server at shutdown behaviour.
+ *
+ * @param stop If true, this server instance will be explicitly stopped when the
+ * JVM is shutdown. Otherwise the JVM is stopped with the server running.
+ * @see Runtime#addShutdownHook(Thread)
+ * @see ShutdownThread
+ */
+ public void setStopAtShutdown(boolean stop)
+ {
+ //if we now want to stop
+ if (stop)
+ {
+ //and we weren't stopping before
+ if (!_stopAtShutdown)
+ {
+ //only register to stop if we're already started (otherwise we'll do it in doStart())
+ if (isStarted())
+ ShutdownThread.register(this);
+ }
+ }
+ else
+ ShutdownThread.deregister(this);
+
+ _stopAtShutdown = stop;
+ }
+
+ /**
+ * @return Returns the connectors.
+ */
+ @ManagedAttribute(value = "connectors for this server", readonly = true)
+ public Connector[] getConnectors()
+ {
+ List<Connector> connectors = new ArrayList<>(_connectors);
+ return connectors.toArray(new Connector[connectors.size()]);
+ }
+
+ public void addConnector(Connector connector)
+ {
+ if (connector.getServer() != this)
+ throw new IllegalArgumentException("Connector " + connector +
+ " cannot be shared among server " + connector.getServer() + " and server " + this);
+ _connectors.add(connector);
+ addBean(connector);
+ }
+
+ /**
+ * Convenience method which calls {@link #getConnectors()} and {@link #setConnectors(Connector[])} to
+ * remove a connector.
+ *
+ * @param connector The connector to remove.
+ */
+ public void removeConnector(Connector connector)
+ {
+ if (_connectors.remove(connector))
+ removeBean(connector);
+ }
+
+ /**
+ * Set the connectors for this server.
+ * Each connector has this server set as it's ThreadPool and its Handler.
+ *
+ * @param connectors The connectors to set.
+ */
+ public void setConnectors(Connector[] connectors)
+ {
+ if (connectors != null)
+ {
+ for (Connector connector : connectors)
+ {
+ if (connector.getServer() != this)
+ throw new IllegalArgumentException("Connector " + connector +
+ " cannot be shared among server " + connector.getServer() + " and server " + this);
+ }
+ }
+
+ Connector[] oldConnectors = getConnectors();
+ updateBeans(oldConnectors, connectors);
+ _connectors.removeAll(Arrays.asList(oldConnectors));
+ if (connectors != null)
+ _connectors.addAll(Arrays.asList(connectors));
+ }
+
+ /**
+ * Add a bean to all connectors on the server.
+ * If the bean is an instance of {@link Connection.Listener} it will also be
+ * registered as a listener on all connections accepted by the connectors.
+ * @param bean the bean to be added.
+ */
+ public void addBeanToAllConnectors(Object bean)
+ {
+ for (Connector connector : getConnectors())
+ {
+ connector.addBean(bean);
+ }
+ }
+
+ /**
+ * @return Returns the threadPool.
+ */
+ @ManagedAttribute("the server thread pool")
+ public ThreadPool getThreadPool()
+ {
+ return _threadPool;
+ }
+
+ /**
+ * @return true if {@link #dumpStdErr()} is called after starting
+ */
+ @ManagedAttribute("dump state to stderr after start")
+ public boolean isDumpAfterStart()
+ {
+ return _dumpAfterStart;
+ }
+
+ /**
+ * @param dumpAfterStart true if {@link #dumpStdErr()} is called after starting
+ */
+ public void setDumpAfterStart(boolean dumpAfterStart)
+ {
+ _dumpAfterStart = dumpAfterStart;
+ }
+
+ /**
+ * @return true if {@link #dumpStdErr()} is called before stopping
+ */
+ @ManagedAttribute("dump state to stderr before stop")
+ public boolean isDumpBeforeStop()
+ {
+ return _dumpBeforeStop;
+ }
+
+ /**
+ * @param dumpBeforeStop true if {@link #dumpStdErr()} is called before stopping
+ */
+ public void setDumpBeforeStop(boolean dumpBeforeStop)
+ {
+ _dumpBeforeStop = dumpBeforeStop;
+ }
+
+ public HttpField getDateField()
+ {
+ long now = System.currentTimeMillis();
+ long seconds = now / 1000;
+ DateField df = _dateField;
+
+ if (df == null || df._seconds != seconds)
+ {
+ try (Locker.Lock lock = _dateLocker.lock())
+ {
+ df = _dateField;
+ if (df == null || df._seconds != seconds)
+ {
+ HttpField field = new PreEncodedHttpField(HttpHeader.DATE, DateGenerator.formatDate(now));
+ _dateField = new DateField(seconds, field);
+ return field;
+ }
+ }
+ }
+ return df._dateField;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ // Create an error handler if there is none
+ if (_errorHandler == null)
+ _errorHandler = getBean(ErrorHandler.class);
+ if (_errorHandler == null)
+ setErrorHandler(new ErrorHandler());
+ if (_errorHandler instanceof ErrorHandler.ErrorPageMapper)
+ LOG.warn("ErrorPageMapper not supported for Server level Error Handling");
+ _errorHandler.setServer(this);
+
+ //If the Server should be stopped when the jvm exits, register
+ //with the shutdown handler thread.
+ if (getStopAtShutdown())
+ ShutdownThread.register(this);
+
+ //Register the Server with the handler thread for receiving
+ //remote stop commands
+ ShutdownMonitor.register(this);
+
+ //Start a thread waiting to receive "stop" commands.
+ ShutdownMonitor.getInstance().start(); // initialize
+
+ String gitHash = Jetty.GIT_HASH;
+ String timestamp = Jetty.BUILD_TIMESTAMP;
+
+ LOG.info("jetty-{}; built: {}; git: {}; jvm {}", getVersion(), timestamp, gitHash, System.getProperty("java.runtime.version", System.getProperty("java.version")));
+ if (!Jetty.STABLE)
+ {
+ LOG.warn("THIS IS NOT A STABLE RELEASE! DO NOT USE IN PRODUCTION!");
+ LOG.warn("Download a stable release from https://download.eclipse.org/jetty/");
+ }
+
+ HttpGenerator.setJettyVersion(HttpConfiguration.SERVER_VERSION);
+
+ MultiException mex = new MultiException();
+ try
+ {
+ super.doStart();
+ }
+ catch (Throwable e)
+ {
+ mex.add(e);
+ }
+
+ // start connectors last
+ if (mex.size() == 0)
+ {
+ for (Connector connector : _connectors)
+ {
+ try
+ {
+ connector.start();
+ }
+ catch (Throwable e)
+ {
+ mex.add(e);
+ }
+ }
+ }
+
+ if (isDumpAfterStart())
+ dumpStdErr();
+
+ mex.ifExceptionThrow();
+
+ LOG.info(String.format("Started @%dms", Uptime.getUptime()));
+ }
+
+ @Override
+ protected void start(LifeCycle l) throws Exception
+ {
+ // start connectors last
+ if (!(l instanceof Connector))
+ super.start(l);
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ if (isDumpBeforeStop())
+ dumpStdErr();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("doStop {}", this);
+
+ MultiException mex = new MultiException();
+
+ try
+ {
+ // list if graceful futures
+ List<Future<Void>> futures = new ArrayList<>();
+ // First shutdown the network connectors to stop accepting new connections
+ for (Connector connector : _connectors)
+ {
+ futures.add(connector.shutdown());
+ }
+ // then shutdown all graceful handlers
+ doShutdown(futures);
+ }
+ catch (Throwable e)
+ {
+ mex.add(e);
+ }
+
+ // Now stop the connectors (this will close existing connections)
+ for (Connector connector : _connectors)
+ {
+ try
+ {
+ connector.stop();
+ }
+ catch (Throwable e)
+ {
+ mex.add(e);
+ }
+ }
+
+ // And finally stop everything else
+ try
+ {
+ super.doStop();
+ }
+ catch (Throwable e)
+ {
+ mex.add(e);
+ }
+
+ if (getStopAtShutdown())
+ ShutdownThread.deregister(this);
+
+ //Unregister the Server with the handler thread for receiving
+ //remote stop commands as we are stopped already
+ ShutdownMonitor.deregister(this);
+
+ mex.ifExceptionThrow();
+ }
+
+ /* Handle a request from a connection.
+ * Called to handle a request on the connection when either the header has been received,
+ * or after the entire request has been received (for short requests of known length), or
+ * on the dispatch of an async request.
+ */
+ public void handle(HttpChannel channel) throws IOException, ServletException
+ {
+ final String target = channel.getRequest().getPathInfo();
+ final Request request = channel.getRequest();
+ final Response response = channel.getResponse();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} {} {} on {}", request.getDispatcherType(), request.getMethod(), target, channel);
+
+ if (HttpMethod.OPTIONS.is(request.getMethod()) || "*".equals(target))
+ {
+ if (!HttpMethod.OPTIONS.is(request.getMethod()))
+ {
+ request.setHandled(true);
+ response.sendError(HttpStatus.BAD_REQUEST_400);
+ }
+ else
+ {
+ handleOptions(request, response);
+ if (!request.isHandled())
+ handle(target, request, request, response);
+ }
+ }
+ else
+ handle(target, request, request, response);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("handled={} async={} committed={} on {}", request.isHandled(), request.isAsyncStarted(), response.isCommitted(), channel);
+ }
+
+ /* Handle Options request to server
+ */
+ protected void handleOptions(Request request, Response response) throws IOException
+ {
+ }
+
+ /* Handle a request from a connection.
+ * Called to handle a request on the connection when either the header has been received,
+ * or after the entire request has been received (for short requests of known length), or
+ * on the dispatch of an async request.
+ */
+ public void handleAsync(HttpChannel channel) throws IOException, ServletException
+ {
+ final HttpChannelState state = channel.getRequest().getHttpChannelState();
+ final AsyncContextEvent event = state.getAsyncContextEvent();
+
+ final Request baseRequest = channel.getRequest();
+ final String path = event.getPath();
+
+ if (path != null)
+ {
+ // this is a dispatch with a path
+ ServletContext context = event.getServletContext();
+ String query = baseRequest.getQueryString();
+ baseRequest.setURIPathQuery(URIUtil.addEncodedPaths(context == null ? null : context.getContextPath(), path));
+ HttpURI uri = baseRequest.getHttpURI();
+ baseRequest.setPathInfo(uri.getDecodedPath());
+ if (uri.getQuery() != null)
+ baseRequest.mergeQueryParameters(query, uri.getQuery(), true); //we have to assume dispatch path and query are UTF8
+ }
+
+ final String target = baseRequest.getPathInfo();
+ final HttpServletRequest request = Request.unwrap(event.getSuppliedRequest());
+ final HttpServletResponse response = Response.unwrap(event.getSuppliedResponse());
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} {} {} on {}", request.getDispatcherType(), request.getMethod(), target, channel);
+ handle(target, baseRequest, request, response);
+ if (LOG.isDebugEnabled())
+ LOG.debug("handledAsync={} async={} committed={} on {}", channel.getRequest().isHandled(), request.isAsyncStarted(), response.isCommitted(), channel);
+ }
+
+ public void join() throws InterruptedException
+ {
+ getThreadPool().join();
+ }
+
+ /**
+ * @return Returns the sessionIdManager.
+ */
+ public SessionIdManager getSessionIdManager()
+ {
+ return _sessionIdManager;
+ }
+
+ /**
+ * @param sessionIdManager The sessionIdManager to set.
+ */
+ public void setSessionIdManager(SessionIdManager sessionIdManager)
+ {
+ updateBean(_sessionIdManager, sessionIdManager);
+ _sessionIdManager = sessionIdManager;
+ }
+
+ /*
+ * @see org.eclipse.util.AttributesMap#clearAttributes()
+ */
+ @Override
+ public void clearAttributes()
+ {
+ _attributes.clearAttributes();
+ }
+
+ /*
+ * @see org.eclipse.util.AttributesMap#getAttribute(java.lang.String)
+ */
+ @Override
+ public Object getAttribute(String name)
+ {
+ return _attributes.getAttribute(name);
+ }
+
+ /*
+ * @see org.eclipse.util.AttributesMap#getAttributeNames()
+ */
+ @Override
+ public Enumeration<String> getAttributeNames()
+ {
+ return _attributes.getAttributeNames();
+ }
+
+ @Override
+ public Set<String> getAttributeNameSet()
+ {
+ return _attributes.getAttributeNameSet();
+ }
+
+ /*
+ * @see org.eclipse.util.AttributesMap#removeAttribute(java.lang.String)
+ */
+ @Override
+ public void removeAttribute(String name)
+ {
+ _attributes.removeAttribute(name);
+ }
+
+ /*
+ * @see org.eclipse.util.AttributesMap#setAttribute(java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void setAttribute(String name, Object attribute)
+ {
+ _attributes.setAttribute(name, attribute);
+ }
+
+ /**
+ * @return The URI of the first {@link NetworkConnector} and first {@link ContextHandler}, or null
+ */
+ public URI getURI()
+ {
+ NetworkConnector connector = null;
+ for (Connector c : _connectors)
+ {
+ if (c instanceof NetworkConnector)
+ {
+ connector = (NetworkConnector)c;
+ break;
+ }
+ }
+
+ if (connector == null)
+ return null;
+
+ ContextHandler context = getChildHandlerByClass(ContextHandler.class);
+
+ try
+ {
+ String protocol = connector.getDefaultConnectionFactory().getProtocol();
+ String scheme = "http";
+ if (protocol.startsWith("SSL-") || protocol.equals("SSL"))
+ scheme = "https";
+
+ String host = connector.getHost();
+ if (context != null && context.getVirtualHosts() != null && context.getVirtualHosts().length > 0)
+ host = context.getVirtualHosts()[0];
+ if (host == null)
+ host = InetAddress.getLocalHost().getHostAddress();
+
+ String path = context == null ? null : context.getContextPath();
+ if (path == null)
+ path = "/";
+ return new URI(scheme, null, host, connector.getLocalPort(), path, null, null);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ return null;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[%s]", super.toString(), getVersion());
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ dumpObjects(out, indent, new ClassLoaderDump(this.getClass().getClassLoader()));
+ }
+
+ public static void main(String... args) throws Exception
+ {
+ System.err.println(getVersion());
+ }
+
+ private static class DateField
+ {
+ final long _seconds;
+ final HttpField _dateField;
+
+ public DateField(long seconds, HttpField dateField)
+ {
+ super();
+ _seconds = seconds;
+ _dateField = dateField;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnectionStatistics.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnectionStatistics.java
new file mode 100644
index 0000000..6771ec1
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnectionStatistics.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import org.eclipse.jetty.io.ConnectionStatistics;
+
+@Deprecated
+public class ServerConnectionStatistics extends ConnectionStatistics
+{
+ /**
+ * @param server the server to use to add {@link ConnectionStatistics} to all Connectors.
+ * @deprecated use {@link Server#addBeanToAllConnectors(Object)} instead.
+ */
+ public static void addToAllConnectors(Server server)
+ {
+ for (Connector connector : server.getConnectors())
+ {
+ connector.addBean(new ConnectionStatistics());
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java
new file mode 100644
index 0000000..e26514f
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java
@@ -0,0 +1,641 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.nio.channels.Channel;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.EventListener;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.ChannelEndPoint;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.ManagedSelector;
+import org.eclipse.jetty.io.SelectorManager;
+import org.eclipse.jetty.io.SocketChannelEndPoint;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.Name;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * This {@link Connector} implementation is the primary connector for the
+ * Jetty server over TCP/IP. By the use of various {@link ConnectionFactory} instances it is able
+ * to accept connections for HTTP, HTTP/2 and WebSocket, either directly or over SSL.
+ * <p>
+ * The connector is a fully asynchronous NIO based implementation that by default will
+ * use all the commons services (eg {@link Executor}, {@link Scheduler}) of the
+ * passed {@link Server} instance, but all services may also be constructor injected
+ * into the connector so that it may operate with dedicated or otherwise shared services.
+ * </p>
+ * <h2>Connection Factories</h2>
+ * <p>
+ * Various convenience constructors are provided to assist with common configurations of
+ * ConnectionFactories, whose generic use is described in {@link AbstractConnector}.
+ * If no connection factories are passed, then the connector will
+ * default to use a {@link HttpConnectionFactory}. If an non null {@link SslContextFactory}
+ * instance is passed, then this used to instantiate a {@link SslConnectionFactory} which is
+ * prepended to the other passed or default factories.
+ * </p>
+ * <h2>Selectors</h2>
+ * <p>
+ * The default number of selectors is equal to half of the number of processors available to the JVM,
+ * which should allow optimal performance even if all the connections used are performing
+ * significant non-blocking work in the callback tasks.
+ * </p>
+ */
+@ManagedObject("HTTP connector using NIO ByteChannels and Selectors")
+public class ServerConnector extends AbstractNetworkConnector
+{
+ private final SelectorManager _manager;
+ private final AtomicReference<Closeable> _acceptor = new AtomicReference<>();
+ private volatile ServerSocketChannel _acceptChannel;
+ private volatile boolean _inheritChannel = false;
+ private volatile int _localPort = -1;
+ private volatile int _acceptQueueSize = 0;
+ private volatile boolean _reuseAddress = true;
+ private volatile boolean _acceptedTcpNoDelay = true;
+ private volatile int _acceptedReceiveBufferSize = -1;
+ private volatile int _acceptedSendBufferSize = -1;
+
+ /**
+ * <p>Construct a ServerConnector with a private instance of {@link HttpConnectionFactory} as the only factory.</p>
+ *
+ * @param server The {@link Server} this connector will accept connection for.
+ */
+ public ServerConnector(
+ @Name("server") Server server)
+ {
+ this(server, null, null, null, -1, -1, new HttpConnectionFactory());
+ }
+
+ /**
+ * <p>Construct a ServerConnector with a private instance of {@link HttpConnectionFactory} as the only factory.</p>
+ *
+ * @param server The {@link Server} this connector will accept connection for.
+ * @param acceptors the number of acceptor threads to use, or -1 for a default value. Acceptors accept new TCP/IP connections. If 0, then
+ * the selector threads are used to accept connections.
+ * @param selectors the number of selector threads, or <=0 for a default value. Selectors notice and schedule established connection that can make IO progress.
+ */
+ public ServerConnector(
+ @Name("server") Server server,
+ @Name("acceptors") int acceptors,
+ @Name("selectors") int selectors)
+ {
+ this(server, null, null, null, acceptors, selectors, new HttpConnectionFactory());
+ }
+
+ /**
+ * <p>Construct a ServerConnector with a private instance of {@link HttpConnectionFactory} as the only factory.</p>
+ *
+ * @param server The {@link Server} this connector will accept connection for.
+ * @param acceptors the number of acceptor threads to use, or -1 for a default value. Acceptors accept new TCP/IP connections. If 0, then
+ * the selector threads are used to accept connections.
+ * @param selectors the number of selector threads, or <=0 for a default value. Selectors notice and schedule established connection that can make IO progress.
+ * @param factories Zero or more {@link ConnectionFactory} instances used to create and configure connections.
+ */
+ public ServerConnector(
+ @Name("server") Server server,
+ @Name("acceptors") int acceptors,
+ @Name("selectors") int selectors,
+ @Name("factories") ConnectionFactory... factories)
+ {
+ this(server, null, null, null, acceptors, selectors, factories);
+ }
+
+ /**
+ * <p>Construct a Server Connector with the passed Connection factories.</p>
+ *
+ * @param server The {@link Server} this connector will accept connection for.
+ * @param factories Zero or more {@link ConnectionFactory} instances used to create and configure connections.
+ */
+ public ServerConnector(
+ @Name("server") Server server,
+ @Name("factories") ConnectionFactory... factories)
+ {
+ this(server, null, null, null, -1, -1, factories);
+ }
+
+ /**
+ * <p>Construct a ServerConnector with a private instance of {@link HttpConnectionFactory} as the primary protocol</p>.
+ *
+ * @param server The {@link Server} this connector will accept connection for.
+ * @param sslContextFactory If non null, then a {@link SslConnectionFactory} is instantiated and prepended to the
+ * list of HTTP Connection Factory.
+ */
+ public ServerConnector(
+ @Name("server") Server server,
+ @Name("sslContextFactory") SslContextFactory sslContextFactory)
+ {
+ this(server, null, null, null, -1, -1, AbstractConnectionFactory.getFactories(sslContextFactory, new HttpConnectionFactory()));
+ }
+
+ /**
+ * <p>Construct a ServerConnector with a private instance of {@link HttpConnectionFactory} as the primary protocol</p>.
+ *
+ * @param server The {@link Server} this connector will accept connection for.
+ * @param sslContextFactory If non null, then a {@link SslConnectionFactory} is instantiated and prepended to the
+ * list of HTTP Connection Factory.
+ * @param acceptors the number of acceptor threads to use, or -1 for a default value. Acceptors accept new TCP/IP connections. If 0, then
+ * the selector threads are used to accept connections.
+ * @param selectors the number of selector threads, or <=0 for a default value. Selectors notice and schedule established connection that can make IO progress.
+ */
+ public ServerConnector(
+ @Name("server") Server server,
+ @Name("acceptors") int acceptors,
+ @Name("selectors") int selectors,
+ @Name("sslContextFactory") SslContextFactory sslContextFactory)
+ {
+ this(server, null, null, null, acceptors, selectors, AbstractConnectionFactory.getFactories(sslContextFactory, new HttpConnectionFactory()));
+ }
+
+ /**
+ * @param server The {@link Server} this connector will accept connection for.
+ * @param sslContextFactory If non null, then a {@link SslConnectionFactory} is instantiated and prepended to the
+ * list of ConnectionFactories, with the first factory being the default protocol for the SslConnectionFactory.
+ * @param factories Zero or more {@link ConnectionFactory} instances used to create and configure connections.
+ */
+ public ServerConnector(
+ @Name("server") Server server,
+ @Name("sslContextFactory") SslContextFactory sslContextFactory,
+ @Name("factories") ConnectionFactory... factories)
+ {
+ this(server, null, null, null, -1, -1, AbstractConnectionFactory.getFactories(sslContextFactory, factories));
+ }
+
+ /**
+ * @param server The server this connector will be accept connection for.
+ * @param executor An executor used to run tasks for handling requests, acceptors and selectors.
+ * If null then use the servers executor
+ * @param scheduler A scheduler used to schedule timeouts. If null then use the servers scheduler
+ * @param bufferPool A ByteBuffer pool used to allocate buffers. If null then create a private pool with default configuration.
+ * @param acceptors the number of acceptor threads to use, or -1 for a default value. Acceptors accept new TCP/IP connections. If 0, then
+ * the selector threads are used to accept connections.
+ * @param selectors the number of selector threads, or <=0 for a default value. Selectors notice and schedule established connection that can make IO progress.
+ * @param factories Zero or more {@link ConnectionFactory} instances used to create and configure connections.
+ */
+ public ServerConnector(
+ @Name("server") Server server,
+ @Name("executor") Executor executor,
+ @Name("scheduler") Scheduler scheduler,
+ @Name("bufferPool") ByteBufferPool bufferPool,
+ @Name("acceptors") int acceptors,
+ @Name("selectors") int selectors,
+ @Name("factories") ConnectionFactory... factories)
+ {
+ super(server, executor, scheduler, bufferPool, acceptors, factories);
+ _manager = newSelectorManager(getExecutor(), getScheduler(), selectors);
+ addBean(_manager, true);
+ setAcceptorPriorityDelta(-2);
+ }
+
+ protected SelectorManager newSelectorManager(Executor executor, Scheduler scheduler, int selectors)
+ {
+ return new ServerConnectorManager(executor, scheduler, selectors);
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ for (EventListener l : getBeans(EventListener.class))
+ {
+ _manager.addEventListener(l);
+ }
+
+ super.doStart();
+
+ if (getAcceptors() == 0)
+ {
+ _acceptChannel.configureBlocking(false);
+ _acceptor.set(_manager.acceptor(_acceptChannel));
+ }
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ super.doStop();
+ for (EventListener l : getBeans(EventListener.class))
+ {
+ _manager.removeEventListener(l);
+ }
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ ServerSocketChannel channel = _acceptChannel;
+ return channel != null && channel.isOpen();
+ }
+
+ /**
+ * @return whether this connector uses a channel inherited from the JVM.
+ * @see System#inheritedChannel()
+ */
+ public boolean isInheritChannel()
+ {
+ return _inheritChannel;
+ }
+
+ /**
+ * <p>Sets whether this connector uses a channel inherited from the JVM.</p>
+ * <p>If true, the connector first tries to inherit from a channel provided by the system.
+ * If there is no inherited channel available, or if the inherited channel is not usable,
+ * then it will fall back using {@link ServerSocketChannel}.</p>
+ * <p>Use it with xinetd/inetd, to launch an instance of Jetty on demand. The port
+ * used to access pages on the Jetty instance is the same as the port used to
+ * launch Jetty.</p>
+ *
+ * @param inheritChannel whether this connector uses a channel inherited from the JVM.
+ * @see ServerConnector#openAcceptChannel()
+ */
+ public void setInheritChannel(boolean inheritChannel)
+ {
+ _inheritChannel = inheritChannel;
+ }
+
+ /**
+ * Open the connector using the passed ServerSocketChannel.
+ * This open method can be called before starting the connector to pass it a ServerSocketChannel
+ * that will be used instead of one returned from {@link #openAcceptChannel()}
+ *
+ * @param acceptChannel the channel to use
+ * @throws IOException if the server channel is not bound
+ */
+ public void open(ServerSocketChannel acceptChannel) throws IOException
+ {
+ if (isStarted())
+ throw new IllegalStateException(getState());
+ updateBean(_acceptChannel, acceptChannel);
+ _acceptChannel = acceptChannel;
+ _localPort = _acceptChannel.socket().getLocalPort();
+ if (_localPort <= 0)
+ throw new IOException("Server channel not bound");
+ }
+
+ @Override
+ public void open() throws IOException
+ {
+ if (_acceptChannel == null)
+ {
+ _acceptChannel = openAcceptChannel();
+ _acceptChannel.configureBlocking(true);
+ _localPort = _acceptChannel.socket().getLocalPort();
+ if (_localPort <= 0)
+ throw new IOException("Server channel not bound");
+ addBean(_acceptChannel);
+ }
+ }
+
+ /**
+ * Called by {@link #open()} to obtain the accepting channel.
+ *
+ * @return ServerSocketChannel used to accept connections.
+ * @throws IOException if unable to obtain or configure the server channel
+ */
+ protected ServerSocketChannel openAcceptChannel() throws IOException
+ {
+ ServerSocketChannel serverChannel = null;
+ if (isInheritChannel())
+ {
+ Channel channel = System.inheritedChannel();
+ if (channel instanceof ServerSocketChannel)
+ serverChannel = (ServerSocketChannel)channel;
+ else
+ LOG.warn("Unable to use System.inheritedChannel() [{}]. Trying a new ServerSocketChannel at {}:{}", channel, getHost(), getPort());
+ }
+
+ if (serverChannel == null)
+ {
+ InetSocketAddress bindAddress = getHost() == null ? new InetSocketAddress(getPort()) : new InetSocketAddress(getHost(), getPort());
+ serverChannel = ServerSocketChannel.open();
+ try
+ {
+ serverChannel.socket().setReuseAddress(getReuseAddress());
+ serverChannel.socket().bind(bindAddress, getAcceptQueueSize());
+ }
+ catch (Throwable e)
+ {
+ IO.close(serverChannel);
+ throw new IOException("Failed to bind to " + bindAddress, e);
+ }
+ }
+
+ return serverChannel;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+
+ ServerSocketChannel serverChannel = _acceptChannel;
+ _acceptChannel = null;
+ if (serverChannel != null)
+ {
+ removeBean(serverChannel);
+
+ if (serverChannel.isOpen())
+ {
+ try
+ {
+ serverChannel.close();
+ }
+ catch (IOException e)
+ {
+ LOG.warn(e);
+ }
+ }
+ }
+ _localPort = -2;
+ }
+
+ @Override
+ public void accept(int acceptorID) throws IOException
+ {
+ ServerSocketChannel serverChannel = _acceptChannel;
+ if (serverChannel != null && serverChannel.isOpen())
+ {
+ SocketChannel channel = serverChannel.accept();
+ accepted(channel);
+ }
+ }
+
+ private void accepted(SocketChannel channel) throws IOException
+ {
+ channel.configureBlocking(false);
+ Socket socket = channel.socket();
+ configure(socket);
+ _manager.accept(channel);
+ }
+
+ protected void configure(Socket socket)
+ {
+ try
+ {
+ socket.setTcpNoDelay(_acceptedTcpNoDelay);
+ if (_acceptedReceiveBufferSize > -1)
+ socket.setReceiveBufferSize(_acceptedReceiveBufferSize);
+ if (_acceptedSendBufferSize > -1)
+ socket.setSendBufferSize(_acceptedSendBufferSize);
+ }
+ catch (SocketException e)
+ {
+ LOG.ignore(e);
+ }
+ }
+
+ @ManagedAttribute("The Selector Manager")
+ public SelectorManager getSelectorManager()
+ {
+ return _manager;
+ }
+
+ @Override
+ public Object getTransport()
+ {
+ return _acceptChannel;
+ }
+
+ @Override
+ @ManagedAttribute("local port")
+ public int getLocalPort()
+ {
+ return _localPort;
+ }
+
+ protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException
+ {
+ SocketChannelEndPoint endpoint = new SocketChannelEndPoint(channel, selectSet, key, getScheduler());
+ endpoint.setIdleTimeout(getIdleTimeout());
+ return endpoint;
+ }
+
+ /**
+ * Returns the socket close linger time.
+ *
+ * @return -1 as the socket close linger time is always disabled.
+ * @see java.net.StandardSocketOptions#SO_LINGER
+ * @deprecated don't use as socket close linger time has undefined behavior for non-blocking sockets
+ */
+ @ManagedAttribute(value = "Socket close linger time. Deprecated, always returns -1", readonly = true)
+ @Deprecated
+ public int getSoLingerTime()
+ {
+ return -1;
+ }
+
+ /**
+ * @param lingerTime the socket close linger time; use -1 to disable.
+ * @see java.net.StandardSocketOptions#SO_LINGER
+ * @deprecated don't use as socket close linger time has undefined behavior for non-blocking sockets
+ */
+ @Deprecated
+ public void setSoLingerTime(int lingerTime)
+ {
+ LOG.warn("Ignoring deprecated socket close linger time");
+ }
+
+ /**
+ * @return the accept queue size
+ */
+ @ManagedAttribute("Accept Queue size")
+ public int getAcceptQueueSize()
+ {
+ return _acceptQueueSize;
+ }
+
+ /**
+ * @param acceptQueueSize the accept queue size (also known as accept backlog)
+ */
+ public void setAcceptQueueSize(int acceptQueueSize)
+ {
+ _acceptQueueSize = acceptQueueSize;
+ }
+
+ /**
+ * @return whether the server socket reuses addresses
+ * @see ServerSocket#getReuseAddress()
+ */
+ @ManagedAttribute("Server Socket SO_REUSEADDR")
+ public boolean getReuseAddress()
+ {
+ return _reuseAddress;
+ }
+
+ /**
+ * @param reuseAddress whether the server socket reuses addresses
+ * @see ServerSocket#setReuseAddress(boolean)
+ */
+ public void setReuseAddress(boolean reuseAddress)
+ {
+ _reuseAddress = reuseAddress;
+ }
+
+ /**
+ * @return whether the accepted socket gets {@link java.net.SocketOptions#TCP_NODELAY TCP_NODELAY} enabled.
+ * @see Socket#getTcpNoDelay()
+ */
+ @ManagedAttribute("Accepted Socket TCP_NODELAY")
+ public boolean getAcceptedTcpNoDelay()
+ {
+ return _acceptedTcpNoDelay;
+ }
+
+ /**
+ * @param tcpNoDelay whether {@link java.net.SocketOptions#TCP_NODELAY TCP_NODELAY} gets enabled on the the accepted socket.
+ * @see Socket#setTcpNoDelay(boolean)
+ */
+ public void setAcceptedTcpNoDelay(boolean tcpNoDelay)
+ {
+ this._acceptedTcpNoDelay = tcpNoDelay;
+ }
+
+ /**
+ * @return the {@link java.net.SocketOptions#SO_RCVBUF SO_RCVBUF} size to set onto the accepted socket.
+ * A value of -1 indicates that it is left to its default value.
+ * @see Socket#getReceiveBufferSize()
+ */
+ @ManagedAttribute("Accepted Socket SO_RCVBUF")
+ public int getAcceptedReceiveBufferSize()
+ {
+ return _acceptedReceiveBufferSize;
+ }
+
+ /**
+ * @param receiveBufferSize the {@link java.net.SocketOptions#SO_RCVBUF SO_RCVBUF} size to set onto the accepted socket.
+ * A value of -1 indicates that it is left to its default value.
+ * @see Socket#setReceiveBufferSize(int)
+ */
+ public void setAcceptedReceiveBufferSize(int receiveBufferSize)
+ {
+ this._acceptedReceiveBufferSize = receiveBufferSize;
+ }
+
+ /**
+ * @return the {@link java.net.SocketOptions#SO_SNDBUF SO_SNDBUF} size to set onto the accepted socket.
+ * A value of -1 indicates that it is left to its default value.
+ * @see Socket#getSendBufferSize()
+ */
+ @ManagedAttribute("Accepted Socket SO_SNDBUF")
+ public int getAcceptedSendBufferSize()
+ {
+ return _acceptedSendBufferSize;
+ }
+
+ /**
+ * @param sendBufferSize the {@link java.net.SocketOptions#SO_SNDBUF SO_SNDBUF} size to set onto the accepted socket.
+ * A value of -1 indicates that it is left to its default value.
+ * @see Socket#setSendBufferSize(int)
+ */
+ public void setAcceptedSendBufferSize(int sendBufferSize)
+ {
+ this._acceptedSendBufferSize = sendBufferSize;
+ }
+
+ @Override
+ public void setAccepting(boolean accepting)
+ {
+ super.setAccepting(accepting);
+ if (getAcceptors() > 0)
+ return;
+
+ try
+ {
+ if (accepting)
+ {
+ if (_acceptor.get() == null)
+ {
+ Closeable acceptor = _manager.acceptor(_acceptChannel);
+ if (!_acceptor.compareAndSet(null, acceptor))
+ acceptor.close();
+ }
+ }
+ else
+ {
+ Closeable acceptor = _acceptor.get();
+ if (acceptor != null && _acceptor.compareAndSet(acceptor, null))
+ acceptor.close();
+ }
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected class ServerConnectorManager extends SelectorManager
+ {
+ public ServerConnectorManager(Executor executor, Scheduler scheduler, int selectors)
+ {
+ super(executor, scheduler, selectors);
+ }
+
+ @Override
+ protected void accepted(SelectableChannel channel) throws IOException
+ {
+ ServerConnector.this.accepted((SocketChannel)channel);
+ }
+
+ @Override
+ protected ChannelEndPoint newEndPoint(SelectableChannel channel, ManagedSelector selectSet, SelectionKey selectionKey) throws IOException
+ {
+ return ServerConnector.this.newEndPoint((SocketChannel)channel, selectSet, selectionKey);
+ }
+
+ @Override
+ public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment) throws IOException
+ {
+ return getDefaultConnectionFactory().newConnection(ServerConnector.this, endpoint);
+ }
+
+ @Override
+ protected void endPointOpened(EndPoint endpoint)
+ {
+ super.endPointOpened(endpoint);
+ onEndPointOpened(endpoint);
+ }
+
+ @Override
+ protected void endPointClosed(EndPoint endpoint)
+ {
+ onEndPointClosed(endpoint);
+ super.endPointClosed(endpoint);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("SelectorManager@%s", ServerConnector.this);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServletAttributes.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServletAttributes.java
new file mode 100644
index 0000000..336e694
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServletAttributes.java
@@ -0,0 +1,71 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.Set;
+
+import org.eclipse.jetty.util.Attributes;
+import org.eclipse.jetty.util.AttributesMap;
+
+public class ServletAttributes implements Attributes
+{
+ private final Attributes _attributes = new AttributesMap();
+ private AsyncAttributes _asyncAttributes;
+
+ public void setAsyncAttributes(String requestURI, String contextPath, String servletPath, String pathInfo, String queryString)
+ {
+ _asyncAttributes = new AsyncAttributes(_attributes, requestURI, contextPath, servletPath, pathInfo, queryString);
+ }
+
+ private Attributes getAttributes()
+ {
+ return (_asyncAttributes == null) ? _attributes : _asyncAttributes;
+ }
+
+ @Override
+ public void removeAttribute(String name)
+ {
+ getAttributes().removeAttribute(name);
+ }
+
+ @Override
+ public void setAttribute(String name, Object attribute)
+ {
+ getAttributes().setAttribute(name, attribute);
+ }
+
+ @Override
+ public Object getAttribute(String name)
+ {
+ return getAttributes().getAttribute(name);
+ }
+
+ @Override
+ public Set<String> getAttributeNameSet()
+ {
+ return getAttributes().getAttributeNameSet();
+ }
+
+ @Override
+ public void clearAttributes()
+ {
+ getAttributes().clearAttributes();
+ _asyncAttributes = null;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java
new file mode 100644
index 0000000..e125062
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java
@@ -0,0 +1,261 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Enumeration;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletRequestWrapper;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpUpgradeHandler;
+import javax.servlet.http.Part;
+
+/**
+ * ServletRequestHttpWrapper
+ *
+ * Class to tunnel a ServletRequest via an HttpServletRequest
+ */
+public class ServletRequestHttpWrapper extends ServletRequestWrapper implements HttpServletRequest
+{
+ public ServletRequestHttpWrapper(ServletRequest request)
+ {
+ super(request);
+ }
+
+ @Override
+ public String getAuthType()
+ {
+ return null;
+ }
+
+ @Override
+ public Cookie[] getCookies()
+ {
+ return null;
+ }
+
+ @Override
+ public long getDateHeader(String name)
+ {
+ return 0;
+ }
+
+ @Override
+ public String getHeader(String name)
+ {
+ return null;
+ }
+
+ @Override
+ public Enumeration<String> getHeaders(String name)
+ {
+ return null;
+ }
+
+ @Override
+ public Enumeration<String> getHeaderNames()
+ {
+ return null;
+ }
+
+ @Override
+ public int getIntHeader(String name)
+ {
+ return 0;
+ }
+
+ @Override
+ public String getMethod()
+ {
+ return null;
+ }
+
+ @Override
+ public String getPathInfo()
+ {
+ return null;
+ }
+
+ @Override
+ public String getPathTranslated()
+ {
+ return null;
+ }
+
+ @Override
+ public String getContextPath()
+ {
+ return null;
+ }
+
+ @Override
+ public String getQueryString()
+ {
+ return null;
+ }
+
+ @Override
+ public String getRemoteUser()
+ {
+ return null;
+ }
+
+ @Override
+ public boolean isUserInRole(String role)
+ {
+ return false;
+ }
+
+ @Override
+ public Principal getUserPrincipal()
+ {
+ return null;
+ }
+
+ @Override
+ public String getRequestedSessionId()
+ {
+ return null;
+ }
+
+ @Override
+ public String getRequestURI()
+ {
+ return null;
+ }
+
+ @Override
+ public StringBuffer getRequestURL()
+ {
+ return null;
+ }
+
+ @Override
+ public String getServletPath()
+ {
+ return null;
+ }
+
+ @Override
+ public HttpSession getSession(boolean create)
+ {
+ return null;
+ }
+
+ @Override
+ public HttpSession getSession()
+ {
+ return null;
+ }
+
+ @Override
+ public boolean isRequestedSessionIdValid()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean isRequestedSessionIdFromCookie()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean isRequestedSessionIdFromURL()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean isRequestedSessionIdFromUrl()
+ {
+ return false;
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServletRequest#authenticate(javax.servlet.http.HttpServletResponse)
+ */
+ @Override
+ public boolean authenticate(HttpServletResponse response) throws IOException, ServletException
+ {
+ return false;
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServletRequest#getPart(java.lang.String)
+ */
+ @Override
+ public Part getPart(String name) throws IOException, ServletException
+ {
+ return null;
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServletRequest#getParts()
+ */
+ @Override
+ public Collection<Part> getParts() throws IOException, ServletException
+ {
+ return null;
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServletRequest#login(java.lang.String, java.lang.String)
+ */
+ @Override
+ public void login(String username, String password) throws ServletException
+ {
+
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServletRequest#logout()
+ */
+ @Override
+ public void logout() throws ServletException
+ {
+
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServletRequest#changeSessionId()
+ */
+ @Override
+ public String changeSessionId()
+ {
+ // TODO 3.1 Auto-generated method stub
+ return null;
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServletRequest#upgrade(java.lang.Class)
+ */
+ @Override
+ public <T extends HttpUpgradeHandler> T upgrade(Class<T> handlerClass) throws IOException, ServletException
+ {
+ // TODO 3.1 Auto-generated method stub
+ return null;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java
new file mode 100644
index 0000000..9692c56
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java
@@ -0,0 +1,165 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.util.Collection;
+import javax.servlet.ServletResponse;
+import javax.servlet.ServletResponseWrapper;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * ServletResponseHttpWrapper
+ *
+ * Wrapper to tunnel a ServletResponse via an HttpServletResponse
+ */
+public class ServletResponseHttpWrapper extends ServletResponseWrapper implements HttpServletResponse
+{
+ public ServletResponseHttpWrapper(ServletResponse response)
+ {
+ super(response);
+ }
+
+ @Override
+ public void addCookie(Cookie cookie)
+ {
+ }
+
+ @Override
+ public boolean containsHeader(String name)
+ {
+ return false;
+ }
+
+ @Override
+ public String encodeURL(String url)
+ {
+ return null;
+ }
+
+ @Override
+ public String encodeRedirectURL(String url)
+ {
+ return null;
+ }
+
+ @Override
+ public String encodeUrl(String url)
+ {
+ return null;
+ }
+
+ @Override
+ public String encodeRedirectUrl(String url)
+ {
+ return null;
+ }
+
+ @Override
+ public void sendError(int sc, String msg) throws IOException
+ {
+ }
+
+ @Override
+ public void sendError(int sc) throws IOException
+ {
+ }
+
+ @Override
+ public void sendRedirect(String location) throws IOException
+ {
+ }
+
+ @Override
+ public void setDateHeader(String name, long date)
+ {
+ }
+
+ @Override
+ public void addDateHeader(String name, long date)
+ {
+ }
+
+ @Override
+ public void setHeader(String name, String value)
+ {
+ }
+
+ @Override
+ public void addHeader(String name, String value)
+ {
+ }
+
+ @Override
+ public void setIntHeader(String name, int value)
+ {
+ }
+
+ @Override
+ public void addIntHeader(String name, int value)
+ {
+ }
+
+ @Override
+ public void setStatus(int sc)
+ {
+ }
+
+ @Override
+ public void setStatus(int sc, String sm)
+ {
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServletResponse#getHeader(java.lang.String)
+ */
+ @Override
+ public String getHeader(String name)
+ {
+ return null;
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServletResponse#getHeaderNames()
+ */
+ @Override
+ public Collection<String> getHeaderNames()
+ {
+ return null;
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServletResponse#getHeaders(java.lang.String)
+ */
+ @Override
+ public Collection<String> getHeaders(String name)
+ {
+ return null;
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServletResponse#getStatus()
+ */
+ @Override
+ public int getStatus()
+ {
+ return 0;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SessionIdManager.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SessionIdManager.java
new file mode 100644
index 0000000..2f57fc9
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SessionIdManager.java
@@ -0,0 +1,117 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.Set;
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.server.session.HouseKeeper;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.util.component.LifeCycle;
+
+/**
+ * Session ID Manager.
+ *
+ * Manages session IDs across multiple contexts.
+ */
+public interface SessionIdManager extends LifeCycle
+{
+
+ /**
+ * @param id The plain session ID (ie no workername extension)
+ * @return True if the session ID is in use by at least one context.
+ */
+ boolean isIdInUse(String id);
+
+ /**
+ * Expire all sessions on all contexts that share the same id.
+ *
+ * @param id The session ID without any cluster node extension
+ */
+ void expireAll(String id);
+
+ /**
+ * Invalidate all sessions on all contexts that share the same id.
+ *
+ * @param id the session id
+ */
+ void invalidateAll(String id);
+
+ /**
+ * Create a new Session ID.
+ *
+ * @param request the request with the sesion
+ * @param created the timestamp for when the session was created
+ * @return the new session id
+ */
+ String newSessionId(HttpServletRequest request, long created);
+
+ /**
+ * @return the unique name of this server instance
+ */
+ String getWorkerName();
+
+ /**
+ * Get just the session id from an id that includes the worker name
+ * as a suffix.
+ *
+ * Strip node identifier from a located session ID.
+ *
+ * @param qualifiedId the session id including the worker name
+ * @return the cluster id
+ */
+ String getId(String qualifiedId);
+
+ /**
+ * Get an extended id for a session. An extended id contains
+ * the workername as a suffix.
+ *
+ * @param id The id of the session
+ * @param request The request that for the session (or null)
+ * @return The session id qualified with the worker name
+ */
+ String getExtendedId(String id, HttpServletRequest request);
+
+ /**
+ * Change the existing session id.
+ *
+ * @param oldId the old plain session id
+ * @param oldExtendedId the old fully qualified id
+ * @param request the request containing the session
+ * @return the new session id
+ */
+ String renewSessionId(String oldId, String oldExtendedId, HttpServletRequest request);
+
+ /**
+ * Get the set of all session handlers for this node
+ *
+ * @return the set of session handlers
+ */
+ Set<SessionHandler> getSessionHandlers();
+
+ /**
+ * @param houseKeeper the housekeeper for doing scavenging
+ */
+ void setSessionHouseKeeper(HouseKeeper houseKeeper);
+
+ /**
+ * @return the housekeeper for doing scavenging
+ */
+ HouseKeeper getSessionHouseKeeper();
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ShutdownMonitor.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ShutdownMonitor.java
new file mode 100644
index 0000000..9935370
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/ShutdownMonitor.java
@@ -0,0 +1,463 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.component.Destroyable;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.thread.ShutdownThread;
+
+/**
+ * Shutdown/Stop Monitor thread.
+ * <p>
+ * This thread listens on the host/port specified by the STOP.HOST/STOP.PORT
+ * system parameter (defaults to 127.0.0.1/-1 for not listening) for request
+ * authenticated with the key given by the STOP.KEY system parameter
+ * for admin requests.
+ * <p>
+ * If the stop port is set to zero, then a random port is assigned and the
+ * port number is printed to stdout.
+ * <p>
+ * Commands "stop" and "status" are currently supported.
+ */
+public class ShutdownMonitor
+{
+ // Implementation of safe lazy init, using Initialization on Demand Holder technique.
+ private static class Holder
+ {
+ static ShutdownMonitor instance = new ShutdownMonitor();
+ }
+
+ public static ShutdownMonitor getInstance()
+ {
+ return Holder.instance;
+ }
+
+ protected static void reset()
+ {
+ Holder.instance = new ShutdownMonitor();
+ }
+
+ public static void register(LifeCycle... lifeCycles)
+ {
+ getInstance().addLifeCycles(lifeCycles);
+ }
+
+ public static void deregister(LifeCycle lifeCycle)
+ {
+ getInstance().removeLifeCycle(lifeCycle);
+ }
+
+ public static boolean isRegistered(LifeCycle lifeCycle)
+ {
+ return getInstance().containsLifeCycle(lifeCycle);
+ }
+
+ private final Set<LifeCycle> _lifeCycles = new LinkedHashSet<>();
+ private boolean debug;
+ private final String host;
+ private int port;
+ private String key;
+ private boolean exitVm;
+ private boolean alive;
+
+ /**
+ * Creates a ShutdownMonitor using configuration from the System properties.
+ * <p>
+ * <code>STOP.PORT</code> = the port to listen on (empty, null, or values less than 0 disable the stop ability)<br>
+ * <code>STOP.KEY</code> = the magic key/passphrase to allow the stop<br>
+ * <p>
+ * Note: server socket will only listen on localhost, and a successful stop will issue a System.exit() call.
+ */
+ private ShutdownMonitor()
+ {
+ this.debug = System.getProperty("DEBUG") != null;
+ this.host = System.getProperty("STOP.HOST", "127.0.0.1");
+ this.port = Integer.getInteger("STOP.PORT", -1);
+ this.key = System.getProperty("STOP.KEY", null);
+ this.exitVm = true;
+ }
+
+ private void addLifeCycles(LifeCycle... lifeCycles)
+ {
+ synchronized (this)
+ {
+ _lifeCycles.addAll(Arrays.asList(lifeCycles));
+ }
+ }
+
+ private void removeLifeCycle(LifeCycle lifeCycle)
+ {
+ synchronized (this)
+ {
+ _lifeCycles.remove(lifeCycle);
+ }
+ }
+
+ private boolean containsLifeCycle(LifeCycle lifeCycle)
+ {
+ synchronized (this)
+ {
+ return _lifeCycles.contains(lifeCycle);
+ }
+ }
+
+ private void debug(String format, Object... args)
+ {
+ if (debug)
+ System.err.printf("[ShutdownMonitor] " + format + "%n", args);
+ }
+
+ private void debug(Throwable t)
+ {
+ if (debug)
+ t.printStackTrace(System.err);
+ }
+
+ public String getKey()
+ {
+ synchronized (this)
+ {
+ return key;
+ }
+ }
+
+ public int getPort()
+ {
+ synchronized (this)
+ {
+ return port;
+ }
+ }
+
+ public boolean isExitVm()
+ {
+ synchronized (this)
+ {
+ return exitVm;
+ }
+ }
+
+ public void setDebug(boolean flag)
+ {
+ this.debug = flag;
+ }
+
+ /**
+ * @param exitVm true to exit the VM on shutdown
+ */
+ public void setExitVm(boolean exitVm)
+ {
+ synchronized (this)
+ {
+ if (alive)
+ throw new IllegalStateException("ShutdownMonitor already started");
+ this.exitVm = exitVm;
+ }
+ }
+
+ public void setKey(String key)
+ {
+ synchronized (this)
+ {
+ if (alive)
+ throw new IllegalStateException("ShutdownMonitor already started");
+ this.key = key;
+ }
+ }
+
+ public void setPort(int port)
+ {
+ synchronized (this)
+ {
+ if (alive)
+ throw new IllegalStateException("ShutdownMonitor already started");
+ this.port = port;
+ }
+ }
+
+ protected void start() throws Exception
+ {
+ synchronized (this)
+ {
+ if (alive)
+ {
+ debug("Already started");
+ return; // cannot start it again
+ }
+ ServerSocket serverSocket = listen();
+ if (serverSocket != null)
+ {
+ alive = true;
+ Thread thread = new Thread(new ShutdownMonitorRunnable(serverSocket));
+ thread.setDaemon(true);
+ thread.setName("ShutdownMonitor");
+ thread.start();
+ }
+ }
+ }
+
+ private void stop()
+ {
+ synchronized (this)
+ {
+ alive = false;
+ notifyAll();
+ }
+ }
+
+ // For test purposes only.
+ void await() throws InterruptedException
+ {
+ synchronized (this)
+ {
+ while (alive)
+ {
+ wait();
+ }
+ }
+ }
+
+ protected boolean isAlive()
+ {
+ synchronized (this)
+ {
+ return alive;
+ }
+ }
+
+ private ServerSocket listen()
+ {
+ int port = getPort();
+ if (port < 0)
+ {
+ debug("Not enabled (port < 0): %d", port);
+ return null;
+ }
+
+ String key = getKey();
+ try
+ {
+ ServerSocket serverSocket = new ServerSocket();
+ try
+ {
+ serverSocket.setReuseAddress(true);
+ serverSocket.bind(new InetSocketAddress(InetAddress.getByName(host), port));
+ }
+ catch (Throwable e)
+ {
+ IO.close(serverSocket);
+ throw e;
+ }
+ if (port == 0)
+ {
+ port = serverSocket.getLocalPort();
+ System.out.printf("STOP.PORT=%d%n", port);
+ setPort(port);
+ }
+
+ if (key == null)
+ {
+ key = Long.toString((long)(Long.MAX_VALUE * Math.random() + this.hashCode() + System.currentTimeMillis()), 36);
+ System.out.printf("STOP.KEY=%s%n", key);
+ setKey(key);
+ }
+
+ return serverSocket;
+ }
+ catch (Throwable x)
+ {
+ debug(x);
+ System.err.println("Error binding ShutdownMonitor to port " + port + ": " + x.toString());
+ return null;
+ }
+ finally
+ {
+ // establish the port and key that are in use
+ debug("STOP.PORT=%d", port);
+ debug("STOP.KEY=%s", key);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[port=%d,alive=%b]", this.getClass().getName(), getPort(), isAlive());
+ }
+
+ /**
+ * Thread for listening to STOP.PORT for command to stop Jetty.
+ * If ShutdownMonitor.exitVm is true, then System.exit will also be
+ * called after the stop.
+ */
+ private class ShutdownMonitorRunnable implements Runnable
+ {
+ private final ServerSocket serverSocket;
+
+ private ShutdownMonitorRunnable(ServerSocket serverSocket)
+ {
+ this.serverSocket = serverSocket;
+ }
+
+ @Override
+ public void run()
+ {
+ debug("Started");
+ try
+ {
+ String key = getKey();
+ while (true)
+ {
+ try (Socket socket = serverSocket.accept())
+ {
+ LineNumberReader reader = new LineNumberReader(new InputStreamReader(socket.getInputStream()));
+ String receivedKey = reader.readLine();
+ if (!key.equals(receivedKey))
+ {
+ debug("Ignoring command with incorrect key: %s", receivedKey);
+ continue;
+ }
+
+ String cmd = reader.readLine();
+ debug("command=%s", cmd);
+ OutputStream out = socket.getOutputStream();
+ boolean exitVm = isExitVm();
+
+ if ("stop".equalsIgnoreCase(cmd)) //historic, for backward compatibility
+ {
+ //Stop the lifecycles, only if they are registered with the ShutdownThread, only destroying if vm is exiting
+ debug("Performing stop command");
+ stopLifeCycles(ShutdownThread::isRegistered, exitVm);
+
+ // Reply to client
+ debug("Informing client that we are stopped");
+ informClient(out, "Stopped\r\n");
+
+ if (!exitVm)
+ break;
+
+ // Kill JVM
+ debug("Killing JVM");
+ System.exit(0);
+ }
+ else if ("forcestop".equalsIgnoreCase(cmd))
+ {
+ debug("Performing forced stop command");
+ stopLifeCycles(l -> true, exitVm);
+
+ // Reply to client
+ debug("Informing client that we are stopped");
+ informClient(out, "Stopped\r\n");
+
+ if (!exitVm)
+ break;
+
+ // Kill JVM
+ debug("Killing JVM");
+ System.exit(0);
+ }
+ else if ("stopexit".equalsIgnoreCase(cmd))
+ {
+ debug("Performing stop and exit commands");
+ stopLifeCycles(ShutdownThread::isRegistered, true);
+
+ // Reply to client
+ debug("Informing client that we are stopped");
+ informClient(out, "Stopped\r\n");
+
+ debug("Killing JVM");
+ System.exit(0);
+ }
+ else if ("exit".equalsIgnoreCase(cmd))
+ {
+ debug("Killing JVM");
+ System.exit(0);
+ }
+ else if ("status".equalsIgnoreCase(cmd))
+ {
+ // Reply to client
+ informClient(out, "OK\r\n");
+ }
+ }
+ catch (Throwable x)
+ {
+ debug(x);
+ }
+ }
+ }
+ catch (Throwable x)
+ {
+ debug(x);
+ }
+ finally
+ {
+ IO.close(serverSocket);
+ stop();
+ debug("Stopped");
+ }
+ }
+
+ private void informClient(OutputStream out, String message) throws IOException
+ {
+ out.write(message.getBytes(StandardCharsets.UTF_8));
+ out.flush();
+ }
+
+ private void stopLifeCycles(Predicate<LifeCycle> predicate, boolean destroy)
+ {
+ List<LifeCycle> lifeCycles = new ArrayList<>();
+ synchronized (this)
+ {
+ lifeCycles.addAll(_lifeCycles);
+ }
+
+ for (LifeCycle l : lifeCycles)
+ {
+ try
+ {
+ if (l.isStarted() && predicate.test(l))
+ l.stop();
+
+ if ((l instanceof Destroyable) && destroy)
+ ((Destroyable)l).destroy();
+ }
+ catch (Throwable x)
+ {
+ debug(x);
+ }
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Slf4jRequestLog.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Slf4jRequestLog.java
new file mode 100644
index 0000000..ab5a4d9
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Slf4jRequestLog.java
@@ -0,0 +1,68 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+
+import org.eclipse.jetty.util.annotation.ManagedObject;
+
+/**
+ * Implementation of NCSARequestLog where output is sent as a SLF4J INFO Log message on the named logger "org.eclipse.jetty.server.RequestLog"
+ *
+ * @deprecated use {@link CustomRequestLog} given format string {@link CustomRequestLog#EXTENDED_NCSA_FORMAT} with an {@link Slf4jRequestLogWriter}
+ */
+@Deprecated
+@ManagedObject("NCSA standard format request log to slf4j bridge")
+public class Slf4jRequestLog extends AbstractNCSARequestLog
+{
+ private final Slf4jRequestLogWriter _requestLogWriter;
+
+ public Slf4jRequestLog()
+ {
+ this(new Slf4jRequestLogWriter());
+ }
+
+ public Slf4jRequestLog(Slf4jRequestLogWriter writer)
+ {
+ super(writer);
+ _requestLogWriter = writer;
+ }
+
+ public void setLoggerName(String loggerName)
+ {
+ _requestLogWriter.setLoggerName(loggerName);
+ }
+
+ public String getLoggerName()
+ {
+ return _requestLogWriter.getLoggerName();
+ }
+
+ @Override
+ protected boolean isEnabled()
+ {
+ return _requestLogWriter.isEnabled();
+ }
+
+ @Override
+ public void write(String requestEntry) throws IOException
+ {
+ _requestLogWriter.write(requestEntry);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Slf4jRequestLogWriter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Slf4jRequestLogWriter.java
new file mode 100644
index 0000000..bc8914c
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Slf4jRequestLogWriter.java
@@ -0,0 +1,71 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.log.Slf4jLog;
+
+/**
+ * Request log writer using a Slf4jLog Logger
+ */
+@ManagedObject("Slf4j RequestLog Writer")
+public class Slf4jRequestLogWriter extends AbstractLifeCycle implements RequestLog.Writer
+{
+ private Slf4jLog logger;
+ private String loggerName;
+
+ public Slf4jRequestLogWriter()
+ {
+ // Default logger name (can be set)
+ this.loggerName = "org.eclipse.jetty.server.RequestLog";
+ }
+
+ public void setLoggerName(String loggerName)
+ {
+ this.loggerName = loggerName;
+ }
+
+ @ManagedAttribute("logger name")
+ public String getLoggerName()
+ {
+ return loggerName;
+ }
+
+ protected boolean isEnabled()
+ {
+ return logger != null;
+ }
+
+ @Override
+ public void write(String requestEntry) throws IOException
+ {
+ logger.info(requestEntry);
+ }
+
+ @Override
+ protected synchronized void doStart() throws Exception
+ {
+ logger = new Slf4jLog(loggerName);
+ super.doStart();
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SocketCustomizationListener.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SocketCustomizationListener.java
new file mode 100644
index 0000000..0c3f3b9
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SocketCustomizationListener.java
@@ -0,0 +1,95 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.net.Socket;
+
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.Connection.Listener;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.SocketChannelEndPoint;
+import org.eclipse.jetty.io.ssl.SslConnection;
+import org.eclipse.jetty.io.ssl.SslConnection.DecryptedEndPoint;
+
+/**
+ * A Connection Lister for customization of SocketConnections.
+ * <p>
+ * Instances of this listener may be added to a {@link Connector} (or
+ * {@link ConnectionFactory}) so that they are applied to all connections
+ * for that connector (or protocol) and thus allow additional Socket
+ * configuration to be applied by implementing {@link #customize(Socket, Class, boolean)}
+ */
+public class SocketCustomizationListener implements Listener
+{
+ private final boolean _ssl;
+
+ /**
+ * Construct with SSL unwrapping on.
+ */
+ public SocketCustomizationListener()
+ {
+ this(true);
+ }
+
+ /**
+ * @param ssl If True, then a Socket underlying an SSLConnection is unwrapped
+ * and notified.
+ */
+ public SocketCustomizationListener(boolean ssl)
+ {
+ _ssl = ssl;
+ }
+
+ @Override
+ public void onOpened(Connection connection)
+ {
+ EndPoint endp = connection.getEndPoint();
+ boolean ssl = false;
+
+ if (_ssl && endp instanceof DecryptedEndPoint)
+ {
+ endp = ((DecryptedEndPoint)endp).getSslConnection().getEndPoint();
+ ssl = true;
+ }
+
+ if (endp instanceof SocketChannelEndPoint)
+ {
+ Socket socket = ((SocketChannelEndPoint)endp).getSocket();
+ customize(socket, connection.getClass(), ssl);
+ }
+ }
+
+ /**
+ * This method may be extended to configure a socket on open
+ * events.
+ *
+ * @param socket The Socket to configure
+ * @param connection The class of the connection (The socket may be wrapped
+ * by an {@link SslConnection} prior to this connection).
+ * @param ssl True if the socket is wrapped with an SslConnection
+ */
+ protected void customize(Socket socket, Class<? extends Connection> connection, boolean ssl)
+ {
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java
new file mode 100644
index 0000000..51e573c
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java
@@ -0,0 +1,163 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.nio.ByteBuffer;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLSession;
+
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.io.AbstractConnection;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.ssl.SslConnection;
+import org.eclipse.jetty.io.ssl.SslHandshakeListener;
+import org.eclipse.jetty.util.annotation.Name;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+public class SslConnectionFactory extends AbstractConnectionFactory implements ConnectionFactory.Detecting
+{
+ private static final int TLS_ALERT_FRAME_TYPE = 0x15;
+ private static final int TLS_HANDSHAKE_FRAME_TYPE = 0x16;
+ private static final int TLS_MAJOR_VERSION = 3;
+
+ private final SslContextFactory _sslContextFactory;
+ private final String _nextProtocol;
+ private boolean _directBuffersForEncryption = false;
+ private boolean _directBuffersForDecryption = false;
+
+ public SslConnectionFactory()
+ {
+ this(HttpVersion.HTTP_1_1.asString());
+ }
+
+ public SslConnectionFactory(@Name("next") String nextProtocol)
+ {
+ this(null, nextProtocol);
+ }
+
+ public SslConnectionFactory(@Name("sslContextFactory") SslContextFactory factory, @Name("next") String nextProtocol)
+ {
+ super("SSL");
+ _sslContextFactory = factory == null ? new SslContextFactory.Server() : factory;
+ _nextProtocol = nextProtocol;
+ addBean(_sslContextFactory);
+ }
+
+ public SslContextFactory getSslContextFactory()
+ {
+ return _sslContextFactory;
+ }
+
+ public void setDirectBuffersForEncryption(boolean useDirectBuffers)
+ {
+ this._directBuffersForEncryption = useDirectBuffers;
+ }
+
+ public void setDirectBuffersForDecryption(boolean useDirectBuffers)
+ {
+ this._directBuffersForDecryption = useDirectBuffers;
+ }
+
+ public boolean isDirectBuffersForDecryption()
+ {
+ return _directBuffersForDecryption;
+ }
+
+ public boolean isDirectBuffersForEncryption()
+ {
+ return _directBuffersForEncryption;
+ }
+
+ public String getNextProtocol()
+ {
+ return _nextProtocol;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+
+ SSLEngine engine = _sslContextFactory.newSSLEngine();
+ engine.setUseClientMode(false);
+ SSLSession session = engine.getSession();
+
+ if (session.getPacketBufferSize() > getInputBufferSize())
+ setInputBufferSize(session.getPacketBufferSize());
+ }
+
+ @Override
+ public Detection detect(ByteBuffer buffer)
+ {
+ if (buffer.remaining() < 2)
+ return Detection.NEED_MORE_BYTES;
+ int tlsFrameType = buffer.get(0) & 0xFF;
+ int tlsMajorVersion = buffer.get(1) & 0xFF;
+ boolean seemsSsl = (tlsFrameType == TLS_HANDSHAKE_FRAME_TYPE || tlsFrameType == TLS_ALERT_FRAME_TYPE) && tlsMajorVersion == TLS_MAJOR_VERSION;
+ return seemsSsl ? Detection.RECOGNIZED : Detection.NOT_RECOGNIZED;
+ }
+
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endPoint)
+ {
+ SSLEngine engine = _sslContextFactory.newSSLEngine(endPoint.getRemoteAddress());
+ engine.setUseClientMode(false);
+
+ SslConnection sslConnection = newSslConnection(connector, endPoint, engine);
+ sslConnection.setRenegotiationAllowed(_sslContextFactory.isRenegotiationAllowed());
+ sslConnection.setRenegotiationLimit(_sslContextFactory.getRenegotiationLimit());
+ configure(sslConnection, connector, endPoint);
+
+ ConnectionFactory next = connector.getConnectionFactory(_nextProtocol);
+ EndPoint decryptedEndPoint = sslConnection.getDecryptedEndPoint();
+ Connection connection = next.newConnection(connector, decryptedEndPoint);
+ decryptedEndPoint.setConnection(connection);
+
+ return sslConnection;
+ }
+
+ protected SslConnection newSslConnection(Connector connector, EndPoint endPoint, SSLEngine engine)
+ {
+ return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(), endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption());
+ }
+
+ @Override
+ protected AbstractConnection configure(AbstractConnection connection, Connector connector, EndPoint endPoint)
+ {
+ if (connection instanceof SslConnection)
+ {
+ SslConnection sslConnection = (SslConnection)connection;
+ if (connector instanceof ContainerLifeCycle)
+ {
+ ContainerLifeCycle container = (ContainerLifeCycle)connector;
+ container.getBeans(SslHandshakeListener.class).forEach(sslConnection::addHandshakeListener);
+ }
+ getBeans(SslHandshakeListener.class).forEach(sslConnection::addHandshakeListener);
+ }
+ return super.configure(connection, connector, endPoint);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{%s->%s}", this.getClass().getSimpleName(), hashCode(), getProtocol(), _nextProtocol);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/UserIdentity.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/UserIdentity.java
new file mode 100644
index 0000000..6dac92d
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/UserIdentity.java
@@ -0,0 +1,119 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.security.Principal;
+import java.util.Map;
+import javax.security.auth.Subject;
+
+import org.eclipse.jetty.server.handler.ContextHandler;
+
+/**
+ * User object that encapsulates user identity and operations such as run-as-role actions,
+ * checking isUserInRole and getUserPrincipal.
+ * <p>
+ * Implementations of UserIdentity should be immutable so that they may be
+ * cached by Authenticators and LoginServices.
+ */
+public interface UserIdentity
+{
+
+ /**
+ * @return The user subject
+ */
+ Subject getSubject();
+
+ /**
+ * @return The user principal
+ */
+ Principal getUserPrincipal();
+
+ /**
+ * Check if the user is in a role.
+ * This call is used to satisfy authorization calls from
+ * container code which will be using translated role names.
+ *
+ * @param role A role name.
+ * @param scope the scope
+ * @return True if the user can act in that role.
+ */
+ boolean isUserInRole(String role, Scope scope);
+
+ /**
+ * A UserIdentity Scope.
+ * A scope is the environment in which a User Identity is to
+ * be interpreted. Typically it is set by the target servlet of
+ * a request.
+ */
+ interface Scope
+ {
+
+ /**
+ * @return The context handler that the identity is being considered within
+ */
+ ContextHandler getContextHandler();
+
+ /**
+ * @return The context path that the identity is being considered within
+ */
+ String getContextPath();
+
+ /**
+ * @return The name of the identity context. Typically this is the servlet name.
+ */
+ String getName();
+
+ /**
+ * @return A map of role reference names that converts from names used by application code
+ * to names used by the context deployment.
+ */
+ Map<String, String> getRoleRefMap();
+ }
+
+ interface UnauthenticatedUserIdentity extends UserIdentity
+ {
+ }
+
+ UserIdentity UNAUTHENTICATED_IDENTITY = new UnauthenticatedUserIdentity()
+ {
+ @Override
+ public Subject getSubject()
+ {
+ return null;
+ }
+
+ @Override
+ public Principal getUserPrincipal()
+ {
+ return null;
+ }
+
+ @Override
+ public boolean isUserInRole(String role, Scope scope)
+ {
+ return false;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "UNAUTHENTICATED";
+ }
+ };
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Utf8HttpWriter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Utf8HttpWriter.java
new file mode 100644
index 0000000..114e345
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/Utf8HttpWriter.java
@@ -0,0 +1,182 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+
+/**
+ * OutputWriter.
+ * A writer that can wrap a {@link HttpOutput} stream and provide
+ * character encodings.
+ *
+ * The UTF-8 encoding is done by this class and no additional
+ * buffers or Writers are used.
+ * The UTF-8 code was inspired by http://javolution.org
+ */
+public class Utf8HttpWriter extends HttpWriter
+{
+ int _surrogate = 0;
+
+ public Utf8HttpWriter(HttpOutput out)
+ {
+ super(out);
+ }
+
+ @Override
+ public void write(char[] s, int offset, int length) throws IOException
+ {
+ HttpOutput out = _out;
+
+ while (length > 0)
+ {
+ _bytes.reset();
+ int chars = Math.min(length, MAX_OUTPUT_CHARS);
+
+ byte[] buffer = _bytes.getBuf();
+ int bytes = _bytes.getCount();
+
+ if (bytes + chars > buffer.length)
+ chars = buffer.length - bytes;
+
+ for (int i = 0; i < chars; i++)
+ {
+ int code = s[offset + i];
+
+ // Do we already have a surrogate?
+ if (_surrogate == 0)
+ {
+ // No - is this char code a surrogate?
+ if (Character.isHighSurrogate((char)code))
+ {
+ _surrogate = code; // UCS-?
+ continue;
+ }
+ }
+ // else handle a low surrogate
+ else if (Character.isLowSurrogate((char)code))
+ {
+ code = Character.toCodePoint((char)_surrogate, (char)code); // UCS-4
+ }
+ // else UCS-2
+ else
+ {
+ code = _surrogate; // UCS-2
+ _surrogate = 0; // USED
+ i--;
+ }
+
+ if ((code & 0xffffff80) == 0)
+ {
+ // 1b
+ if (bytes >= buffer.length)
+ {
+ chars = i;
+ break;
+ }
+ buffer[bytes++] = (byte)(code);
+ }
+ else
+ {
+ if ((code & 0xfffff800) == 0)
+ {
+ // 2b
+ if (bytes + 2 > buffer.length)
+ {
+ chars = i;
+ break;
+ }
+ buffer[bytes++] = (byte)(0xc0 | (code >> 6));
+ buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
+ }
+ else if ((code & 0xffff0000) == 0)
+ {
+ // 3b
+ if (bytes + 3 > buffer.length)
+ {
+ chars = i;
+ break;
+ }
+ buffer[bytes++] = (byte)(0xe0 | (code >> 12));
+ buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
+ buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
+ }
+ else if ((code & 0xff200000) == 0)
+ {
+ // 4b
+ if (bytes + 4 > buffer.length)
+ {
+ chars = i;
+ break;
+ }
+ buffer[bytes++] = (byte)(0xf0 | (code >> 18));
+ buffer[bytes++] = (byte)(0x80 | ((code >> 12) & 0x3f));
+ buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
+ buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
+ }
+ else if ((code & 0xf4000000) == 0)
+ {
+ // 5b
+ if (bytes + 5 > buffer.length)
+ {
+ chars = i;
+ break;
+ }
+ buffer[bytes++] = (byte)(0xf8 | (code >> 24));
+ buffer[bytes++] = (byte)(0x80 | ((code >> 18) & 0x3f));
+ buffer[bytes++] = (byte)(0x80 | ((code >> 12) & 0x3f));
+ buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
+ buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
+ }
+ else if ((code & 0x80000000) == 0)
+ {
+ // 6b
+ if (bytes + 6 > buffer.length)
+ {
+ chars = i;
+ break;
+ }
+ buffer[bytes++] = (byte)(0xfc | (code >> 30));
+ buffer[bytes++] = (byte)(0x80 | ((code >> 24) & 0x3f));
+ buffer[bytes++] = (byte)(0x80 | ((code >> 18) & 0x3f));
+ buffer[bytes++] = (byte)(0x80 | ((code >> 12) & 0x3f));
+ buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
+ buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
+ }
+ else
+ {
+ buffer[bytes++] = (byte)('?');
+ }
+
+ _surrogate = 0; // USED
+
+ if (bytes == buffer.length)
+ {
+ chars = i + 1;
+ break;
+ }
+ }
+ }
+ _bytes.setCount(bytes);
+
+ _bytes.writeTo(out);
+ length -= chars;
+ offset += chars;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandler.java
new file mode 100644
index 0000000..fb70a6e
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandler.java
@@ -0,0 +1,173 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import javax.servlet.DispatcherType;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpConnection;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * AbstractHandler.
+ * <p>A convenience implementation of {@link Handler} that uses the
+ * {@link ContainerLifeCycle} to provide:<ul>
+ * <li>start/stop behavior
+ * <li>a bean container
+ * <li>basic {@link Dumpable} support
+ * <li>a {@link Server} reference
+ * <li>optional error dispatch handling
+ * </ul>
+ */
+@ManagedObject("Jetty Handler")
+public abstract class AbstractHandler extends ContainerLifeCycle implements Handler
+{
+ private static final Logger LOG = Log.getLogger(AbstractHandler.class);
+
+ private Server _server;
+
+ public AbstractHandler()
+ {
+ }
+
+ @Override
+ public abstract void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException;
+
+ /**
+ * Deprecated error page generation
+ * @param target The target of the request - either a URI or a name.
+ * @param baseRequest The original unwrapped request object.
+ * @param request The request either as the {@link Request} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getRequest() getRequest()}</code>
+ * method can be used access the Request object if required.
+ * @param response The response as the {@link Response} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getResponse() getResponse()}</code>
+ * method can be used access the Response object if required.
+ * @throws IOException if unable to handle the request or response processing
+ * @throws ServletException if unable to handle the request or response due to underlying servlet issue
+ */
+ @Deprecated
+ protected void doError(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ Object o = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
+ int code = (o instanceof Integer) ? ((Integer)o).intValue() : (o != null ? Integer.parseInt(o.toString()) : 500);
+ response.setStatus(code);
+ baseRequest.setHandled(true);
+ }
+
+ /*
+ * @see org.eclipse.thread.LifeCycle#start()
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("starting {}", this);
+ if (_server == null)
+ LOG.warn("No Server set for {}", this);
+ super.doStart();
+ }
+
+ /*
+ * @see org.eclipse.thread.LifeCycle#stop()
+ */
+ @Override
+ protected void doStop() throws Exception
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("stopping {}", this);
+ super.doStop();
+ }
+
+ @Override
+ public void setServer(Server server)
+ {
+ if (_server == server)
+ return;
+ if (isStarted())
+ throw new IllegalStateException(STARTED);
+ _server = server;
+ }
+
+ @Override
+ public Server getServer()
+ {
+ return _server;
+ }
+
+ @Override
+ public void destroy()
+ {
+ if (!isStopped())
+ throw new IllegalStateException("!STOPPED");
+ super.destroy();
+ }
+
+ /**
+ * An extension of AbstractHandler that handles {@link DispatcherType#ERROR} dispatches.
+ * <p>
+ * {@link DispatcherType#ERROR} dispatches are handled by calling the {@link #doError(String, Request, HttpServletRequest, HttpServletResponse)}
+ * method. All other dispatches are passed to the abstract {@link #doNonErrorHandle(String, Request, HttpServletRequest, HttpServletResponse)}
+ * method, which should be implemented with specific handler behavior
+ * @deprecated This class is no longer required as ERROR dispatch is only done if there is an error page target.
+ */
+ @Deprecated
+ public abstract static class ErrorDispatchHandler extends AbstractHandler
+ {
+ @Override
+ public final void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (baseRequest.getDispatcherType() == DispatcherType.ERROR)
+ doError(target, baseRequest, request, response);
+ else
+ doNonErrorHandle(target, baseRequest, request, response);
+ }
+
+ /**
+ * Called by {@link #handle(String, Request, HttpServletRequest, HttpServletResponse)}
+ * for all non-{@link DispatcherType#ERROR} dispatches.
+ *
+ * @param target The target of the request - either a URI or a name.
+ * @param baseRequest The original unwrapped request object.
+ * @param request The request either as the {@link Request} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getRequest() getRequest()}</code>
+ * method can be used access the Request object if required.
+ * @param response The response as the {@link Response} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getResponse() getResponse()}</code>
+ * method can be used access the Response object if required.
+ * @throws IOException if unable to handle the request or response processing
+ * @throws ServletException if unable to handle the request or response due to underlying servlet issue
+ */
+ @Deprecated
+ protected abstract void doNonErrorHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandlerContainer.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandlerContainer.java
new file mode 100644
index 0000000..ebd8e41
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandlerContainer.java
@@ -0,0 +1,199 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HandlerContainer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.MultiException;
+import org.eclipse.jetty.util.component.Graceful;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Abstract Handler Container.
+ * This is the base class for handlers that may contain other handlers.
+ */
+public abstract class AbstractHandlerContainer extends AbstractHandler implements HandlerContainer
+{
+ private static final Logger LOG = Log.getLogger(AbstractHandlerContainer.class);
+
+ public AbstractHandlerContainer()
+ {
+ }
+
+ @Override
+ public Handler[] getChildHandlers()
+ {
+ List<Handler> list = new ArrayList<>();
+ expandChildren(list, null);
+ return list.toArray(new Handler[list.size()]);
+ }
+
+ @Override
+ public Handler[] getChildHandlersByClass(Class<?> byclass)
+ {
+ List<Handler> list = new ArrayList<>();
+ expandChildren(list, byclass);
+ return list.toArray(new Handler[list.size()]);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <T extends Handler> T getChildHandlerByClass(Class<T> byclass)
+ {
+ List<Handler> list = new ArrayList<>();
+ expandChildren(list, byclass);
+ if (list.isEmpty())
+ return null;
+ return (T)list.get(0);
+ }
+
+ protected void expandChildren(List<Handler> list, Class<?> byClass)
+ {
+ }
+
+ protected void expandHandler(Handler handler, List<Handler> list, Class<?> byClass)
+ {
+ if (handler == null)
+ return;
+
+ if (byClass == null || byClass.isAssignableFrom(handler.getClass()))
+ list.add(handler);
+
+ if (handler instanceof AbstractHandlerContainer)
+ ((AbstractHandlerContainer)handler).expandChildren(list, byClass);
+ else if (handler instanceof HandlerContainer)
+ {
+ HandlerContainer container = (HandlerContainer)handler;
+ Handler[] handlers = byClass == null ? container.getChildHandlers() : container.getChildHandlersByClass(byClass);
+ list.addAll(Arrays.asList(handlers));
+ }
+ }
+
+ public static <T extends HandlerContainer> T findContainerOf(HandlerContainer root, Class<T> type, Handler handler)
+ {
+ if (root == null || handler == null)
+ return null;
+
+ Handler[] branches = root.getChildHandlersByClass(type);
+ if (branches != null)
+ {
+ for (Handler h : branches)
+ {
+ @SuppressWarnings("unchecked")
+ T container = (T)h;
+ Handler[] candidates = container.getChildHandlersByClass(handler.getClass());
+ if (candidates != null)
+ {
+ for (Handler c : candidates)
+ {
+ if (c == handler)
+ return container;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void setServer(Server server)
+ {
+ if (server == getServer())
+ return;
+
+ if (isStarted())
+ throw new IllegalStateException(STARTED);
+
+ super.setServer(server);
+ Handler[] handlers = getHandlers();
+ if (handlers != null)
+ for (Handler h : handlers)
+ {
+ h.setServer(server);
+ }
+ }
+
+ /**
+ * Shutdown nested Gracefule handlers
+ *
+ * @param futures A list of Futures which must also be waited on for the shutdown (or null)
+ * returns A MultiException to which any failures are added or null
+ */
+ protected void doShutdown(List<Future<Void>> futures) throws MultiException
+ {
+ MultiException mex = null;
+
+ // tell the graceful handlers that we are shutting down
+ Handler[] gracefuls = getChildHandlersByClass(Graceful.class);
+ if (futures == null)
+ futures = new ArrayList<>(gracefuls.length);
+ for (Handler graceful : gracefuls)
+ {
+ futures.add(((Graceful)graceful).shutdown());
+ }
+
+ // Wait for all futures with a reducing time budget
+ long stopTimeout = getStopTimeout();
+ if (stopTimeout > 0)
+ {
+ long stopBy = System.currentTimeMillis() + stopTimeout;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Graceful shutdown {} by ", this, new Date(stopBy));
+
+ // Wait for shutdowns
+ for (Future<Void> future : futures)
+ {
+ try
+ {
+ if (!future.isDone())
+ future.get(Math.max(1L, stopBy - System.currentTimeMillis()), TimeUnit.MILLISECONDS);
+ }
+ catch (Exception e)
+ {
+ // If the future is also a callback, fail it here (rather than cancel) so we can capture the exception
+ if (future instanceof Callback && !future.isDone())
+ ((Callback)future).failed(e);
+ if (mex == null)
+ mex = new MultiException();
+ mex.add(e);
+ }
+ }
+ }
+
+ // Cancel any shutdowns not done
+ for (Future<Void> future : futures)
+ {
+ if (!future.isDone())
+ future.cancel(true);
+ }
+
+ if (mex != null)
+ mex.ifExceptionThrowMulti();
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java
new file mode 100644
index 0000000..9133532
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java
@@ -0,0 +1,94 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.jetty.server.handler.ContextHandler.AliasCheck;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.PathResource;
+import org.eclipse.jetty.util.resource.Resource;
+
+/**
+ * Symbolic Link AliasChecker.
+ * <p>An instance of this class can be registered with {@link ContextHandler#addAliasCheck(AliasCheck)}
+ * to check resources that are aliased to other locations. The checker uses the
+ * Java {@link Files#readSymbolicLink(Path)} and {@link Path#toRealPath(java.nio.file.LinkOption...)}
+ * APIs to check if a file is aliased with symbolic links.</p>
+ */
+public class AllowSymLinkAliasChecker implements AliasCheck
+{
+ private static final Logger LOG = Log.getLogger(AllowSymLinkAliasChecker.class);
+
+ @Override
+ public boolean check(String uri, Resource resource)
+ {
+ // Only support PathResource alias checking
+ if (!(resource instanceof PathResource))
+ return false;
+
+ PathResource pathResource = (PathResource)resource;
+
+ try
+ {
+ Path path = pathResource.getPath();
+ Path alias = pathResource.getAliasPath();
+
+ if (PathResource.isSameName(alias, path))
+ return false; // Unknown why this is an alias
+
+ if (hasSymbolicLink(path) && Files.isSameFile(path, alias))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Allow symlink {} --> {}", resource, pathResource.getAliasPath());
+ return true;
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+
+ return false;
+ }
+
+ private boolean hasSymbolicLink(Path path)
+ {
+ // Is file itself a symlink?
+ if (Files.isSymbolicLink(path))
+ {
+ return true;
+ }
+
+ // Lets try each path segment
+ Path base = path.getRoot();
+ for (Path segment : path)
+ {
+ base = base.resolve(segment);
+ if (Files.isSymbolicLink(base))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AsyncDelayHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AsyncDelayHandler.java
new file mode 100644
index 0000000..9c2461e
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AsyncDelayHandler.java
@@ -0,0 +1,148 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import javax.servlet.AsyncContext;
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+
+/**
+ * A handler wrapper that provides the framework to asynchronously
+ * delay the handling of a request. While it uses standard servlet
+ * API for asynchronous servlets, it adjusts the dispatch type of the
+ * request so that it does not appear to be asynchronous during the
+ * delayed dispatch.
+ */
+public class AsyncDelayHandler extends HandlerWrapper
+{
+ public static final String AHW_ATTR = "o.e.j.s.h.AsyncHandlerWrapper";
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (!isStarted() || _handler == null)
+ return;
+
+ // Get the dispatcher types
+ DispatcherType ctype = baseRequest.getDispatcherType();
+ DispatcherType dtype = (DispatcherType)baseRequest.getAttribute(AHW_ATTR);
+ Object asyncContextPath = null;
+ Object asyncPathInfo = null;
+ Object asyncQueryString = null;
+ Object asyncRequestUri = null;
+ Object asyncServletPath = null;
+
+ // Is this request a restarted one?
+ boolean restart = false;
+ if (dtype != null)
+ {
+ // fake the dispatch type to the original
+ baseRequest.setAttribute(AHW_ATTR, null);
+ baseRequest.setDispatcherType(dtype);
+ restart = true;
+
+ asyncContextPath = baseRequest.getAttribute(AsyncContext.ASYNC_CONTEXT_PATH);
+ baseRequest.setAttribute(AsyncContext.ASYNC_CONTEXT_PATH, null);
+ asyncPathInfo = baseRequest.getAttribute(AsyncContext.ASYNC_PATH_INFO);
+ baseRequest.setAttribute(AsyncContext.ASYNC_PATH_INFO, null);
+ asyncQueryString = baseRequest.getAttribute(AsyncContext.ASYNC_QUERY_STRING);
+ baseRequest.setAttribute(AsyncContext.ASYNC_QUERY_STRING, null);
+ asyncRequestUri = baseRequest.getAttribute(AsyncContext.ASYNC_REQUEST_URI);
+ baseRequest.setAttribute(AsyncContext.ASYNC_REQUEST_URI, null);
+ asyncServletPath = baseRequest.getAttribute(AsyncContext.ASYNC_SERVLET_PATH);
+ baseRequest.setAttribute(AsyncContext.ASYNC_SERVLET_PATH, null);
+ }
+
+ // Should we handle this request now?
+ if (!startHandling(baseRequest, restart))
+ {
+ // No, so go async and remember dispatch type
+ AsyncContext context = baseRequest.startAsync();
+ baseRequest.setAttribute(AHW_ATTR, ctype);
+
+ delayHandling(baseRequest, context);
+ return;
+ }
+
+ // Handle the request
+ try
+ {
+ _handler.handle(target, baseRequest, request, response);
+ }
+ finally
+ {
+ if (restart)
+ {
+ // reset the request
+ baseRequest.setDispatcherType(ctype);
+ baseRequest.setAttribute(AsyncContext.ASYNC_CONTEXT_PATH, asyncContextPath);
+ baseRequest.setAttribute(AsyncContext.ASYNC_PATH_INFO, asyncPathInfo);
+ baseRequest.setAttribute(AsyncContext.ASYNC_QUERY_STRING, asyncQueryString);
+ baseRequest.setAttribute(AsyncContext.ASYNC_REQUEST_URI, asyncRequestUri);
+ baseRequest.setAttribute(AsyncContext.ASYNC_SERVLET_PATH, asyncServletPath);
+ }
+
+ // signal the request is leaving the handler
+ endHandling(baseRequest);
+ }
+ }
+
+ /**
+ * Called to indicate that a request has been presented for handling
+ *
+ * @param request The request to handle
+ * @param restart True if this request is being restarted after a delay
+ * @return True if the request should be handled now
+ */
+ protected boolean startHandling(Request request, boolean restart)
+ {
+ return true;
+ }
+
+ /**
+ * Called to indicate that a requests handling is being delayed/
+ * The implementation should arrange for context.dispatch() to be
+ * called when the request should be handled. It may also set
+ * timeouts on the context.
+ *
+ * @param request The request to be delayed
+ * @param context The AsyncContext of the delayed request
+ */
+ protected void delayHandling(Request request, AsyncContext context)
+ {
+ context.dispatch();
+ }
+
+ /**
+ * Called to indicated the handling of the request is ending.
+ * This is only the end of the current dispatch of the request and
+ * if the request is asynchronous, it may be handled again.
+ *
+ * @param request The request
+ */
+ protected void endHandling(Request request)
+ {
+
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java
new file mode 100644
index 0000000..309f0aa
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java
@@ -0,0 +1,341 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+import java.util.Queue;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.http.pathmap.PathSpecSet;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpOutput;
+import org.eclipse.jetty.server.HttpOutput.Interceptor;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.IncludeExclude;
+import org.eclipse.jetty.util.IteratingCallback;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * <p>
+ * A Handler that can apply a {@link org.eclipse.jetty.server.HttpOutput.Interceptor}
+ * mechanism to buffer the entire response content until the output is closed.
+ * This allows the commit to be delayed until the response is complete and thus
+ * headers and response status can be changed while writing the body.
+ * </p>
+ * <p>
+ * Note that the decision to buffer is influenced by the headers and status at the
+ * first write, and thus subsequent changes to those headers will not influence the
+ * decision to buffer or not.
+ * </p>
+ * <p>
+ * Note also that there are no memory limits to the size of the buffer, thus
+ * this handler can represent an unbounded memory commitment if the content
+ * generated can also be unbounded.
+ * </p>
+ */
+public class BufferedResponseHandler extends HandlerWrapper
+{
+ private static final Logger LOG = Log.getLogger(BufferedResponseHandler.class);
+
+ private final IncludeExclude<String> _methods = new IncludeExclude<>();
+ private final IncludeExclude<String> _paths = new IncludeExclude<>(PathSpecSet.class);
+ private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>();
+
+ public BufferedResponseHandler()
+ {
+ _methods.include(HttpMethod.GET.asString());
+ for (String type : MimeTypes.getKnownMimeTypes())
+ {
+ if (type.startsWith("image/") ||
+ type.startsWith("audio/") ||
+ type.startsWith("video/"))
+ _mimeTypes.exclude(type);
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} mime types {}", this, _mimeTypes);
+ }
+
+ public IncludeExclude<String> getMethodIncludeExclude()
+ {
+ return _methods;
+ }
+
+ public IncludeExclude<String> getPathIncludeExclude()
+ {
+ return _paths;
+ }
+
+ public IncludeExclude<String> getMimeIncludeExclude()
+ {
+ return _mimeTypes;
+ }
+
+ protected boolean isMimeTypeBufferable(String mimetype)
+ {
+ return _mimeTypes.test(mimetype);
+ }
+
+ protected boolean isPathBufferable(String requestURI)
+ {
+ if (requestURI == null)
+ return true;
+
+ return _paths.test(requestURI);
+ }
+
+ protected boolean shouldBuffer(HttpChannel channel, boolean last)
+ {
+ if (last)
+ return false;
+
+ Response response = channel.getResponse();
+ int status = response.getStatus();
+ if (HttpStatus.hasNoBody(status) || HttpStatus.isRedirection(status))
+ return false;
+
+ String ct = response.getContentType();
+ if (ct == null)
+ return true;
+
+ ct = MimeTypes.getContentTypeWithoutCharset(ct);
+ return isMimeTypeBufferable(StringUtil.asciiToLowerCase(ct));
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ ServletContext context = baseRequest.getServletContext();
+ String path = context == null ? baseRequest.getRequestURI() : URIUtil.addPaths(baseRequest.getServletPath(), baseRequest.getPathInfo());
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} handle {} in {}", this, baseRequest, context);
+
+ // Are we already buffering?
+ HttpOutput out = baseRequest.getResponse().getHttpOutput();
+ HttpOutput.Interceptor interceptor = out.getInterceptor();
+ while (interceptor != null)
+ {
+ if (interceptor instanceof BufferedInterceptor)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} already intercepting {}", this, request);
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+ interceptor = interceptor.getNextInterceptor();
+ }
+
+ // If not a supported method this URI is always excluded.
+ if (!_methods.test(baseRequest.getMethod()))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} excluded by method {}", this, request);
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+
+ // If not a supported path this URI is always excluded.
+ if (!isPathBufferable(path))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} excluded by path {}", this, request);
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+
+ // If the mime type is known from the path then apply mime type filtering.
+ String mimeType = context == null ? MimeTypes.getDefaultMimeByExtension(path) : context.getMimeType(path);
+ if (mimeType != null)
+ {
+ mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
+ if (!isMimeTypeBufferable(mimeType))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} excluded by path suffix mime type {}", this, request);
+
+ // handle normally without setting vary header
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+ }
+
+ // Install buffered interceptor and handle.
+ out.setInterceptor(newBufferedInterceptor(baseRequest.getHttpChannel(), out.getInterceptor()));
+ if (_handler != null)
+ _handler.handle(target, baseRequest, request, response);
+ }
+
+ protected BufferedInterceptor newBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
+ {
+ return new ArrayBufferedInterceptor(httpChannel, interceptor);
+ }
+
+ /**
+ * An {@link HttpOutput.Interceptor} which is created by {@link #newBufferedInterceptor(HttpChannel, Interceptor)}
+ * and is used by the implementation to buffer outgoing content.
+ */
+ protected interface BufferedInterceptor extends HttpOutput.Interceptor
+ {
+ }
+
+ class ArrayBufferedInterceptor implements BufferedInterceptor
+ {
+ private final Interceptor _next;
+ private final HttpChannel _channel;
+ private final Queue<ByteBuffer> _buffers = new ArrayDeque<>();
+ private Boolean _aggregating;
+ private ByteBuffer _aggregate;
+
+ public ArrayBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
+ {
+ _next = interceptor;
+ _channel = httpChannel;
+ }
+
+ @Override
+ public Interceptor getNextInterceptor()
+ {
+ return _next;
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return false;
+ }
+
+ @Override
+ public void resetBuffer()
+ {
+ _buffers.clear();
+ _aggregating = null;
+ _aggregate = null;
+ BufferedInterceptor.super.resetBuffer();
+ }
+
+ @Override
+ public void write(ByteBuffer content, boolean last, Callback callback)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} write last={} {}", this, last, BufferUtil.toDetailString(content));
+
+ // If we are not committed, have to decide if we should aggregate or not.
+ if (_aggregating == null)
+ _aggregating = shouldBuffer(_channel, last);
+
+ // If we are not aggregating, then handle normally.
+ if (!_aggregating)
+ {
+ getNextInterceptor().write(content, last, callback);
+ return;
+ }
+
+ if (last)
+ {
+ // Add the current content to the buffer list without a copy.
+ if (BufferUtil.length(content) > 0)
+ _buffers.offer(content);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} committing {}", this, _buffers.size());
+ commit(callback);
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} aggregating", this);
+
+ // Aggregate the content into buffer chain.
+ while (BufferUtil.hasContent(content))
+ {
+ // Do we need a new aggregate buffer.
+ if (BufferUtil.space(_aggregate) == 0)
+ {
+ // TODO: use a buffer pool always allocating with outputBufferSize to avoid polluting the ByteBufferPool.
+ int size = Math.max(_channel.getHttpConfiguration().getOutputBufferSize(), BufferUtil.length(content));
+ _aggregate = BufferUtil.allocate(size);
+ _buffers.offer(_aggregate);
+ }
+
+ BufferUtil.append(_aggregate, content);
+ }
+ callback.succeeded();
+ }
+ }
+
+ private void commit(Callback callback)
+ {
+ if (_buffers.size() == 0)
+ {
+ getNextInterceptor().write(BufferUtil.EMPTY_BUFFER, true, callback);
+ }
+ else if (_buffers.size() == 1)
+ {
+ getNextInterceptor().write(_buffers.poll(), true, callback);
+ }
+ else
+ {
+ // Create an iterating callback to do the writing.
+ IteratingCallback icb = new IteratingCallback()
+ {
+ @Override
+ protected Action process()
+ {
+ ByteBuffer buffer = _buffers.poll();
+ if (buffer == null)
+ return Action.SUCCEEDED;
+
+ getNextInterceptor().write(buffer, _buffers.isEmpty(), this);
+ return Action.SCHEDULED;
+ }
+
+ @Override
+ protected void onCompleteSuccess()
+ {
+ // Signal last callback.
+ callback.succeeded();
+ }
+
+ @Override
+ protected void onCompleteFailure(Throwable cause)
+ {
+ // Signal last callback.
+ callback.failed(cause);
+ }
+ };
+ icb.iterate();
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
new file mode 100644
index 0000000..45cef63
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
@@ -0,0 +1,3037 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.EventListener;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.servlet.DispatcherType;
+import javax.servlet.Filter;
+import javax.servlet.FilterRegistration;
+import javax.servlet.FilterRegistration.Dynamic;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.Servlet;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextAttributeEvent;
+import javax.servlet.ServletContextAttributeListener;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRegistration;
+import javax.servlet.ServletRequestAttributeListener;
+import javax.servlet.ServletRequestEvent;
+import javax.servlet.ServletRequestListener;
+import javax.servlet.SessionCookieConfig;
+import javax.servlet.SessionTrackingMode;
+import javax.servlet.descriptor.JspConfigDescriptor;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSessionAttributeListener;
+import javax.servlet.http.HttpSessionIdListener;
+import javax.servlet.http.HttpSessionListener;
+
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.server.ClassLoaderDump;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Dispatcher;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HandlerContainer;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.Attributes;
+import org.eclipse.jetty.util.AttributesMap;
+import org.eclipse.jetty.util.FutureCallback;
+import org.eclipse.jetty.util.Loader;
+import org.eclipse.jetty.util.MultiException;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.DumpableCollection;
+import org.eclipse.jetty.util.component.Graceful;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+
+/**
+ * ContextHandler.
+ *
+ * <p>
+ * This handler wraps a call to handle by setting the context and servlet path, plus setting the context classloader.
+ * </p>
+ * <p>
+ * If the context init parameter {@code org.eclipse.jetty.server.context.ManagedAttributes} is set to a comma separated list of names, then they are treated as
+ * context attribute names, which if set as attributes are passed to the servers Container so that they may be managed with JMX.
+ * </p>
+ * <p>
+ * The maximum size of a form that can be processed by this context is controlled by the system properties {@code org.eclipse.jetty.server.Request.maxFormKeys} and
+ * {@code org.eclipse.jetty.server.Request.maxFormContentSize}. These can also be configured with {@link #setMaxFormContentSize(int)} and {@link #setMaxFormKeys(int)}
+ * </p>
+ * <p>
+ * The executor is made available via a context attributed {@code org.eclipse.jetty.server.Executor}.
+ * </p>
+ * <p>
+ * By default, the context is created with alias checkers for {@link AllowSymLinkAliasChecker} (unix only) and {@link ApproveNonExistentDirectoryAliases}. If
+ * these alias checkers are not required, then {@link #clearAliasChecks()} or {@link #setAliasChecks(List)} should be called.
+ * </p>
+ */
+@ManagedObject("URI Context")
+public class ContextHandler extends ScopedHandler implements Attributes, Graceful
+{
+ public static final int SERVLET_MAJOR_VERSION = 3;
+ public static final int SERVLET_MINOR_VERSION = 1;
+ public static final Class<?>[] SERVLET_LISTENER_TYPES =
+ {
+ ServletContextListener.class,
+ ServletContextAttributeListener.class,
+ ServletRequestListener.class,
+ ServletRequestAttributeListener.class,
+ HttpSessionIdListener.class,
+ HttpSessionListener.class,
+ HttpSessionAttributeListener.class
+ };
+
+ public static final int DEFAULT_LISTENER_TYPE_INDEX = 1;
+
+ public static final int EXTENDED_LISTENER_TYPE_INDEX = 0;
+
+ private static final String UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER = "Unimplemented {} - use org.eclipse.jetty.servlet.ServletContextHandler";
+
+ private static final Logger LOG = Log.getLogger(ContextHandler.class);
+
+ private static final ThreadLocal<Context> __context = new ThreadLocal<>();
+
+ private static String __serverInfo = "jetty/" + Server.getVersion();
+
+ /**
+ * If a context attribute with this name is set, it is interpreted as a comma separated list of attribute name. Any other context attributes that are set
+ * with a name from this list will result in a call to {@link #setManagedAttribute(String, Object)}, which typically initiates the creation of a JMX MBean
+ * for the attribute value.
+ */
+ public static final String MANAGED_ATTRIBUTES = "org.eclipse.jetty.server.context.ManagedAttributes";
+
+ public static final String MAX_FORM_KEYS_KEY = "org.eclipse.jetty.server.Request.maxFormKeys";
+ public static final String MAX_FORM_CONTENT_SIZE_KEY = "org.eclipse.jetty.server.Request.maxFormContentSize";
+ public static final int DEFAULT_MAX_FORM_KEYS = 1000;
+ public static final int DEFAULT_MAX_FORM_CONTENT_SIZE = 200000;
+
+ /**
+ * Get the current ServletContext implementation.
+ *
+ * @return ServletContext implementation
+ */
+ public static Context getCurrentContext()
+ {
+ return __context.get();
+ }
+
+ public static ContextHandler getContextHandler(ServletContext context)
+ {
+ if (context instanceof ContextHandler.Context)
+ return ((ContextHandler.Context)context).getContextHandler();
+ Context c = getCurrentContext();
+ if (c != null)
+ return c.getContextHandler();
+ return null;
+ }
+
+ public static String getServerInfo()
+ {
+ return __serverInfo;
+ }
+
+ public static void setServerInfo(String serverInfo)
+ {
+ __serverInfo = serverInfo;
+ }
+
+ public enum ContextStatus
+ {
+ NOTSET,
+ INITIALIZED,
+ DESTROYED
+ }
+
+ protected ContextStatus _contextStatus = ContextStatus.NOTSET;
+ protected Context _scontext;
+ private final AttributesMap _attributes;
+ private final Map<String, String> _initParams;
+ private ClassLoader _classLoader;
+ private String _contextPath = "/";
+ private String _contextPathEncoded = "/";
+
+ private String _displayName;
+
+ private Resource _baseResource;
+ private MimeTypes _mimeTypes;
+ private Map<String, String> _localeEncodingMap;
+ private String[] _welcomeFiles;
+ private ErrorHandler _errorHandler;
+
+ private String[] _vhosts; // Host name portion, matching _vconnectors array
+ private boolean[] _vhostswildcard;
+ private String[] _vconnectors; // connector portion, matching _vhosts array
+
+ private Logger _logger;
+ private boolean _allowNullPathInfo;
+ private int _maxFormKeys = Integer.getInteger(MAX_FORM_KEYS_KEY, DEFAULT_MAX_FORM_KEYS);
+ private int _maxFormContentSize = Integer.getInteger(MAX_FORM_CONTENT_SIZE_KEY, DEFAULT_MAX_FORM_CONTENT_SIZE);
+ private boolean _compactPath = false;
+ private boolean _usingSecurityManager = System.getSecurityManager() != null;
+
+ private final List<EventListener> _eventListeners = new CopyOnWriteArrayList<>();
+ private final List<EventListener> _programmaticListeners = new CopyOnWriteArrayList<>();
+ private final List<ServletContextListener> _servletContextListeners = new CopyOnWriteArrayList<>();
+ private final List<ServletContextListener> _destroyServletContextListeners = new ArrayList<>();
+ private final List<ServletContextAttributeListener> _servletContextAttributeListeners = new CopyOnWriteArrayList<>();
+ private final List<ServletRequestListener> _servletRequestListeners = new CopyOnWriteArrayList<>();
+ private final List<ServletRequestAttributeListener> _servletRequestAttributeListeners = new CopyOnWriteArrayList<>();
+ private final List<ContextScopeListener> _contextListeners = new CopyOnWriteArrayList<>();
+ private final List<EventListener> _durableListeners = new CopyOnWriteArrayList<>();
+ private String[] _protectedTargets;
+ private final CopyOnWriteArrayList<AliasCheck> _aliasChecks = new CopyOnWriteArrayList<>();
+
+ public enum Availability
+ {
+ STOPPED, // stopped and can't be made unavailable nor shutdown
+ STARTING, // starting inside of doStart. It may go to any of the next states.
+ AVAILABLE, // running normally
+ UNAVAILABLE, // Either a startup error or explicit call to setAvailable(false)
+ SHUTDOWN, // graceful shutdown
+ }
+
+ private final AtomicReference<Availability> _availability = new AtomicReference<>(Availability.STOPPED);
+
+ public ContextHandler()
+ {
+ this(null, null, null);
+ }
+
+ protected ContextHandler(Context context)
+ {
+ this(context, null, null);
+ }
+
+ public ContextHandler(String contextPath)
+ {
+ this(null, null, contextPath);
+ }
+
+ public ContextHandler(HandlerContainer parent, String contextPath)
+ {
+ this(null, parent, contextPath);
+ }
+
+ private ContextHandler(Context context, HandlerContainer parent, String contextPath)
+ {
+ _scontext = context == null ? new Context() : context;
+ _attributes = new AttributesMap();
+ _initParams = new HashMap<>();
+ addAliasCheck(new ApproveNonExistentDirectoryAliases());
+ if (File.separatorChar == '/')
+ addAliasCheck(new AllowSymLinkAliasChecker());
+
+ if (contextPath != null)
+ setContextPath(contextPath);
+ if (parent instanceof HandlerWrapper)
+ ((HandlerWrapper)parent).setHandler(this);
+ else if (parent instanceof HandlerCollection)
+ ((HandlerCollection)parent).addHandler(this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ dumpObjects(out, indent,
+ new ClassLoaderDump(getClassLoader()),
+ new DumpableCollection("eventListeners " + this, _eventListeners),
+ new DumpableCollection("handler attributes " + this, ((AttributesMap)getAttributes()).getAttributeEntrySet()),
+ new DumpableCollection("context attributes " + this, getServletContext().getAttributeEntrySet()),
+ new DumpableCollection("initparams " + this, getInitParams().entrySet()));
+ }
+
+ public Context getServletContext()
+ {
+ return _scontext;
+ }
+
+ /**
+ * @return the allowNullPathInfo true if /context is not redirected to /context/
+ */
+ @ManagedAttribute("Checks if the /context is not redirected to /context/")
+ public boolean getAllowNullPathInfo()
+ {
+ return _allowNullPathInfo;
+ }
+
+ /**
+ * @param allowNullPathInfo true if /context is not redirected to /context/
+ */
+ public void setAllowNullPathInfo(boolean allowNullPathInfo)
+ {
+ _allowNullPathInfo = allowNullPathInfo;
+ }
+
+ @Override
+ public void setServer(Server server)
+ {
+ super.setServer(server);
+ if (_errorHandler != null)
+ _errorHandler.setServer(server);
+ }
+
+ public boolean isUsingSecurityManager()
+ {
+ return _usingSecurityManager;
+ }
+
+ public void setUsingSecurityManager(boolean usingSecurityManager)
+ {
+ if (usingSecurityManager && System.getSecurityManager() == null)
+ throw new IllegalStateException("No security manager");
+ _usingSecurityManager = usingSecurityManager;
+ }
+
+ /**
+ * Set the virtual hosts for the context. Only requests that have a matching host header or fully qualified URL will be passed to that context with a
+ * virtual host name. A context with no virtual host names or a null virtual host name is available to all requests that are not served by a context with a
+ * matching virtual host name.
+ *
+ * @param vhosts Array of virtual hosts that this context responds to. A null/empty array means any hostname is acceptable. Host names may be String
+ * representation of IP addresses. Host names may start with '*.' to wildcard one level of names. Hosts and wildcard hosts may be followed with
+ * '@connectorname', in which case they will match only if the the {@link Connector#getName()} for the request also matches. If an entry is just
+ * '@connectorname' it will match any host if that connector was used. Note - In previous versions if one or more connectorname only entries existed
+ * and non of the connectors matched the handler would not match regardless of any hostname entries. If there is one or more connectorname only
+ * entries and one or more host only entries but no hostname and connector entries we assume the old behavior and will log a warning. The warning
+ * can be removed by removing the host entries that were previously being ignored, or modifying to include a hostname and connectorname entry.
+ */
+ public void setVirtualHosts(String[] vhosts)
+ {
+
+ if (vhosts == null)
+ {
+ _vhosts = vhosts;
+ }
+ else
+ {
+
+ boolean hostMatch = false;
+ boolean connectorHostMatch = false;
+ _vhosts = new String[vhosts.length];
+ _vconnectors = new String[vhosts.length];
+ _vhostswildcard = new boolean[vhosts.length];
+ ArrayList<Integer> connectorOnlyIndexes = null;
+ for (int i = 0; i < vhosts.length; i++)
+ {
+ boolean connectorMatch = false;
+ _vhosts[i] = vhosts[i];
+ if (vhosts[i] == null)
+ continue;
+ int connectorIndex = _vhosts[i].indexOf('@');
+ if (connectorIndex >= 0)
+ {
+ connectorMatch = true;
+ _vconnectors[i] = _vhosts[i].substring(connectorIndex + 1);
+ _vhosts[i] = _vhosts[i].substring(0, connectorIndex);
+ if (connectorIndex == 0)
+ {
+ if (connectorOnlyIndexes == null)
+ connectorOnlyIndexes = new ArrayList<>();
+ connectorOnlyIndexes.add(i);
+ }
+ }
+
+ if (_vhosts[i].startsWith("*."))
+ {
+ _vhosts[i] = _vhosts[i].substring(1);
+ _vhostswildcard[i] = true;
+ }
+ if (_vhosts[i].isEmpty())
+ _vhosts[i] = null;
+ else
+ {
+ hostMatch = true;
+ connectorHostMatch = connectorHostMatch || connectorMatch;
+ }
+ _vhosts[i] = normalizeHostname(_vhosts[i]);
+ }
+
+ if (connectorOnlyIndexes != null && hostMatch && !connectorHostMatch)
+ {
+ LOG.warn("ContextHandler {} has a connector only entry e.g. \"@connector\" and one or more host only entries. \n" +
+ "The host entries will be ignored to match legacy behavior. " +
+ "To clear this warning remove the host entries or update to use " +
+ "at least one host@connector syntax entry that will match a host for an specific connector",
+ Arrays.asList(vhosts));
+ String[] filteredHosts = new String[connectorOnlyIndexes.size()];
+ for (int i = 0; i < connectorOnlyIndexes.size(); i++)
+ {
+ filteredHosts[i] = vhosts[connectorOnlyIndexes.get(i)];
+ }
+ setVirtualHosts(filteredHosts);
+ }
+ }
+ }
+
+ /**
+ * Either set virtual hosts or add to an existing set of virtual hosts.
+ *
+ * @param virtualHosts Array of virtual hosts that this context responds to. A null/empty array means any hostname is acceptable. Host names may be String
+ * representation of IP addresses. Host names may start with '*.' to wildcard one level of names. Hosts and wildcard hosts may be followed with
+ * '@connectorname', in which case they will match only if the the {@link Connector#getName()} for the request also matches. If an entry is just
+ * '@connectorname' it will match any host if that connector was used. Note - In previous versions if one or more connectorname only entries existed
+ * and non of the connectors matched the handler would not match regardless of any hostname entries. If there is one or more connectorname only
+ * entries and one or more host only entries but no hostname and connector entries we assume the old behavior and will log a warning. The warning
+ * can be removed by removing the host entries that were previously being ignored, or modifying to include a hostname and connectorname entry.
+ */
+ public void addVirtualHosts(String[] virtualHosts)
+ {
+ if (virtualHosts == null || virtualHosts.length == 0) // since this is add, we don't null the old ones
+ return;
+
+ if (_vhosts == null)
+ {
+ setVirtualHosts(virtualHosts);
+ }
+ else
+ {
+ Set<String> currentVirtualHosts = new HashSet<>(Arrays.asList(getVirtualHosts()));
+ for (String vh : virtualHosts)
+ {
+ currentVirtualHosts.add(normalizeHostname(vh));
+ }
+ setVirtualHosts(currentVirtualHosts.toArray(new String[0]));
+ }
+ }
+
+ /**
+ * Removes an array of virtual host entries, if this removes all entries the _vhosts will be set to null
+ *
+ * @param virtualHosts Array of virtual hosts that this context responds to. A null/empty array means any hostname is acceptable. Host names may be String
+ * representation of IP addresses. Host names may start with '*.' to wildcard one level of names. Hosts and wildcard hosts may be followed with
+ * '@connectorname', in which case they will match only if the the {@link Connector#getName()} for the request also matches. If an entry is just
+ * '@connectorname' it will match any host if that connector was used. Note - In previous versions if one or more connectorname only entries existed
+ * and non of the connectors matched the handler would not match regardless of any hostname entries. If there is one or more connectorname only
+ * entries and one or more host only entries but no hostname and connector entries we assume the old behavior and will log a warning. The warning
+ * can be removed by removing the host entries that were previously being ignored, or modifying to include a hostname and connectorname entry.
+ */
+ public void removeVirtualHosts(String[] virtualHosts)
+ {
+ if (virtualHosts == null || virtualHosts.length == 0 || _vhosts == null || _vhosts.length == 0)
+ return; // do nothing
+
+ Set<String> existingVirtualHosts = new HashSet<>(Arrays.asList(getVirtualHosts()));
+ for (String vh : virtualHosts)
+ {
+ existingVirtualHosts.remove(normalizeHostname(vh));
+ }
+ if (existingVirtualHosts.isEmpty())
+ setVirtualHosts(null); // if we ended up removing them all, just null out _vhosts
+ else
+ setVirtualHosts(existingVirtualHosts.toArray(new String[0]));
+ }
+
+ /**
+ * Get the virtual hosts for the context. Only requests that have a matching host header or fully qualified URL will be passed to that context with a
+ * virtual host name. A context with no virtual host names or a null virtual host name is available to all requests that are not served by a context with a
+ * matching virtual host name.
+ *
+ * @return Array of virtual hosts that this context responds to. A null/empty array means any hostname is acceptable. Host names may be String
+ * representation of IP addresses. Host names may start with '*.' to wildcard one level of names. Hosts and wildcard hosts may be followed with
+ * '@connectorname', in which case they will match only if the the {@link Connector#getName()} for the request also matches. If an entry is just
+ * '@connectorname' it will match any host if that connector was used. Note - In previous versions if one or more connectorname only entries existed
+ * and non of the connectors matched the handler would not match regardless of any hostname entries. If there is one or more connectorname only
+ * entries and one or more host only entries but no hostname and connector entries we assume the old behavior and will log a warning. The warning
+ * can be removed by removing the host entries that were previously being ignored, or modifying to include a hostname and connectorname entry.
+ */
+ @ManagedAttribute(value = "Virtual hosts accepted by the context", readonly = true)
+ public String[] getVirtualHosts()
+ {
+ if (_vhosts == null)
+ return null;
+
+ String[] vhosts = new String[_vhosts.length];
+ for (int i = 0; i < _vhosts.length; i++)
+ {
+ StringBuilder sb = new StringBuilder();
+ if (_vhostswildcard[i])
+ sb.append("*");
+ if (_vhosts[i] != null)
+ sb.append(_vhosts[i]);
+ if (_vconnectors[i] != null)
+ sb.append("@").append(_vconnectors[i]);
+ vhosts[i] = sb.toString();
+ }
+ return vhosts;
+ }
+
+ /*
+ * @see javax.servlet.ServletContext#getAttribute(java.lang.String)
+ */
+ @Override
+ public Object getAttribute(String name)
+ {
+ return _attributes.getAttribute(name);
+ }
+
+ /*
+ * @see javax.servlet.ServletContext#getAttributeNames()
+ */
+ @Override
+ public Enumeration<String> getAttributeNames()
+ {
+ return AttributesMap.getAttributeNamesCopy(_attributes);
+ }
+
+ @Override
+ public Set<String> getAttributeNameSet()
+ {
+ return _attributes.getAttributeNameSet();
+ }
+
+ /**
+ * @return Returns the attributes.
+ */
+ public Attributes getAttributes()
+ {
+ return _attributes;
+ }
+
+ /**
+ * @return Returns the classLoader.
+ */
+ public ClassLoader getClassLoader()
+ {
+ return _classLoader;
+ }
+
+ /**
+ * Make best effort to extract a file classpath from the context classloader
+ *
+ * @return Returns the classLoader.
+ */
+ @ManagedAttribute("The file classpath")
+ public String getClassPath()
+ {
+ if (_classLoader == null || !(_classLoader instanceof URLClassLoader))
+ return null;
+ URLClassLoader loader = (URLClassLoader)_classLoader;
+ URL[] urls = loader.getURLs();
+ StringBuilder classpath = new StringBuilder();
+ for (int i = 0; i < urls.length; i++)
+ {
+ try
+ {
+ Resource resource = newResource(urls[i]);
+ File file = resource.getFile();
+ if (file != null && file.exists())
+ {
+ if (classpath.length() > 0)
+ classpath.append(File.pathSeparatorChar);
+ classpath.append(file.getAbsolutePath());
+ }
+ }
+ catch (IOException e)
+ {
+ LOG.debug(e);
+ }
+ }
+ if (classpath.length() == 0)
+ return null;
+ return classpath.toString();
+ }
+
+ /**
+ * @return Returns the contextPath.
+ */
+ @ManagedAttribute("True if URLs are compacted to replace the multiple '/'s with a single '/'")
+ public String getContextPath()
+ {
+ return _contextPath;
+ }
+
+ /**
+ * @return Returns the encoded contextPath.
+ */
+ public String getContextPathEncoded()
+ {
+ return _contextPathEncoded;
+ }
+
+ /**
+ * Get the context path in a form suitable to be returned from {@link HttpServletRequest#getContextPath()}
+ * or {@link ServletContext#getContextPath()}.
+ * @return Returns the encoded contextPath, or empty string for root context
+ */
+ public String getRequestContextPath()
+ {
+ String contextPathEncoded = getContextPathEncoded();
+ return "/".equals(contextPathEncoded) ? "" : contextPathEncoded;
+ }
+
+ /*
+ * @see javax.servlet.ServletContext#getInitParameter(java.lang.String)
+ */
+ public String getInitParameter(String name)
+ {
+ return _initParams.get(name);
+ }
+
+ public String setInitParameter(String name, String value)
+ {
+ return _initParams.put(name, value);
+ }
+
+ /*
+ * @see javax.servlet.ServletContext#getInitParameterNames()
+ */
+ public Enumeration<String> getInitParameterNames()
+ {
+ return Collections.enumeration(_initParams.keySet());
+ }
+
+ /**
+ * @return Returns the initParams.
+ */
+ @ManagedAttribute("Initial Parameter map for the context")
+ public Map<String, String> getInitParams()
+ {
+ return _initParams;
+ }
+
+ /*
+ * @see javax.servlet.ServletContext#getServletContextName()
+ */
+ @ManagedAttribute(value = "Display name of the Context", readonly = true)
+ public String getDisplayName()
+ {
+ return _displayName;
+ }
+
+ public EventListener[] getEventListeners()
+ {
+ return _eventListeners.toArray(new EventListener[0]);
+ }
+
+ /**
+ * Set the context event listeners.
+ *
+ * @param eventListeners the event listeners
+ * @see ServletContextListener
+ * @see ServletContextAttributeListener
+ * @see ServletRequestListener
+ * @see ServletRequestAttributeListener
+ */
+ public void setEventListeners(EventListener[] eventListeners)
+ {
+ _contextListeners.clear();
+ _servletContextListeners.clear();
+ _servletContextAttributeListeners.clear();
+ _servletRequestListeners.clear();
+ _servletRequestAttributeListeners.clear();
+ _eventListeners.clear();
+
+ if (eventListeners != null)
+ for (EventListener listener : eventListeners)
+ {
+ addEventListener(listener);
+ }
+ }
+
+ /**
+ * Add a context event listeners.
+ *
+ * @param listener the event listener to add
+ * @see ServletContextListener
+ * @see ServletContextAttributeListener
+ * @see ServletRequestListener
+ * @see ServletRequestAttributeListener
+ */
+ public void addEventListener(EventListener listener)
+ {
+ _eventListeners.add(listener);
+
+ if (!(isStarted() || isStarting()))
+ {
+ _durableListeners.add(listener);
+ }
+
+ if (listener instanceof ContextScopeListener)
+ {
+ _contextListeners.add((ContextScopeListener)listener);
+ if (__context.get() != null)
+ ((ContextScopeListener)listener).enterScope(__context.get(), null, "Listener registered");
+ }
+
+ if (listener instanceof ServletContextListener)
+ _servletContextListeners.add((ServletContextListener)listener);
+
+ if (listener instanceof ServletContextAttributeListener)
+ _servletContextAttributeListeners.add((ServletContextAttributeListener)listener);
+
+ if (listener instanceof ServletRequestListener)
+ _servletRequestListeners.add((ServletRequestListener)listener);
+
+ if (listener instanceof ServletRequestAttributeListener)
+ _servletRequestAttributeListeners.add((ServletRequestAttributeListener)listener);
+ }
+
+ /**
+ * Remove a context event listeners.
+ *
+ * @param listener the event listener to remove
+ * @see ServletContextListener
+ * @see ServletContextAttributeListener
+ * @see ServletRequestListener
+ * @see ServletRequestAttributeListener
+ */
+ public void removeEventListener(EventListener listener)
+ {
+ _eventListeners.remove(listener);
+
+ if (listener instanceof ContextScopeListener)
+ _contextListeners.remove(listener);
+
+ if (listener instanceof ServletContextListener)
+ {
+ _servletContextListeners.remove(listener);
+ _destroyServletContextListeners.remove(listener);
+ }
+
+ if (listener instanceof ServletContextAttributeListener)
+ _servletContextAttributeListeners.remove(listener);
+
+ if (listener instanceof ServletRequestListener)
+ _servletRequestListeners.remove(listener);
+
+ if (listener instanceof ServletRequestAttributeListener)
+ _servletRequestAttributeListeners.remove(listener);
+ }
+
+ /**
+ * Apply any necessary restrictions on a programmatic added listener.
+ *
+ * @param listener the programmatic listener to add
+ */
+ protected void addProgrammaticListener(EventListener listener)
+ {
+ _programmaticListeners.add(listener);
+ }
+
+ public boolean isProgrammaticListener(EventListener listener)
+ {
+ return _programmaticListeners.contains(listener);
+ }
+
+ public boolean isDurableListener(EventListener listener)
+ {
+ return _durableListeners.contains(listener);
+ }
+
+ /**
+ * @return true if this context is shutting down
+ */
+ @ManagedAttribute("true for graceful shutdown, which allows existing requests to complete")
+ public boolean isShutdown()
+ {
+ return _availability.get() == Availability.SHUTDOWN;
+ }
+
+ /**
+ * Set shutdown status. This field allows for graceful shutdown of a context. A started context may be put into non accepting state so that existing
+ * requests can complete, but no new requests are accepted.
+ */
+ @Override
+ public Future<Void> shutdown()
+ {
+ while (true)
+ {
+ Availability availability = _availability.get();
+ switch (availability)
+ {
+ case STOPPED:
+ return new FutureCallback(new IllegalStateException(getState()));
+ case STARTING:
+ case AVAILABLE:
+ case UNAVAILABLE:
+ if (!_availability.compareAndSet(availability, Availability.SHUTDOWN))
+ continue;
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ return new FutureCallback(true);
+ }
+
+ /**
+ * @return false if this context is unavailable (sends 503)
+ */
+ public boolean isAvailable()
+ {
+ return _availability.get() == Availability.AVAILABLE;
+ }
+
+ /**
+ * Set Available status.
+ *
+ * @param available true to set as enabled
+ */
+ public void setAvailable(boolean available)
+ {
+ // Only supported state transitions are:
+ // UNAVAILABLE --true---> AVAILABLE
+ // STARTING -----false--> UNAVAILABLE
+ // AVAILABLE ----false--> UNAVAILABLE
+ if (available)
+ {
+ while (true)
+ {
+ Availability availability = _availability.get();
+ switch (availability)
+ {
+ case AVAILABLE:
+ break;
+ case UNAVAILABLE:
+ if (!_availability.compareAndSet(availability, Availability.AVAILABLE))
+ continue;
+ break;
+ default:
+ throw new IllegalStateException(availability.toString());
+ }
+ break;
+ }
+ }
+ else
+ {
+ while (true)
+ {
+ Availability availability = _availability.get();
+ switch (availability)
+ {
+ case STARTING:
+ case AVAILABLE:
+ if (!_availability.compareAndSet(availability, Availability.UNAVAILABLE))
+ continue;
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ }
+ }
+
+ public Logger getLogger()
+ {
+ return _logger;
+ }
+
+ public void setLogger(Logger logger)
+ {
+ _logger = logger;
+ }
+
+ /*
+ * @see org.eclipse.thread.AbstractLifeCycle#doStart()
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ _availability.set(Availability.STARTING);
+
+ if (_contextPath == null)
+ throw new IllegalStateException("Null contextPath");
+
+ if (_logger == null)
+ _logger = Log.getLogger(ContextHandler.class.getName() + getLogNameSuffix());
+
+ ClassLoader oldClassloader = null;
+ Thread currentThread = null;
+ Context oldContext = null;
+
+ _attributes.setAttribute("org.eclipse.jetty.server.Executor", getServer().getThreadPool());
+
+ if (_mimeTypes == null)
+ _mimeTypes = new MimeTypes();
+
+ try
+ {
+ // Set the classloader, context and enter scope
+ if (_classLoader != null)
+ {
+ currentThread = Thread.currentThread();
+ oldClassloader = currentThread.getContextClassLoader();
+ currentThread.setContextClassLoader(_classLoader);
+ }
+ oldContext = __context.get();
+ __context.set(_scontext);
+ enterScope(null, getState());
+
+ // defers the calling of super.doStart()
+ startContext();
+
+ contextInitialized();
+
+ _availability.compareAndSet(Availability.STARTING, Availability.AVAILABLE);
+ LOG.info("Started {}", this);
+ }
+ finally
+ {
+ _availability.compareAndSet(Availability.STARTING, Availability.UNAVAILABLE);
+ exitScope(null);
+ __context.set(oldContext);
+ // reset the classloader
+ if (_classLoader != null && currentThread != null)
+ currentThread.setContextClassLoader(oldClassloader);
+ }
+ }
+
+ private String getLogNameSuffix()
+ {
+ // Use display name first
+ String logName = getDisplayName();
+ if (StringUtil.isBlank(logName))
+ {
+ // try context path
+ logName = getContextPath();
+ if (logName != null)
+ {
+ // Strip prefix slash
+ if (logName.startsWith("/"))
+ {
+ logName = logName.substring(1);
+ }
+ }
+
+ if (StringUtil.isBlank(logName))
+ {
+ // an empty context path is the ROOT context
+ logName = "ROOT";
+ }
+ }
+
+ // Replace bad characters.
+ return '.' + logName.replaceAll("\\W", "_");
+ }
+
+ /**
+ * Extensible startContext. this method is called from {@link ContextHandler#doStart()} instead of a call to super.doStart(). This allows derived classes to
+ * insert additional handling (Eg configuration) before the call to super.doStart by this method will start contained handlers.
+ *
+ * @throws Exception if unable to start the context
+ * @see org.eclipse.jetty.server.handler.ContextHandler.Context
+ */
+ protected void startContext() throws Exception
+ {
+ String managedAttributes = _initParams.get(MANAGED_ATTRIBUTES);
+ if (managedAttributes != null)
+ addEventListener(new ManagedAttributeListener(this, StringUtil.csvSplit(managedAttributes)));
+
+ super.doStart();
+ }
+
+ /**
+ * Call the ServletContextListeners contextInitialized methods.
+ * This can be called from a ServletHandler during the proper sequence
+ * of initializing filters, servlets and listeners. However, if there is
+ * no ServletHandler, the ContextHandler will call this method during
+ * doStart().
+ *
+ * @throws Exception
+ */
+ public void contextInitialized() throws Exception
+ {
+ // Call context listeners
+ switch (_contextStatus)
+ {
+ case NOTSET:
+ {
+ try
+ {
+ _destroyServletContextListeners.clear();
+ if (!_servletContextListeners.isEmpty())
+ {
+ ServletContextEvent event = new ServletContextEvent(_scontext);
+ for (ServletContextListener listener : _servletContextListeners)
+ {
+ callContextInitialized(listener, event);
+ _destroyServletContextListeners.add(listener);
+ }
+ }
+ }
+ finally
+ {
+ _contextStatus = ContextStatus.INITIALIZED;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Call the ServletContextListeners with contextDestroyed.
+ * This method can be called from a ServletHandler in the
+ * proper sequence of destroying filters, servlets and listeners.
+ * If there is no ServletHandler, the ContextHandler must ensure
+ * these listeners are called instead.
+ *
+ * @throws Exception
+ */
+ public void contextDestroyed() throws Exception
+ {
+ switch (_contextStatus)
+ {
+ case INITIALIZED:
+ {
+ try
+ {
+ //Call context listeners
+ MultiException ex = new MultiException();
+ ServletContextEvent event = new ServletContextEvent(_scontext);
+ Collections.reverse(_destroyServletContextListeners);
+ for (ServletContextListener listener : _destroyServletContextListeners)
+ {
+ try
+ {
+ callContextDestroyed(listener, event);
+ }
+ catch (Exception x)
+ {
+ ex.add(x);
+ }
+ }
+ ex.ifExceptionThrow();
+ }
+ finally
+ {
+ _contextStatus = ContextStatus.DESTROYED;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ protected void stopContext() throws Exception
+ {
+ // stop all the handler hierarchy
+ super.doStop();
+ }
+
+ protected void callContextInitialized(ServletContextListener l, ServletContextEvent e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("contextInitialized: {}->{}", e, l);
+ l.contextInitialized(e);
+ }
+
+ protected void callContextDestroyed(ServletContextListener l, ServletContextEvent e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("contextDestroyed: {}->{}", e, l);
+ l.contextDestroyed(e);
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ // Should we attempt a graceful shutdown?
+ MultiException mex = null;
+
+ if (getStopTimeout() > 0)
+ {
+ try
+ {
+ doShutdown(null);
+ }
+ catch (MultiException e)
+ {
+ mex = e;
+ }
+ }
+
+ _availability.set(Availability.STOPPED);
+
+ ClassLoader oldClassloader = null;
+ ClassLoader oldWebapploader = null;
+ Thread currentThread = null;
+ Context oldContext = __context.get();
+ enterScope(null, "doStop");
+ __context.set(_scontext);
+ try
+ {
+ // Set the classloader
+ if (_classLoader != null)
+ {
+ oldWebapploader = _classLoader;
+ currentThread = Thread.currentThread();
+ oldClassloader = currentThread.getContextClassLoader();
+ currentThread.setContextClassLoader(_classLoader);
+ }
+
+ stopContext();
+
+ contextDestroyed();
+
+ // retain only durable listeners
+ setEventListeners(_durableListeners.toArray(new EventListener[0]));
+ _durableListeners.clear();
+
+ if (_errorHandler != null)
+ _errorHandler.stop();
+
+ for (EventListener l : _programmaticListeners)
+ {
+ removeEventListener(l);
+ if (l instanceof ContextScopeListener)
+ {
+ try
+ {
+ ((ContextScopeListener)l).exitScope(_scontext, null);
+ }
+ catch (Throwable e)
+ {
+ LOG.warn(e);
+ }
+ }
+ }
+ _programmaticListeners.clear();
+ }
+ catch (Throwable x)
+ {
+ if (mex == null)
+ mex = new MultiException();
+ mex.add(x);
+ }
+ finally
+ {
+ _contextStatus = ContextStatus.NOTSET;
+ __context.set(oldContext);
+ exitScope(null);
+ LOG.info("Stopped {}", this);
+ // reset the classloader
+ if ((oldClassloader == null || (oldClassloader != oldWebapploader)) && currentThread != null)
+ currentThread.setContextClassLoader(oldClassloader);
+
+ _scontext.clearAttributes();
+ }
+
+ if (mex != null)
+ mex.ifExceptionThrow();
+ }
+
+ public boolean checkVirtualHost(final Request baseRequest)
+ {
+ if (_vhosts == null || _vhosts.length == 0)
+ return true;
+
+ String vhost = normalizeHostname(baseRequest.getServerName());
+ String connectorName = baseRequest.getHttpChannel().getConnector().getName();
+
+ for (int i = 0; i < _vhosts.length; i++)
+ {
+ String contextVhost = _vhosts[i];
+ String contextVConnector = _vconnectors[i];
+
+ if (contextVConnector != null)
+ {
+ if (!contextVConnector.equalsIgnoreCase(connectorName))
+ continue;
+
+ if (contextVhost == null)
+ {
+ return true;
+ }
+ }
+
+ if (contextVhost != null)
+ {
+ if (_vhostswildcard[i])
+ {
+ // wildcard only at the beginning, and only for one additional subdomain level
+ int index = vhost.indexOf(".");
+ if (index >= 0 && vhost.substring(index).equalsIgnoreCase(contextVhost))
+ {
+ return true;
+ }
+ }
+ else if (vhost.equalsIgnoreCase(contextVhost))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public boolean checkContextPath(String uri)
+ {
+ // Are we not the root context?
+ if (_contextPath.length() > 1)
+ {
+ // reject requests that are not for us
+ if (!uri.startsWith(_contextPath))
+ return false;
+ return uri.length() <= _contextPath.length() || uri.charAt(_contextPath.length()) == '/';
+ }
+ return true;
+ }
+
+ /*
+ * @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+ */
+ public boolean checkContext(final String target, final Request baseRequest, final HttpServletResponse response) throws IOException
+ {
+ DispatcherType dispatch = baseRequest.getDispatcherType();
+
+ // Check the vhosts
+ if (!checkVirtualHost(baseRequest))
+ return false;
+
+ if (!checkContextPath(target))
+ return false;
+
+ // Are we not the root context?
+ // redirect null path infos
+ if (!_allowNullPathInfo && _contextPath.length() == target.length() && _contextPath.length() > 1)
+ {
+ // context request must end with /
+ baseRequest.setHandled(true);
+ String queryString = baseRequest.getQueryString();
+ baseRequest.getResponse().sendRedirect(
+ HttpServletResponse.SC_MOVED_TEMPORARILY,
+ baseRequest.getRequestURI() + (queryString == null ? "/" : ("/?" + queryString)),
+ true);
+ return false;
+ }
+
+ switch (_availability.get())
+ {
+ case STOPPED:
+ return false;
+ case SHUTDOWN:
+ case UNAVAILABLE:
+ baseRequest.setHandled(true);
+ response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+ return false;
+ default:
+ if ((DispatcherType.REQUEST.equals(dispatch) && baseRequest.isHandled()))
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.handler.ScopedHandler#doScope(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest,
+ * javax.servlet.http.HttpServletResponse)
+ */
+ @Override
+ public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("scope {}|{}|{} @ {}", baseRequest.getContextPath(), baseRequest.getServletPath(), baseRequest.getPathInfo(), this);
+
+ Context oldContext;
+ String oldContextPath = null;
+ String oldServletPath = null;
+ String oldPathInfo = null;
+ ClassLoader oldClassloader = null;
+ Thread currentThread = null;
+ String pathInfo = target;
+
+ DispatcherType dispatch = baseRequest.getDispatcherType();
+
+ oldContext = baseRequest.getContext();
+
+ // Are we already in this context?
+ if (oldContext != _scontext)
+ {
+ // check the target.
+ if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch))
+ {
+ if (isCompactPath())
+ target = URIUtil.compactPath(target);
+ if (!checkContext(target, baseRequest, response))
+ return;
+
+ if (target.length() > _contextPath.length())
+ {
+ if (_contextPath.length() > 1)
+ target = target.substring(_contextPath.length());
+ pathInfo = target;
+ }
+ else if (_contextPath.length() == 1)
+ {
+ target = URIUtil.SLASH;
+ pathInfo = URIUtil.SLASH;
+ }
+ else
+ {
+ target = URIUtil.SLASH;
+ pathInfo = null;
+ }
+ }
+
+ // Set the classloader
+ if (_classLoader != null)
+ {
+ currentThread = Thread.currentThread();
+ oldClassloader = currentThread.getContextClassLoader();
+ currentThread.setContextClassLoader(_classLoader);
+ }
+ }
+
+ try
+ {
+ oldContextPath = baseRequest.getContextPath();
+ oldServletPath = baseRequest.getServletPath();
+ oldPathInfo = baseRequest.getPathInfo();
+
+ // Update the paths
+ baseRequest.setContext(_scontext);
+ __context.set(_scontext);
+ if (!DispatcherType.INCLUDE.equals(dispatch) && target.startsWith("/"))
+ {
+ baseRequest.setContextPath(getRequestContextPath());
+ baseRequest.setServletPath(null);
+ baseRequest.setPathInfo(pathInfo);
+ }
+
+ if (oldContext != _scontext)
+ enterScope(baseRequest, dispatch);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("context={}|{}|{} @ {}", baseRequest.getContextPath(), baseRequest.getServletPath(), baseRequest.getPathInfo(), this);
+
+ nextScope(target, baseRequest, request, response);
+ }
+ finally
+ {
+ if (oldContext != _scontext)
+ {
+ exitScope(baseRequest);
+
+ // reset the classloader
+ if (_classLoader != null && currentThread != null)
+ {
+ currentThread.setContextClassLoader(oldClassloader);
+ }
+
+ // reset the context and servlet path.
+ baseRequest.setContext(oldContext);
+ __context.set(oldContext);
+ baseRequest.setContextPath(oldContextPath);
+ baseRequest.setServletPath(oldServletPath);
+ baseRequest.setPathInfo(oldPathInfo);
+ }
+ }
+ }
+
+ protected void requestInitialized(Request baseRequest, HttpServletRequest request)
+ {
+ // Handle the REALLY SILLY request events!
+ if (!_servletRequestAttributeListeners.isEmpty())
+ for (ServletRequestAttributeListener l : _servletRequestAttributeListeners)
+ {
+ baseRequest.addEventListener(l);
+ }
+
+ if (!_servletRequestListeners.isEmpty())
+ {
+ final ServletRequestEvent sre = new ServletRequestEvent(_scontext, request);
+ for (ServletRequestListener l : _servletRequestListeners)
+ {
+ l.requestInitialized(sre);
+ }
+ }
+ }
+
+ protected void requestDestroyed(Request baseRequest, HttpServletRequest request)
+ {
+ // Handle more REALLY SILLY request events!
+ if (!_servletRequestListeners.isEmpty())
+ {
+ final ServletRequestEvent sre = new ServletRequestEvent(_scontext, request);
+ for (int i = _servletRequestListeners.size(); i-- > 0; )
+ {
+ _servletRequestListeners.get(i).requestDestroyed(sre);
+ }
+ }
+
+ if (!_servletRequestAttributeListeners.isEmpty())
+ {
+ for (int i = _servletRequestAttributeListeners.size(); i-- > 0; )
+ {
+ baseRequest.removeEventListener(_servletRequestAttributeListeners.get(i));
+ }
+ }
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.handler.ScopedHandler#doHandle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest,
+ * javax.servlet.http.HttpServletResponse)
+ */
+ @Override
+ public void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ final DispatcherType dispatch = baseRequest.getDispatcherType();
+ final boolean new_context = baseRequest.takeNewContext();
+ try
+ {
+ if (new_context)
+ requestInitialized(baseRequest, request);
+
+ if (dispatch == DispatcherType.REQUEST && isProtectedTarget(target))
+ {
+ baseRequest.setHandled(true);
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ nextHandle(target, baseRequest, request, response);
+ }
+ finally
+ {
+ if (new_context)
+ requestDestroyed(baseRequest, request);
+ }
+ }
+
+ /**
+ * @param request A request that is applicable to the scope, or null
+ * @param reason An object that indicates the reason the scope is being entered.
+ */
+ protected void enterScope(Request request, Object reason)
+ {
+ if (!_contextListeners.isEmpty())
+ {
+ for (ContextScopeListener listener : _contextListeners)
+ {
+ try
+ {
+ listener.enterScope(_scontext, request, reason);
+ }
+ catch (Throwable e)
+ {
+ LOG.warn(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param request A request that is applicable to the scope, or null
+ */
+ protected void exitScope(Request request)
+ {
+ if (!_contextListeners.isEmpty())
+ {
+ for (int i = _contextListeners.size(); i-- > 0; )
+ {
+ try
+ {
+ _contextListeners.get(i).exitScope(_scontext, request);
+ }
+ catch (Throwable e)
+ {
+ LOG.warn(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Handle a runnable in the scope of this context and a particular request
+ *
+ * @param request The request to scope the thread to (may be null if no particular request is in scope)
+ * @param runnable The runnable to run.
+ */
+ public void handle(Request request, Runnable runnable)
+ {
+ ClassLoader oldClassloader = null;
+ Thread currentThread = null;
+ Context oldContext = __context.get();
+
+ // Are we already in the scope?
+ if (oldContext == _scontext)
+ {
+ runnable.run();
+ return;
+ }
+
+ // Nope, so enter the scope and then exit
+ try
+ {
+ __context.set(_scontext);
+
+ // Set the classloader
+ if (_classLoader != null)
+ {
+ currentThread = Thread.currentThread();
+ oldClassloader = currentThread.getContextClassLoader();
+ currentThread.setContextClassLoader(_classLoader);
+ }
+
+ enterScope(request, runnable);
+ runnable.run();
+ }
+ finally
+ {
+ exitScope(request);
+
+ __context.set(oldContext);
+ if (oldClassloader != null)
+ {
+ currentThread.setContextClassLoader(oldClassloader);
+ }
+ }
+ }
+
+ /*
+ * Handle a runnable in the scope of this context
+ */
+ public void handle(Runnable runnable)
+ {
+ handle(null, runnable);
+ }
+
+ /**
+ * Check the target. Called by {@link #handle(String, Request, HttpServletRequest, HttpServletResponse)} when a target within a context is determined. If
+ * the target is protected, 404 is returned.
+ *
+ * @param target the target to test
+ * @return true if target is a protected target
+ */
+ public boolean isProtectedTarget(String target)
+ {
+ if (target == null || _protectedTargets == null)
+ return false;
+
+ if (target.startsWith("//"))
+ target = URIUtil.compactPath(target);
+
+ for (String t : _protectedTargets)
+ {
+ if (StringUtil.startsWithIgnoreCase(target, t))
+ {
+ if (target.length() == t.length())
+ return true;
+
+ // Check that the target prefix really is a path segment, thus
+ // it can end with /, a query, a target or a parameter
+ char c = target.charAt(t.length());
+ if (c == '/' || c == '?' || c == '#' || c == ';')
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param targets Array of URL prefix. Each prefix is in the form /path and will match either /path exactly or /path/anything
+ */
+ public void setProtectedTargets(String[] targets)
+ {
+ if (targets == null)
+ {
+ _protectedTargets = null;
+ return;
+ }
+
+ _protectedTargets = Arrays.copyOf(targets, targets.length);
+ }
+
+ public String[] getProtectedTargets()
+ {
+ if (_protectedTargets == null)
+ return null;
+
+ return Arrays.copyOf(_protectedTargets, _protectedTargets.length);
+ }
+
+ /*
+ * @see javax.servlet.ServletContext#removeAttribute(java.lang.String)
+ */
+ @Override
+ public void removeAttribute(String name)
+ {
+ _attributes.removeAttribute(name);
+ }
+
+ /*
+ * Set a context attribute. Attributes set via this API cannot be overridden by the ServletContext.setAttribute API. Their lifecycle spans the stop/start of
+ * a context. No attribute listener events are triggered by this API.
+ *
+ * @see javax.servlet.ServletContext#setAttribute(java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void setAttribute(String name, Object value)
+ {
+ _attributes.setAttribute(name, value);
+ }
+
+ /**
+ * @param attributes The attributes to set.
+ */
+ public void setAttributes(Attributes attributes)
+ {
+ _attributes.clearAttributes();
+ _attributes.addAll(attributes);
+ }
+
+ @Override
+ public void clearAttributes()
+ {
+ _attributes.clearAttributes();
+ }
+
+ @Deprecated
+ public void setManagedAttribute(String name, Object value)
+ {
+ }
+
+ /**
+ * @param classLoader The classLoader to set.
+ */
+ public void setClassLoader(ClassLoader classLoader)
+ {
+ if (isStarted())
+ throw new IllegalStateException(getState());
+ _classLoader = classLoader;
+ }
+
+ /**
+ * @param contextPath The _contextPath to set.
+ */
+ public void setContextPath(String contextPath)
+ {
+ if (contextPath == null)
+ throw new IllegalArgumentException("null contextPath");
+
+ if (contextPath.endsWith("/*"))
+ {
+ LOG.warn(this + " contextPath ends with /*");
+ contextPath = contextPath.substring(0, contextPath.length() - 2);
+ }
+ else if (contextPath.length() > 1 && contextPath.endsWith("/"))
+ {
+ LOG.warn(this + " contextPath ends with /");
+ contextPath = contextPath.substring(0, contextPath.length() - 1);
+ }
+
+ if (contextPath.length() == 0)
+ {
+ LOG.warn("Empty contextPath");
+ contextPath = "/";
+ }
+
+ _contextPath = contextPath;
+ _contextPathEncoded = URIUtil.encodePath(contextPath);
+
+ if (getServer() != null && (getServer().isStarting() || getServer().isStarted()))
+ {
+ Class<ContextHandlerCollection> handlerClass = ContextHandlerCollection.class;
+ Handler[] contextCollections = getServer().getChildHandlersByClass(handlerClass);
+ if (contextCollections != null)
+ {
+ for (Handler contextCollection : contextCollections)
+ {
+ handlerClass.cast(contextCollection).mapContexts();
+ }
+ }
+ }
+ }
+
+ /**
+ * @param servletContextName The servletContextName to set.
+ */
+ public void setDisplayName(String servletContextName)
+ {
+ _displayName = servletContextName;
+ }
+
+ /**
+ * @return Returns the resourceBase.
+ */
+ public Resource getBaseResource()
+ {
+ if (_baseResource == null)
+ return null;
+ return _baseResource;
+ }
+
+ /**
+ * @return Returns the base resource as a string.
+ */
+ @ManagedAttribute("document root for context")
+ public String getResourceBase()
+ {
+ if (_baseResource == null)
+ return null;
+ return _baseResource.toString();
+ }
+
+ /**
+ * Set the base resource for this context.
+ *
+ * @param base The resource used as the base for all static content of this context.
+ * @see #setResourceBase(String)
+ */
+ public void setBaseResource(Resource base)
+ {
+ _baseResource = base;
+ }
+
+ /**
+ * Set the base resource for this context.
+ *
+ * @param resourceBase A string representing the base resource for the context. Any string accepted by {@link Resource#newResource(String)} may be passed and the
+ * call is equivalent to <code>setBaseResource(newResource(resourceBase));</code>
+ */
+ public void setResourceBase(String resourceBase)
+ {
+ try
+ {
+ setBaseResource(newResource(resourceBase));
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ throw new IllegalArgumentException(resourceBase);
+ }
+ }
+
+ /**
+ * @return Returns the mimeTypes.
+ */
+ public MimeTypes getMimeTypes()
+ {
+ if (_mimeTypes == null)
+ _mimeTypes = new MimeTypes();
+ return _mimeTypes;
+ }
+
+ /**
+ * @param mimeTypes The mimeTypes to set.
+ */
+ public void setMimeTypes(MimeTypes mimeTypes)
+ {
+ _mimeTypes = mimeTypes;
+ }
+
+ public void setWelcomeFiles(String[] files)
+ {
+ _welcomeFiles = files;
+ }
+
+ /**
+ * @return The names of the files which the server should consider to be welcome files in this context.
+ * @see <a href="http://jcp.org/aboutJava/communityprocess/final/jsr154/index.html">The Servlet Specification</a>
+ * @see #setWelcomeFiles
+ */
+ @ManagedAttribute(value = "Partial URIs of directory welcome files", readonly = true)
+ public String[] getWelcomeFiles()
+ {
+ return _welcomeFiles;
+ }
+
+ /**
+ * @return Returns the errorHandler.
+ */
+ @ManagedAttribute("The error handler to use for the context")
+ public ErrorHandler getErrorHandler()
+ {
+ return _errorHandler;
+ }
+
+ /**
+ * @param errorHandler The errorHandler to set.
+ */
+ public void setErrorHandler(ErrorHandler errorHandler)
+ {
+ if (errorHandler != null)
+ errorHandler.setServer(getServer());
+ updateBean(_errorHandler, errorHandler, true);
+ _errorHandler = errorHandler;
+ }
+
+ @ManagedAttribute("The maximum content size")
+ public int getMaxFormContentSize()
+ {
+ return _maxFormContentSize;
+ }
+
+ /**
+ * Set the maximum size of a form post, to protect against DOS attacks from large forms.
+ *
+ * @param maxSize the maximum size of the form content (in bytes)
+ */
+ public void setMaxFormContentSize(int maxSize)
+ {
+ _maxFormContentSize = maxSize;
+ }
+
+ public int getMaxFormKeys()
+ {
+ return _maxFormKeys;
+ }
+
+ /**
+ * Set the maximum number of form Keys to protect against DOS attack from crafted hash keys.
+ *
+ * @param max the maximum number of form keys
+ */
+ public void setMaxFormKeys(int max)
+ {
+ _maxFormKeys = max;
+ }
+
+ /**
+ * @return True if URLs are compacted to replace multiple '/'s with a single '/'
+ */
+ public boolean isCompactPath()
+ {
+ return _compactPath;
+ }
+
+ /**
+ * @param compactPath True if URLs are compacted to replace multiple '/'s with a single '/'
+ */
+ public void setCompactPath(boolean compactPath)
+ {
+ _compactPath = compactPath;
+ }
+
+ @Override
+ public String toString()
+ {
+ String[] vhosts = getVirtualHosts();
+
+ StringBuilder b = new StringBuilder();
+
+ Package pkg = getClass().getPackage();
+ if (pkg != null)
+ {
+ String p = pkg.getName();
+ if (p != null && p.length() > 0)
+ {
+ String[] ss = p.split("\\.");
+ for (String s : ss)
+ {
+ b.append(s.charAt(0)).append('.');
+ }
+ }
+ }
+ b.append(getClass().getSimpleName()).append('@').append(Integer.toString(hashCode(), 16));
+ b.append('{');
+ if (getDisplayName() != null)
+ b.append(getDisplayName()).append(',');
+ b.append(getContextPath()).append(',').append(getBaseResource()).append(',').append(_availability.get());
+
+ if (vhosts != null && vhosts.length > 0)
+ b.append(',').append(vhosts[0]);
+ b.append('}');
+
+ return b.toString();
+ }
+
+ public Class<?> loadClass(String className) throws ClassNotFoundException
+ {
+ if (className == null)
+ return null;
+
+ if (_classLoader == null)
+ return Loader.loadClass(className);
+
+ return _classLoader.loadClass(className);
+ }
+
+ public void addLocaleEncoding(String locale, String encoding)
+ {
+ if (_localeEncodingMap == null)
+ _localeEncodingMap = new HashMap<>();
+ _localeEncodingMap.put(locale, encoding);
+ }
+
+ public String getLocaleEncoding(String locale)
+ {
+ if (_localeEncodingMap == null)
+ return null;
+ String encoding = _localeEncodingMap.get(locale);
+ return encoding;
+ }
+
+ /**
+ * Get the character encoding for a locale. The full locale name is first looked up in the map of encodings. If no encoding is found, then the locale
+ * language is looked up.
+ *
+ * @param locale a <code>Locale</code> value
+ * @return a <code>String</code> representing the character encoding for the locale or null if none found.
+ */
+ public String getLocaleEncoding(Locale locale)
+ {
+ if (_localeEncodingMap == null)
+ return null;
+ String encoding = _localeEncodingMap.get(locale.toString());
+ if (encoding == null)
+ encoding = _localeEncodingMap.get(locale.getLanguage());
+ return encoding;
+ }
+
+ /**
+ * Get all of the locale encodings
+ *
+ * @return a map of all the locale encodings: key is name of the locale and value is the char encoding
+ */
+ public Map<String, String> getLocaleEncodings()
+ {
+ if (_localeEncodingMap == null)
+ return null;
+ return Collections.unmodifiableMap(_localeEncodingMap);
+ }
+
+ public Resource getResource(String path) throws MalformedURLException
+ {
+ if (path == null || !path.startsWith(URIUtil.SLASH))
+ throw new MalformedURLException(path);
+
+ if (_baseResource == null)
+ return null;
+
+ try
+ {
+ // addPath with accept non-canonical paths that don't go above the root,
+ // but will treat them as aliases. So unless allowed by an AliasChecker
+ // they will be rejected below.
+ Resource resource = _baseResource.addPath(path);
+
+ if (checkAlias(path, resource))
+ return resource;
+ return null;
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+
+ return null;
+ }
+
+ /**
+ * @param path the path to check the alias for
+ * @param resource the resource
+ * @return True if the alias is OK
+ */
+ public boolean checkAlias(String path, Resource resource)
+ {
+ // Is the resource aliased?
+ if (resource.isAlias())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Aliased resource: " + resource + "~=" + resource.getAlias());
+
+ // alias checks
+ for (AliasCheck check : getAliasChecks())
+ {
+ if (check.check(path, resource))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Aliased resource: " + resource + " approved by " + check);
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Convert URL to Resource wrapper for {@link Resource#newResource(URL)} enables extensions to provide alternate resource implementations.
+ *
+ * @param url the url to convert to a Resource
+ * @return the Resource for that url
+ * @throws IOException if unable to create a Resource from the URL
+ */
+ public Resource newResource(URL url) throws IOException
+ {
+ return Resource.newResource(url);
+ }
+
+ /**
+ * Convert URL to Resource wrapper for {@link Resource#newResource(URL)} enables extensions to provide alternate resource implementations.
+ *
+ * @param uri the URI to convert to a Resource
+ * @return the Resource for that URI
+ * @throws IOException if unable to create a Resource from the URL
+ */
+ public Resource newResource(URI uri) throws IOException
+ {
+ return Resource.newResource(uri);
+ }
+
+ /**
+ * Convert a URL or path to a Resource. The default implementation is a wrapper for {@link Resource#newResource(String)}.
+ *
+ * @param urlOrPath The URL or path to convert
+ * @return The Resource for the URL/path
+ * @throws IOException The Resource could not be created.
+ */
+ public Resource newResource(String urlOrPath) throws IOException
+ {
+ return Resource.newResource(urlOrPath);
+ }
+
+ public Set<String> getResourcePaths(String path)
+ {
+ try
+ {
+ Resource resource = getResource(path);
+
+ if (resource != null && resource.exists())
+ {
+ if (!path.endsWith(URIUtil.SLASH))
+ path = path + URIUtil.SLASH;
+
+ String[] l = resource.list();
+ if (l != null)
+ {
+ HashSet<String> set = new HashSet<>();
+ for (int i = 0; i < l.length; i++)
+ {
+ set.add(path + l[i]);
+ }
+ return set;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+ return Collections.emptySet();
+ }
+
+ private String normalizeHostname(String host)
+ {
+ if (host == null)
+ return null;
+ int connectorIndex = host.indexOf('@');
+ String connector = null;
+ if (connectorIndex > 0)
+ {
+ host = host.substring(0, connectorIndex);
+ connector = host.substring(connectorIndex);
+ }
+
+ if (host.endsWith("."))
+ host = host.substring(0, host.length() - 1);
+ if (connector != null)
+ host += connector;
+
+ return host;
+ }
+
+ /**
+ * Add an AliasCheck instance to possibly permit aliased resources
+ *
+ * @param check The alias checker
+ */
+ public void addAliasCheck(AliasCheck check)
+ {
+ getAliasChecks().add(check);
+ }
+
+ /**
+ * @return Mutable list of Alias checks
+ */
+ public List<AliasCheck> getAliasChecks()
+ {
+ return _aliasChecks;
+ }
+
+ /**
+ * @param checks list of AliasCheck instances
+ */
+ public void setAliasChecks(List<AliasCheck> checks)
+ {
+ getAliasChecks().clear();
+ getAliasChecks().addAll(checks);
+ }
+
+ /**
+ * clear the list of AliasChecks
+ */
+ public void clearAliasChecks()
+ {
+ getAliasChecks().clear();
+ }
+
+ /**
+ * Context.
+ * <p>
+ * A partial implementation of {@link javax.servlet.ServletContext}. A complete implementation is provided by the
+ * derived {@link ContextHandler} implementations.
+ * </p>
+ */
+ public class Context extends StaticContext
+ {
+ protected boolean _enabled = true; // whether or not the dynamic API is enabled for callers
+ protected boolean _extendedListenerTypes = false;
+
+ protected Context()
+ {
+ }
+
+ public ContextHandler getContextHandler()
+ {
+ return ContextHandler.this;
+ }
+
+ @Override
+ public ServletContext getContext(String uripath)
+ {
+ List<ContextHandler> contexts = new ArrayList<>();
+ Handler[] handlers = getServer().getChildHandlersByClass(ContextHandler.class);
+ String matchedPath = null;
+
+ for (Handler handler : handlers)
+ {
+ if (handler == null)
+ continue;
+ ContextHandler ch = (ContextHandler)handler;
+ String contextPath = ch.getContextPath();
+
+ if (uripath.equals(contextPath) ||
+ (uripath.startsWith(contextPath) && uripath.charAt(contextPath.length()) == '/') ||
+ "/".equals(contextPath))
+ {
+ // look first for vhost matching context only
+ if (getVirtualHosts() != null && getVirtualHosts().length > 0)
+ {
+ if (ch.getVirtualHosts() != null && ch.getVirtualHosts().length > 0)
+ {
+ for (String h1 : getVirtualHosts())
+ {
+ for (String h2 : ch.getVirtualHosts())
+ {
+ if (h1.equals(h2))
+ {
+ if (matchedPath == null || contextPath.length() > matchedPath.length())
+ {
+ contexts.clear();
+ matchedPath = contextPath;
+ }
+
+ if (matchedPath.equals(contextPath))
+ contexts.add(ch);
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ if (matchedPath == null || contextPath.length() > matchedPath.length())
+ {
+ contexts.clear();
+ matchedPath = contextPath;
+ }
+
+ if (matchedPath.equals(contextPath))
+ contexts.add(ch);
+ }
+ }
+ }
+
+ if (contexts.size() > 0)
+ return contexts.get(0)._scontext;
+
+ // try again ignoring virtual hosts
+ matchedPath = null;
+ for (Handler handler : handlers)
+ {
+ if (handler == null)
+ continue;
+ ContextHandler ch = (ContextHandler)handler;
+ String contextPath = ch.getContextPath();
+
+ if (uripath.equals(contextPath) ||
+ (uripath.startsWith(contextPath) && uripath.charAt(contextPath.length()) == '/') ||
+ "/".equals(contextPath))
+ {
+ if (matchedPath == null || contextPath.length() > matchedPath.length())
+ {
+ contexts.clear();
+ matchedPath = contextPath;
+ }
+
+ if (matchedPath.equals(contextPath))
+ contexts.add(ch);
+ }
+ }
+
+ if (contexts.size() > 0)
+ return contexts.get(0)._scontext;
+ return null;
+ }
+
+ @Override
+ public String getMimeType(String file)
+ {
+ if (_mimeTypes == null)
+ return null;
+ return _mimeTypes.getMimeByExtension(file);
+ }
+
+ @Override
+ public RequestDispatcher getRequestDispatcher(String uriInContext)
+ {
+ // uriInContext is encoded, potentially with query.
+ if (uriInContext == null)
+ return null;
+
+ if (!uriInContext.startsWith("/"))
+ return null;
+
+ try
+ {
+ // The uriInContext will be canonicalized by HttpURI.
+ HttpURI uri = new HttpURI(null, null, 0, uriInContext);
+ String pathInfo = uri.getDecodedPath();
+ String contextPath = getContextPath();
+ if (contextPath != null && contextPath.length() > 0)
+ uri.setPath(URIUtil.addPaths(contextPath, uri.getPath()));
+
+ return new Dispatcher(ContextHandler.this, uri, pathInfo);
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+ return null;
+ }
+
+ @Override
+ public String getRealPath(String path)
+ {
+ // This is an API call from the application which may have arbitrary non canonical paths passed
+ // Thus we canonicalize here, to avoid the enforcement of only canonical paths in
+ // ContextHandler.this.getResource(path).
+ path = URIUtil.canonicalPath(path);
+ if (path == null)
+ return null;
+ if (path.length() == 0)
+ path = URIUtil.SLASH;
+ else if (path.charAt(0) != '/')
+ path = URIUtil.SLASH + path;
+
+ try
+ {
+ Resource resource = ContextHandler.this.getResource(path);
+ if (resource != null)
+ {
+ File file = resource.getFile();
+ if (file != null)
+ return file.getCanonicalPath();
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+
+ return null;
+ }
+
+ @Override
+ public URL getResource(String path) throws MalformedURLException
+ {
+ // This is an API call from the application which may have arbitrary non canonical paths passed
+ // Thus we canonicalize here, to avoid the enforcement of only canonical paths in
+ // ContextHandler.this.getResource(path).
+ path = URIUtil.canonicalPath(path);
+ if (path == null)
+ return null;
+ Resource resource = ContextHandler.this.getResource(path);
+ if (resource != null && resource.exists())
+ return resource.getURI().toURL();
+ return null;
+ }
+
+ @Override
+ public InputStream getResourceAsStream(String path)
+ {
+ try
+ {
+ URL url = getResource(path);
+ if (url == null)
+ return null;
+ Resource r = Resource.newResource(url);
+ // Cannot serve directories as an InputStream
+ if (r.isDirectory())
+ return null;
+ return r.getInputStream();
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ return null;
+ }
+ }
+
+ @Override
+ public Set<String> getResourcePaths(String path)
+ {
+ // This is an API call from the application which may have arbitrary non canonical paths passed
+ // Thus we canonicalize here, to avoid the enforcement of only canonical paths in
+ // ContextHandler.this.getResource(path).
+ path = URIUtil.canonicalPath(path);
+ if (path == null)
+ return null;
+ return ContextHandler.this.getResourcePaths(path);
+ }
+
+ @Override
+ public void log(Exception exception, String msg)
+ {
+ _logger.warn(msg, exception);
+ }
+
+ @Override
+ public void log(String msg)
+ {
+ _logger.info(msg);
+ }
+
+ @Override
+ public void log(String message, Throwable throwable)
+ {
+ _logger.warn(message, throwable);
+ }
+
+ @Override
+ public String getInitParameter(String name)
+ {
+ return ContextHandler.this.getInitParameter(name);
+ }
+
+ @Override
+ public Enumeration<String> getInitParameterNames()
+ {
+ return ContextHandler.this.getInitParameterNames();
+ }
+
+ @Override
+ public Object getAttribute(String name)
+ {
+ Object o = ContextHandler.this.getAttribute(name);
+ if (o == null)
+ o = super.getAttribute(name);
+ return o;
+ }
+
+ @Override
+ public Enumeration<String> getAttributeNames()
+ {
+ HashSet<String> set = new HashSet<>();
+ Enumeration<String> e = super.getAttributeNames();
+ while (e.hasMoreElements())
+ {
+ set.add(e.nextElement());
+ }
+ e = _attributes.getAttributeNames();
+ while (e.hasMoreElements())
+ {
+ set.add(e.nextElement());
+ }
+
+ return Collections.enumeration(set);
+ }
+
+ @Override
+ public void setAttribute(String name, Object value)
+ {
+ Object oldValue = super.getAttribute(name);
+
+ if (value == null)
+ super.removeAttribute(name);
+ else
+ super.setAttribute(name, value);
+
+ if (!_servletContextAttributeListeners.isEmpty())
+ {
+ ServletContextAttributeEvent event = new ServletContextAttributeEvent(_scontext, name, oldValue == null ? value : oldValue);
+
+ for (ServletContextAttributeListener l : _servletContextAttributeListeners)
+ {
+ if (oldValue == null)
+ l.attributeAdded(event);
+ else if (value == null)
+ l.attributeRemoved(event);
+ else
+ l.attributeReplaced(event);
+ }
+ }
+ }
+
+ @Override
+ public void removeAttribute(String name)
+ {
+ Object oldValue = super.getAttribute(name);
+ super.removeAttribute(name);
+ if (oldValue != null && !_servletContextAttributeListeners.isEmpty())
+ {
+ ServletContextAttributeEvent event = new ServletContextAttributeEvent(_scontext, name, oldValue);
+
+ for (ServletContextAttributeListener l : _servletContextAttributeListeners)
+ {
+ l.attributeRemoved(event);
+ }
+ }
+ }
+
+ @Override
+ public String getServletContextName()
+ {
+ String name = ContextHandler.this.getDisplayName();
+ if (name == null)
+ name = ContextHandler.this.getContextPath();
+ return name;
+ }
+
+ @Override
+ public String getContextPath()
+ {
+ return getRequestContextPath();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ServletContext@" + ContextHandler.this.toString();
+ }
+
+ @Override
+ public boolean setInitParameter(String name, String value)
+ {
+ if (ContextHandler.this.getInitParameter(name) != null)
+ return false;
+ ContextHandler.this.getInitParams().put(name, value);
+ return true;
+ }
+
+ @Override
+ public void addListener(String className)
+ {
+ if (!_enabled)
+ throw new UnsupportedOperationException();
+
+ try
+ {
+ @SuppressWarnings(
+ {"unchecked", "rawtypes"})
+ Class<? extends EventListener> clazz = _classLoader == null ? Loader.loadClass(className) : (Class<? extends EventListener>)_classLoader.loadClass(className);
+ addListener(clazz);
+ }
+ catch (ClassNotFoundException e)
+ {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ public <T extends EventListener> void addListener(T t)
+ {
+ if (!_enabled)
+ throw new UnsupportedOperationException();
+
+ checkListener(t.getClass());
+
+ ContextHandler.this.addEventListener(t);
+ ContextHandler.this.addProgrammaticListener(t);
+ }
+
+ @Override
+ public void addListener(Class<? extends EventListener> listenerClass)
+ {
+ if (!_enabled)
+ throw new UnsupportedOperationException();
+
+ try
+ {
+ EventListener e = createListener(listenerClass);
+ addListener(e);
+ }
+ catch (ServletException e)
+ {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public void checkListener(Class<? extends EventListener> listener) throws IllegalStateException
+ {
+ boolean ok = false;
+ int startIndex = (isExtendedListenerTypes() ? EXTENDED_LISTENER_TYPE_INDEX : DEFAULT_LISTENER_TYPE_INDEX);
+ for (int i = startIndex; i < SERVLET_LISTENER_TYPES.length; i++)
+ {
+ if (SERVLET_LISTENER_TYPES[i].isAssignableFrom(listener))
+ {
+ ok = true;
+ break;
+ }
+ }
+ if (!ok)
+ throw new IllegalArgumentException("Inappropriate listener class " + listener.getName());
+ }
+
+ public void setExtendedListenerTypes(boolean extended)
+ {
+ _extendedListenerTypes = extended;
+ }
+
+ public boolean isExtendedListenerTypes()
+ {
+ return _extendedListenerTypes;
+ }
+
+ @Override
+ public ClassLoader getClassLoader()
+ {
+ if (!_enabled)
+ throw new UnsupportedOperationException();
+
+ // no security manager just return the classloader
+ if (!isUsingSecurityManager())
+ {
+ return _classLoader;
+ }
+ else
+ {
+ // check to see if the classloader of the caller is the same as the context
+ // classloader, or a parent of it, as required by the javadoc specification.
+
+ // Wrap in a PrivilegedAction so that only Jetty code will require the
+ // "createSecurityManager" permission, not also application code that calls this method.
+ Caller caller = AccessController.doPrivileged((PrivilegedAction<Caller>)Caller::new);
+ ClassLoader callerLoader = caller.getCallerClassLoader(2);
+ while (callerLoader != null)
+ {
+ if (callerLoader == _classLoader)
+ return _classLoader;
+ else
+ callerLoader = callerLoader.getParent();
+ }
+ System.getSecurityManager().checkPermission(new RuntimePermission("getClassLoader"));
+ return _classLoader;
+ }
+ }
+
+ @Override
+ public JspConfigDescriptor getJspConfigDescriptor()
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getJspConfigDescriptor()");
+ return null;
+ }
+
+ public void setJspConfigDescriptor(JspConfigDescriptor d)
+ {
+
+ }
+
+ @Override
+ public void declareRoles(String... roleNames)
+ {
+ if (!isStarting())
+ throw new IllegalStateException();
+ if (!_enabled)
+ throw new UnsupportedOperationException();
+ }
+
+ public void setEnabled(boolean enabled)
+ {
+ _enabled = enabled;
+ }
+
+ public boolean isEnabled()
+ {
+ return _enabled;
+ }
+
+ @Override
+ public String getVirtualServerName()
+ {
+ String[] hosts = getVirtualHosts();
+ if (hosts != null && hosts.length > 0)
+ return hosts[0];
+ return null;
+ }
+ }
+
+ /**
+ * A simple implementation of ServletContext that is used when there is no
+ * ContextHandler. This is also used as the base for all other ServletContext
+ * implementations.
+ */
+ public static class StaticContext extends AttributesMap implements ServletContext
+ {
+ private int _effectiveMajorVersion = SERVLET_MAJOR_VERSION;
+ private int _effectiveMinorVersion = SERVLET_MINOR_VERSION;
+
+ @Override
+ public ServletContext getContext(String uripath)
+ {
+ return null;
+ }
+
+ @Override
+ public int getMajorVersion()
+ {
+ return SERVLET_MAJOR_VERSION;
+ }
+
+ @Override
+ public String getMimeType(String file)
+ {
+ return null;
+ }
+
+ @Override
+ public int getMinorVersion()
+ {
+ return SERVLET_MINOR_VERSION;
+ }
+
+ @Override
+ public RequestDispatcher getNamedDispatcher(String name)
+ {
+ return null;
+ }
+
+ @Override
+ public RequestDispatcher getRequestDispatcher(String uriInContext)
+ {
+ return null;
+ }
+
+ @Override
+ public String getRealPath(String path)
+ {
+ return null;
+ }
+
+ @Override
+ public URL getResource(String path) throws MalformedURLException
+ {
+ return null;
+ }
+
+ @Override
+ public InputStream getResourceAsStream(String path)
+ {
+ return null;
+ }
+
+ @Override
+ public Set<String> getResourcePaths(String path)
+ {
+ return null;
+ }
+
+ @Override
+ public String getServerInfo()
+ {
+ return ContextHandler.getServerInfo();
+ }
+
+ @Override
+ @Deprecated
+ public Servlet getServlet(String name) throws ServletException
+ {
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ @Deprecated
+ public Enumeration<String> getServletNames()
+ {
+ return Collections.enumeration(Collections.EMPTY_LIST);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ @Deprecated
+ public Enumeration<Servlet> getServlets()
+ {
+ return Collections.enumeration(Collections.EMPTY_LIST);
+ }
+
+ @Override
+ public void log(Exception exception, String msg)
+ {
+ LOG.warn(msg, exception);
+ }
+
+ @Override
+ public void log(String msg)
+ {
+ LOG.info(msg);
+ }
+
+ @Override
+ public void log(String message, Throwable throwable)
+ {
+ LOG.warn(message, throwable);
+ }
+
+ @Override
+ public String getInitParameter(String name)
+ {
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Enumeration<String> getInitParameterNames()
+ {
+ return Collections.enumeration(Collections.EMPTY_LIST);
+ }
+
+ @Override
+ public String getServletContextName()
+ {
+ return "No Context";
+ }
+
+ @Override
+ public String getContextPath()
+ {
+ return null;
+ }
+
+ @Override
+ public boolean setInitParameter(String name, String value)
+ {
+ return false;
+ }
+
+ @Override
+ public Dynamic addFilter(String filterName, Class<? extends Filter> filterClass)
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addFilter(String, Class)");
+ return null;
+ }
+
+ @Override
+ public Dynamic addFilter(String filterName, Filter filter)
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addFilter(String, Filter)");
+ return null;
+ }
+
+ @Override
+ public Dynamic addFilter(String filterName, String className)
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addFilter(String, String)");
+ return null;
+ }
+
+ @Override
+ public javax.servlet.ServletRegistration.Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass)
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addServlet(String, Class)");
+ return null;
+ }
+
+ @Override
+ public javax.servlet.ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addServlet(String, Servlet)");
+ return null;
+ }
+
+ @Override
+ public javax.servlet.ServletRegistration.Dynamic addServlet(String servletName, String className)
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addServlet(String, String)");
+ return null;
+ }
+
+ @Override
+ public Set<SessionTrackingMode> getDefaultSessionTrackingModes()
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getDefaultSessionTrackingModes()");
+ return null;
+ }
+
+ @Override
+ public Set<SessionTrackingMode> getEffectiveSessionTrackingModes()
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getEffectiveSessionTrackingModes()");
+ return null;
+ }
+
+ @Override
+ public FilterRegistration getFilterRegistration(String filterName)
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getFilterRegistration(String)");
+ return null;
+ }
+
+ @Override
+ public Map<String, ? extends FilterRegistration> getFilterRegistrations()
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getFilterRegistrations()");
+ return null;
+ }
+
+ @Override
+ public ServletRegistration getServletRegistration(String servletName)
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getServletRegistration(String)");
+ return null;
+ }
+
+ @Override
+ public Map<String, ? extends ServletRegistration> getServletRegistrations()
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getServletRegistrations()");
+ return null;
+ }
+
+ @Override
+ public SessionCookieConfig getSessionCookieConfig()
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getSessionCookieConfig()");
+ return null;
+ }
+
+ @Override
+ public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes)
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "setSessionTrackingModes(Set<SessionTrackingMode>)");
+ }
+
+ @Override
+ public void addListener(String className)
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addListener(String)");
+ }
+
+ @Override
+ public <T extends EventListener> void addListener(T t)
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addListener(T)");
+ }
+
+ @Override
+ public void addListener(Class<? extends EventListener> listenerClass)
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addListener(Class)");
+ }
+
+ protected <T> T createInstance(Class<T> clazz) throws ServletException
+ {
+ try
+ {
+ return clazz.getDeclaredConstructor().newInstance();
+ }
+ catch (Exception e)
+ {
+ throw new ServletException(e);
+ }
+ }
+
+ @Override
+ public <T extends EventListener> T createListener(Class<T> clazz) throws ServletException
+ {
+ return createInstance(clazz);
+ }
+
+ @Override
+ public <T extends Servlet> T createServlet(Class<T> clazz) throws ServletException
+ {
+ return createInstance(clazz);
+ }
+
+ @Override
+ public <T extends Filter> T createFilter(Class<T> clazz) throws ServletException
+ {
+ return createInstance(clazz);
+ }
+
+ @Override
+ public ClassLoader getClassLoader()
+ {
+ return ContextHandler.class.getClassLoader();
+ }
+
+ @Override
+ public int getEffectiveMajorVersion()
+ {
+ return _effectiveMajorVersion;
+ }
+
+ @Override
+ public int getEffectiveMinorVersion()
+ {
+ return _effectiveMinorVersion;
+ }
+
+ public void setEffectiveMajorVersion(int v)
+ {
+ _effectiveMajorVersion = v;
+ }
+
+ public void setEffectiveMinorVersion(int v)
+ {
+ _effectiveMinorVersion = v;
+ }
+
+ @Override
+ public JspConfigDescriptor getJspConfigDescriptor()
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getJspConfigDescriptor()");
+ return null;
+ }
+
+ @Override
+ public void declareRoles(String... roleNames)
+ {
+ LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "declareRoles(String...)");
+ }
+
+ @Override
+ public String getVirtualServerName()
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Interface to check aliases
+ */
+ public interface AliasCheck
+ {
+
+ /**
+ * Check an alias
+ *
+ * @param path The path the aliased resource was created for
+ * @param resource The aliased resourced
+ * @return True if the resource is OK to be served.
+ */
+ boolean check(String path, Resource resource);
+ }
+
+ /**
+ * Approve all aliases.
+ */
+ public static class ApproveAliases implements AliasCheck
+ {
+ @Override
+ public boolean check(String path, Resource resource)
+ {
+ return true;
+ }
+ }
+
+ /**
+ * Approve Aliases of a non existent directory. If a directory "/foobar/" does not exist, then the resource is aliased to "/foobar". Accept such aliases.
+ */
+ public static class ApproveNonExistentDirectoryAliases implements AliasCheck
+ {
+ @Override
+ public boolean check(String path, Resource resource)
+ {
+ if (resource.exists())
+ return false;
+
+ String a = resource.getAlias().toString();
+ String r = resource.getURI().toString();
+
+ if (a.length() > r.length())
+ return a.startsWith(r) && a.length() == r.length() + 1 && a.endsWith("/");
+ if (a.length() < r.length())
+ return r.startsWith(a) && r.length() == a.length() + 1 && r.endsWith("/");
+
+ return a.equals(r);
+ }
+ }
+
+ /**
+ * Listener for all threads entering context scope, including async IO callbacks
+ */
+ public interface ContextScopeListener extends EventListener
+ {
+ /**
+ * @param context The context being entered
+ * @param request A request that is applicable to the scope, or null
+ * @param reason An object that indicates the reason the scope is being entered.
+ */
+ void enterScope(Context context, Request request, Object reason);
+
+ /**
+ * @param context The context being exited
+ * @param request A request that is applicable to the scope, or null
+ */
+ void exitScope(Context context, Request request);
+ }
+
+ private static class Caller extends SecurityManager
+ {
+ public ClassLoader getCallerClassLoader(int depth)
+ {
+ if (depth < 0)
+ return null;
+ Class<?>[] classContext = getClassContext();
+ if (classContext.length <= depth)
+ return null;
+ return classContext[depth].getClassLoader();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandlerCollection.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandlerCollection.java
new file mode 100644
index 0000000..4f74b97
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandlerCollection.java
@@ -0,0 +1,440 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HandlerContainer;
+import org.eclipse.jetty.server.HttpChannelState;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.ArrayTernaryTrie;
+import org.eclipse.jetty.util.ArrayUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.Trie;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.SerializedExecutor;
+
+/**
+ * This {@link org.eclipse.jetty.server.handler.HandlerCollection} is creates a
+ * Map of contexts to it's contained handlers based
+ * on the context path and virtual hosts of any contained {@link org.eclipse.jetty.server.handler.ContextHandler}s.
+ * The contexts do not need to be directly contained, only children of the contained handlers.
+ * Multiple contexts may have the same context path and they are called in order until one
+ * handles the request.
+ */
+@ManagedObject("Context Handler Collection")
+public class ContextHandlerCollection extends HandlerCollection
+{
+ private static final Logger LOG = Log.getLogger(ContextHandlerCollection.class);
+ private final SerializedExecutor _serializedExecutor = new SerializedExecutor();
+
+ @Deprecated
+ private Class<? extends ContextHandler> _contextClass = ContextHandler.class;
+
+ public ContextHandlerCollection()
+ {
+ super(true);
+ }
+
+ public ContextHandlerCollection(ContextHandler... contexts)
+ {
+ super(true);
+ setHandlers(contexts);
+ }
+
+ /**
+ * Remap the contexts. Normally this is not required as context
+ * mapping is maintained as a side effect of {@link #setHandlers(Handler[])}
+ * However, if configuration changes in the deep handler structure (eg contextpath is changed), then
+ * this call will trigger a remapping.
+ * This method is mutually excluded from {@link #deployHandler(Handler, Callback)} and
+ * {@link #undeployHandler(Handler, Callback)}
+ */
+ @ManagedOperation("Update the mapping of context path to context")
+ public void mapContexts()
+ {
+ _serializedExecutor.execute(() ->
+ {
+ while (true)
+ {
+ Handlers handlers = _handlers.get();
+ if (handlers == null)
+ break;
+ if (updateHandlers(handlers, newHandlers(handlers.getHandlers())))
+ break;
+ }
+ });
+ }
+
+ @Override
+ protected Handlers newHandlers(Handler[] handlers)
+ {
+ if (handlers == null || handlers.length == 0)
+ return null;
+
+ // Create map of contextPath to handler Branch
+ // A branch is a Handler that could contain 0 or more ContextHandlers
+ Map<String, Branch[]> path2Branches = new HashMap<>();
+ for (Handler handler : handlers)
+ {
+ Branch branch = new Branch(handler);
+ for (String contextPath : branch.getContextPaths())
+ {
+ Branch[] branches = path2Branches.get(contextPath);
+ path2Branches.put(contextPath, ArrayUtil.addToArray(branches, branch, Branch.class));
+ }
+ }
+
+ // Sort the branches for each contextPath so those with virtual hosts are considered before those without
+ for (Map.Entry<String, Branch[]> entry : path2Branches.entrySet())
+ {
+ Branch[] branches = entry.getValue();
+ Branch[] sorted = new Branch[branches.length];
+ int i = 0;
+ for (Branch branch : branches)
+ {
+ if (branch.hasVirtualHost())
+ sorted[i++] = branch;
+ }
+ for (Branch branch : branches)
+ {
+ if (!branch.hasVirtualHost())
+ sorted[i++] = branch;
+ }
+ entry.setValue(sorted);
+ }
+
+ // Loop until we have a big enough trie to hold all the context paths
+ int capacity = 512;
+ Mapping mapping;
+ loop:
+ while (true)
+ {
+ mapping = new Mapping(handlers, capacity);
+ for (Map.Entry<String, Branch[]> entry : path2Branches.entrySet())
+ {
+ if (!mapping._pathBranches.put(entry.getKey().substring(1), entry))
+ {
+ capacity += 512;
+ continue loop;
+ }
+ }
+ break;
+ }
+
+ if (LOG.isDebugEnabled())
+ {
+ for (String ctx : mapping._pathBranches.keySet())
+ {
+ LOG.debug("{}->{}", ctx, Arrays.asList(mapping._pathBranches.get(ctx).getValue()));
+ }
+ }
+
+ // add new context branches to concurrent map
+ for (Branch[] branches : path2Branches.values())
+ {
+ for (Branch branch : branches)
+ {
+ for (ContextHandler context : branch.getContextHandlers())
+ {
+ mapping._contextBranches.put(context, branch.getHandler());
+ }
+ }
+ }
+
+ return mapping;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ Mapping mapping = (Mapping)_handlers.get();
+
+ // Handle no contexts
+ if (mapping == null)
+ return;
+ Handler[] handlers = mapping.getHandlers();
+ if (handlers == null || handlers.length == 0)
+ return;
+
+ // handle only a single context.
+ if (handlers.length == 1)
+ {
+ handlers[0].handle(target, baseRequest, request, response);
+ return;
+ }
+
+ // handle async dispatch to specific context
+ HttpChannelState async = baseRequest.getHttpChannelState();
+ if (async.isAsync())
+ {
+ ContextHandler context = async.getContextHandler();
+ if (context != null)
+ {
+ Handler branch = mapping._contextBranches.get(context);
+
+ if (branch == null)
+ context.handle(target, baseRequest, request, response);
+ else
+ branch.handle(target, baseRequest, request, response);
+ return;
+ }
+ }
+
+ // handle many contexts
+ if (target.startsWith("/"))
+ {
+ Trie<Map.Entry<String, Branch[]>> pathBranches = mapping._pathBranches;
+ if (pathBranches == null)
+ return;
+
+ int limit = target.length() - 1;
+
+ while (limit >= 0)
+ {
+ // Get best match
+ Map.Entry<String, Branch[]> branches = pathBranches.getBest(target, 1, limit);
+
+ if (branches == null)
+ break;
+
+ int l = branches.getKey().length();
+ if (l == 1 || target.length() == l || target.charAt(l) == '/')
+ {
+ for (Branch branch : branches.getValue())
+ {
+ branch.getHandler().handle(target, baseRequest, request, response);
+ if (baseRequest.isHandled())
+ return;
+ }
+ }
+
+ limit = l - 2;
+ }
+ }
+ else
+ {
+ for (Handler handler : handlers)
+ {
+ handler.handle(target, baseRequest, request, response);
+ if (baseRequest.isHandled())
+ return;
+ }
+ }
+ }
+
+ /**
+ * Adds a context handler.
+ *
+ * @param contextPath The context path to add
+ * @param resourceBase the base (root) Resource
+ * @return the ContextHandler just added
+ * @deprecated Unused convenience method no longer supported.
+ */
+ @Deprecated
+ public ContextHandler addContext(String contextPath, String resourceBase)
+ {
+ try
+ {
+ ContextHandler context = _contextClass.getDeclaredConstructor().newInstance();
+ context.setContextPath(contextPath);
+ context.setResourceBase(resourceBase);
+ addHandler(context);
+ return context;
+ }
+ catch (Exception e)
+ {
+ LOG.debug(e);
+ throw new Error(e);
+ }
+ }
+
+ /**
+ * Thread safe deploy of a Handler.
+ * <p>
+ * This method is the equivalent of {@link #addHandler(Handler)},
+ * but its execution is non-block and mutually excluded from all
+ * other calls to {@link #deployHandler(Handler, Callback)} and
+ * {@link #undeployHandler(Handler, Callback)}.
+ * The handler may be added after this call returns.
+ * </p>
+ *
+ * @param handler the handler to deploy
+ * @param callback Called after handler has been added
+ */
+ public void deployHandler(Handler handler, Callback callback)
+ {
+ if (handler.getServer() != getServer())
+ handler.setServer(getServer());
+
+ _serializedExecutor.execute(new SerializedExecutor.ErrorHandlingTask()
+ {
+ @Override
+ public void run()
+ {
+ addHandler(handler);
+ callback.succeeded();
+ }
+
+ @Override
+ public void accept(Throwable throwable)
+ {
+ callback.failed(throwable);
+ }
+ });
+ }
+
+ /**
+ * Thread safe undeploy of a Handler.
+ * <p>
+ * This method is the equivalent of {@link #removeHandler(Handler)},
+ * but its execution is non-block and mutually excluded from all
+ * other calls to {@link #deployHandler(Handler, Callback)} and
+ * {@link #undeployHandler(Handler, Callback)}.
+ * The handler may be removed after this call returns.
+ * </p>
+ *
+ * @param handler The handler to undeploy
+ * @param callback Called after handler has been removed
+ */
+ public void undeployHandler(Handler handler, Callback callback)
+ {
+ _serializedExecutor.execute(new SerializedExecutor.ErrorHandlingTask()
+ {
+ @Override
+ public void run()
+ {
+ removeHandler(handler);
+ callback.succeeded();
+ }
+
+ @Override
+ public void accept(Throwable throwable)
+ {
+ callback.failed(throwable);
+ }
+ });
+ }
+
+ /**
+ * @return The class to use to add new Contexts
+ * @deprecated Unused convenience mechanism not used.
+ */
+ @Deprecated
+ public Class<?> getContextClass()
+ {
+ return _contextClass;
+ }
+
+ /**
+ * @param contextClass The class to use to add new Contexts
+ * @deprecated Unused convenience mechanism not used.
+ */
+ @Deprecated
+ public void setContextClass(Class<? extends ContextHandler> contextClass)
+ {
+ if (contextClass == null || !(ContextHandler.class.isAssignableFrom(contextClass)))
+ throw new IllegalArgumentException();
+ _contextClass = contextClass;
+ }
+
+ private static final class Branch
+ {
+ private final Handler _handler;
+ private final ContextHandler[] _contexts;
+
+ Branch(Handler handler)
+ {
+ _handler = handler;
+
+ if (handler instanceof ContextHandler)
+ {
+ _contexts = new ContextHandler[]{(ContextHandler)handler};
+ }
+ else if (handler instanceof HandlerContainer)
+ {
+ Handler[] contexts = ((HandlerContainer)handler).getChildHandlersByClass(ContextHandler.class);
+ _contexts = new ContextHandler[contexts.length];
+ System.arraycopy(contexts, 0, _contexts, 0, contexts.length);
+ }
+ else
+ _contexts = new ContextHandler[0];
+ }
+
+ Set<String> getContextPaths()
+ {
+ Set<String> set = new HashSet<>();
+ for (ContextHandler context : _contexts)
+ {
+ set.add(context.getContextPath());
+ }
+ return set;
+ }
+
+ boolean hasVirtualHost()
+ {
+ for (ContextHandler context : _contexts)
+ {
+ if (context.getVirtualHosts() != null && context.getVirtualHosts().length > 0)
+ return true;
+ }
+ return false;
+ }
+
+ ContextHandler[] getContextHandlers()
+ {
+ return _contexts;
+ }
+
+ Handler getHandler()
+ {
+ return _handler;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("{%s,%s}", _handler, Arrays.asList(_contexts));
+ }
+ }
+
+ private static class Mapping extends Handlers
+ {
+ private final Map<ContextHandler, Handler> _contextBranches = new HashMap<>();
+ private final Trie<Map.Entry<String, Branch[]>> _pathBranches;
+
+ private Mapping(Handler[] handlers, int capacity)
+ {
+ super(handlers);
+ _pathBranches = new ArrayTernaryTrie<>(false, capacity);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java
new file mode 100644
index 0000000..e0f3126
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java
@@ -0,0 +1,185 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.Locale;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.server.AbstractConnector;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.DebugListener;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.DateCache;
+import org.eclipse.jetty.util.RolloverFileOutputStream;
+
+/**
+ * Debug Handler.
+ * A lightweight debug handler that can be used in production code.
+ * Details of the request and response are written to an output stream
+ * and the current thread name is updated with information that will link
+ * to the details in that output.
+ *
+ * @deprecated Use {@link DebugListener}
+ */
+public class DebugHandler extends HandlerWrapper implements Connection.Listener
+{
+ private DateCache _date = new DateCache("HH:mm:ss", Locale.US);
+ private OutputStream _out;
+ private PrintStream _print;
+
+ /*
+ * @see org.eclipse.jetty.server.Handler#handle(java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
+ */
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ final Response base_response = baseRequest.getResponse();
+ final Thread thread = Thread.currentThread();
+ final String old_name = thread.getName();
+
+ boolean retry = false;
+ String name = (String)request.getAttribute("org.eclipse.jetty.thread.name");
+ if (name == null)
+ name = old_name + ":" + baseRequest.getHttpURI();
+ else
+ retry = true;
+
+ String ex = null;
+ try
+ {
+ if (retry)
+ print(name, "RESUME");
+ else
+ print(name, "REQUEST " + baseRequest.getRemoteAddr() + " " + request.getMethod() + " " + baseRequest.getHeader("Cookie") + "; " + baseRequest.getHeader("User-Agent"));
+ thread.setName(name);
+
+ getHandler().handle(target, baseRequest, request, response);
+ }
+ catch (IOException ioe)
+ {
+ ex = ioe.toString();
+ throw ioe;
+ }
+ catch (ServletException cause)
+ {
+ ex = cause.toString() + ":" + cause.getCause();
+ throw cause;
+ }
+ catch (RuntimeException rte)
+ {
+ ex = rte.toString();
+ throw rte;
+ }
+ catch (Error e)
+ {
+ ex = e.toString();
+ throw e;
+ }
+ finally
+ {
+ thread.setName(old_name);
+ if (baseRequest.getHttpChannelState().isAsyncStarted())
+ {
+ request.setAttribute("org.eclipse.jetty.thread.name", name);
+ print(name, "ASYNC");
+ }
+ else
+ print(name, "RESPONSE " + base_response.getStatus() + (ex == null ? "" : ("/" + ex)) + " " + base_response.getContentType());
+ }
+ }
+
+ private void print(String name, String message)
+ {
+ long now = System.currentTimeMillis();
+ final String d = _date.formatNow(now);
+ final int ms = (int)(now % 1000);
+
+ _print.println(d + (ms > 99 ? "." : (ms > 9 ? ".0" : ".00")) + ms + ":" + name + " " + message);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jetty.server.handler.HandlerWrapper#doStart()
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (_out == null)
+ _out = new RolloverFileOutputStream("./logs/yyyy_mm_dd.debug.log", true);
+ _print = new PrintStream(_out);
+
+ for (Connector connector : getServer().getConnectors())
+ {
+ if (connector instanceof AbstractConnector)
+ connector.addBean(this, false);
+ }
+
+ super.doStart();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jetty.server.handler.HandlerWrapper#doStop()
+ */
+ @Override
+ protected void doStop() throws Exception
+ {
+ super.doStop();
+ _print.close();
+ for (Connector connector : getServer().getConnectors())
+ {
+ if (connector instanceof AbstractConnector)
+ connector.removeBean(this);
+ }
+ }
+
+ /**
+ * @return the out
+ */
+ public OutputStream getOutputStream()
+ {
+ return _out;
+ }
+
+ /**
+ * @param out the out to set
+ */
+ public void setOutputStream(OutputStream out)
+ {
+ _out = out;
+ }
+
+ @Override
+ public void onOpened(Connection connection)
+ {
+ print(Thread.currentThread().getName(), "OPENED " + connection.toString());
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ print(Thread.currentThread().getName(), "CLOSED " + connection.toString());
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DefaultHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DefaultHandler.java
new file mode 100644
index 0000000..f2f3171
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DefaultHandler.java
@@ -0,0 +1,231 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.URL;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Default Handler.
+ *
+ * This handle will deal with unhandled requests in the server.
+ * For requests for favicon.ico, the Jetty icon is served.
+ * For reqests to '/' a 404 with a list of known contexts is served.
+ * For all other requests a normal 404 is served.
+ */
+public class DefaultHandler extends AbstractHandler
+{
+ private static final Logger LOG = Log.getLogger(DefaultHandler.class);
+
+ final long _faviconModified = (System.currentTimeMillis() / 1000) * 1000L;
+ final byte[] _favicon;
+ boolean _serveIcon = true;
+ boolean _showContexts = true;
+
+ public DefaultHandler()
+ {
+ byte[] favbytes = null;
+ try
+ {
+ URL fav = this.getClass().getClassLoader().getResource("org/eclipse/jetty/favicon.ico");
+ if (fav != null)
+ {
+ Resource r = Resource.newResource(fav);
+ favbytes = IO.readBytes(r.getInputStream());
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ finally
+ {
+ _favicon = favbytes;
+ }
+ }
+
+ /*
+ * @see org.eclipse.jetty.server.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
+ */
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (response.isCommitted() || baseRequest.isHandled())
+ return;
+
+ baseRequest.setHandled(true);
+
+ String method = request.getMethod();
+
+ // little cheat for common request
+ if (_serveIcon && _favicon != null && HttpMethod.GET.is(method) && target.equals("/favicon.ico"))
+ {
+ if (request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.toString()) == _faviconModified)
+ response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ else
+ {
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.setContentType("image/x-icon");
+ response.setContentLength(_favicon.length);
+ response.setDateHeader(HttpHeader.LAST_MODIFIED.toString(), _faviconModified);
+ response.setHeader(HttpHeader.CACHE_CONTROL.toString(), "max-age=360000,public");
+ response.getOutputStream().write(_favicon);
+ }
+ return;
+ }
+
+ if (!_showContexts || !HttpMethod.GET.is(method) || !request.getRequestURI().equals("/"))
+ {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ response.setContentType(MimeTypes.Type.TEXT_HTML_UTF_8.toString());
+
+ try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ OutputStreamWriter writer = new OutputStreamWriter(outputStream, UTF_8))
+ {
+ writer.append("<!DOCTYPE html>\n");
+ writer.append("<html lang=\"en\">\n<head>\n");
+ writer.append("<title>Error 404 - Not Found</title>\n");
+ writer.append("<meta charset=\"utf-8\">\n");
+ writer.append("<style>body { font-family: sans-serif; } table, td { border: 1px solid #333; } td, th { padding: 5px; } thead, tfoot { background-color: #333; color: #fff; } </style>\n");
+ writer.append("</head>\n<body>\n");
+ writer.append("<h2>Error 404 - Not Found.</h2>\n");
+ writer.append("<p>No context on this server matched or handled this request.</p>\n");
+ writer.append("<p>Contexts known to this server are:</p>\n");
+
+ Server server = getServer();
+ Handler[] handlers = server == null ? null : server.getChildHandlersByClass(ContextHandler.class);
+
+ writer.append("<table class=\"contexts\"><thead><tr>");
+ writer.append("<th>Context Path</th>");
+ writer.append("<th>Display Name</th>");
+ writer.append("<th>Status</th>");
+ writer.append("<th>LifeCycle</th>");
+ writer.append("</tr></thead><tbody>\n");
+
+ for (int i = 0; handlers != null && i < handlers.length; i++)
+ {
+ writer.append("<tr><td>");
+ // Context Path
+ ContextHandler context = (ContextHandler)handlers[i];
+
+ String contextPath = context.getContextPath();
+ String href = URIUtil.encodePath(contextPath);
+ if (contextPath.length() > 1 && !contextPath.endsWith("/"))
+ {
+ href += '/';
+ }
+
+ if (context.isRunning())
+ {
+ writer.append("<a href=\"").append(href).append("\">");
+ }
+ writer.append(StringUtil.replace(contextPath, "%", "%"));
+ if (context.isRunning())
+ {
+ writer.append("</a>");
+ }
+ writer.append("</td><td>");
+ // Display Name
+
+ if (StringUtil.isNotBlank(context.getDisplayName()))
+ {
+ writer.append(StringUtil.sanitizeXmlString(context.getDisplayName()));
+ }
+ writer.append(" </td><td>");
+ // Available
+
+ if (context.isAvailable())
+ {
+ writer.append("Available");
+ }
+ else
+ {
+ writer.append("<em>Not</em> Available");
+ }
+ writer.append("</td><td>");
+ // State
+ writer.append(context.getState());
+ writer.append("</td></tr>\n");
+ }
+
+ writer.append("</tbody></table><hr/>\n");
+ writer.append("<a href=\"https://eclipse.org/jetty\"><img alt=\"icon\" src=\"/favicon.ico\"/></a> ");
+ writer.append("<a href=\"https://eclipse.org/jetty\">Powered by Eclipse Jetty:// Server</a><hr/>\n");
+ writer.append("</body>\n</html>\n");
+ writer.flush();
+ byte[] content = outputStream.toByteArray();
+ response.setContentLength(content.length);
+ try (OutputStream out = response.getOutputStream())
+ {
+ out.write(content);
+ }
+ }
+ }
+
+ /**
+ * @return Returns true if the handle can server the jetty favicon.ico
+ */
+ public boolean getServeIcon()
+ {
+ return _serveIcon;
+ }
+
+ /**
+ * @param serveIcon true if the handle can server the jetty favicon.ico
+ */
+ public void setServeIcon(boolean serveIcon)
+ {
+ _serveIcon = serveIcon;
+ }
+
+ public boolean getShowContexts()
+ {
+ return _showContexts;
+ }
+
+ public void setShowContexts(boolean show)
+ {
+ _showContexts = show;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java
new file mode 100644
index 0000000..231cb46
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java
@@ -0,0 +1,634 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.http.QuotedQualityCSV;
+import org.eclipse.jetty.io.ByteBufferOutputStream;
+import org.eclipse.jetty.server.Dispatcher;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.QuotedStringTokenizer;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Handler for Error pages
+ * An ErrorHandler is registered with {@link ContextHandler#setErrorHandler(ErrorHandler)} or
+ * {@link Server#setErrorHandler(ErrorHandler)}.
+ * It is called by the HttpResponse.sendError method to write an error page via {@link #handle(String, Request, HttpServletRequest, HttpServletResponse)}
+ * or via {@link #badMessageError(int, String, HttpFields)} for bad requests for which a dispatch cannot be done.
+ */
+public class ErrorHandler extends AbstractHandler
+{
+ // TODO This classes API needs to be majorly refactored/cleanup in jetty-10
+ private static final Logger LOG = Log.getLogger(ErrorHandler.class);
+ public static final String ERROR_PAGE = "org.eclipse.jetty.server.error_page";
+ public static final String ERROR_CONTEXT = "org.eclipse.jetty.server.error_context";
+ public static final String ERROR_CHARSET = "org.eclipse.jetty.server.error_charset";
+
+ boolean _showServlet = true;
+ boolean _showStacks = true;
+ boolean _disableStacks = false;
+ boolean _showMessageInTitle = true;
+ String _cacheControl = "must-revalidate,no-cache,no-store";
+
+ public ErrorHandler()
+ {
+ }
+
+ public boolean errorPageForMethod(String method)
+ {
+ switch (method)
+ {
+ case "GET":
+ case "POST":
+ case "HEAD":
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ // TODO inline this and remove method in jetty-10
+ doError(target, baseRequest, request, response);
+ }
+
+ @Override
+ public void doError(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ String cacheControl = getCacheControl();
+ if (cacheControl != null)
+ response.setHeader(HttpHeader.CACHE_CONTROL.asString(), cacheControl);
+
+ // Look for an error page dispatcher
+ // This logic really should be in ErrorPageErrorHandler, but some implementations extend ErrorHandler
+ // and implement ErrorPageMapper directly, so we do this here in the base class.
+ String errorPage = (this instanceof ErrorPageMapper) ? ((ErrorPageMapper)this).getErrorPage(request) : null;
+ ContextHandler.Context context = baseRequest.getErrorContext();
+ Dispatcher errorDispatcher = (errorPage != null && context != null)
+ ? (Dispatcher)context.getRequestDispatcher(errorPage) : null;
+
+ try
+ {
+ if (errorDispatcher != null)
+ {
+ try
+ {
+ errorDispatcher.error(request, response);
+ return;
+ }
+ catch (ServletException e)
+ {
+ LOG.debug(e);
+ if (response.isCommitted())
+ return;
+ }
+ }
+
+ String message = (String)request.getAttribute(Dispatcher.ERROR_MESSAGE);
+ if (message == null)
+ message = baseRequest.getResponse().getReason();
+ generateAcceptableResponse(baseRequest, request, response, response.getStatus(), message);
+ }
+ finally
+ {
+ baseRequest.setHandled(true);
+ }
+ }
+
+ /**
+ * Generate an acceptable error response.
+ * <p>This method is called to generate an Error page of a mime type that is
+ * acceptable to the user-agent. The Accept header is evaluated in
+ * quality order and the method
+ * {@link #generateAcceptableResponse(Request, HttpServletRequest, HttpServletResponse, int, String, String)}
+ * is called for each mimetype until the response is written to or committed.</p>
+ *
+ * @param baseRequest The base request
+ * @param request The servlet request (may be wrapped)
+ * @param response The response (may be wrapped)
+ * @param code the http error code
+ * @param message the http error message
+ * @throws IOException if the response cannot be generated
+ */
+ protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message)
+ throws IOException
+ {
+ List<String> acceptable = baseRequest.getHttpFields().getQualityCSV(HttpHeader.ACCEPT, QuotedQualityCSV.MOST_SPECIFIC_MIME_ORDERING);
+
+ if (acceptable.isEmpty() && !baseRequest.getHttpFields().contains(HttpHeader.ACCEPT))
+ {
+ generateAcceptableResponse(baseRequest, request, response, code, message, MimeTypes.Type.TEXT_HTML.asString());
+ }
+ else
+ {
+ for (String mimeType : acceptable)
+ {
+ generateAcceptableResponse(baseRequest, request, response, code, message, mimeType);
+ if (response.isCommitted() || baseRequest.getResponse().isWritingOrStreaming())
+ break;
+ }
+ }
+ }
+
+ /**
+ * Returns an acceptable writer for an error page.
+ * <p>Uses the user-agent's <code>Accept-Charset</code> to get response
+ * {@link Writer}. The acceptable charsets are tested in quality order
+ * if they are known to the JVM and the first known is set on
+ * {@link HttpServletResponse#setCharacterEncoding(String)} and the
+ * {@link HttpServletResponse#getWriter()} method used to return a writer.
+ * If there is no <code>Accept-Charset</code> header then
+ * <code>ISO-8859-1</code> is used. If '*' is the highest quality known
+ * charset, then <code>utf-8</code> is used.
+ * </p>
+ *
+ * @param baseRequest The base request
+ * @param request The servlet request (may be wrapped)
+ * @param response The response (may be wrapped)
+ * @return A {@link Writer} if there is a known acceptable charset or null
+ * @throws IOException if a Writer cannot be returned
+ */
+ @Deprecated
+ protected Writer getAcceptableWriter(Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException
+ {
+ List<String> acceptable = baseRequest.getHttpFields().getQualityCSV(HttpHeader.ACCEPT_CHARSET);
+ if (acceptable.isEmpty())
+ {
+ response.setCharacterEncoding(StandardCharsets.ISO_8859_1.name());
+ return response.getWriter();
+ }
+
+ for (String charset : acceptable)
+ {
+ try
+ {
+ if ("*".equals(charset))
+ response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+ else
+ response.setCharacterEncoding(Charset.forName(charset).name());
+ return response.getWriter();
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Generate an acceptable error response for a mime type.
+ * <p>This method is called for each mime type in the users agent's
+ * <code>Accept</code> header, until {@link Request#isHandled()} is true and a
+ * response of the appropriate type is generated.
+ * </p>
+ * <p>The default implementation handles "text/html", "text/*" and "*/*".
+ * The method can be overridden to handle other types. Implementations must
+ * immediate produce a response and may not be async.
+ * </p>
+ *
+ * @param baseRequest The base request
+ * @param request The servlet request (may be wrapped)
+ * @param response The response (may be wrapped)
+ * @param code the http error code
+ * @param message the http error message
+ * @param contentType The mimetype to generate (may be */*or other wildcard)
+ * @throws IOException if a response cannot be generated
+ */
+ protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String contentType)
+ throws IOException
+ {
+ // We can generate an acceptable contentType, but can we generate an acceptable charset?
+ // TODO refactor this in jetty-10 to be done in the other calling loop
+ Charset charset = null;
+ List<String> acceptable = baseRequest.getHttpFields().getQualityCSV(HttpHeader.ACCEPT_CHARSET);
+ if (!acceptable.isEmpty())
+ {
+ for (String name : acceptable)
+ {
+ if ("*".equals(name))
+ {
+ charset = StandardCharsets.UTF_8;
+ break;
+ }
+
+ try
+ {
+ charset = Charset.forName(name);
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+ }
+ if (charset == null)
+ return;
+ }
+
+ MimeTypes.Type type;
+ switch (contentType)
+ {
+ case "text/html":
+ case "text/*":
+ case "*/*":
+ type = MimeTypes.Type.TEXT_HTML;
+ if (charset == null)
+ charset = StandardCharsets.ISO_8859_1;
+ break;
+
+ case "text/json":
+ case "application/json":
+ type = MimeTypes.Type.TEXT_JSON;
+ if (charset == null)
+ charset = StandardCharsets.UTF_8;
+ break;
+
+ case "text/plain":
+ type = MimeTypes.Type.TEXT_PLAIN;
+ if (charset == null)
+ charset = StandardCharsets.ISO_8859_1;
+ break;
+
+ default:
+ return;
+ }
+
+ // write into the response aggregate buffer and flush it asynchronously.
+ while (true)
+ {
+ try
+ {
+ // TODO currently the writer used here is of fixed size, so a large
+ // TODO error page may cause a BufferOverflow. In which case we try
+ // TODO again with stacks disabled. If it still overflows, it is
+ // TODO written without a body.
+ ByteBuffer buffer = baseRequest.getResponse().getHttpOutput().getBuffer();
+ ByteBufferOutputStream out = new ByteBufferOutputStream(buffer);
+ PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, charset));
+
+ switch (type)
+ {
+ case TEXT_HTML:
+ response.setContentType(MimeTypes.Type.TEXT_HTML.asString());
+ response.setCharacterEncoding(charset.name());
+ request.setAttribute(ERROR_CHARSET, charset);
+ handleErrorPage(request, writer, code, message);
+ break;
+ case TEXT_JSON:
+ response.setContentType(contentType);
+ writeErrorJson(request, writer, code, message);
+ break;
+ case TEXT_PLAIN:
+ response.setContentType(MimeTypes.Type.TEXT_PLAIN.asString());
+ response.setCharacterEncoding(charset.name());
+ writeErrorPlain(request, writer, code, message);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+
+ writer.flush();
+ break;
+ }
+ catch (BufferOverflowException e)
+ {
+ LOG.warn("Error page too large: {} {} {}", code, message, request);
+ if (LOG.isDebugEnabled())
+ LOG.warn(e);
+ baseRequest.getResponse().resetContent();
+ if (!_disableStacks)
+ {
+ LOG.info("Disabling showsStacks for " + this);
+ _disableStacks = true;
+ continue;
+ }
+ break;
+ }
+ }
+
+ // Do an asynchronous completion.
+ baseRequest.getHttpChannel().sendResponseAndComplete();
+ }
+
+ protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message)
+ throws IOException
+ {
+ writeErrorPage(request, writer, code, message, _showStacks);
+ }
+
+ protected void writeErrorPage(HttpServletRequest request, Writer writer, int code, String message, boolean showStacks)
+ throws IOException
+ {
+ if (message == null)
+ message = HttpStatus.getMessage(code);
+
+ writer.write("<html>\n<head>\n");
+ writeErrorPageHead(request, writer, code, message);
+ writer.write("</head>\n<body>");
+ writeErrorPageBody(request, writer, code, message, showStacks);
+ writer.write("\n</body>\n</html>\n");
+ }
+
+ protected void writeErrorPageHead(HttpServletRequest request, Writer writer, int code, String message)
+ throws IOException
+ {
+ Charset charset = (Charset)request.getAttribute(ERROR_CHARSET);
+ if (charset != null)
+ {
+ writer.write("<meta http-equiv=\"Content-Type\" content=\"text/html;charset=");
+ writer.write(charset.name());
+ writer.write("\"/>\n");
+ }
+ writer.write("<title>Error ");
+ // TODO this code is duplicated in writeErrorPageMessage
+ String status = Integer.toString(code);
+ writer.write(status);
+ if (message != null && !message.equals(status))
+ {
+ writer.write(' ');
+ writer.write(StringUtil.sanitizeXmlString(message));
+ }
+ writer.write("</title>\n");
+ }
+
+ protected void writeErrorPageBody(HttpServletRequest request, Writer writer, int code, String message, boolean showStacks)
+ throws IOException
+ {
+ String uri = request.getRequestURI();
+
+ writeErrorPageMessage(request, writer, code, message, uri);
+ if (showStacks && !_disableStacks)
+ writeErrorPageStacks(request, writer);
+
+ Request.getBaseRequest(request).getHttpChannel().getHttpConfiguration()
+ .writePoweredBy(writer, "<hr/>", "<hr/>\n");
+ }
+
+ protected void writeErrorPageMessage(HttpServletRequest request, Writer writer, int code, String message, String uri)
+ throws IOException
+ {
+ writer.write("<h2>HTTP ERROR ");
+ String status = Integer.toString(code);
+ writer.write(status);
+ if (message != null && !message.equals(status))
+ {
+ writer.write(' ');
+ writer.write(StringUtil.sanitizeXmlString(message));
+ }
+ writer.write("</h2>\n");
+ writer.write("<table>\n");
+ htmlRow(writer, "URI", uri);
+ htmlRow(writer, "STATUS", status);
+ htmlRow(writer, "MESSAGE", message);
+ if (isShowServlet())
+ {
+ htmlRow(writer, "SERVLET", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME));
+ }
+ Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION);
+ while (cause != null)
+ {
+ htmlRow(writer, "CAUSED BY", cause);
+ cause = cause.getCause();
+ }
+ writer.write("</table>\n");
+ }
+
+ private void htmlRow(Writer writer, String tag, Object value)
+ throws IOException
+ {
+ writer.write("<tr><th>");
+ writer.write(tag);
+ writer.write(":</th><td>");
+ if (value == null)
+ writer.write("-");
+ else
+ writer.write(StringUtil.sanitizeXmlString(value.toString()));
+ writer.write("</td></tr>\n");
+ }
+
+ private void writeErrorPlain(HttpServletRequest request, PrintWriter writer, int code, String message)
+ {
+ writer.write("HTTP ERROR ");
+ writer.write(Integer.toString(code));
+ writer.write(' ');
+ writer.write(StringUtil.sanitizeXmlString(message));
+ writer.write("\n");
+ writer.printf("URI: %s%n", request.getRequestURI());
+ writer.printf("STATUS: %s%n", code);
+ writer.printf("MESSAGE: %s%n", message);
+ if (isShowServlet())
+ {
+ writer.printf("SERVLET: %s%n", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME));
+ }
+ Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION);
+ while (cause != null)
+ {
+ writer.printf("CAUSED BY %s%n", cause);
+ if (isShowStacks() && !_disableStacks)
+ {
+ cause.printStackTrace(writer);
+ }
+ cause = cause.getCause();
+ }
+ }
+
+ private void writeErrorJson(HttpServletRequest request, PrintWriter writer, int code, String message)
+ {
+ Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION);
+ Object servlet = request.getAttribute(Dispatcher.ERROR_SERVLET_NAME);
+ Map<String, String> json = new HashMap<>();
+
+ json.put("url", request.getRequestURI());
+ json.put("status", Integer.toString(code));
+ json.put("message", message);
+ if (isShowServlet() && servlet != null)
+ {
+ json.put("servlet", servlet.toString());
+ }
+ int c = 0;
+ while (cause != null)
+ {
+ json.put("cause" + c++, cause.toString());
+ cause = cause.getCause();
+ }
+
+ writer.append(json.entrySet().stream()
+ .map(e -> QuotedStringTokenizer.quote(e.getKey()) +
+ ":" +
+ QuotedStringTokenizer.quote(StringUtil.sanitizeXmlString((e.getValue()))))
+ .collect(Collectors.joining(",\n", "{\n", "\n}")));
+ }
+
+ protected void writeErrorPageStacks(HttpServletRequest request, Writer writer)
+ throws IOException
+ {
+ Throwable th = (Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
+ if (th != null)
+ {
+ writer.write("<h3>Caused by:</h3><pre>");
+ // You have to pre-generate and then use #write(writer, String)
+ try (StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw))
+ {
+ th.printStackTrace(pw);
+ pw.flush();
+ write(writer, sw.getBuffer().toString()); // sanitize
+ }
+ writer.write("</pre>\n");
+ }
+ }
+
+ /**
+ * Bad Message Error body
+ * <p>Generate an error response body to be sent for a bad message.
+ * In this case there is something wrong with the request, so either
+ * a request cannot be built, or it is not safe to build a request.
+ * This method allows for a simple error page body to be returned
+ * and some response headers to be set.
+ *
+ * @param status The error code that will be sent
+ * @param reason The reason for the error code (may be null)
+ * @param fields The header fields that will be sent with the response.
+ * @return The content as a ByteBuffer, or null for no body.
+ */
+ public ByteBuffer badMessageError(int status, String reason, HttpFields fields)
+ {
+ if (reason == null)
+ reason = HttpStatus.getMessage(status);
+ fields.put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.TEXT_HTML_8859_1.asString());
+ return BufferUtil.toBuffer("<h1>Bad Message " + status + "</h1><pre>reason: " + reason + "</pre>");
+ }
+
+ /**
+ * Get the cacheControl.
+ *
+ * @return the cacheControl header to set on error responses.
+ */
+ public String getCacheControl()
+ {
+ return _cacheControl;
+ }
+
+ /**
+ * Set the cacheControl.
+ *
+ * @param cacheControl the cacheControl header to set on error responses.
+ */
+ public void setCacheControl(String cacheControl)
+ {
+ _cacheControl = cacheControl;
+ }
+
+ /**
+ * @return True if the error page will show the Servlet that generated the error
+ */
+ public boolean isShowServlet()
+ {
+ return _showServlet;
+ }
+
+ /**
+ * @param showServlet True if the error page will show the Servlet that generated the error
+ */
+ public void setShowServlet(boolean showServlet)
+ {
+ _showServlet = showServlet;
+ }
+
+ /**
+ * @return True if stack traces are shown in the error pages
+ */
+ public boolean isShowStacks()
+ {
+ return _showStacks;
+ }
+
+ /**
+ * @param showStacks True if stack traces are shown in the error pages
+ */
+ public void setShowStacks(boolean showStacks)
+ {
+ _showStacks = showStacks;
+ }
+
+ /**
+ * @param showMessageInTitle if true, the error message appears in page title
+ */
+ public void setShowMessageInTitle(boolean showMessageInTitle)
+ {
+ _showMessageInTitle = showMessageInTitle;
+ }
+
+ public boolean getShowMessageInTitle()
+ {
+ return _showMessageInTitle;
+ }
+
+ protected void write(Writer writer, String string)
+ throws IOException
+ {
+ if (string == null)
+ return;
+
+ writer.write(StringUtil.sanitizeXmlString(string));
+ }
+
+ public interface ErrorPageMapper
+ {
+ String getErrorPage(HttpServletRequest request);
+ }
+
+ public static ErrorHandler getErrorHandler(Server server, ContextHandler context)
+ {
+ ErrorHandler errorHandler = null;
+ if (context != null)
+ errorHandler = context.getErrorHandler();
+ if (errorHandler == null && server != null)
+ errorHandler = server.getBean(ErrorHandler.class);
+ return errorHandler;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandler.java
new file mode 100644
index 0000000..fc245f0
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandler.java
@@ -0,0 +1,259 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Objects;
+
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpOutput.Interceptor;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.IteratingCallback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * <p>
+ * A Handler that can apply a {@link org.eclipse.jetty.server.HttpOutput.Interceptor}
+ * mechanism to buffer the entire response content until the output is closed.
+ * This allows the commit to be delayed until the response is complete and thus
+ * headers and response status can be changed while writing the body.
+ * </p>
+ * <p>
+ * Note that the decision to buffer is influenced by the headers and status at the
+ * first write, and thus subsequent changes to those headers will not influence the
+ * decision to buffer or not.
+ * </p>
+ * <p>
+ * Note also that there are no memory limits to the size of the buffer, thus
+ * this handler can represent an unbounded memory commitment if the content
+ * generated can also be unbounded.
+ * </p>
+ */
+public class FileBufferedResponseHandler extends BufferedResponseHandler
+{
+ private static final Logger LOG = Log.getLogger(FileBufferedResponseHandler.class);
+
+ private Path _tempDir = new File(System.getProperty("java.io.tmpdir")).toPath();
+
+ public Path getTempDir()
+ {
+ return _tempDir;
+ }
+
+ public void setTempDir(Path tempDir)
+ {
+ _tempDir = Objects.requireNonNull(tempDir);
+ }
+
+ @Override
+ protected BufferedInterceptor newBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
+ {
+ return new FileBufferedInterceptor(httpChannel, interceptor);
+ }
+
+ class FileBufferedInterceptor implements BufferedResponseHandler.BufferedInterceptor
+ {
+ private static final int MAX_MAPPED_BUFFER_SIZE = Integer.MAX_VALUE / 2;
+
+ private final Interceptor _next;
+ private final HttpChannel _channel;
+ private Boolean _aggregating;
+ private Path _filePath;
+ private OutputStream _fileOutputStream;
+
+ public FileBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
+ {
+ _next = interceptor;
+ _channel = httpChannel;
+ }
+
+ @Override
+ public Interceptor getNextInterceptor()
+ {
+ return _next;
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return false;
+ }
+
+ @Override
+ public void resetBuffer()
+ {
+ dispose();
+ BufferedInterceptor.super.resetBuffer();
+ }
+
+ private void closeFileOutput()
+ {
+ if (_fileOutputStream != null)
+ {
+ try
+ {
+ _fileOutputStream.flush();
+ }
+ catch (IOException e)
+ {
+ LOG.debug("flush failure", e);
+ }
+ IO.close(_fileOutputStream);
+ _fileOutputStream = null;
+ }
+ }
+
+ protected void dispose()
+ {
+ closeFileOutput();
+ _aggregating = null;
+
+ if (_filePath != null)
+ {
+ try
+ {
+ Files.deleteIfExists(_filePath);
+ }
+ catch (Throwable t)
+ {
+ LOG.debug("Could not immediately delete file (delaying to jvm exit) {}", _filePath, t);
+ _filePath.toFile().deleteOnExit();
+ }
+ _filePath = null;
+ }
+ }
+
+ @Override
+ public void write(ByteBuffer content, boolean last, Callback callback)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} write last={} {}", this, last, BufferUtil.toDetailString(content));
+
+ // If we are not committed, must decide if we should aggregate or not.
+ if (_aggregating == null)
+ _aggregating = shouldBuffer(_channel, last);
+
+ // If we are not aggregating, then handle normally.
+ if (!_aggregating)
+ {
+ getNextInterceptor().write(content, last, callback);
+ return;
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} aggregating", this);
+
+ try
+ {
+ if (BufferUtil.hasContent(content))
+ aggregate(content);
+ }
+ catch (Throwable t)
+ {
+ dispose();
+ callback.failed(t);
+ return;
+ }
+
+ if (last)
+ commit(callback);
+ else
+ callback.succeeded();
+ }
+
+ private void aggregate(ByteBuffer content) throws IOException
+ {
+ if (_fileOutputStream == null)
+ {
+ // Create a new OutputStream to a file.
+ _filePath = Files.createTempFile(_tempDir, "BufferedResponse", "");
+ _fileOutputStream = Files.newOutputStream(_filePath, StandardOpenOption.WRITE);
+ }
+
+ BufferUtil.writeTo(content, _fileOutputStream);
+ }
+
+ private void commit(Callback callback)
+ {
+ if (_fileOutputStream == null)
+ {
+ // We have no content to write, signal next interceptor that we are finished.
+ getNextInterceptor().write(BufferUtil.EMPTY_BUFFER, true, callback);
+ return;
+ }
+
+ try
+ {
+ closeFileOutput();
+ }
+ catch (Throwable t)
+ {
+ dispose();
+ callback.failed(t);
+ return;
+ }
+
+ // Create an iterating callback to do the writing
+ IteratingCallback icb = new IteratingCallback()
+ {
+ private final long fileLength = _filePath.toFile().length();
+ private long _pos = 0;
+ private boolean _last = false;
+
+ @Override
+ protected Action process() throws Exception
+ {
+ if (_last)
+ return Action.SUCCEEDED;
+
+ long len = Math.min(MAX_MAPPED_BUFFER_SIZE, fileLength - _pos);
+ _last = (_pos + len == fileLength);
+ ByteBuffer buffer = BufferUtil.toMappedBuffer(_filePath, _pos, len);
+ getNextInterceptor().write(buffer, _last, this);
+ _pos += len;
+ return Action.SCHEDULED;
+ }
+
+ @Override
+ protected void onCompleteSuccess()
+ {
+ dispose();
+ callback.succeeded();
+ }
+
+ @Override
+ protected void onCompleteFailure(Throwable cause)
+ {
+ dispose();
+ callback.failed(cause);
+ }
+ };
+ icb.iterate();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerCollection.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerCollection.java
new file mode 100644
index 0000000..29a8617
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerCollection.java
@@ -0,0 +1,252 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HandlerContainer;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.ArrayUtil;
+import org.eclipse.jetty.util.MultiException;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+
+/**
+ * A collection of handlers.
+ * <p>
+ * The default implementations calls all handlers in list order,
+ * regardless of the response status or exceptions. Derived implementation
+ * may alter the order or the conditions of calling the contained
+ * handlers.
+ */
+@ManagedObject("Handler of multiple handlers")
+public class HandlerCollection extends AbstractHandlerContainer
+{
+ private final boolean _mutableWhenRunning;
+ protected final AtomicReference<Handlers> _handlers = new AtomicReference<>();
+
+ public HandlerCollection()
+ {
+ this(false);
+ }
+
+ public HandlerCollection(Handler... handlers)
+ {
+ this(false, handlers);
+ }
+
+ public HandlerCollection(boolean mutableWhenRunning, Handler... handlers)
+ {
+ _mutableWhenRunning = mutableWhenRunning;
+ if (handlers.length > 0)
+ setHandlers(handlers);
+ }
+
+ /**
+ * @return the array of handlers.
+ */
+ @Override
+ @ManagedAttribute(value = "Wrapped handlers", readonly = true)
+ public Handler[] getHandlers()
+ {
+ Handlers handlers = _handlers.get();
+ return handlers == null ? null : handlers._handlers;
+ }
+
+ /**
+ * @param handlers the array of handlers to set.
+ */
+ public void setHandlers(Handler[] handlers)
+ {
+ if (!_mutableWhenRunning && isStarted())
+ throw new IllegalStateException(STARTED);
+
+ while (true)
+ {
+ if (updateHandlers(_handlers.get(), newHandlers(handlers)))
+ break;
+ }
+ }
+
+ protected Handlers newHandlers(Handler[] handlers)
+ {
+ if (handlers == null || handlers.length == 0)
+ return null;
+ return new Handlers(handlers);
+ }
+
+ protected boolean updateHandlers(Handlers old, Handlers handlers)
+ {
+ if (handlers != null)
+ {
+ // check for loops
+ for (Handler handler : handlers._handlers)
+ {
+ if (handler == this || (handler instanceof HandlerContainer &&
+ Arrays.asList(((HandlerContainer)handler).getChildHandlers()).contains(this)))
+ throw new IllegalStateException("setHandler loop");
+ }
+
+ // Set server
+ for (Handler handler : handlers._handlers)
+ {
+ if (handler.getServer() != getServer())
+ handler.setServer(getServer());
+ }
+ }
+
+ if (_handlers.compareAndSet(old, handlers))
+ {
+ Handler[] oldBeans = old == null ? null : old._handlers;
+ Handler[] newBeans = handlers == null ? null : handlers._handlers;
+ updateBeans(oldBeans, newBeans);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ if (isStarted())
+ {
+ Handlers handlers = _handlers.get();
+ if (handlers == null)
+ return;
+
+ MultiException mex = null;
+ for (Handler handler : handlers._handlers)
+ {
+ try
+ {
+ handler.handle(target, baseRequest, request, response);
+ }
+ catch (IOException | RuntimeException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ if (mex == null)
+ mex = new MultiException();
+ mex.add(e);
+ }
+ }
+ if (mex != null)
+ {
+ if (mex.size() == 1)
+ throw new ServletException(mex.getThrowable(0));
+ else
+ throw new ServletException(mex);
+ }
+ }
+ }
+
+ /**
+ * Adds a handler.
+ * This implementation adds the passed handler to the end of the existing collection of handlers.
+ * If the handler is already added, it is removed and readded
+ */
+ public void addHandler(Handler handler)
+ {
+ while (true)
+ {
+ Handlers old = _handlers.get();
+ Handlers handlers = newHandlers(ArrayUtil.addToArray(old == null ? null : ArrayUtil.removeFromArray(old._handlers, handler), handler, Handler.class));
+ if (updateHandlers(old, handlers))
+ break;
+ }
+ }
+
+ /**
+ * Prepends a handler.
+ * This implementation adds the passed handler to the start of the existing collection of handlers.
+ */
+ public void prependHandler(Handler handler)
+ {
+ while (true)
+ {
+ Handlers old = _handlers.get();
+ Handlers handlers = newHandlers(ArrayUtil.prependToArray(handler, old == null ? null : old._handlers, Handler.class));
+ if (updateHandlers(old, handlers))
+ break;
+ }
+ }
+
+ public void removeHandler(Handler handler)
+ {
+ while (true)
+ {
+ Handlers old = _handlers.get();
+ if (old == null || old._handlers.length == 0)
+ break;
+ Handlers handlers = newHandlers(ArrayUtil.removeFromArray(old._handlers, handler));
+ if (updateHandlers(old, handlers))
+ break;
+ }
+ }
+
+ @Override
+ protected void expandChildren(List<Handler> list, Class<?> byClass)
+ {
+ Handler[] handlers = getHandlers();
+ if (handlers != null)
+ for (Handler h : handlers)
+ {
+ expandHandler(h, list, byClass);
+ }
+ }
+
+ @Override
+ public void destroy()
+ {
+ if (!isStopped())
+ throw new IllegalStateException("!STOPPED");
+ Handler[] children = getChildHandlers();
+ setHandlers(null);
+ for (Handler child : children)
+ {
+ child.destroy();
+ }
+ super.destroy();
+ }
+
+ protected static class Handlers
+ {
+ private final Handler[] _handlers;
+
+ protected Handlers(Handler[] handlers)
+ {
+ this._handlers = handlers;
+ }
+
+ public Handler[] getHandlers()
+ {
+ return _handlers;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerList.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerList.java
new file mode 100644
index 0000000..d184309
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerList.java
@@ -0,0 +1,65 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+
+/**
+ * HandlerList.
+ * This extension of {@link HandlerCollection} will call
+ * each contained handler in turn until either an exception is thrown, the response
+ * is committed or a positive response status is set.
+ */
+public class HandlerList extends HandlerCollection
+{
+ public HandlerList()
+ {
+ }
+
+ public HandlerList(Handler... handlers)
+ {
+ super(handlers);
+ }
+
+ /**
+ * @see Handler#handle(String, Request, HttpServletRequest, HttpServletResponse)
+ */
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ Handler[] handlers = getHandlers();
+
+ if (handlers != null && isStarted())
+ {
+ for (int i = 0; i < handlers.length; i++)
+ {
+ handlers[i].handle(target, baseRequest, request, response);
+ if (baseRequest.isHandled())
+ return;
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerWrapper.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerWrapper.java
new file mode 100644
index 0000000..dc99a07
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerWrapper.java
@@ -0,0 +1,149 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HandlerContainer;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.LifeCycle;
+
+/**
+ * A <code>HandlerWrapper</code> acts as a {@link Handler} but delegates the {@link Handler#handle handle} method and
+ * {@link LifeCycle life cycle} events to a delegate. This is primarily used to implement the <i>Decorator</i> pattern.
+ */
+@ManagedObject("Handler wrapping another Handler")
+public class HandlerWrapper extends AbstractHandlerContainer
+{
+ protected Handler _handler;
+
+ /**
+ *
+ */
+ public HandlerWrapper()
+ {
+ }
+
+ /**
+ * @return Returns the handlers.
+ */
+ @ManagedAttribute(value = "Wrapped Handler", readonly = true)
+ public Handler getHandler()
+ {
+ return _handler;
+ }
+
+ /**
+ * @return Returns the handlers.
+ */
+ @Override
+ public Handler[] getHandlers()
+ {
+ if (_handler == null)
+ return new Handler[0];
+ return new Handler[]{_handler};
+ }
+
+ /**
+ * @param handler Set the {@link Handler} which should be wrapped.
+ */
+ public void setHandler(Handler handler)
+ {
+ if (isStarted())
+ throw new IllegalStateException(STARTED);
+
+ // check for loops
+ if (handler == this || (handler instanceof HandlerContainer &&
+ Arrays.asList(((HandlerContainer)handler).getChildHandlers()).contains(this)))
+ throw new IllegalStateException("setHandler loop");
+
+ if (handler != null)
+ handler.setServer(getServer());
+
+ Handler old = _handler;
+ _handler = handler;
+ updateBean(old, _handler, true);
+ }
+
+ /**
+ * Replace the current handler with another HandlerWrapper
+ * linked to the current handler.
+ * <p>
+ * This is equivalent to:
+ * <pre>
+ * wrapper.setHandler(getHandler());
+ * setHandler(wrapper);
+ * </pre>
+ *
+ * @param wrapper the wrapper to insert
+ */
+ public void insertHandler(HandlerWrapper wrapper)
+ {
+ if (wrapper == null)
+ throw new IllegalArgumentException();
+
+ HandlerWrapper tail = wrapper;
+ while (tail.getHandler() instanceof HandlerWrapper)
+ {
+ tail = (HandlerWrapper)tail.getHandler();
+ }
+ if (tail.getHandler() != null)
+ throw new IllegalArgumentException("bad tail of inserted wrapper chain");
+
+ Handler next = getHandler();
+ setHandler(wrapper);
+ tail.setHandler(next);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ Handler handler = _handler;
+ if (handler != null)
+ handler.handle(target, baseRequest, request, response);
+ }
+
+ @Override
+ protected void expandChildren(List<Handler> list, Class<?> byClass)
+ {
+ expandHandler(_handler, list, byClass);
+ }
+
+ @Override
+ public void destroy()
+ {
+ if (!isStopped())
+ throw new IllegalStateException("!STOPPED");
+ Handler child = getHandler();
+ if (child != null)
+ {
+ setHandler(null);
+ child.destroy();
+ }
+ super.destroy();
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HotSwapHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HotSwapHandler.java
new file mode 100644
index 0000000..c450fef
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HotSwapHandler.java
@@ -0,0 +1,136 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.util.List;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+
+/**
+ * A <code>HandlerContainer</code> that allows a hot swap of a wrapped handler.
+ */
+public class HotSwapHandler extends AbstractHandlerContainer
+{
+ private volatile Handler _handler;
+
+ /**
+ *
+ */
+ public HotSwapHandler()
+ {
+ }
+
+ /**
+ * @return Returns the handlers.
+ */
+ public Handler getHandler()
+ {
+ return _handler;
+ }
+
+ /**
+ * @return Returns the handlers.
+ */
+ @Override
+ public Handler[] getHandlers()
+ {
+ Handler handler = _handler;
+ if (handler == null)
+ return new Handler[0];
+ return new Handler[]{handler};
+ }
+
+ /**
+ * @param handler Set the {@link Handler} which should be wrapped.
+ */
+ public void setHandler(Handler handler)
+ {
+ try
+ {
+ Server server = getServer();
+ if (handler != null)
+ handler.setServer(server);
+ updateBean(_handler, handler, true);
+ _handler = handler;
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /*
+ * @see org.eclipse.thread.AbstractLifeCycle#doStart()
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+ }
+
+ /*
+ * @see org.eclipse.thread.AbstractLifeCycle#doStop()
+ */
+ @Override
+ protected void doStop() throws Exception
+ {
+ super.doStop();
+ }
+
+ /*
+ * @see org.eclipse.jetty.server.server.EventHandler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+ */
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ Handler handler = _handler;
+ if (handler != null && isStarted() && handler.isStarted())
+ {
+ handler.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @Override
+ protected void expandChildren(List<Handler> list, Class<?> byClass)
+ {
+ Handler handler = _handler;
+ if (handler != null)
+ expandHandler(handler, list, byClass);
+ }
+
+ @Override
+ public void destroy()
+ {
+ if (!isStopped())
+ throw new IllegalStateException("!STOPPED");
+ Handler child = getHandler();
+ if (child != null)
+ {
+ setHandler(null);
+ child.destroy();
+ }
+ super.destroy();
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IPAccessHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IPAccessHandler.java
new file mode 100644
index 0000000..e775f65
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IPAccessHandler.java
@@ -0,0 +1,343 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Map;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.PathMap;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.IPAddressMap;
+import org.eclipse.jetty.util.component.DumpableCollection;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * IP Access Handler
+ * <p>
+ * Controls access to the wrapped handler by the real remote IP. Control is provided
+ * by white/black lists that include both internet addresses and URIs. This handler
+ * uses the real internet address of the connection, not one reported in the forwarded
+ * for headers, as this cannot be as easily forged.
+ * <p>
+ * Typically, the black/white lists will be used in one of three modes:
+ * <ul>
+ * <li>Blocking a few specific IPs/URLs by specifying several black list entries.
+ * <li>Allowing only some specific IPs/URLs by specifying several white lists entries.
+ * <li>Allowing a general range of IPs/URLs by specifying several general white list
+ * entries, that are then further refined by several specific black list exceptions
+ * </ul>
+ * <p>
+ * By default an empty white list is treated as match all. If there is at least one entry in
+ * the white list, then a request must match a white list entry. Black list entries
+ * are always applied, so that even if an entry matches the white list, a black list
+ * entry will override it.
+ * <p>
+ * You can change white list policy setting whiteListByPath to true. In this mode a request will be white listed
+ * IF it has a matching URL in the white list, otherwise the black list applies, e.g. in default mode when
+ * whiteListByPath = false and wl = "127.0.0.1|/foo", /bar request from 127.0.0.1 will be blacklisted,
+ * if whiteListByPath=true then not.
+ * <p>
+ * Internet addresses may be specified as absolute address or as a combination of
+ * four octet wildcard specifications (a.b.c.d) that are defined as follows.
+ * </p>
+ * <pre>
+ * nnn - an absolute value (0-255)
+ * mmm-nnn - an inclusive range of absolute values,
+ * with following shorthand notations:
+ * nnn- => nnn-255
+ * -nnn => 0-nnn
+ * - => 0-255
+ * a,b,... - a list of wildcard specifications
+ * </pre>
+ * <p>
+ * Internet address specification is separated from the URI pattern using the "|" (pipe)
+ * character. URI patterns follow the servlet specification for simple * prefix and
+ * suffix wild cards (e.g. /, /foo, /foo/bar, /foo/bar/*, *.baz).
+ * <p>
+ * Earlier versions of the handler used internet address prefix wildcard specification
+ * to define a range of the internet addresses (e.g. 127., 10.10., 172.16.1.).
+ * They also used the first "/" character of the URI pattern to separate it from the
+ * internet address. Both of these features have been deprecated in the current version.
+ * <p>
+ * Examples of the entry specifications are:
+ * <ul>
+ * <li>10.10.1.2 - all requests from IP 10.10.1.2
+ * <li>10.10.1.2|/foo/bar - all requests from IP 10.10.1.2 to URI /foo/bar
+ * <li>10.10.1.2|/foo/* - all requests from IP 10.10.1.2 to URIs starting with /foo/
+ * <li>10.10.1.2|*.html - all requests from IP 10.10.1.2 to URIs ending with .html
+ * <li>10.10.0-255.0-255 - all requests from IPs within 10.10.0.0/16 subnet
+ * <li>10.10.0-.-255|/foo/bar - all requests from IPs within 10.10.0.0/16 subnet to URI /foo/bar
+ * <li>10.10.0-3,1,3,7,15|/foo/* - all requests from IPs addresses with last octet equal
+ * to 1,3,7,15 in subnet 10.10.0.0/22 to URIs starting with /foo/
+ * </ul>
+ * <p>
+ * Earlier versions of the handler used internet address prefix wildcard specification
+ * to define a range of the internet addresses (e.g. 127., 10.10., 172.16.1.).
+ * They also used the first "/" character of the URI pattern to separate it from the
+ * internet address. Both of these features have been deprecated in the current version.
+ *
+ * @see InetAccessHandler
+ * @deprecated Use @{@link InetAccessHandler}.
+ */
+@Deprecated
+public class IPAccessHandler extends HandlerWrapper
+{
+ private static final Logger LOG = Log.getLogger(IPAccessHandler.class);
+ // true means nodefault match
+ PathMap<IPAddressMap<Boolean>> _white = new PathMap<IPAddressMap<Boolean>>(true);
+ PathMap<IPAddressMap<Boolean>> _black = new PathMap<IPAddressMap<Boolean>>(true);
+ boolean _whiteListByPath = false;
+
+ /**
+ * Creates new handler object
+ */
+ public IPAccessHandler()
+ {
+ super();
+ }
+
+ /**
+ * Creates new handler object and initializes white- and black-list
+ *
+ * @param white array of whitelist entries
+ * @param black array of blacklist entries
+ */
+ public IPAccessHandler(String[] white, String[] black)
+ {
+ super();
+
+ if (white != null && white.length > 0)
+ setWhite(white);
+ if (black != null && black.length > 0)
+ setBlack(black);
+ }
+
+ /**
+ * Add a whitelist entry to an existing handler configuration
+ *
+ * @param entry new whitelist entry
+ */
+ public void addWhite(String entry)
+ {
+ add(entry, _white);
+ }
+
+ /**
+ * Add a blacklist entry to an existing handler configuration
+ *
+ * @param entry new blacklist entry
+ */
+ public void addBlack(String entry)
+ {
+ add(entry, _black);
+ }
+
+ /**
+ * Re-initialize the whitelist of existing handler object
+ *
+ * @param entries array of whitelist entries
+ */
+ public void setWhite(String[] entries)
+ {
+ set(entries, _white);
+ }
+
+ /**
+ * Re-initialize the blacklist of existing handler object
+ *
+ * @param entries array of blacklist entries
+ */
+ public void setBlack(String[] entries)
+ {
+ set(entries, _black);
+ }
+
+ /**
+ * Re-initialize the mode of path matching
+ *
+ * @param whiteListByPath matching mode
+ */
+ public void setWhiteListByPath(boolean whiteListByPath)
+ {
+ this._whiteListByPath = whiteListByPath;
+ }
+
+ /**
+ * Checks the incoming request against the whitelist and blacklist
+ *
+ * @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+ */
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ // Get the real remote IP (not the one set by the forwarded headers (which may be forged))
+ HttpChannel channel = baseRequest.getHttpChannel();
+ if (channel != null)
+ {
+ EndPoint endp = channel.getEndPoint();
+ if (endp != null)
+ {
+ InetSocketAddress address = endp.getRemoteAddress();
+ if (address != null && !isAddrUriAllowed(address.getHostString(), baseRequest.getMetaData().getURI().getDecodedPath()))
+ {
+ response.sendError(HttpStatus.FORBIDDEN_403);
+ baseRequest.setHandled(true);
+ return;
+ }
+ }
+ }
+
+ getHandler().handle(target, baseRequest, request, response);
+ }
+
+ /**
+ * Helper method to parse the new entry and add it to
+ * the specified address pattern map.
+ *
+ * @param entry new entry
+ * @param patternMap target address pattern map
+ */
+ protected void add(String entry, PathMap<IPAddressMap<Boolean>> patternMap)
+ {
+ if (entry != null && entry.length() > 0)
+ {
+ boolean deprecated = false;
+ int idx;
+ if (entry.indexOf('|') > 0)
+ {
+ idx = entry.indexOf('|');
+ }
+ else
+ {
+ idx = entry.indexOf('/');
+ deprecated = (idx >= 0);
+ }
+
+ String addr = idx > 0 ? entry.substring(0, idx) : entry;
+ String path = idx > 0 ? entry.substring(idx) : "/*";
+
+ if (addr.endsWith("."))
+ deprecated = true;
+ if (path != null && (path.startsWith("|") || path.startsWith("/*.")))
+ path = path.substring(1);
+
+ IPAddressMap<Boolean> addrMap = patternMap.get(path);
+ if (addrMap == null)
+ {
+ addrMap = new IPAddressMap<Boolean>();
+ patternMap.put(path, addrMap);
+ }
+ if (addr != null && !"".equals(addr))
+ // MUST NOT BE null
+ addrMap.put(addr, true);
+
+ if (deprecated)
+ LOG.debug(toString() + " - deprecated specification syntax: " + entry);
+ }
+ }
+
+ /**
+ * Helper method to process a list of new entries and replace
+ * the content of the specified address pattern map
+ *
+ * @param entries new entries
+ * @param patternMap target address pattern map
+ */
+ protected void set(String[] entries, PathMap<IPAddressMap<Boolean>> patternMap)
+ {
+ patternMap.clear();
+
+ if (entries != null && entries.length > 0)
+ {
+ for (String addrPath : entries)
+ {
+ add(addrPath, patternMap);
+ }
+ }
+ }
+
+ /**
+ * Check if specified request is allowed by current IPAccess rules.
+ *
+ * @param addr internet address
+ * @param path request URI path
+ * @return true if request is allowed
+ */
+ protected boolean isAddrUriAllowed(String addr, String path)
+ {
+ if (_white.size() > 0)
+ {
+ boolean match = false;
+ boolean matchedByPath = false;
+
+ for (Map.Entry<String, IPAddressMap<Boolean>> entry : _white.getMatches(path))
+ {
+ matchedByPath = true;
+ IPAddressMap<Boolean> addrMap = entry.getValue();
+ if ((addrMap != null && (addrMap.size() == 0 || addrMap.match(addr) != null)))
+ {
+ match = true;
+ break;
+ }
+ }
+
+ if (_whiteListByPath)
+ {
+ if (matchedByPath && !match)
+ return false;
+ }
+ else
+ {
+ if (!match)
+ return false;
+ }
+ }
+
+ if (_black.size() > 0)
+ {
+ for (Map.Entry<String, IPAddressMap<Boolean>> entry : _black.getMatches(path))
+ {
+ IPAddressMap<Boolean> addrMap = entry.getValue();
+ if (addrMap != null && (addrMap.size() == 0 || addrMap.match(addr) != null))
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Dump the handler configuration
+ */
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ dumpObjects(out, indent,
+ DumpableCollection.from("white", _white),
+ DumpableCollection.from("black", _black),
+ DumpableCollection.from("whiteListByPath", _whiteListByPath));
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IdleTimeoutHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IdleTimeoutHandler.java
new file mode 100644
index 0000000..fef2570
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IdleTimeoutHandler.java
@@ -0,0 +1,121 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.Request;
+
+/**
+ * Handler to adjust the idle timeout of requests while dispatched.
+ * Can be applied in jetty.xml with
+ * <pre>
+ * <Get id='handler' name='Handler'/>
+ * <Set name='Handler'>
+ * <New id='idleTimeoutHandler' class='org.eclipse.jetty.server.handler.IdleTimeoutHandler'>
+ * <Set name='Handler'><Ref id='handler'/></Set>
+ * <Set name='IdleTimeoutMs'>5000</Set>
+ * </New>
+ * </Set>
+ * </pre>
+ */
+public class IdleTimeoutHandler extends HandlerWrapper
+{
+ private long _idleTimeoutMs = 1000;
+ private boolean _applyToAsync = false;
+
+ public boolean isApplyToAsync()
+ {
+ return _applyToAsync;
+ }
+
+ /**
+ * Should the adjusted idle time be maintained for asynchronous requests
+ *
+ * @param applyToAsync true if alternate idle timeout is applied to asynchronous requests
+ */
+ public void setApplyToAsync(boolean applyToAsync)
+ {
+ _applyToAsync = applyToAsync;
+ }
+
+ public long getIdleTimeoutMs()
+ {
+ return _idleTimeoutMs;
+ }
+
+ /**
+ * @param idleTimeoutMs The idle timeout in MS to apply while dispatched or async
+ */
+ public void setIdleTimeoutMs(long idleTimeoutMs)
+ {
+ this._idleTimeoutMs = idleTimeoutMs;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ final HttpChannel channel = baseRequest.getHttpChannel();
+ final long idle_timeout = baseRequest.getHttpChannel().getIdleTimeout();
+ channel.setIdleTimeout(_idleTimeoutMs);
+
+ try
+ {
+ super.handle(target, baseRequest, request, response);
+ }
+ finally
+ {
+ if (_applyToAsync && request.isAsyncStarted())
+ {
+ request.getAsyncContext().addListener(new AsyncListener()
+ {
+ @Override
+ public void onTimeout(AsyncEvent event) throws IOException
+ {
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event) throws IOException
+ {
+ }
+
+ @Override
+ public void onError(AsyncEvent event) throws IOException
+ {
+ channel.setIdleTimeout(idle_timeout);
+ }
+
+ @Override
+ public void onComplete(AsyncEvent event) throws IOException
+ {
+ channel.setIdleTimeout(idle_timeout);
+ }
+ });
+ }
+ else
+ channel.setIdleTimeout(idle_timeout);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java
new file mode 100644
index 0000000..c4e7f5c
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java
@@ -0,0 +1,212 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.IncludeExclude;
+import org.eclipse.jetty.util.IncludeExcludeSet;
+import org.eclipse.jetty.util.InetAddressSet;
+import org.eclipse.jetty.util.component.DumpableCollection;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * InetAddress Access Handler
+ * <p>
+ * Controls access to the wrapped handler using the real remote IP. Control is
+ * provided by and {@link IncludeExcludeSet} over a {@link InetAddressSet}. This
+ * handler uses the real internet address of the connection, not one reported in
+ * the forwarded for headers, as this cannot be as easily forged.
+ * <p>
+ * Additionally, there may be times when you want to only apply this handler to
+ * a subset of your connectors. In this situation you can use
+ * <b>connectorNames</b> to specify the connector names that you want this IP
+ * access filter to apply to.
+ */
+public class InetAccessHandler extends HandlerWrapper
+{
+ private static final Logger LOG = Log.getLogger(InetAccessHandler.class);
+
+ private final IncludeExcludeSet<String, InetAddress> _addrs = new IncludeExcludeSet<>(InetAddressSet.class);
+ private final IncludeExclude<String> _names = new IncludeExclude<>();
+
+ /**
+ * Clears all the includes, excludes, included connector names and excluded
+ * connector names.
+ */
+ public void clear()
+ {
+ _addrs.clear();
+ _names.clear();
+ }
+
+ /**
+ * Includes an InetAddress pattern
+ *
+ * @param pattern InetAddress pattern to include
+ * @see InetAddressSet
+ */
+ public void include(String pattern)
+ {
+ _addrs.include(pattern);
+ }
+
+ /**
+ * Includes InetAddress patterns
+ *
+ * @param patterns InetAddress patterns to include
+ * @see InetAddressSet
+ */
+ public void include(String... patterns)
+ {
+ _addrs.include(patterns);
+ }
+
+ /**
+ * Excludes an InetAddress pattern
+ *
+ * @param pattern InetAddress pattern to exclude
+ * @see InetAddressSet
+ */
+ public void exclude(String pattern)
+ {
+ _addrs.exclude(pattern);
+ }
+
+ /**
+ * Excludes InetAddress patterns
+ *
+ * @param patterns InetAddress patterns to exclude
+ * @see InetAddressSet
+ */
+ public void exclude(String... patterns)
+ {
+ _addrs.exclude(patterns);
+ }
+
+ /**
+ * Includes a connector name.
+ *
+ * @param name Connector name to include in this handler.
+ */
+ public void includeConnector(String name)
+ {
+ _names.include(name);
+ }
+
+ /**
+ * Excludes a connector name.
+ *
+ * @param name Connector name to exclude in this handler.
+ */
+ public void excludeConnector(String name)
+ {
+ _names.exclude(name);
+ }
+
+ /**
+ * Includes connector names.
+ *
+ * @param names Connector names to include in this handler.
+ */
+ public void includeConnectors(String... names)
+ {
+ _names.include(names);
+ }
+
+ /**
+ * Excludes connector names.
+ *
+ * @param names Connector names to exclude in this handler.
+ */
+ public void excludeConnectors(String... names)
+ {
+ _names.exclude(names);
+ }
+
+ /**
+ * Checks the incoming request against the whitelist and blacklist
+ */
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ // Get the real remote IP (not the one set by the forwarded headers (which may be forged))
+ HttpChannel channel = baseRequest.getHttpChannel();
+ if (channel != null)
+ {
+ EndPoint endp = channel.getEndPoint();
+ if (endp != null)
+ {
+ InetSocketAddress address = endp.getRemoteAddress();
+ if (address != null && !isAllowed(address.getAddress(), baseRequest, request))
+ {
+ response.sendError(HttpStatus.FORBIDDEN_403);
+ baseRequest.setHandled(true);
+ return;
+ }
+ }
+ }
+
+ getHandler().handle(target, baseRequest, request, response);
+ }
+
+ /**
+ * Checks if specified address and request are allowed by current InetAddress rules.
+ *
+ * @param addr the inetAddress to check
+ * @param baseRequest the base request to check
+ * @param request the HttpServletRequest request to check
+ * @return true if inetAddress and request are allowed
+ */
+ protected boolean isAllowed(InetAddress addr, Request baseRequest, HttpServletRequest request)
+ {
+ String name = baseRequest.getHttpChannel().getConnector().getName();
+ boolean filterAppliesToConnector = _names.test(name);
+ boolean allowedByAddr = _addrs.test(addr);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("name = {}/{} addr={}/{} appliesToConnector={} allowedByAddr={}",
+ name, _names, addr, _addrs, filterAppliesToConnector, allowedByAddr);
+ }
+ if (!filterAppliesToConnector)
+ return true;
+ return allowedByAddr;
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ dumpObjects(out, indent,
+ new DumpableCollection("included", _addrs.getIncluded()),
+ new DumpableCollection("excluded", _addrs.getExcluded()),
+ new DumpableCollection("includedConnector", _names.getIncluded()),
+ new DumpableCollection("excludedConnector", _names.getExcluded()));
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ManagedAttributeListener.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ManagedAttributeListener.java
new file mode 100644
index 0000000..eeda802
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ManagedAttributeListener.java
@@ -0,0 +1,102 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.util.HashSet;
+import java.util.Set;
+import javax.servlet.ServletContextAttributeEvent;
+import javax.servlet.ServletContextAttributeListener;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Enable Jetty style JMX MBeans from within a Context
+ */
+public class ManagedAttributeListener implements ServletContextListener, ServletContextAttributeListener
+{
+ private static final Logger LOG = Log.getLogger(ManagedAttributeListener.class);
+
+ final Set<String> _managedAttributes = new HashSet<>();
+ final ContextHandler _context;
+
+ public ManagedAttributeListener(ContextHandler context, String... managedAttributes)
+ {
+ _context = context;
+
+ for (String attr : managedAttributes)
+ {
+ _managedAttributes.add(attr);
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("managedAttributes {}", _managedAttributes);
+ }
+
+ @Override
+ public void attributeReplaced(ServletContextAttributeEvent event)
+ {
+ if (_managedAttributes.contains(event.getName()))
+ updateBean(event.getName(), event.getValue(), event.getServletContext().getAttribute(event.getName()));
+ }
+
+ @Override
+ public void attributeRemoved(ServletContextAttributeEvent event)
+ {
+ if (_managedAttributes.contains(event.getName()))
+ updateBean(event.getName(), event.getValue(), null);
+ }
+
+ @Override
+ public void attributeAdded(ServletContextAttributeEvent event)
+ {
+ if (_managedAttributes.contains(event.getName()))
+ updateBean(event.getName(), null, event.getValue());
+ }
+
+ @Override
+ public void contextInitialized(ServletContextEvent event)
+ {
+ // Update existing attributes
+ for (String name : _context.getServletContext().getAttributeNameSet())
+ {
+ if (_managedAttributes.contains(name))
+ updateBean(name, null, event.getServletContext().getAttribute(name));
+ }
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent event)
+ {
+ for (String name : _context.getServletContext().getAttributeNameSet())
+ {
+ if (_managedAttributes.contains(name))
+ updateBean(name, event.getServletContext().getAttribute(name), null);
+ }
+ }
+
+ protected void updateBean(String name, Object oldBean, Object newBean)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("update {} {}->{} on {}", name, oldBean, newBean, _context);
+ _context.updateBean(oldBean, newBean, false);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java
new file mode 100644
index 0000000..1a482ee
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java
@@ -0,0 +1,150 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.server.HandlerContainer;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.URIUtil;
+
+/**
+ * Moved ContextHandler.
+ * This context can be used to replace a context that has changed
+ * location. Requests are redirected (either to a fixed URL or to a
+ * new context base).
+ */
+public class MovedContextHandler extends ContextHandler
+{
+ final Redirector _redirector;
+ String _newContextURL;
+ boolean _discardPathInfo;
+ boolean _discardQuery;
+ boolean _permanent;
+ String _expires;
+
+ public MovedContextHandler()
+ {
+ _redirector = new Redirector();
+ setHandler(_redirector);
+ setAllowNullPathInfo(true);
+ }
+
+ public MovedContextHandler(HandlerContainer parent, String contextPath, String newContextURL)
+ {
+ super(parent, contextPath);
+ _newContextURL = newContextURL;
+ _redirector = new Redirector();
+ setHandler(_redirector);
+ }
+
+ public boolean isDiscardPathInfo()
+ {
+ return _discardPathInfo;
+ }
+
+ public void setDiscardPathInfo(boolean discardPathInfo)
+ {
+ _discardPathInfo = discardPathInfo;
+ }
+
+ public String getNewContextURL()
+ {
+ return _newContextURL;
+ }
+
+ public void setNewContextURL(String newContextURL)
+ {
+ _newContextURL = newContextURL;
+ }
+
+ public boolean isPermanent()
+ {
+ return _permanent;
+ }
+
+ public void setPermanent(boolean permanent)
+ {
+ _permanent = permanent;
+ }
+
+ public boolean isDiscardQuery()
+ {
+ return _discardQuery;
+ }
+
+ public void setDiscardQuery(boolean discardQuery)
+ {
+ _discardQuery = discardQuery;
+ }
+
+ private class Redirector extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (_newContextURL == null)
+ return;
+
+ String path = _newContextURL;
+ if (!_discardPathInfo && request.getPathInfo() != null)
+ path = URIUtil.addPaths(path, request.getPathInfo());
+
+ StringBuilder location = URIUtil.hasScheme(path) ? new StringBuilder() : baseRequest.getRootURL();
+
+ location.append(path);
+ if (!_discardQuery && request.getQueryString() != null)
+ {
+ location.append('?');
+ String q = request.getQueryString();
+ q = q.replaceAll("\r\n?&=", "!");
+ location.append(q);
+ }
+
+ response.setHeader(HttpHeader.LOCATION.asString(), location.toString());
+
+ if (_expires != null)
+ response.setHeader(HttpHeader.EXPIRES.asString(), _expires);
+
+ response.setStatus(_permanent ? HttpServletResponse.SC_MOVED_PERMANENTLY : HttpServletResponse.SC_FOUND);
+ response.setContentLength(0);
+ baseRequest.setHandled(true);
+ }
+ }
+
+ /**
+ * @return the expires header value or null if no expires header
+ */
+ public String getExpires()
+ {
+ return _expires;
+ }
+
+ /**
+ * @param expires the expires header value or null if no expires header
+ */
+ public void setExpires(String expires)
+ {
+ _expires = expires;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/RequestLogHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/RequestLogHandler.java
new file mode 100644
index 0000000..53539eb
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/RequestLogHandler.java
@@ -0,0 +1,67 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.RequestLog;
+import org.eclipse.jetty.server.Server;
+
+/**
+ * <p>This handler provides an alternate way (other than {@link Server#setRequestLog(RequestLog)})
+ * to log request, that can be applied to a particular handler (eg context).
+ * This handler can be used to wrap an individual context for context logging, or can be listed
+ * prior to a handler.
+ * </p>
+ *
+ * @see Server#setRequestLog(RequestLog)
+ */
+public class RequestLogHandler extends HandlerWrapper
+{
+ private RequestLog _requestLog;
+
+ /*
+ * @see org.eclipse.jetty.server.server.Handler#handle(java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
+ */
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ if (baseRequest.getDispatcherType() == DispatcherType.REQUEST)
+ baseRequest.getHttpChannel().addRequestLog(_requestLog);
+ if (_handler != null)
+ _handler.handle(target, baseRequest, request, response);
+ }
+
+ public void setRequestLog(RequestLog requestLog)
+ {
+ updateBean(_requestLog, requestLog);
+ _requestLog = requestLog;
+ }
+
+ public RequestLog getRequestLog()
+ {
+ return _requestLog;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java
new file mode 100644
index 0000000..e98f204
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java
@@ -0,0 +1,507 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.CompressedContentFormat;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.http.PreEncodedHttpField;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.ResourceContentFactory;
+import org.eclipse.jetty.server.ResourceService;
+import org.eclipse.jetty.server.ResourceService.WelcomeFactory;
+import org.eclipse.jetty.server.handler.ContextHandler.Context;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+
+/**
+ * Resource Handler.
+ *
+ * This handle will serve static content and handle If-Modified-Since headers. No caching is done. Requests for resources that do not exist are let pass (Eg no
+ * 404's).
+ */
+public class ResourceHandler extends HandlerWrapper implements ResourceFactory, WelcomeFactory
+{
+ private static final Logger LOG = Log.getLogger(ResourceHandler.class);
+
+ Resource _baseResource;
+ ContextHandler _context;
+ Resource _defaultStylesheet;
+ MimeTypes _mimeTypes;
+ private final ResourceService _resourceService;
+ Resource _stylesheet;
+ String[] _welcomes = {"index.html"};
+
+ public ResourceHandler(ResourceService resourceService)
+ {
+ _resourceService = resourceService;
+ }
+
+ public ResourceHandler()
+ {
+ this(new ResourceService()
+ {
+ @Override
+ protected void notFound(HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ }
+ });
+ _resourceService.setGzipEquivalentFileExtensions(new ArrayList<>(Arrays.asList(".svgz")));
+ }
+
+ @Override
+ public String getWelcomeFile(String pathInContext)
+ {
+ if (_welcomes == null)
+ return null;
+
+ for (int i = 0; i < _welcomes.length; i++)
+ {
+ String welcomeInContext = URIUtil.addPaths(pathInContext, _welcomes[i]);
+ Resource welcome = getResource(welcomeInContext);
+ if (welcome != null && welcome.exists())
+ return welcomeInContext;
+ }
+ // not found
+ return null;
+ }
+
+ @Override
+ public void doStart() throws Exception
+ {
+ Context scontext = ContextHandler.getCurrentContext();
+ _context = (scontext == null ? null : scontext.getContextHandler());
+ if (_mimeTypes == null)
+ _mimeTypes = _context == null ? new MimeTypes() : _context.getMimeTypes();
+
+ _resourceService.setContentFactory(new ResourceContentFactory(this, _mimeTypes, _resourceService.getPrecompressedFormats()));
+ _resourceService.setWelcomeFactory(this);
+
+ super.doStart();
+ }
+
+ /**
+ * @return Returns the resourceBase.
+ */
+ public Resource getBaseResource()
+ {
+ if (_baseResource == null)
+ return null;
+ return _baseResource;
+ }
+
+ /**
+ * @return the cacheControl header to set on all static content.
+ */
+ public String getCacheControl()
+ {
+ return _resourceService.getCacheControl().getValue();
+ }
+
+ /**
+ * @return file extensions that signify that a file is gzip compressed. Eg ".svgz"
+ */
+ public List<String> getGzipEquivalentFileExtensions()
+ {
+ return _resourceService.getGzipEquivalentFileExtensions();
+ }
+
+ public MimeTypes getMimeTypes()
+ {
+ return _mimeTypes;
+ }
+
+ /**
+ * Get the minimum content length for async handling.
+ *
+ * @return The minimum size in bytes of the content before asynchronous handling is used, or -1 for no async handling or 0 (default) for using
+ * {@link HttpServletResponse#getBufferSize()} as the minimum length.
+ */
+ @Deprecated
+ public int getMinAsyncContentLength()
+ {
+ return -1;
+ }
+
+ /**
+ * Get minimum memory mapped file content length.
+ *
+ * @return the minimum size in bytes of a file resource that will be served using a memory mapped buffer, or -1 (default) for no memory mapped buffers.
+ */
+ @Deprecated
+ public int getMinMemoryMappedContentLength()
+ {
+ return -1;
+ }
+
+ @Override
+ public Resource getResource(String path)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} getResource({})", _context == null ? _baseResource : _context, _baseResource, path);
+
+ if (path == null || !path.startsWith("/"))
+ return null;
+
+ try
+ {
+ Resource r = null;
+
+ if (_baseResource != null)
+ {
+ r = _baseResource.addPath(path);
+
+ if (r != null && r.isAlias() && (_context == null || !_context.checkAlias(path, r)))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("resource={} alias={}", r, r.getAlias());
+ return null;
+ }
+ }
+ else if (_context != null)
+ {
+ r = _context.getResource(path);
+ }
+
+ if ((r == null || !r.exists()) && path.endsWith("/jetty-dir.css"))
+ r = getStylesheet();
+
+ return r;
+ }
+ catch (Exception e)
+ {
+ LOG.debug(e);
+ }
+
+ return null;
+ }
+
+ /**
+ * @return Returns the base resource as a string.
+ */
+ public String getResourceBase()
+ {
+ if (_baseResource == null)
+ return null;
+ return _baseResource.toString();
+ }
+
+ /**
+ * @return Returns the stylesheet as a Resource.
+ */
+ public Resource getStylesheet()
+ {
+ if (_stylesheet != null)
+ {
+ return _stylesheet;
+ }
+ else
+ {
+ if (_defaultStylesheet == null)
+ {
+ _defaultStylesheet = getDefaultStylesheet();
+ }
+ return _defaultStylesheet;
+ }
+ }
+
+ public static Resource getDefaultStylesheet()
+ {
+ return Resource.newResource(ResourceHandler.class.getResource("/jetty-dir.css"));
+ }
+
+ public String[] getWelcomeFiles()
+ {
+ return _welcomes;
+ }
+
+ /*
+ * @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
+ */
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (baseRequest.isHandled())
+ return;
+
+ if (!HttpMethod.GET.is(request.getMethod()) && !HttpMethod.HEAD.is(request.getMethod()))
+ {
+ // try another handler
+ super.handle(target, baseRequest, request, response);
+ return;
+ }
+
+ if (_resourceService.doGet(request, response))
+ baseRequest.setHandled(true);
+ else
+ // no resource - try other handlers
+ super.handle(target, baseRequest, request, response);
+ }
+
+ /**
+ * @return If true, range requests and responses are supported
+ */
+ public boolean isAcceptRanges()
+ {
+ return _resourceService.isAcceptRanges();
+ }
+
+ /**
+ * @return If true, directory listings are returned if no welcome file is found. Else 403 Forbidden.
+ */
+ public boolean isDirAllowed()
+ {
+ return _resourceService.isDirAllowed();
+ }
+
+ /**
+ * Get the directory option.
+ *
+ * @return true if directories are listed.
+ */
+ public boolean isDirectoriesListed()
+ {
+ return _resourceService.isDirAllowed();
+ }
+
+ /**
+ * @return True if ETag processing is done
+ */
+ public boolean isEtags()
+ {
+ return _resourceService.isEtags();
+ }
+
+ /**
+ * @return If set to true, then static content will be served as gzip content encoded if a matching resource is found ending with ".gz"
+ */
+ @Deprecated
+ public boolean isGzip()
+ {
+ for (CompressedContentFormat formats : _resourceService.getPrecompressedFormats())
+ {
+ if (CompressedContentFormat.GZIP.getEncoding().equals(formats.getEncoding()))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return Precompressed resources formats that can be used to serve compressed variant of resources.
+ */
+ public CompressedContentFormat[] getPrecompressedFormats()
+ {
+ return _resourceService.getPrecompressedFormats();
+ }
+
+ /**
+ * @return true, only the path info will be applied to the resourceBase
+ */
+ public boolean isPathInfoOnly()
+ {
+ return _resourceService.isPathInfoOnly();
+ }
+
+ /**
+ * @return If true, welcome files are redirected rather than forwarded to.
+ */
+ public boolean isRedirectWelcome()
+ {
+ return _resourceService.isRedirectWelcome();
+ }
+
+ /**
+ * @param acceptRanges If true, range requests and responses are supported
+ */
+ public void setAcceptRanges(boolean acceptRanges)
+ {
+ _resourceService.setAcceptRanges(acceptRanges);
+ }
+
+ /**
+ * @param base The resourceBase to server content from. If null the
+ * context resource base is used.
+ */
+ public void setBaseResource(Resource base)
+ {
+ _baseResource = base;
+ }
+
+ /**
+ * @param cacheControl the cacheControl header to set on all static content.
+ */
+ public void setCacheControl(String cacheControl)
+ {
+ _resourceService.setCacheControl(new PreEncodedHttpField(HttpHeader.CACHE_CONTROL, cacheControl));
+ }
+
+ /**
+ * @param dirAllowed If true, directory listings are returned if no welcome file is found. Else 403 Forbidden.
+ */
+ public void setDirAllowed(boolean dirAllowed)
+ {
+ _resourceService.setDirAllowed(dirAllowed);
+ }
+
+ /**
+ * Set the directory.
+ *
+ * @param directory true if directories are listed.
+ */
+ public void setDirectoriesListed(boolean directory)
+ {
+ _resourceService.setDirAllowed(directory);
+ }
+
+ /**
+ * @param etags True if ETag processing is done
+ */
+ public void setEtags(boolean etags)
+ {
+ _resourceService.setEtags(etags);
+ }
+
+ /**
+ * @param gzip If set to true, then static content will be served as gzip content encoded if a matching resource is found ending with ".gz"
+ */
+ @Deprecated
+ public void setGzip(boolean gzip)
+ {
+ setPrecompressedFormats(gzip ? new CompressedContentFormat[]{
+ CompressedContentFormat.GZIP
+ } : new CompressedContentFormat[0]);
+ }
+
+ /**
+ * @param gzipEquivalentFileExtensions file extensions that signify that a file is gzip compressed. Eg ".svgz"
+ */
+ public void setGzipEquivalentFileExtensions(List<String> gzipEquivalentFileExtensions)
+ {
+ _resourceService.setGzipEquivalentFileExtensions(gzipEquivalentFileExtensions);
+ }
+
+ /**
+ * @param precompressedFormats The list of precompresed formats to serve in encoded format if matching resource found.
+ * For example serve gzip encoded file if ".gz" suffixed resource is found.
+ */
+ public void setPrecompressedFormats(CompressedContentFormat[] precompressedFormats)
+ {
+ _resourceService.setPrecompressedFormats(precompressedFormats);
+ }
+
+ public void setMimeTypes(MimeTypes mimeTypes)
+ {
+ _mimeTypes = mimeTypes;
+ }
+
+ /**
+ * Set the minimum content length for async handling.
+ *
+ * @param minAsyncContentLength The minimum size in bytes of the content before asynchronous handling is used, or -1 for no async handling or 0 for using
+ * {@link HttpServletResponse#getBufferSize()} as the minimum length.
+ */
+ @Deprecated
+ public void setMinAsyncContentLength(int minAsyncContentLength)
+ {
+ }
+
+ /**
+ * Set minimum memory mapped file content length.
+ *
+ * @param minMemoryMappedFileSize the minimum size in bytes of a file resource that will be served using a memory mapped buffer, or -1 for no memory mapped buffers.
+ */
+ @Deprecated
+ public void setMinMemoryMappedContentLength(int minMemoryMappedFileSize)
+ {
+ }
+
+ /**
+ * @param pathInfoOnly true, only the path info will be applied to the resourceBase
+ */
+ public void setPathInfoOnly(boolean pathInfoOnly)
+ {
+ _resourceService.setPathInfoOnly(pathInfoOnly);
+ }
+
+ /**
+ * @param redirectWelcome If true, welcome files are redirected rather than forwarded to.
+ * redirection is always used if the ResourceHandler is not scoped by
+ * a ContextHandler
+ */
+ public void setRedirectWelcome(boolean redirectWelcome)
+ {
+ _resourceService.setRedirectWelcome(redirectWelcome);
+ }
+
+ /**
+ * @param resourceBase The base resource as a string.
+ */
+ public void setResourceBase(String resourceBase)
+ {
+ try
+ {
+ setBaseResource(Resource.newResource(resourceBase));
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ throw new IllegalArgumentException(resourceBase);
+ }
+ }
+
+ /**
+ * @param stylesheet The location of the stylesheet to be used as a String.
+ */
+ public void setStylesheet(String stylesheet)
+ {
+ try
+ {
+ _stylesheet = Resource.newResource(stylesheet);
+ if (!_stylesheet.exists())
+ {
+ LOG.warn("unable to find custom stylesheet: " + stylesheet);
+ _stylesheet = null;
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ throw new IllegalArgumentException(stylesheet);
+ }
+ }
+
+ public void setWelcomeFiles(String[] welcomeFiles)
+ {
+ _welcomes = welcomeFiles;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ScopedHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ScopedHandler.java
new file mode 100644
index 0000000..037096c
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ScopedHandler.java
@@ -0,0 +1,237 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpConnection;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+
+/**
+ * ScopedHandler.
+ *
+ * A ScopedHandler is a HandlerWrapper where the wrapped handlers
+ * each define a scope.
+ *
+ * <p>When {@link #handle(String, Request, HttpServletRequest, HttpServletResponse)}
+ * is called on the first ScopedHandler in a chain of HandlerWrappers,
+ * the {@link #doScope(String, Request, HttpServletRequest, HttpServletResponse)} method is
+ * called on all contained ScopedHandlers, before the
+ * {@link #doHandle(String, Request, HttpServletRequest, HttpServletResponse)} method
+ * is called on all contained handlers.</p>
+ *
+ * <p>For example if Scoped handlers A, B & C were chained together, then
+ * the calling order would be:</p>
+ * <pre>
+ * A.handle(...)
+ * A.doScope(...)
+ * B.doScope(...)
+ * C.doScope(...)
+ * A.doHandle(...)
+ * B.doHandle(...)
+ * C.doHandle(...)
+ * </pre>
+ *
+ * <p>If non scoped handler X was in the chained A, B, X & C, then
+ * the calling order would be:</p>
+ * <pre>
+ * A.handle(...)
+ * A.doScope(...)
+ * B.doScope(...)
+ * C.doScope(...)
+ * A.doHandle(...)
+ * B.doHandle(...)
+ * X.handle(...)
+ * C.handle(...)
+ * C.doHandle(...)
+ * </pre>
+ *
+ * <p>A typical usage pattern is:</p>
+ * <pre>
+ * private static class MyHandler extends ScopedHandler
+ * {
+ * public void doScope(String target, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ * {
+ * try
+ * {
+ * setUpMyScope();
+ * super.doScope(target,request,response);
+ * }
+ * finally
+ * {
+ * tearDownMyScope();
+ * }
+ * }
+ *
+ * public void doHandle(String target, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ * {
+ * try
+ * {
+ * doMyHandling();
+ * super.doHandle(target,request,response);
+ * }
+ * finally
+ * {
+ * cleanupMyHandling();
+ * }
+ * }
+ * }
+ * </pre>
+ */
+public abstract class ScopedHandler extends HandlerWrapper
+{
+ private static final ThreadLocal<ScopedHandler> __outerScope = new ThreadLocal<ScopedHandler>();
+ protected ScopedHandler _outerScope;
+ protected ScopedHandler _nextScope;
+
+ /**
+ * @see org.eclipse.jetty.server.handler.HandlerWrapper#doStart()
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ try
+ {
+ _outerScope = __outerScope.get();
+ if (_outerScope == null)
+ __outerScope.set(this);
+
+ super.doStart();
+
+ _nextScope = getChildHandlerByClass(ScopedHandler.class);
+ }
+ finally
+ {
+ if (_outerScope == null)
+ __outerScope.set(null);
+ }
+ }
+
+ /**
+ * ------------------------------------------------------------
+ */
+
+ @Override
+ public final void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (isStarted())
+ {
+ if (_outerScope == null)
+ doScope(target, baseRequest, request, response);
+ else
+ doHandle(target, baseRequest, request, response);
+ }
+ }
+
+ /**
+ * Scope the handler
+ * <p>Derived implementations should call {@link #nextScope(String, Request, HttpServletRequest, HttpServletResponse)}
+ *
+ * @param target The target of the request - either a URI or a name.
+ * @param baseRequest The original unwrapped request object.
+ * @param request The request either as the {@link Request} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getRequest() getRequest()}</code>
+ * method can be used access the Request object if required.
+ * @param response The response as the {@link Response} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getResponse() getResponse()}</code>
+ * method can be used access the Response object if required.
+ * @throws IOException if unable to handle the request or response processing
+ * @throws ServletException if unable to handle the request or response due to underlying servlet issue
+ */
+ public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ nextScope(target, baseRequest, request, response);
+ }
+
+ /**
+ * Scope the handler
+ *
+ * @param target The target of the request - either a URI or a name.
+ * @param baseRequest The original unwrapped request object.
+ * @param request The request either as the {@link Request} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getRequest() getRequest()}</code>
+ * method can be used access the Request object if required.
+ * @param response The response as the {@link Response} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getResponse() getResponse()}</code>
+ * method can be used access the Response object if required.
+ * @throws IOException if unable to handle the request or response processing
+ * @throws ServletException if unable to handle the request or response due to underlying servlet issue
+ */
+ public final void nextScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ if (_nextScope != null)
+ _nextScope.doScope(target, baseRequest, request, response);
+ else if (_outerScope != null)
+ _outerScope.doHandle(target, baseRequest, request, response);
+ else
+ doHandle(target, baseRequest, request, response);
+ }
+
+ /**
+ * Do the handler work within the scope.
+ * <p>Derived implementations should call {@link #nextHandle(String, Request, HttpServletRequest, HttpServletResponse)}
+ *
+ * @param target The target of the request - either a URI or a name.
+ * @param baseRequest The original unwrapped request object.
+ * @param request The request either as the {@link Request} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getRequest() getRequest()}</code>
+ * method can be used access the Request object if required.
+ * @param response The response as the {@link Response} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getResponse() getResponse()}</code>
+ * method can be used access the Response object if required.
+ * @throws IOException if unable to handle the request or response processing
+ * @throws ServletException if unable to handle the request or response due to underlying servlet issue
+ */
+ public abstract void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException;
+
+ /*
+ * Do the handler work within the scope.
+ * @param target
+ * The target of the request - either a URI or a name.
+ * @param baseRequest
+ * The original unwrapped request object.
+ * @param request
+ * The request either as the {@link Request} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getRequest() getRequest()}</code>
+ * method can be used access the Request object if required.
+ * @param response
+ * The response as the {@link Response} object or a wrapper of that request. The
+ * <code>{@link HttpConnection#getCurrentConnection()}.{@link HttpConnection#getHttpChannel() getHttpChannel()}.{@link HttpChannel#getResponse() getResponse()}</code>
+ * method can be used access the Response object if required.
+ * @throws IOException
+ * if unable to handle the request or response processing
+ * @throws ServletException
+ * if unable to handle the request or response due to underlying servlet issue
+ */
+ public final void nextHandle(String target, final Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (_nextScope != null && _nextScope == _handler)
+ _nextScope.doHandle(target, baseRequest, request, response);
+ else if (_handler != null)
+ super.handle(target, baseRequest, request, response);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SecuredRedirectHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SecuredRedirectHandler.java
new file mode 100644
index 0000000..3551a86
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SecuredRedirectHandler.java
@@ -0,0 +1,74 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.URIUtil;
+
+/**
+ * Secured Redirect Handler
+ * <p>
+ * Using information present in the {@link HttpConfiguration}, will attempt to redirect to the {@link HttpConfiguration#getSecureScheme()} and
+ * {@link HttpConfiguration#getSecurePort()} for any request that {@link HttpServletRequest#isSecure()} == false.
+ */
+public class SecuredRedirectHandler extends AbstractHandler
+{
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ HttpChannel channel = baseRequest.getHttpChannel();
+ if (baseRequest.isSecure() || (channel == null))
+ {
+ // nothing to do
+ return;
+ }
+
+ HttpConfiguration httpConfig = channel.getHttpConfiguration();
+ if (httpConfig == null)
+ {
+ // no config, show error
+ response.sendError(HttpStatus.FORBIDDEN_403, "No http configuration available");
+ return;
+ }
+
+ if (httpConfig.getSecurePort() > 0)
+ {
+ String scheme = httpConfig.getSecureScheme();
+ int port = httpConfig.getSecurePort();
+
+ String url = URIUtil.newURI(scheme, baseRequest.getServerName(), port, baseRequest.getRequestURI(), baseRequest.getQueryString());
+ response.setContentLength(0);
+ baseRequest.getResponse().sendRedirect(HttpServletResponse.SC_MOVED_TEMPORARILY, url, true);
+ }
+ else
+ {
+ response.sendError(HttpStatus.FORBIDDEN_403, "Not Secure");
+ }
+
+ baseRequest.setHandled(true);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java
new file mode 100644
index 0000000..e479d6b
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java
@@ -0,0 +1,278 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.net.URL;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.NetworkConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * A handler that shuts the server down on a valid request. Used to do "soft" restarts from Java.
+ * If _exitJvm is set to true a hard System.exit() call is being made.
+ * If _sendShutdownAtStart is set to true, starting the server will try to shut down an existing server at the same port.
+ * If _sendShutdownAtStart is set to true, make an http call to
+ * "http://localhost:" + port + "/shutdown?token=" + shutdownCookie
+ * in order to shut down the server.
+ *
+ * This handler is a contribution from Johannes Brodwall: https://bugs.eclipse.org/bugs/show_bug.cgi?id=357687
+ *
+ * Usage:
+ *
+ * <pre>
+ * Server server = new Server(8080);
+ * HandlerList handlers = new HandlerList();
+ * handlers.setHandlers(new Handler[]
+ * { someOtherHandler, new ShutdownHandler("secret password", false, true) });
+ * server.setHandler(handlers);
+ * server.start();
+ * </pre>
+ *
+ * <pre>
+ * public static void attemptShutdown(int port, String shutdownCookie) {
+ * try {
+ * URL url = new URL("http://localhost:" + port + "/shutdown?token=" + shutdownCookie);
+ * HttpURLConnection connection = (HttpURLConnection)url.openConnection();
+ * connection.setRequestMethod("POST");
+ * connection.getResponseCode();
+ * logger.info("Shutting down " + url + ": " + connection.getResponseMessage());
+ * } catch (SocketException e) {
+ * logger.debug("Not running");
+ * // Okay - the server is not running
+ * } catch (IOException e) {
+ * throw new RuntimeException(e);
+ * }
+ * }
+ * </pre>
+ */
+public class ShutdownHandler extends HandlerWrapper
+{
+ private static final Logger LOG = Log.getLogger(ShutdownHandler.class);
+
+ private final String _shutdownToken;
+ private boolean _sendShutdownAtStart;
+ private boolean _exitJvm = false;
+
+ /**
+ * Creates a listener that lets the server be shut down remotely (but only from localhost).
+ *
+ * @param server the Jetty instance that should be shut down
+ * @param shutdownToken a secret password to avoid unauthorized shutdown attempts
+ */
+ @Deprecated
+ public ShutdownHandler(Server server, String shutdownToken)
+ {
+ this(shutdownToken);
+ }
+
+ public ShutdownHandler(String shutdownToken)
+ {
+ this(shutdownToken, false, false);
+ }
+
+ /**
+ * @param shutdownToken a secret password to avoid unauthorized shutdown attempts
+ * @param exitJVM If true, when the shutdown is executed, the handler class System.exit()
+ * @param sendShutdownAtStart If true, a shutdown is sent as an HTTP post
+ * during startup, which will shutdown any previously running instances of
+ * this server with an identically configured ShutdownHandler
+ */
+ public ShutdownHandler(String shutdownToken, boolean exitJVM, boolean sendShutdownAtStart)
+ {
+ this._shutdownToken = shutdownToken;
+ setExitJvm(exitJVM);
+ setSendShutdownAtStart(sendShutdownAtStart);
+ }
+
+ public void sendShutdown() throws IOException
+ {
+ URL url = new URL(getServerUrl() + "/shutdown?token=" + _shutdownToken);
+ try
+ {
+ HttpURLConnection connection = (HttpURLConnection)url.openConnection();
+ connection.setRequestMethod("POST");
+ connection.getResponseCode();
+ LOG.info("Shutting down " + url + ": " + connection.getResponseCode() + " " + connection.getResponseMessage());
+ }
+ catch (SocketException e)
+ {
+ LOG.debug("Not running");
+ // Okay - the server is not running
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @SuppressWarnings("resource")
+ private String getServerUrl()
+ {
+ NetworkConnector connector = null;
+ for (Connector c : getServer().getConnectors())
+ {
+ if (c instanceof NetworkConnector)
+ {
+ connector = (NetworkConnector)c;
+ break;
+ }
+ }
+
+ if (connector == null)
+ return "http://localhost";
+
+ return "http://localhost:" + connector.getPort();
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+ if (_sendShutdownAtStart)
+ sendShutdown();
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (!target.equals("/shutdown"))
+ {
+ super.handle(target, baseRequest, request, response);
+ return;
+ }
+
+ if (!request.getMethod().equals("POST"))
+ {
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+ if (!hasCorrectSecurityToken(request))
+ {
+ LOG.warn("Unauthorized tokenless shutdown attempt from " + request.getRemoteAddr());
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+ }
+ if (!requestFromLocalhost(baseRequest))
+ {
+ LOG.warn("Unauthorized non-loopback shutdown attempt from " + request.getRemoteAddr());
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+ }
+
+ LOG.info("Shutting down by request from " + request.getRemoteAddr());
+ doShutdown(baseRequest, response);
+ }
+
+ protected void doShutdown(Request baseRequest, HttpServletResponse response) throws IOException
+ {
+ for (Connector connector : getServer().getConnectors())
+ {
+ connector.shutdown();
+ }
+
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.flushBuffer();
+
+ final Server server = getServer();
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ shutdownServer(server);
+ }
+ catch (InterruptedException e)
+ {
+ LOG.ignore(e);
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException("Shutting down server", e);
+ }
+ }
+ }.start();
+ }
+
+ private boolean requestFromLocalhost(Request request)
+ {
+ InetSocketAddress addr = request.getRemoteInetSocketAddress();
+ if (addr == null)
+ {
+ return false;
+ }
+ return addr.getAddress().isLoopbackAddress();
+ }
+
+ private boolean hasCorrectSecurityToken(HttpServletRequest request)
+ {
+ String tok = request.getParameter("token");
+ if (LOG.isDebugEnabled())
+ LOG.debug("Token: {}", tok);
+ return _shutdownToken.equals(tok);
+ }
+
+ private void shutdownServer(Server server) throws Exception
+ {
+ server.stop();
+
+ if (_exitJvm)
+ {
+ System.exit(0);
+ }
+ }
+
+ public void setExitJvm(boolean exitJvm)
+ {
+ this._exitJvm = exitJvm;
+ }
+
+ public boolean isSendShutdownAtStart()
+ {
+ return _sendShutdownAtStart;
+ }
+
+ public void setSendShutdownAtStart(boolean sendShutdownAtStart)
+ {
+ _sendShutdownAtStart = sendShutdownAtStart;
+ }
+
+ public String getShutdownToken()
+ {
+ return _shutdownToken;
+ }
+
+ public boolean isExitJvm()
+ {
+ return _exitJvm;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java
new file mode 100644
index 0000000..112b02a
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java
@@ -0,0 +1,626 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.LongAdder;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.AsyncContextEvent;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpChannelState;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.FutureCallback;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.component.Graceful;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.statistic.CounterStatistic;
+import org.eclipse.jetty.util.statistic.SampleStatistic;
+
+@ManagedObject("Request Statistics Gathering")
+public class StatisticsHandler extends HandlerWrapper implements Graceful
+{
+ private static final Logger LOG = Log.getLogger(StatisticsHandler.class);
+ private final AtomicLong _statsStartedAt = new AtomicLong();
+
+ private final CounterStatistic _requestStats = new CounterStatistic();
+ private final SampleStatistic _requestTimeStats = new SampleStatistic();
+ private final CounterStatistic _dispatchedStats = new CounterStatistic();
+ private final SampleStatistic _dispatchedTimeStats = new SampleStatistic();
+ private final CounterStatistic _asyncWaitStats = new CounterStatistic();
+
+ private final LongAdder _asyncDispatches = new LongAdder();
+ private final LongAdder _expires = new LongAdder();
+ private final LongAdder _errors = new LongAdder();
+
+ private final LongAdder _responses1xx = new LongAdder();
+ private final LongAdder _responses2xx = new LongAdder();
+ private final LongAdder _responses3xx = new LongAdder();
+ private final LongAdder _responses4xx = new LongAdder();
+ private final LongAdder _responses5xx = new LongAdder();
+ private final LongAdder _responsesTotalBytes = new LongAdder();
+
+ private boolean _gracefulShutdownWaitsForRequests = true;
+
+ private final Graceful.Shutdown _shutdown = new Graceful.Shutdown()
+ {
+ @Override
+ protected FutureCallback newShutdownCallback()
+ {
+ return new FutureCallback(_requestStats.getCurrent() == 0);
+ }
+ };
+
+ private final AsyncListener _onCompletion = new AsyncListener()
+ {
+ @Override
+ public void onStartAsync(AsyncEvent event)
+ {
+ event.getAsyncContext().addListener(this);
+ }
+
+ @Override
+ public void onTimeout(AsyncEvent event)
+ {
+ _expires.increment();
+ }
+
+ @Override
+ public void onError(AsyncEvent event)
+ {
+ _errors.increment();
+ }
+
+ @Override
+ public void onComplete(AsyncEvent event)
+ {
+ HttpChannelState state = ((AsyncContextEvent)event).getHttpChannelState();
+
+ Request request = state.getBaseRequest();
+ final long elapsed = System.currentTimeMillis() - request.getTimeStamp();
+
+ long numRequests = _requestStats.decrement();
+ _requestTimeStats.record(elapsed);
+
+ updateResponse(request);
+
+ _asyncWaitStats.decrement();
+
+ if (numRequests == 0 && _gracefulShutdownWaitsForRequests)
+ {
+ FutureCallback shutdown = _shutdown.get();
+ if (shutdown != null)
+ shutdown.succeeded();
+ }
+ }
+ };
+
+ /**
+ * Resets the current request statistics.
+ */
+ @ManagedOperation(value = "resets statistics", impact = "ACTION")
+ public void statsReset()
+ {
+ _statsStartedAt.set(System.currentTimeMillis());
+
+ _requestStats.reset();
+ _requestTimeStats.reset();
+ _dispatchedStats.reset();
+ _dispatchedTimeStats.reset();
+ _asyncWaitStats.reset();
+
+ _asyncDispatches.reset();
+ _expires.reset();
+ _responses1xx.reset();
+ _responses2xx.reset();
+ _responses3xx.reset();
+ _responses4xx.reset();
+ _responses5xx.reset();
+ _responsesTotalBytes.reset();
+ }
+
+ @Override
+ public void handle(String path, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ Handler handler = getHandler();
+ if (handler == null || !isStarted() || isShutdown())
+ {
+ if (!baseRequest.getResponse().isCommitted())
+ response.sendError(HttpStatus.SERVICE_UNAVAILABLE_503);
+ return;
+ }
+
+ _dispatchedStats.increment();
+
+ final long start;
+ HttpChannelState state = baseRequest.getHttpChannelState();
+ if (state.isInitial())
+ {
+ // new request
+ _requestStats.increment();
+ start = baseRequest.getTimeStamp();
+ }
+ else
+ {
+ // resumed request
+ start = System.currentTimeMillis();
+ _asyncDispatches.increment();
+ }
+
+ try
+ {
+ handler.handle(path, baseRequest, request, response);
+ }
+ finally
+ {
+ final long now = System.currentTimeMillis();
+ final long dispatched = now - start;
+
+ long numRequests = -1;
+ long numDispatches = _dispatchedStats.decrement();
+ _dispatchedTimeStats.record(dispatched);
+
+ if (state.isInitial())
+ {
+ if (state.isAsyncStarted())
+ {
+ state.addListener(_onCompletion);
+ _asyncWaitStats.increment();
+ }
+ else
+ {
+ numRequests = _requestStats.decrement();
+ _requestTimeStats.record(dispatched);
+ updateResponse(baseRequest);
+ }
+ }
+
+ FutureCallback shutdown = _shutdown.get();
+ if (shutdown != null)
+ {
+ response.flushBuffer();
+ if (_gracefulShutdownWaitsForRequests ? (numRequests == 0) : (numDispatches == 0))
+ shutdown.succeeded();
+ }
+ }
+ }
+
+ protected void updateResponse(Request request)
+ {
+ Response response = request.getResponse();
+ if (request.isHandled())
+ {
+ switch (response.getStatus() / 100)
+ {
+ case 1:
+ _responses1xx.increment();
+ break;
+ case 2:
+ _responses2xx.increment();
+ break;
+ case 3:
+ _responses3xx.increment();
+ break;
+ case 4:
+ _responses4xx.increment();
+ break;
+ case 5:
+ _responses5xx.increment();
+ break;
+ default:
+ break;
+ }
+ }
+ else
+ // will fall through to not found handler
+ _responses4xx.increment();
+ _responsesTotalBytes.add(response.getContentCount());
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (getHandler() == null)
+ throw new IllegalStateException("StatisticsHandler has no Wrapped Handler");
+ _shutdown.cancel();
+ super.doStart();
+ statsReset();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ _shutdown.cancel();
+ super.doStop();
+ }
+
+ /**
+ * Set whether the graceful shutdown should wait for all requests to complete including
+ * async requests which are not currently dispatched, or whether it should only wait for all the
+ * actively dispatched requests to complete.
+ * @param gracefulShutdownWaitsForRequests true to wait for async requests on graceful shutdown.
+ */
+ public void setGracefulShutdownWaitsForRequests(boolean gracefulShutdownWaitsForRequests)
+ {
+ _gracefulShutdownWaitsForRequests = gracefulShutdownWaitsForRequests;
+ }
+
+ /**
+ * @return whether the graceful shutdown will wait for all requests to complete including
+ * async requests which are not currently dispatched, or whether it will only wait for all the
+ * actively dispatched requests to complete.
+ * @see #getAsyncDispatches()
+ */
+ @ManagedAttribute("if graceful shutdown will wait for all requests")
+ public boolean getGracefulShutdownWaitsForRequests()
+ {
+ return _gracefulShutdownWaitsForRequests;
+ }
+
+ /**
+ * @return the number of requests handled by this handler
+ * since {@link #statsReset()} was last called, excluding
+ * active requests
+ * @see #getAsyncDispatches()
+ */
+ @ManagedAttribute("number of requests")
+ public int getRequests()
+ {
+ return (int)_requestStats.getTotal();
+ }
+
+ /**
+ * @return the number of requests currently active.
+ * since {@link #statsReset()} was last called.
+ */
+ @ManagedAttribute("number of requests currently active")
+ public int getRequestsActive()
+ {
+ return (int)_requestStats.getCurrent();
+ }
+
+ /**
+ * @return the maximum number of active requests
+ * since {@link #statsReset()} was last called.
+ */
+ @ManagedAttribute("maximum number of active requests")
+ public int getRequestsActiveMax()
+ {
+ return (int)_requestStats.getMax();
+ }
+
+ /**
+ * @return the maximum time (in milliseconds) of request handling
+ * since {@link #statsReset()} was last called.
+ */
+ @ManagedAttribute("maximum time spend handling requests (in ms)")
+ public long getRequestTimeMax()
+ {
+ return _requestTimeStats.getMax();
+ }
+
+ /**
+ * @return the total time (in milliseconds) of requests handling
+ * since {@link #statsReset()} was last called.
+ */
+ @ManagedAttribute("total time spend in all request handling (in ms)")
+ public long getRequestTimeTotal()
+ {
+ return _requestTimeStats.getTotal();
+ }
+
+ /**
+ * @return the mean time (in milliseconds) of request handling
+ * since {@link #statsReset()} was last called.
+ * @see #getRequestTimeTotal()
+ * @see #getRequests()
+ */
+ @ManagedAttribute("mean time spent handling requests (in ms)")
+ public double getRequestTimeMean()
+ {
+ return _requestTimeStats.getMean();
+ }
+
+ /**
+ * @return the standard deviation of time (in milliseconds) of request handling
+ * since {@link #statsReset()} was last called.
+ * @see #getRequestTimeTotal()
+ * @see #getRequests()
+ */
+ @ManagedAttribute("standard deviation for request handling (in ms)")
+ public double getRequestTimeStdDev()
+ {
+ return _requestTimeStats.getStdDev();
+ }
+
+ /**
+ * @return the number of dispatches seen by this handler
+ * since {@link #statsReset()} was last called, excluding
+ * active dispatches
+ */
+ @ManagedAttribute("number of dispatches")
+ public int getDispatched()
+ {
+ return (int)_dispatchedStats.getTotal();
+ }
+
+ /**
+ * @return the number of dispatches currently in this handler
+ * since {@link #statsReset()} was last called, including
+ * resumed requests
+ */
+ @ManagedAttribute("number of dispatches currently active")
+ public int getDispatchedActive()
+ {
+ return (int)_dispatchedStats.getCurrent();
+ }
+
+ /**
+ * @return the max number of dispatches currently in this handler
+ * since {@link #statsReset()} was last called, including
+ * resumed requests
+ */
+ @ManagedAttribute("maximum number of active dispatches being handled")
+ public int getDispatchedActiveMax()
+ {
+ return (int)_dispatchedStats.getMax();
+ }
+
+ /**
+ * @return the maximum time (in milliseconds) of request dispatch
+ * since {@link #statsReset()} was last called.
+ */
+ @ManagedAttribute("maximum time spend in dispatch handling")
+ public long getDispatchedTimeMax()
+ {
+ return _dispatchedTimeStats.getMax();
+ }
+
+ /**
+ * @return the total time (in milliseconds) of requests handling
+ * since {@link #statsReset()} was last called.
+ */
+ @ManagedAttribute("total time spent in dispatch handling (in ms)")
+ public long getDispatchedTimeTotal()
+ {
+ return _dispatchedTimeStats.getTotal();
+ }
+
+ /**
+ * @return the mean time (in milliseconds) of request handling
+ * since {@link #statsReset()} was last called.
+ * @see #getRequestTimeTotal()
+ * @see #getRequests()
+ */
+ @ManagedAttribute("mean time spent in dispatch handling (in ms)")
+ public double getDispatchedTimeMean()
+ {
+ return _dispatchedTimeStats.getMean();
+ }
+
+ /**
+ * @return the standard deviation of time (in milliseconds) of request handling
+ * since {@link #statsReset()} was last called.
+ * @see #getRequestTimeTotal()
+ * @see #getRequests()
+ */
+ @ManagedAttribute("standard deviation for dispatch handling (in ms)")
+ public double getDispatchedTimeStdDev()
+ {
+ return _dispatchedTimeStats.getStdDev();
+ }
+
+ /**
+ * @return the number of requests handled by this handler
+ * since {@link #statsReset()} was last called, including
+ * resumed requests
+ * @see #getAsyncDispatches()
+ */
+ @ManagedAttribute("total number of async requests")
+ public int getAsyncRequests()
+ {
+ return (int)_asyncWaitStats.getTotal();
+ }
+
+ /**
+ * @return the number of requests currently suspended.
+ * since {@link #statsReset()} was last called.
+ */
+ @ManagedAttribute("currently waiting async requests")
+ public int getAsyncRequestsWaiting()
+ {
+ return (int)_asyncWaitStats.getCurrent();
+ }
+
+ /**
+ * @return the maximum number of current suspended requests
+ * since {@link #statsReset()} was last called.
+ */
+ @ManagedAttribute("maximum number of waiting async requests")
+ public int getAsyncRequestsWaitingMax()
+ {
+ return (int)_asyncWaitStats.getMax();
+ }
+
+ /**
+ * @return the number of requests that have been asynchronously dispatched
+ */
+ @ManagedAttribute("number of requested that have been asynchronously dispatched")
+ public int getAsyncDispatches()
+ {
+ return _asyncDispatches.intValue();
+ }
+
+ /**
+ * @return the number of requests that expired while suspended.
+ * @see #getAsyncDispatches()
+ */
+ @ManagedAttribute("number of async requests requests that have expired")
+ public int getExpires()
+ {
+ return _expires.intValue();
+ }
+
+ /**
+ * @return the number of async errors that occurred.
+ * @see #getAsyncDispatches()
+ */
+ @ManagedAttribute("number of async errors that occurred")
+ public int getErrors()
+ {
+ return _errors.intValue();
+ }
+
+ /**
+ * @return the number of responses with a 1xx status returned by this context
+ * since {@link #statsReset()} was last called.
+ */
+ @ManagedAttribute("number of requests with 1xx response status")
+ public int getResponses1xx()
+ {
+ return _responses1xx.intValue();
+ }
+
+ /**
+ * @return the number of responses with a 2xx status returned by this context
+ * since {@link #statsReset()} was last called.
+ */
+ @ManagedAttribute("number of requests with 2xx response status")
+ public int getResponses2xx()
+ {
+ return _responses2xx.intValue();
+ }
+
+ /**
+ * @return the number of responses with a 3xx status returned by this context
+ * since {@link #statsReset()} was last called.
+ */
+ @ManagedAttribute("number of requests with 3xx response status")
+ public int getResponses3xx()
+ {
+ return _responses3xx.intValue();
+ }
+
+ /**
+ * @return the number of responses with a 4xx status returned by this context
+ * since {@link #statsReset()} was last called.
+ */
+ @ManagedAttribute("number of requests with 4xx response status")
+ public int getResponses4xx()
+ {
+ return _responses4xx.intValue();
+ }
+
+ /**
+ * @return the number of responses with a 5xx status returned by this context
+ * since {@link #statsReset()} was last called.
+ */
+ @ManagedAttribute("number of requests with 5xx response status")
+ public int getResponses5xx()
+ {
+ return _responses5xx.intValue();
+ }
+
+ /**
+ * @return the milliseconds since the statistics were started with {@link #statsReset()}.
+ */
+ @ManagedAttribute("time in milliseconds stats have been collected for")
+ public long getStatsOnMs()
+ {
+ return System.currentTimeMillis() - _statsStartedAt.get();
+ }
+
+ /**
+ * @return the total bytes of content sent in responses
+ */
+ @ManagedAttribute("total number of bytes across all responses")
+ public long getResponsesBytesTotal()
+ {
+ return _responsesTotalBytes.longValue();
+ }
+
+ public String toStatsHTML()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("<h1>Statistics:</h1>\n");
+ sb.append("Statistics gathering started ").append(getStatsOnMs()).append("ms ago").append("<br />\n");
+
+ sb.append("<h2>Requests:</h2>\n");
+ sb.append("Total requests: ").append(getRequests()).append("<br />\n");
+ sb.append("Active requests: ").append(getRequestsActive()).append("<br />\n");
+ sb.append("Max active requests: ").append(getRequestsActiveMax()).append("<br />\n");
+ sb.append("Total requests time: ").append(getRequestTimeTotal()).append("<br />\n");
+ sb.append("Mean request time: ").append(getRequestTimeMean()).append("<br />\n");
+ sb.append("Max request time: ").append(getRequestTimeMax()).append("<br />\n");
+ sb.append("Request time standard deviation: ").append(getRequestTimeStdDev()).append("<br />\n");
+
+ sb.append("<h2>Dispatches:</h2>\n");
+ sb.append("Total dispatched: ").append(getDispatched()).append("<br />\n");
+ sb.append("Active dispatched: ").append(getDispatchedActive()).append("<br />\n");
+ sb.append("Max active dispatched: ").append(getDispatchedActiveMax()).append("<br />\n");
+ sb.append("Total dispatched time: ").append(getDispatchedTimeTotal()).append("<br />\n");
+ sb.append("Mean dispatched time: ").append(getDispatchedTimeMean()).append("<br />\n");
+ sb.append("Max dispatched time: ").append(getDispatchedTimeMax()).append("<br />\n");
+ sb.append("Dispatched time standard deviation: ").append(getDispatchedTimeStdDev()).append("<br />\n");
+
+ sb.append("Total requests suspended: ").append(getAsyncRequests()).append("<br />\n");
+ sb.append("Total requests expired: ").append(getExpires()).append("<br />\n");
+ sb.append("Total requests resumed: ").append(getAsyncDispatches()).append("<br />\n");
+
+ sb.append("<h2>Responses:</h2>\n");
+ sb.append("1xx responses: ").append(getResponses1xx()).append("<br />\n");
+ sb.append("2xx responses: ").append(getResponses2xx()).append("<br />\n");
+ sb.append("3xx responses: ").append(getResponses3xx()).append("<br />\n");
+ sb.append("4xx responses: ").append(getResponses4xx()).append("<br />\n");
+ sb.append("5xx responses: ").append(getResponses5xx()).append("<br />\n");
+ sb.append("Bytes sent total: ").append(getResponsesBytesTotal()).append("<br />\n");
+
+ return sb.toString();
+ }
+
+ @Override
+ public Future<Void> shutdown()
+ {
+ return _shutdown.shutdown();
+ }
+
+ @Override
+ public boolean isShutdown()
+ {
+ return _shutdown.isShutdown();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{%s,r=%d,d=%d}", getClass().getSimpleName(), hashCode(), getState(), _requestStats.getCurrent(), _dispatchedStats.getCurrent());
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java
new file mode 100644
index 0000000..e1d7ff6
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java
@@ -0,0 +1,440 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HostPortHttpField;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.QuotedCSV;
+import org.eclipse.jetty.server.ForwardedRequestCustomizer;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.IncludeExcludeSet;
+import org.eclipse.jetty.util.InetAddressSet;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.annotation.Name;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Locker;
+
+/**
+ * <p>Handler to limit the threads per IP address for DOS protection</p>
+ * <p>The ThreadLimitHandler applies a limit to the number of Threads
+ * that can be used simultaneously per remote IP address.
+ * </p>
+ * <p>The handler makes a determination of the remote IP separately to
+ * any that may be made by the {@link ForwardedRequestCustomizer} or similar:
+ * <ul>
+ * <li>This handler will use either only a single style
+ * of forwarded header. This is on the assumption that a trusted local proxy
+ * will produce only a single forwarded header and that any additional
+ * headers are likely from untrusted client side proxies.</li>
+ * <li>If multiple instances of a forwarded header are provided, this
+ * handler will use the right-most instance, which will have been set from
+ * the trusted local proxy</li>
+ * </ul>
+ * Requests in excess of the limit will be asynchronously suspended until
+ * a thread is available.
+ * <p>This is a simpler alternative to DosFilter</p>
+ */
+public class ThreadLimitHandler extends HandlerWrapper
+{
+ private static final Logger LOG = Log.getLogger(ThreadLimitHandler.class);
+
+ private static final String REMOTE = "o.e.j.s.h.TLH.REMOTE";
+ private static final String PERMIT = "o.e.j.s.h.TLH.PASS";
+ private final boolean _rfc7239;
+ private final String _forwardedHeader;
+ private final IncludeExcludeSet<String, InetAddress> _includeExcludeSet = new IncludeExcludeSet<>(InetAddressSet.class);
+ private final ConcurrentMap<String, Remote> _remotes = new ConcurrentHashMap<>();
+ private volatile boolean _enabled;
+ private int _threadLimit = 10;
+
+ public ThreadLimitHandler()
+ {
+ this(null, false);
+ }
+
+ public ThreadLimitHandler(@Name("forwardedHeader") String forwardedHeader)
+ {
+ this(forwardedHeader, HttpHeader.FORWARDED.is(forwardedHeader));
+ }
+
+ public ThreadLimitHandler(@Name("forwardedHeader") String forwardedHeader, @Name("rfc7239") boolean rfc7239)
+ {
+ super();
+ _rfc7239 = rfc7239;
+ _forwardedHeader = forwardedHeader;
+ _enabled = true;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+ LOG.info(String.format("ThreadLimitHandler enable=%b limit=%d include=%s", _enabled, _threadLimit, _includeExcludeSet));
+ }
+
+ @ManagedAttribute("true if this handler is enabled")
+ public boolean isEnabled()
+ {
+ return _enabled;
+ }
+
+ public void setEnabled(boolean enabled)
+ {
+ _enabled = enabled;
+ LOG.info(String.format("ThreadLimitHandler enable=%b limit=%d include=%s", _enabled, _threadLimit, _includeExcludeSet));
+ }
+
+ @ManagedAttribute("The maximum threads that can be dispatched per remote IP")
+ public int getThreadLimit()
+ {
+ return _threadLimit;
+ }
+
+ public void setThreadLimit(int threadLimit)
+ {
+ if (threadLimit <= 0)
+ throw new IllegalArgumentException("limit must be >0");
+ _threadLimit = threadLimit;
+ }
+
+ @ManagedOperation("Include IP in thread limits")
+ public void include(String inetAddressPattern)
+ {
+ _includeExcludeSet.include(inetAddressPattern);
+ }
+
+ @ManagedOperation("Exclude IP from thread limits")
+ public void exclude(String inetAddressPattern)
+ {
+ _includeExcludeSet.exclude(inetAddressPattern);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ // Allow ThreadLimit to be enabled dynamically without restarting server
+ if (!_enabled)
+ {
+ // if disabled, handle normally
+ super.handle(target, baseRequest, request, response);
+ }
+ else
+ {
+ // Get the remote address of the request
+ Remote remote = getRemote(baseRequest);
+ if (remote == null)
+ {
+ // if remote is not known, handle normally
+ super.handle(target, baseRequest, request, response);
+ }
+ else
+ {
+ // Do we already have a future permit from a previous invocation?
+ Closeable permit = (Closeable)baseRequest.getAttribute(PERMIT);
+ try
+ {
+ if (permit != null)
+ {
+ // Yes, remove it from any future async cycles.
+ baseRequest.removeAttribute(PERMIT);
+ }
+ else
+ {
+ // No, then lets try to acquire one
+ CompletableFuture<Closeable> futurePermit = remote.acquire();
+
+ // Did we get a permit?
+ if (futurePermit.isDone())
+ {
+ // yes
+ permit = futurePermit.get();
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Threadlimited {} {}", remote, target);
+ // No, lets asynchronously suspend the request
+ AsyncContext async = baseRequest.startAsync();
+ // let's never timeout the async. If this is a DOS, then good to make them wait, if this is not
+ // then give them maximum time to get a thread.
+ async.setTimeout(0);
+
+ // dispatch the request when we do eventually get a pass
+ futurePermit.thenAccept(c ->
+ {
+ baseRequest.setAttribute(PERMIT, c);
+ async.dispatch();
+ });
+ return;
+ }
+ }
+
+ // Use the permit
+ super.handle(target, baseRequest, request, response);
+ }
+ catch (InterruptedException | ExecutionException e)
+ {
+ throw new ServletException(e);
+ }
+ finally
+ {
+ if (permit != null)
+ permit.close();
+ }
+ }
+ }
+ }
+
+ protected int getThreadLimit(String ip)
+ {
+ if (!_includeExcludeSet.isEmpty())
+ {
+ try
+ {
+ if (!_includeExcludeSet.test(InetAddress.getByName(ip)))
+ {
+ LOG.debug("excluded {}", ip);
+ return 0;
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+ }
+ return _threadLimit;
+ }
+
+ protected Remote getRemote(Request baseRequest)
+ {
+ Remote remote = (Remote)baseRequest.getAttribute(REMOTE);
+ if (remote != null)
+ return remote;
+
+ String ip = getRemoteIP(baseRequest);
+ LOG.debug("ip={}", ip);
+ if (ip == null)
+ return null;
+
+ int limit = getThreadLimit(ip);
+ if (limit <= 0)
+ return null;
+
+ remote = _remotes.get(ip);
+ if (remote == null)
+ {
+ Remote r = new Remote(ip, limit);
+ remote = _remotes.putIfAbsent(ip, r);
+ if (remote == null)
+ remote = r;
+ }
+
+ baseRequest.setAttribute(REMOTE, remote);
+
+ return remote;
+ }
+
+ protected String getRemoteIP(Request baseRequest)
+ {
+ // Do we have a forwarded header set?
+ if (_forwardedHeader != null && !_forwardedHeader.isEmpty())
+ {
+ // Yes, then try to get the remote IP from the header
+ String remote = _rfc7239 ? getForwarded(baseRequest) : getXForwardedFor(baseRequest);
+ if (remote != null && !remote.isEmpty())
+ return remote;
+ }
+
+ // If no remote IP from a header, determine it directly from the channel
+ // Do not use the request methods, as they may have been lied to by the
+ // RequestCustomizer!
+ InetSocketAddress inetAddr = baseRequest.getHttpChannel().getRemoteAddress();
+ if (inetAddr != null && inetAddr.getAddress() != null)
+ return inetAddr.getAddress().getHostAddress();
+ return null;
+ }
+
+ private String getForwarded(Request request)
+ {
+ // Get the right most Forwarded for value.
+ // This is the value from the closest proxy and the only one that
+ // can be trusted.
+ RFC7239 rfc7239 = new RFC7239();
+ HttpFields httpFields = request.getHttpFields();
+ for (HttpField field : httpFields)
+ {
+ if (_forwardedHeader.equalsIgnoreCase(field.getName()))
+ rfc7239.addValue(field.getValue());
+ }
+
+ if (rfc7239.getFor() != null)
+ return new HostPortHttpField(rfc7239.getFor()).getHost();
+
+ return null;
+ }
+
+ private String getXForwardedFor(Request request)
+ {
+ // Get the right most XForwarded-For for value.
+ // This is the value from the closest proxy and the only one that
+ // can be trusted.
+ String forwardedFor = null;
+ HttpFields httpFields = request.getHttpFields();
+ for (HttpField field : httpFields)
+ {
+ if (_forwardedHeader.equalsIgnoreCase(field.getName()))
+ forwardedFor = field.getValue();
+ }
+
+ if (forwardedFor == null || forwardedFor.isEmpty())
+ return null;
+
+ int comma = forwardedFor.lastIndexOf(',');
+ return (comma >= 0) ? forwardedFor.substring(comma + 1).trim() : forwardedFor;
+ }
+
+ private final class Remote implements Closeable
+ {
+ private final String _ip;
+ private final int _limit;
+ private final Locker _locker = new Locker();
+ private int _permits;
+ private Deque<CompletableFuture<Closeable>> _queue = new ArrayDeque<>();
+ private final CompletableFuture<Closeable> _permitted = CompletableFuture.completedFuture(this);
+
+ public Remote(String ip, int limit)
+ {
+ _ip = ip;
+ _limit = limit;
+ }
+
+ public CompletableFuture<Closeable> acquire()
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ // Do we have available passes?
+ if (_permits < _limit)
+ {
+ // Yes - increment the allocated passes
+ _permits++;
+ // return the already completed future
+ return _permitted; // TODO is it OK to share/reuse this?
+ }
+
+ // No pass available, so queue a new future
+ CompletableFuture<Closeable> pass = new CompletableFuture<Closeable>();
+ _queue.addLast(pass);
+ return pass;
+ }
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ // reduce the allocated passes
+ _permits--;
+ while (true)
+ {
+ // Are there any future passes waiting?
+ CompletableFuture<Closeable> permit = _queue.pollFirst();
+
+ // No - we are done
+ if (permit == null)
+ break;
+
+ // Yes - if we can complete them, we are done
+ if (permit.complete(this))
+ {
+ _permits++;
+ break;
+ }
+
+ // Somebody else must have completed/failed that future pass,
+ // so let's try for another.
+ }
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ return String.format("R[ip=%s,p=%d,l=%d,q=%d]", _ip, _permits, _limit, _queue.size());
+ }
+ }
+ }
+
+ private final class RFC7239 extends QuotedCSV
+ {
+ String _for;
+
+ private RFC7239()
+ {
+ super(false);
+ }
+
+ String getFor()
+ {
+ return _for;
+ }
+
+ @Override
+ protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
+ {
+ if (valueLength == 0 && paramValue > paramName)
+ {
+ String name = StringUtil.asciiToLowerCase(buffer.substring(paramName, paramValue - 1));
+ if ("for".equalsIgnoreCase(name))
+ {
+ String value = buffer.substring(paramValue);
+
+ // if unknown, clear any leftward values
+ if ("unknown".equalsIgnoreCase(value))
+ _for = null;
+ // Otherwise accept IP or token(starting with '_') as remote keys
+ else
+ _for = value;
+ }
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipFactory.java
new file mode 100644
index 0000000..bddf3c3
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipFactory.java
@@ -0,0 +1,32 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler.gzip;
+
+import java.util.zip.Deflater;
+
+import org.eclipse.jetty.server.Request;
+
+public interface GzipFactory
+{
+ Deflater getDeflater(Request request, long contentLength);
+
+ boolean isMimeTypeGzipable(String mimetype);
+
+ void recycle(Deflater deflater);
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java
new file mode 100644
index 0000000..bec5100
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java
@@ -0,0 +1,1037 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler.gzip;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.ListIterator;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.zip.Deflater;
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.CompressedContentFormat;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.http.PreEncodedHttpField;
+import org.eclipse.jetty.http.pathmap.PathSpecSet;
+import org.eclipse.jetty.server.HttpOutput;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.HandlerWrapper;
+import org.eclipse.jetty.util.IncludeExclude;
+import org.eclipse.jetty.util.RegexSet;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.compression.DeflaterPool;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * A Handler that can dynamically GZIP uncompress requests, and compress responses.
+ * <p>
+ * The GzipHandler can be applied to the entire server (a {@code gzip.mod} is included in
+ * the {@code jetty-home}) or it may be applied to individual contexts.
+ * </p>
+ * <p>
+ * Both Request uncompress and Response compress are gated by a configurable
+ * {@link DispatcherType} check on the GzipHandler.
+ * (This is similar in behavior to a {@link javax.servlet.Filter} configuration
+ * you would find in a Servlet Descriptor file ({@code WEB-INF/web.xml})
+ * <br>(Default: {@link DispatcherType#REQUEST}).
+ * </p>
+ * <p>
+ * Requests with a {@code Content-Encoding} header with the value {@code gzip} will
+ * be uncompressed by a {@link GzipHttpInputInterceptor} for any API that uses
+ * {@link HttpServletRequest#getInputStream()} or {@link HttpServletRequest#getReader()}.
+ * </p>
+ * <p>
+ * Response compression has a number of checks before GzipHandler will perform compression.
+ * </p>
+ * <ol>
+ * <li>
+ * Does the request contain a {@code Accept-Encoding} header that specifies
+ * {@code gzip} value?
+ * </li>
+ * <li>
+ * Is the {@link HttpServletRequest#getMethod()} allowed by the configured HTTP Method Filter.
+ * <br> (Default: {@code GET})
+ * </li>
+ * <li>
+ * Is the incoming Path allowed by the configured Path Specs filters?
+ * <br> (Default: all paths are allowed)
+ * </li>
+ * <li>
+ * Is the Request User-Agent allowed by the configured User-Agent filters?
+ * <br> (Default: MSIE 6 is excluded)
+ * </li>
+ * <li>
+ * Is the Response {@code Content-Length} header present, and does its
+ * value meet the minimum gzip size requirements (default 32 bytes)?
+ * </li>
+ * <li>
+ * Is the Request {@code Accept} header present and does it contain the
+ * required {@code gzip} value?
+ * </li>
+ * </ol>
+ * <p>
+ * When you encounter a configurable filter in the GzipHandler (method, paths, user-agent,
+ * mime-types, etc) that has both Included and Excluded values, note that the Included
+ * values always win over the Excluded values.
+ * </p>
+ * <p>
+ * <em>Important note about Default Values</em>:
+ * It is important to note that the GzipHandler will automatically configure itself from the
+ * MimeType present on the Server, System, and Contexts and the ultimate set of default values
+ * for the various filters (paths, methods, mime-types, etc) can be influenced by the
+ * available mime types to work with.
+ * </p>
+ * <p>
+ * ETag (or Entity Tag) information: any Request headers for {@code If-None-Match} or
+ * {@code If-Match} will be evaluated by the GzipHandler to determine if it was involved
+ * in compression of the response earlier. This is usually present as a {@code --gzip} suffix
+ * on the ETag that the Client User-Agent is tracking and handed to the Jetty server.
+ * The special {@code --gzip} suffix on the ETag is how GzipHandler knows that the content
+ * passed through itself, and this suffix will be stripped from the Request header values
+ * before the request is sent onwards to the specific webapp / servlet endpoint for
+ * handling.
+ * If a ETag is present in the Response headers, and GzipHandler is compressing the
+ * contents, it will add the {@code --gzip} suffix before the Response headers are committed
+ * and sent to the User Agent.
+ * Note that the suffix used is determined by {@link CompressedContentFormat#ETAG_SEPARATOR}
+ * </p>
+ * <p>
+ * This implementation relies on an Jetty internal {@link org.eclipse.jetty.server.HttpOutput.Interceptor}
+ * mechanism to allow for effective and efficient compression of the response on all Output API usages:
+ * </p>
+ * <ul>
+ * <li>
+ * {@link javax.servlet.ServletOutputStream} - Obtained from {@link HttpServletResponse#getOutputStream()}
+ * using the traditional Blocking I/O techniques
+ * </li>
+ * <li>
+ * {@link javax.servlet.WriteListener} - Provided to
+ * {@link javax.servlet.ServletOutputStream#setWriteListener(javax.servlet.WriteListener)}
+ * using the new (since Servlet 3.1) Async I/O techniques
+ * </li>
+ * <li>
+ * {@link java.io.PrintWriter} - Obtained from {@link HttpServletResponse#getWriter()}
+ * using Blocking I/O techniques
+ * </li>
+ * </ul>
+ * <p>
+ * Historically the compression of responses were accomplished via
+ * Servlet Filters (eg: {@code GzipFilter}) and usage of {@link javax.servlet.http.HttpServletResponseWrapper}.
+ * Since the introduction of Async I/O in Servlet 3.1, this older form of Gzip support
+ * in web applications has been problematic and bug ridden.
+ * </p>
+ */
+public class GzipHandler extends HandlerWrapper implements GzipFactory
+{
+ public static final String GZIP_HANDLER_ETAGS = "o.e.j.s.h.gzip.GzipHandler.etag";
+ public static final String GZIP = "gzip";
+ public static final String DEFLATE = "deflate";
+ public static final int DEFAULT_MIN_GZIP_SIZE = 32;
+ public static final int BREAK_EVEN_GZIP_SIZE = 23;
+ private static final Logger LOG = Log.getLogger(GzipHandler.class);
+ private static final HttpField X_CE_GZIP = new PreEncodedHttpField("X-Content-Encoding", "gzip");
+ private static final Pattern COMMA_GZIP = Pattern.compile(".*, *gzip");
+
+ private int _poolCapacity = -1;
+ private DeflaterPool _deflaterPool = null;
+
+ private int _minGzipSize = DEFAULT_MIN_GZIP_SIZE;
+ private int _compressionLevel = Deflater.DEFAULT_COMPRESSION;
+ /**
+ * @deprecated feature will be removed in Jetty 10.x, with no replacement.
+ */
+ @Deprecated
+ private boolean _checkGzExists = false;
+ private boolean _syncFlush = false;
+ private int _inflateBufferSize = -1;
+ private EnumSet<DispatcherType> _dispatchers = EnumSet.of(DispatcherType.REQUEST);
+ // non-static, as other GzipHandler instances may have different configurations
+ private final IncludeExclude<String> _agentPatterns = new IncludeExclude<>(RegexSet.class);
+ private final IncludeExclude<String> _methods = new IncludeExclude<>();
+ private final IncludeExclude<String> _paths = new IncludeExclude<>(PathSpecSet.class);
+ private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>();
+ private HttpField _vary;
+
+ /**
+ * Instantiates a new GzipHandler.
+ */
+ public GzipHandler()
+ {
+ _methods.include(HttpMethod.GET.asString());
+ for (String type : MimeTypes.getKnownMimeTypes())
+ {
+ if ("image/svg+xml".equals(type))
+ _paths.exclude("*.svgz");
+ else if (type.startsWith("image/") ||
+ type.startsWith("audio/") ||
+ type.startsWith("video/"))
+ _mimeTypes.exclude(type);
+ }
+ _mimeTypes.exclude("application/compress");
+ _mimeTypes.exclude("application/zip");
+ _mimeTypes.exclude("application/gzip");
+ _mimeTypes.exclude("application/bzip2");
+ _mimeTypes.exclude("application/brotli");
+ _mimeTypes.exclude("application/x-xz");
+ _mimeTypes.exclude("application/x-rar-compressed");
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} mime types {}", this, _mimeTypes);
+
+ _agentPatterns.exclude(".*MSIE 6.0.*");
+ }
+
+ /**
+ * Add excluded to the User-Agent filtering.
+ *
+ * @param patterns Regular expressions matching user agents to exclude
+ * @see #addIncludedAgentPatterns(String...)
+ */
+ public void addExcludedAgentPatterns(String... patterns)
+ {
+ _agentPatterns.exclude(patterns);
+ }
+
+ /**
+ * Add excluded to the HTTP methods filtering.
+ *
+ * @param methods The methods to exclude in compression
+ * @see #addIncludedMethods(String...)
+ */
+ public void addExcludedMethods(String... methods)
+ {
+ for (String m : methods)
+ {
+ _methods.exclude(m);
+ }
+ }
+
+ /**
+ * Get the Set of {@link DispatcherType} that this Filter will operate on.
+ *
+ * @return the set of {@link DispatcherType} this filter will operate on
+ */
+ public EnumSet<DispatcherType> getDispatcherTypes()
+ {
+ return _dispatchers;
+ }
+
+ /**
+ * Set of supported {@link DispatcherType} that this filter will operate on.
+ *
+ * @param dispatchers the set of {@link DispatcherType} that this filter will operate on
+ */
+ public void setDispatcherTypes(EnumSet<DispatcherType> dispatchers)
+ {
+ _dispatchers = dispatchers;
+ }
+
+ /**
+ * Set the list of supported {@link DispatcherType} that this filter will operate on.
+ *
+ * @param dispatchers the list of {@link DispatcherType} that this filter will operate on
+ */
+ public void setDispatcherTypes(DispatcherType... dispatchers)
+ {
+ _dispatchers = EnumSet.copyOf(Arrays.asList(dispatchers));
+ }
+
+ /**
+ * Adds excluded MIME types for response filtering.
+ *
+ * <p>
+ * <em>Deprecation Warning: </em>
+ * For backward compatibility the MIME types parameters may be comma separated strings,
+ * but this will not be supported in future versions of Jetty.
+ * </p>
+ *
+ * @param types The mime types to exclude (without charset or other parameters).
+ * @see #addIncludedMimeTypes(String...)
+ */
+ public void addExcludedMimeTypes(String... types)
+ {
+ for (String t : types)
+ {
+ _mimeTypes.exclude(StringUtil.csvSplit(t));
+ }
+ }
+
+ /**
+ * Adds excluded Path Specs for request filtering.
+ *
+ * <p>
+ * There are 2 syntaxes supported, Servlet <code>url-pattern</code> based, and
+ * Regex based. This means that the initial characters on the path spec
+ * line are very strict, and determine the behavior of the path matching.
+ * <ul>
+ * <li>If the spec starts with <code>'^'</code> the spec is assumed to be
+ * a regex based path spec and will match with normal Java regex rules.</li>
+ * <li>If the spec starts with <code>'/'</code> then spec is assumed to be
+ * a Servlet url-pattern rules path spec for either an exact match
+ * or prefix based match.</li>
+ * <li>If the spec starts with <code>'*.'</code> then spec is assumed to be
+ * a Servlet url-pattern rules path spec for a suffix based match.</li>
+ * <li>All other syntaxes are unsupported</li>
+ * </ul>
+ * <p>
+ * Note: inclusion takes precedence over exclude.
+ *
+ * @param pathspecs Path specs (as per servlet spec) to exclude. If a
+ * ServletContext is available, the paths are relative to the context path,
+ * otherwise they are absolute.<br>
+ * For backward compatibility the pathspecs may be comma separated strings, but this
+ * will not be supported in future versions.
+ * @see #addIncludedPaths(String...)
+ */
+ public void addExcludedPaths(String... pathspecs)
+ {
+ for (String p : pathspecs)
+ {
+ _paths.exclude(StringUtil.csvSplit(p));
+ }
+ }
+
+ /**
+ * Adds included User-Agents for filtering.
+ *
+ * @param patterns Regular expressions matching user agents to include
+ * @see #addExcludedAgentPatterns(String...)
+ */
+ public void addIncludedAgentPatterns(String... patterns)
+ {
+ _agentPatterns.include(patterns);
+ }
+
+ /**
+ * Adds included HTTP Methods (eg: POST, PATCH, DELETE) for filtering.
+ *
+ * @param methods The HTTP methods to include in compression.
+ * @see #addExcludedMethods(String...)
+ */
+ public void addIncludedMethods(String... methods)
+ {
+ for (String m : methods)
+ {
+ _methods.include(m);
+ }
+ }
+
+ /**
+ * Is the {@link Deflater} running {@link Deflater#SYNC_FLUSH} or not.
+ *
+ * @return True if {@link Deflater#SYNC_FLUSH} is used, else {@link Deflater#NO_FLUSH}
+ * @see #setSyncFlush(boolean)
+ */
+ public boolean isSyncFlush()
+ {
+ return _syncFlush;
+ }
+
+ /**
+ * Set the {@link Deflater} flush mode to use. {@link Deflater#SYNC_FLUSH}
+ * should be used if the application wishes to stream the data, but this may
+ * hurt compression performance.
+ *
+ * @param syncFlush True if {@link Deflater#SYNC_FLUSH} is used, else {@link Deflater#NO_FLUSH}
+ * @see #isSyncFlush()
+ */
+ public void setSyncFlush(boolean syncFlush)
+ {
+ _syncFlush = syncFlush;
+ }
+
+ /**
+ * Add included MIME types for response filtering
+ *
+ * @param types The mime types to include (without charset or other parameters)
+ * For backward compatibility the mimetypes may be comma separated strings, but this
+ * will not be supported in future versions.
+ * @see #addExcludedMimeTypes(String...)
+ */
+ public void addIncludedMimeTypes(String... types)
+ {
+ for (String t : types)
+ {
+ _mimeTypes.include(StringUtil.csvSplit(t));
+ }
+ }
+
+ /**
+ * Add included Path Specs for filtering.
+ *
+ * <p>
+ * There are 2 syntaxes supported, Servlet <code>url-pattern</code> based, and
+ * Regex based. This means that the initial characters on the path spec
+ * line are very strict, and determine the behavior of the path matching.
+ * <ul>
+ * <li>If the spec starts with <code>'^'</code> the spec is assumed to be
+ * a regex based path spec and will match with normal Java regex rules.</li>
+ * <li>If the spec starts with <code>'/'</code> then spec is assumed to be
+ * a Servlet url-pattern rules path spec for either an exact match
+ * or prefix based match.</li>
+ * <li>If the spec starts with <code>'*.'</code> then spec is assumed to be
+ * a Servlet url-pattern rules path spec for a suffix based match.</li>
+ * <li>All other syntaxes are unsupported</li>
+ * </ul>
+ * <p>
+ * Note: inclusion takes precedence over exclusion.
+ *
+ * @param pathspecs Path specs (as per servlet spec) to include. If a
+ * ServletContext is available, the paths are relative to the context path,
+ * otherwise they are absolute
+ */
+ public void addIncludedPaths(String... pathspecs)
+ {
+ for (String p : pathspecs)
+ {
+ _paths.include(StringUtil.csvSplit(p));
+ }
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ _deflaterPool = newDeflaterPool(_poolCapacity);
+ addBean(_deflaterPool);
+ _vary = (_agentPatterns.size() > 0) ? GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING_USER_AGENT : GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING;
+ super.doStart();
+ }
+
+ /**
+ * @deprecated feature will be removed in Jetty 10.x, with no replacement.
+ */
+ @Deprecated
+ public boolean getCheckGzExists()
+ {
+ return _checkGzExists;
+ }
+
+ public int getCompressionLevel()
+ {
+ return _compressionLevel;
+ }
+
+ @Override
+ public Deflater getDeflater(Request request, long contentLength)
+ {
+ HttpFields httpFields = request.getHttpFields();
+ String ua = httpFields.get(HttpHeader.USER_AGENT);
+ if (ua != null && !isAgentGzipable(ua))
+ {
+ LOG.debug("{} excluded user agent {}", this, request);
+ return null;
+ }
+
+ if (contentLength >= 0 && contentLength < _minGzipSize)
+ {
+ LOG.debug("{} excluded minGzipSize {}", this, request);
+ return null;
+ }
+
+ // check the accept encoding header
+ if (!httpFields.contains(HttpHeader.ACCEPT_ENCODING, "gzip"))
+ {
+ LOG.debug("{} excluded not gzip accept {}", this, request);
+ return null;
+ }
+
+ return _deflaterPool.acquire();
+ }
+
+ /**
+ * Get the current filter list of excluded User-Agent patterns
+ *
+ * @return the filter list of excluded User-Agent patterns
+ * @see #getIncludedAgentPatterns()
+ */
+ public String[] getExcludedAgentPatterns()
+ {
+ Set<String> excluded = _agentPatterns.getExcluded();
+ return excluded.toArray(new String[0]);
+ }
+
+ /**
+ * Get the current filter list of excluded HTTP methods
+ *
+ * @return the filter list of excluded HTTP methods
+ * @see #getIncludedMethods()
+ */
+ public String[] getExcludedMethods()
+ {
+ Set<String> excluded = _methods.getExcluded();
+ return excluded.toArray(new String[0]);
+ }
+
+ /**
+ * Get the current filter list of excluded MIME types
+ *
+ * @return the filter list of excluded MIME types
+ * @see #getIncludedMimeTypes()
+ */
+ public String[] getExcludedMimeTypes()
+ {
+ Set<String> excluded = _mimeTypes.getExcluded();
+ return excluded.toArray(new String[0]);
+ }
+
+ /**
+ * Get the current filter list of excluded Path Specs
+ *
+ * @return the filter list of excluded Path Specs
+ * @see #getIncludedPaths()
+ */
+ public String[] getExcludedPaths()
+ {
+ Set<String> excluded = _paths.getExcluded();
+ return excluded.toArray(new String[0]);
+ }
+
+ /**
+ * Get the current filter list of included User-Agent patterns
+ *
+ * @return the filter list of included User-Agent patterns
+ * @see #getExcludedAgentPatterns()
+ */
+ public String[] getIncludedAgentPatterns()
+ {
+ Set<String> includes = _agentPatterns.getIncluded();
+ return includes.toArray(new String[0]);
+ }
+
+ /**
+ * Get the current filter list of included HTTP Methods
+ *
+ * @return the filter list of included HTTP methods
+ * @see #getExcludedMethods()
+ */
+ public String[] getIncludedMethods()
+ {
+ Set<String> includes = _methods.getIncluded();
+ return includes.toArray(new String[0]);
+ }
+
+ /**
+ * Get the current filter list of included MIME types
+ *
+ * @return the filter list of included MIME types
+ * @see #getExcludedMimeTypes()
+ */
+ public String[] getIncludedMimeTypes()
+ {
+ Set<String> includes = _mimeTypes.getIncluded();
+ return includes.toArray(new String[0]);
+ }
+
+ /**
+ * Get the current filter list of included Path Specs
+ *
+ * @return the filter list of included Path Specs
+ * @see #getExcludedPaths()
+ */
+ public String[] getIncludedPaths()
+ {
+ Set<String> includes = _paths.getIncluded();
+ return includes.toArray(new String[0]);
+ }
+
+ /**
+ * Get the current filter list of included HTTP methods
+ *
+ * @return the filter list of included HTTP methods
+ * @deprecated use {@link #getIncludedMethods()} instead. (Will be removed in Jetty 10)
+ */
+ @Deprecated
+ public String[] getMethods()
+ {
+ return getIncludedMethods();
+ }
+
+ /**
+ * Get the minimum size, in bytes, that a response {@code Content-Length} must be
+ * before compression will trigger.
+ *
+ * @return minimum response size (in bytes) that triggers compression
+ * @see #setMinGzipSize(int)
+ */
+ public int getMinGzipSize()
+ {
+ return _minGzipSize;
+ }
+
+ protected HttpField getVaryField()
+ {
+ return _vary;
+ }
+
+ /**
+ * Get the size (in bytes) of the {@link java.util.zip.Inflater} buffer used to inflate
+ * compressed requests.
+ *
+ * @return size in bytes of the buffer, or 0 for no inflation.
+ */
+ public int getInflateBufferSize()
+ {
+ return _inflateBufferSize;
+ }
+
+ /**
+ * Set the size (in bytes) of the {@link java.util.zip.Inflater} buffer used to inflate comrpessed requests.
+ *
+ * @param size size in bytes of the buffer, or 0 for no inflation.
+ */
+ public void setInflateBufferSize(int size)
+ {
+ _inflateBufferSize = size;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ ServletContext context = baseRequest.getServletContext();
+ String path = context == null ? baseRequest.getRequestURI() : URIUtil.addPaths(baseRequest.getServletPath(), baseRequest.getPathInfo());
+ LOG.debug("{} handle {} in {}", this, baseRequest, context);
+
+ if (!_dispatchers.contains(baseRequest.getDispatcherType()))
+ {
+ LOG.debug("{} excluded by dispatcherType {}", this, baseRequest.getDispatcherType());
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+
+ // Handle request inflation
+ if (_inflateBufferSize > 0)
+ {
+ boolean inflate = false;
+ for (ListIterator<HttpField> i = baseRequest.getHttpFields().listIterator(); i.hasNext(); )
+ {
+ HttpField field = i.next();
+
+ if (field.getHeader() == HttpHeader.CONTENT_ENCODING)
+ {
+ if (field.getValue().equalsIgnoreCase("gzip"))
+ {
+ i.set(X_CE_GZIP);
+ inflate = true;
+ break;
+ }
+
+ if (COMMA_GZIP.matcher(field.getValue()).matches())
+ {
+ String v = field.getValue();
+ v = v.substring(0, v.lastIndexOf(','));
+ i.set(new HttpField(HttpHeader.CONTENT_ENCODING, v));
+ i.add(X_CE_GZIP);
+ inflate = true;
+ break;
+ }
+ }
+ }
+
+ if (inflate)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} inflate {}", this, request);
+
+ baseRequest.getHttpInput().addInterceptor(new GzipHttpInputInterceptor(baseRequest.getHttpChannel().getByteBufferPool(), _inflateBufferSize));
+
+ baseRequest.getHttpFields().computeField(HttpHeader.CONTENT_LENGTH, (header, fields) ->
+ {
+ if (fields == null)
+ return null;
+ String length = fields.stream().map(HttpField::getValue).findAny().orElse("0");
+ return new HttpField("X-Content-Length", length);
+ });
+ }
+ }
+
+ // Are we already being gzipped?
+ HttpOutput out = baseRequest.getResponse().getHttpOutput();
+ HttpOutput.Interceptor interceptor = out.getInterceptor();
+ while (interceptor != null)
+ {
+ if (interceptor instanceof GzipHttpOutputInterceptor)
+ {
+ LOG.debug("{} already intercepting {}", this, request);
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+ interceptor = interceptor.getNextInterceptor();
+ }
+
+ // Special handling for etags
+ if (!StringUtil.isEmpty(CompressedContentFormat.ETAG_SEPARATOR))
+ {
+ for (ListIterator<HttpField> fields = baseRequest.getHttpFields().listIterator(); fields.hasNext(); )
+ {
+ HttpField field = fields.next();
+ if (field.getHeader() == HttpHeader.IF_NONE_MATCH || field.getHeader() == HttpHeader.IF_MATCH)
+ {
+ String etags = field.getValue();
+ String etagsNoSuffix = CompressedContentFormat.GZIP.stripSuffixes(etags);
+ if (!etagsNoSuffix.equals(etags))
+ {
+ fields.set(new HttpField(field.getHeader(), etagsNoSuffix));
+ baseRequest.setAttribute(GZIP_HANDLER_ETAGS, etags);
+ }
+ }
+ }
+ }
+
+ // If not a supported method - no Vary because no matter what client, this URI is always excluded
+ if (!_methods.test(baseRequest.getMethod()))
+ {
+ LOG.debug("{} excluded by method {}", this, request);
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+
+ // If not a supported URI- no Vary because no matter what client, this URI is always excluded
+ // Use pathInfo because this is be
+ if (!isPathGzipable(path))
+ {
+ LOG.debug("{} excluded by path {}", this, request);
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+
+ // Exclude non compressible mime-types known from URI extension. - no Vary because no matter what client, this URI is always excluded
+ String mimeType = context == null ? MimeTypes.getDefaultMimeByExtension(path) : context.getMimeType(path);
+ if (mimeType != null)
+ {
+ mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
+ if (!isMimeTypeGzipable(mimeType))
+ {
+ LOG.debug("{} excluded by path suffix mime type {}", this, request);
+ // handle normally without setting vary header
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+ }
+
+ if (_checkGzExists && context != null)
+ {
+ String realpath = request.getServletContext().getRealPath(path);
+ if (realpath != null)
+ {
+ File gz = new File(realpath + ".gz");
+ if (gz.exists())
+ {
+ LOG.debug("{} gzip exists {}", this, request);
+ // allow default servlet to handle
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+ }
+ }
+
+ HttpOutput.Interceptor origInterceptor = out.getInterceptor();
+ try
+ {
+ // install interceptor and handle
+ out.setInterceptor(new GzipHttpOutputInterceptor(this, getVaryField(), baseRequest.getHttpChannel(), origInterceptor, isSyncFlush()));
+
+ if (_handler != null)
+ _handler.handle(target, baseRequest, request, response);
+ }
+ finally
+ {
+ // reset interceptor if request not handled
+ if (!baseRequest.isHandled() && !baseRequest.isAsyncStarted())
+ out.setInterceptor(origInterceptor);
+ }
+ }
+
+ /**
+ * Test if the provided User-Agent is allowed based on the User-Agent filters.
+ *
+ * @param ua the user agent
+ * @return whether compressing is allowed for the given user agent
+ */
+ protected boolean isAgentGzipable(String ua)
+ {
+ if (ua == null)
+ return false;
+
+ return _agentPatterns.test(ua);
+ }
+
+ /**
+ * Test if the provided MIME type is allowed based on the MIME type filters.
+ *
+ * @param mimetype the MIME type to test
+ * @return true if allowed, false otherwise
+ */
+ @Override
+ public boolean isMimeTypeGzipable(String mimetype)
+ {
+ return _mimeTypes.test(mimetype);
+ }
+
+ /**
+ * Test if the provided Request URI is allowed based on the Path Specs filters.
+ *
+ * @param requestURI the request uri
+ * @return whether compressing is allowed for the given the path
+ */
+ protected boolean isPathGzipable(String requestURI)
+ {
+ if (requestURI == null)
+ return true;
+
+ return _paths.test(requestURI);
+ }
+
+ @Override
+ public void recycle(Deflater deflater)
+ {
+ _deflaterPool.release(deflater);
+ }
+
+ /**
+ * Set the Check if {@code *.gz} file for the incoming file exists.
+ *
+ * @param checkGzExists whether to check if a static gz file exists for
+ * the resource that the DefaultServlet may serve as precompressed.
+ * @deprecated feature will be removed in Jetty 10.x, with no replacement.
+ */
+ @Deprecated
+ public void setCheckGzExists(boolean checkGzExists)
+ {
+ _checkGzExists = checkGzExists;
+ }
+
+ /**
+ * Set the Compression level that {@link Deflater} uses.
+ *
+ * @param compressionLevel The compression level to use to initialize {@link Deflater#setLevel(int)}
+ * @see Deflater#setLevel(int)
+ */
+ public void setCompressionLevel(int compressionLevel)
+ {
+ if (isStarted())
+ throw new IllegalStateException(getState());
+
+ _compressionLevel = compressionLevel;
+ }
+
+ /**
+ * Set the excluded filter list of User-Agent patterns (replacing any previously set)
+ *
+ * @param patterns Regular expressions list matching user agents to exclude
+ * @see #setIncludedAgentPatterns(String...)
+ */
+ public void setExcludedAgentPatterns(String... patterns)
+ {
+ _agentPatterns.getExcluded().clear();
+ addExcludedAgentPatterns(patterns);
+ }
+
+ /**
+ * Set the excluded filter list of HTTP methods (replacing any previously set)
+ *
+ * @param methods the HTTP methods to exclude
+ * @see #setIncludedMethods(String...)
+ */
+ public void setExcludedMethods(String... methods)
+ {
+ _methods.getExcluded().clear();
+ _methods.exclude(methods);
+ }
+
+ /**
+ * Set the excluded filter list of MIME types (replacing any previously set)
+ *
+ * @param types The mime types to exclude (without charset or other parameters)
+ * @see #setIncludedMimeTypes(String...)
+ */
+ public void setExcludedMimeTypes(String... types)
+ {
+ _mimeTypes.getExcluded().clear();
+ _mimeTypes.exclude(types);
+ }
+
+ /**
+ * Set the excluded filter list of Path specs (replacing any previously set)
+ *
+ * @param pathspecs Path specs (as per servlet spec) to exclude. If a
+ * ServletContext is available, the paths are relative to the context path,
+ * otherwise they are absolute.
+ * @see #setIncludedPaths(String...)
+ */
+ public void setExcludedPaths(String... pathspecs)
+ {
+ _paths.getExcluded().clear();
+ _paths.exclude(pathspecs);
+ }
+
+ /**
+ * Set the included filter list of User-Agent patterns (replacing any previously set)
+ *
+ * @param patterns Regular expressions matching user agents to include
+ * @see #setExcludedAgentPatterns(String...)
+ */
+ public void setIncludedAgentPatterns(String... patterns)
+ {
+ _agentPatterns.getIncluded().clear();
+ addIncludedAgentPatterns(patterns);
+ }
+
+ /**
+ * Set the included filter list of HTTP methods (replacing any previously set)
+ *
+ * @param methods The methods to include in compression
+ * @see #setExcludedMethods(String...)
+ */
+ public void setIncludedMethods(String... methods)
+ {
+ _methods.getIncluded().clear();
+ _methods.include(methods);
+ }
+
+ /**
+ * Set the included filter list of MIME types (replacing any previously set)
+ *
+ * @param types The mime types to include (without charset or other parameters)
+ * @see #setExcludedMimeTypes(String...)
+ */
+ public void setIncludedMimeTypes(String... types)
+ {
+ _mimeTypes.getIncluded().clear();
+ _mimeTypes.include(types);
+ }
+
+ /**
+ * Set the included filter list of Path specs (replacing any previously set)
+ *
+ * @param pathspecs Path specs (as per servlet spec) to include. If a
+ * ServletContext is available, the paths are relative to the context path,
+ * otherwise they are absolute
+ * @see #setExcludedPaths(String...)
+ */
+ public void setIncludedPaths(String... pathspecs)
+ {
+ _paths.getIncluded().clear();
+ _paths.include(pathspecs);
+ }
+
+ /**
+ * Set the minimum response size to trigger dynamic compression.
+ * <p>
+ * Sizes below {@link #BREAK_EVEN_GZIP_SIZE} will result a compressed response that is larger than the
+ * original data.
+ * </p>
+ *
+ * @param minGzipSize minimum response size in bytes (not allowed to be lower then {@link #BREAK_EVEN_GZIP_SIZE})
+ */
+ public void setMinGzipSize(int minGzipSize)
+ {
+ if (minGzipSize < BREAK_EVEN_GZIP_SIZE)
+ LOG.warn("minGzipSize of {} is inefficient for short content, break even is size {}", minGzipSize, BREAK_EVEN_GZIP_SIZE);
+ _minGzipSize = Math.max(0, minGzipSize);
+ }
+
+ /**
+ * Set the included filter list of HTTP Methods (replacing any previously set)
+ *
+ * @param csvMethods the list of methods, CSV format
+ * @see #setExcludedMethodList(String)
+ */
+ public void setIncludedMethodList(String csvMethods)
+ {
+ setIncludedMethods(StringUtil.csvSplit(csvMethods));
+ }
+
+ /**
+ * Get the included filter list of HTTP methods in CSV format
+ *
+ * @return the included filter list of HTTP methods in CSV format
+ * @see #getExcludedMethodList()
+ */
+ public String getIncludedMethodList()
+ {
+ return String.join(",", getIncludedMethods());
+ }
+
+ /**
+ * Set the excluded filter list of HTTP Methods (replacing any previously set)
+ *
+ * @param csvMethods the list of methods, CSV format
+ * @see #setIncludedMethodList(String)
+ */
+ public void setExcludedMethodList(String csvMethods)
+ {
+ setExcludedMethods(StringUtil.csvSplit(csvMethods));
+ }
+
+ /**
+ * Get the excluded filter list of HTTP methods in CSV format
+ *
+ * @return the excluded filter list of HTTP methods in CSV format
+ * @see #getIncludedMethodList()
+ */
+ public String getExcludedMethodList()
+ {
+ return String.join(",", getExcludedMethods());
+ }
+
+ /**
+ * Gets the maximum number of Deflaters that the DeflaterPool can hold.
+ *
+ * @return the Deflater pool capacity
+ */
+ public int getDeflaterPoolCapacity()
+ {
+ return _poolCapacity;
+ }
+
+ /**
+ * Sets the maximum number of Deflaters that the DeflaterPool can hold.
+ */
+ public void setDeflaterPoolCapacity(int capacity)
+ {
+ if (isStarted())
+ throw new IllegalStateException(getState());
+
+ _poolCapacity = capacity;
+ }
+
+ protected DeflaterPool newDeflaterPool(int capacity)
+ {
+ return new DeflaterPool(capacity, getCompressionLevel(), true);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{%s,min=%s,inflate=%s}", getClass().getSimpleName(), hashCode(), getState(), _minGzipSize, _inflateBufferSize);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java
new file mode 100644
index 0000000..a7ffe11
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java
@@ -0,0 +1,94 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler.gzip;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.http.GZIPContentDecoder;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.server.HttpInput;
+import org.eclipse.jetty.server.HttpInput.Content;
+import org.eclipse.jetty.util.component.Destroyable;
+
+/**
+ * An HttpInput Interceptor that inflates GZIP encoded request content.
+ */
+public class GzipHttpInputInterceptor implements HttpInput.Interceptor, Destroyable
+{
+ private final Decoder _decoder;
+ private ByteBuffer _chunk;
+
+ public GzipHttpInputInterceptor(ByteBufferPool pool, int bufferSize)
+ {
+ _decoder = new Decoder(pool, bufferSize);
+ }
+
+ @Override
+ public Content readFrom(Content content)
+ {
+ _decoder.decodeChunks(content.getByteBuffer());
+ final ByteBuffer chunk = _chunk;
+
+ if (chunk == null)
+ return null;
+
+ return new Content(chunk)
+ {
+ @Override
+ public void succeeded()
+ {
+ _decoder.release(chunk);
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ _decoder.release(chunk);
+ }
+ };
+ }
+
+ @Override
+ public void destroy()
+ {
+ _decoder.destroy();
+ }
+
+ private class Decoder extends GZIPContentDecoder
+ {
+ private Decoder(ByteBufferPool pool, int bufferSize)
+ {
+ super(pool, bufferSize);
+ }
+
+ @Override
+ protected boolean decodedChunk(final ByteBuffer chunk)
+ {
+ _chunk = chunk;
+ return true;
+ }
+
+ @Override
+ public void decodeChunks(ByteBuffer compressed)
+ {
+ _chunk = null;
+ super.decodeChunks(compressed);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java
new file mode 100644
index 0000000..3ba0b36
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java
@@ -0,0 +1,433 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler.gzip;
+
+import java.nio.ByteBuffer;
+import java.nio.channels.WritePendingException;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.zip.CRC32;
+import java.util.zip.Deflater;
+
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.http.PreEncodedHttpField;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpOutput;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.IteratingNestedCallback;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+import static org.eclipse.jetty.http.CompressedContentFormat.GZIP;
+
+public class GzipHttpOutputInterceptor implements HttpOutput.Interceptor
+{
+ public static Logger LOG = Log.getLogger(GzipHttpOutputInterceptor.class);
+ private static final byte[] GZIP_HEADER = new byte[]{(byte)0x1f, (byte)0x8b, Deflater.DEFLATED, 0, 0, 0, 0, 0, 0, 0};
+
+ public static final HttpField VARY_ACCEPT_ENCODING_USER_AGENT = new PreEncodedHttpField(HttpHeader.VARY, HttpHeader.ACCEPT_ENCODING + ", " + HttpHeader.USER_AGENT);
+ public static final HttpField VARY_ACCEPT_ENCODING = new PreEncodedHttpField(HttpHeader.VARY, HttpHeader.ACCEPT_ENCODING.asString());
+
+ private enum GZState
+ {
+ MIGHT_COMPRESS, NOT_COMPRESSING, COMMITTING, COMPRESSING, FINISHED
+ }
+
+ private final AtomicReference<GZState> _state = new AtomicReference<>(GZState.MIGHT_COMPRESS);
+ private final CRC32 _crc = new CRC32();
+
+ private final GzipFactory _factory;
+ private final HttpOutput.Interceptor _interceptor;
+ private final HttpChannel _channel;
+ private final HttpField _vary;
+ private final int _bufferSize;
+ private final boolean _syncFlush;
+
+ private Deflater _deflater;
+ private ByteBuffer _buffer;
+
+ public GzipHttpOutputInterceptor(GzipFactory factory, HttpChannel channel, HttpOutput.Interceptor next, boolean syncFlush)
+ {
+ this(factory, VARY_ACCEPT_ENCODING_USER_AGENT, channel.getHttpConfiguration().getOutputBufferSize(), channel, next, syncFlush);
+ }
+
+ public GzipHttpOutputInterceptor(GzipFactory factory, HttpField vary, HttpChannel channel, HttpOutput.Interceptor next, boolean syncFlush)
+ {
+ this(factory, vary, channel.getHttpConfiguration().getOutputBufferSize(), channel, next, syncFlush);
+ }
+
+ public GzipHttpOutputInterceptor(GzipFactory factory, HttpField vary, int bufferSize, HttpChannel channel, HttpOutput.Interceptor next, boolean syncFlush)
+ {
+ _factory = factory;
+ _channel = channel;
+ _interceptor = next;
+ _vary = vary;
+ _bufferSize = bufferSize;
+ _syncFlush = syncFlush;
+ }
+
+ @Override
+ public HttpOutput.Interceptor getNextInterceptor()
+ {
+ return _interceptor;
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return false; // No point as deflator is in user space.
+ }
+
+ @Override
+ public void write(ByteBuffer content, boolean complete, Callback callback)
+ {
+ switch (_state.get())
+ {
+ case MIGHT_COMPRESS:
+ commit(content, complete, callback);
+ break;
+
+ case NOT_COMPRESSING:
+ _interceptor.write(content, complete, callback);
+ return;
+
+ case COMMITTING:
+ callback.failed(new WritePendingException());
+ break;
+
+ case COMPRESSING:
+ gzip(content, complete, callback);
+ break;
+
+ default:
+ callback.failed(new IllegalStateException("state=" + _state.get()));
+ break;
+ }
+ }
+
+ private void addTrailer()
+ {
+ BufferUtil.putIntLittleEndian(_buffer, (int)_crc.getValue());
+ BufferUtil.putIntLittleEndian(_buffer, _deflater.getTotalIn());
+ }
+
+ private void gzip(ByteBuffer content, boolean complete, final Callback callback)
+ {
+ if (content.hasRemaining() || complete)
+ new GzipBufferCB(content, complete, callback).iterate();
+ else
+ callback.succeeded();
+ }
+
+ protected void commit(ByteBuffer content, boolean complete, Callback callback)
+ {
+ // Are we excluding because of status?
+ Response response = _channel.getResponse();
+ int sc = response.getStatus();
+ if (sc > 0 && (sc < 200 || sc == 204 || sc == 205 || sc >= 300))
+ {
+ LOG.debug("{} exclude by status {}", this, sc);
+ noCompression();
+
+ if (sc == HttpStatus.NOT_MODIFIED_304)
+ {
+ String requestEtags = (String)_channel.getRequest().getAttribute(GzipHandler.GZIP_HANDLER_ETAGS);
+ String responseEtag = response.getHttpFields().get(HttpHeader.ETAG);
+ if (requestEtags != null && responseEtag != null)
+ {
+ String responseEtagGzip = etagGzip(responseEtag);
+ if (requestEtags.contains(responseEtagGzip))
+ response.getHttpFields().put(HttpHeader.ETAG, responseEtagGzip);
+ }
+ }
+
+ _interceptor.write(content, complete, callback);
+ return;
+ }
+
+ // Are we excluding because of mime-type?
+ String ct = response.getContentType();
+ if (ct != null)
+ {
+ ct = MimeTypes.getContentTypeWithoutCharset(ct);
+ if (!_factory.isMimeTypeGzipable(StringUtil.asciiToLowerCase(ct)))
+ {
+ LOG.debug("{} exclude by mimeType {}", this, ct);
+ noCompression();
+ _interceptor.write(content, complete, callback);
+ return;
+ }
+ }
+
+ // Has the Content-Encoding header already been set?
+ HttpFields fields = response.getHttpFields();
+ String ce = fields.get(HttpHeader.CONTENT_ENCODING);
+ if (ce != null)
+ {
+ LOG.debug("{} exclude by content-encoding {}", this, ce);
+ noCompression();
+ _interceptor.write(content, complete, callback);
+ return;
+ }
+
+ // Are we the thread that commits?
+ if (_state.compareAndSet(GZState.MIGHT_COMPRESS, GZState.COMMITTING))
+ {
+ // We are varying the response due to accept encoding header.
+ if (_vary != null)
+ {
+ if (fields.contains(HttpHeader.VARY))
+ fields.addCSV(HttpHeader.VARY, _vary.getValues());
+ else
+ fields.add(_vary);
+ }
+
+ long contentLength = response.getContentLength();
+ if (contentLength < 0 && complete)
+ contentLength = content.remaining();
+
+ _deflater = _factory.getDeflater(_channel.getRequest(), contentLength);
+
+ if (_deflater == null)
+ {
+ LOG.debug("{} exclude no deflater", this);
+ _state.set(GZState.NOT_COMPRESSING);
+ _interceptor.write(content, complete, callback);
+ return;
+ }
+
+ fields.put(GZIP.getContentEncoding());
+ _crc.reset();
+
+ // Adjust headers
+ response.setContentLength(-1);
+ String etag = fields.get(HttpHeader.ETAG);
+ if (etag != null)
+ fields.put(HttpHeader.ETAG, etagGzip(etag));
+
+ LOG.debug("{} compressing {}", this, _deflater);
+ _state.set(GZState.COMPRESSING);
+
+ if (BufferUtil.isEmpty(content))
+ {
+ // We are committing, but have no content to compress, so flush empty buffer to write headers.
+ _interceptor.write(BufferUtil.EMPTY_BUFFER, complete, callback);
+ }
+ else
+ {
+ gzip(content, complete, callback);
+ }
+ }
+ else
+ callback.failed(new WritePendingException());
+ }
+
+ private String etagGzip(String etag)
+ {
+ return GZIP.etag(etag);
+ }
+
+ public void noCompression()
+ {
+ while (true)
+ {
+ switch (_state.get())
+ {
+ case NOT_COMPRESSING:
+ return;
+
+ case MIGHT_COMPRESS:
+ if (_state.compareAndSet(GZState.MIGHT_COMPRESS, GZState.NOT_COMPRESSING))
+ return;
+ break;
+
+ default:
+ throw new IllegalStateException(_state.get().toString());
+ }
+ }
+ }
+
+ public void noCompressionIfPossible()
+ {
+ while (true)
+ {
+ switch (_state.get())
+ {
+ case COMPRESSING:
+ case NOT_COMPRESSING:
+ return;
+
+ case MIGHT_COMPRESS:
+ if (_state.compareAndSet(GZState.MIGHT_COMPRESS, GZState.NOT_COMPRESSING))
+ return;
+ break;
+
+ default:
+ throw new IllegalStateException(_state.get().toString());
+ }
+ }
+ }
+
+ public boolean mightCompress()
+ {
+ return _state.get() == GZState.MIGHT_COMPRESS;
+ }
+
+ private class GzipBufferCB extends IteratingNestedCallback
+ {
+ private ByteBuffer _copy;
+ private final ByteBuffer _content;
+ private final boolean _last;
+
+ public GzipBufferCB(ByteBuffer content, boolean complete, Callback callback)
+ {
+ super(callback);
+ _content = content;
+ _last = complete;
+ }
+
+ @Override
+ protected void onCompleteFailure(Throwable x)
+ {
+ _factory.recycle(_deflater);
+ _deflater = null;
+ super.onCompleteFailure(x);
+ }
+
+ @Override
+ protected Action process() throws Exception
+ {
+ // If we have no deflator
+ if (_deflater == null)
+ {
+ // then the trailer has been generated and written below.
+ // we have finished compressing the entire content, so
+ // cleanup and succeed.
+ if (_buffer != null)
+ {
+ _channel.getByteBufferPool().release(_buffer);
+ _buffer = null;
+ }
+ if (_copy != null)
+ {
+ _channel.getByteBufferPool().release(_copy);
+ _copy = null;
+ }
+ return Action.SUCCEEDED;
+ }
+
+ // If we have no buffer
+ if (_buffer == null)
+ {
+ // allocate a buffer and add the gzip header
+ _buffer = _channel.getByteBufferPool().acquire(_bufferSize, false);
+ BufferUtil.fill(_buffer, GZIP_HEADER, 0, GZIP_HEADER.length);
+ }
+ else
+ {
+ // otherwise clear the buffer as previous writes will always fully consume.
+ BufferUtil.clear(_buffer);
+ }
+
+ // If the deflator is not finished, then compress more data
+ if (!_deflater.finished())
+ {
+ if (_deflater.needsInput())
+ {
+ // if there is no more content available to compress
+ // then we are either finished all content or just the current write.
+ if (BufferUtil.isEmpty(_content))
+ {
+ if (_last)
+ _deflater.finish();
+ else
+ return Action.SUCCEEDED;
+ }
+ else
+ {
+ // If there is more content available to compress, we have to make sure
+ // it is available in an array for the current deflator API, maybe slicing
+ // of content.
+ ByteBuffer slice;
+ if (_content.hasArray())
+ slice = _content;
+ else
+ {
+ if (_copy == null)
+ _copy = _channel.getByteBufferPool().acquire(_bufferSize, false);
+ else
+ BufferUtil.clear(_copy);
+ slice = _copy;
+ BufferUtil.append(_copy, _content);
+ }
+
+ // transfer the data from the slice to the the deflator
+ byte[] array = slice.array();
+ int off = slice.arrayOffset() + slice.position();
+ int len = slice.remaining();
+ _crc.update(array, off, len);
+ _deflater.setInput(array, off, len); // TODO use ByteBuffer API in Jetty-10
+ slice.position(slice.position() + len);
+ if (_last && BufferUtil.isEmpty(_content))
+ _deflater.finish();
+ }
+ }
+
+ // deflate the content into the available space in the buffer
+ int off = _buffer.arrayOffset() + _buffer.limit();
+ int len = BufferUtil.space(_buffer);
+ int produced = _deflater.deflate(_buffer.array(), off, len, _syncFlush ? Deflater.SYNC_FLUSH : Deflater.NO_FLUSH);
+ _buffer.limit(_buffer.limit() + produced);
+ }
+
+ // If we have finished deflation and there is room for the trailer.
+ if (_deflater.finished() && BufferUtil.space(_buffer) >= 8)
+ {
+ // add the trailer and recycle the deflator to flag that we will have had completeSuccess when
+ // the write below completes.
+ addTrailer();
+ _factory.recycle(_deflater);
+ _deflater = null;
+ }
+
+ // write the compressed buffer.
+ _interceptor.write(_buffer, _deflater == null, this);
+ return Action.SCHEDULED;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[content=%s last=%b copy=%s buffer=%s deflate=%s %s]",
+ super.toString(),
+ BufferUtil.toDetailString(_content),
+ _last,
+ BufferUtil.toDetailString(_copy),
+ BufferUtil.toDetailString(_buffer),
+ _deflater,
+ _deflater != null && _deflater.finished() ? "(finished)" : "");
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/package-info.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/package-info.java
new file mode 100644
index 0000000..d49343d
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty GZIP Handler
+ */
+package org.eclipse.jetty.server.handler.gzip;
+
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/jmx/AbstractHandlerMBean.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/jmx/AbstractHandlerMBean.java
new file mode 100644
index 0000000..8842658
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/jmx/AbstractHandlerMBean.java
@@ -0,0 +1,104 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler.jmx;
+
+import java.io.IOException;
+
+import org.eclipse.jetty.jmx.ObjectMBean;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.AbstractHandlerContainer;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class AbstractHandlerMBean extends ObjectMBean
+{
+ private static final Logger LOG = Log.getLogger(AbstractHandlerMBean.class);
+
+ public AbstractHandlerMBean(Object managedObject)
+ {
+ super(managedObject);
+ }
+
+ @Override
+ public String getObjectContextBasis()
+ {
+ if (_managed != null)
+ {
+ String basis = null;
+ if (_managed instanceof ContextHandler)
+ {
+ ContextHandler handler = (ContextHandler)_managed;
+ String context = getContextName(handler);
+ if (context == null)
+ context = handler.getDisplayName();
+ if (context != null)
+ return context;
+ }
+ else if (_managed instanceof AbstractHandler)
+ {
+ AbstractHandler handler = (AbstractHandler)_managed;
+ Server server = handler.getServer();
+ if (server != null)
+ {
+ ContextHandler context = AbstractHandlerContainer.findContainerOf(server, ContextHandler.class, handler);
+
+ if (context != null)
+ basis = getContextName(context);
+ }
+ }
+ if (basis != null)
+ return basis;
+ }
+ return super.getObjectContextBasis();
+ }
+
+ protected String getContextName(ContextHandler context)
+ {
+ String name = null;
+
+ if (context.getContextPath() != null && context.getContextPath().length() > 0)
+ {
+ int idx = context.getContextPath().lastIndexOf('/');
+ name = idx < 0 ? context.getContextPath() : context.getContextPath().substring(++idx);
+ if (name == null || name.length() == 0)
+ name = "ROOT";
+ }
+
+ if (name == null && context.getBaseResource() != null)
+ {
+ try
+ {
+ if (context.getBaseResource().getFile() != null)
+ name = context.getBaseResource().getFile().getName();
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ name = context.getBaseResource().getName();
+ }
+ }
+
+ if (context.getVirtualHosts() != null && context.getVirtualHosts().length > 0)
+ name = '"' + name + "@" + context.getVirtualHosts()[0] + '"';
+
+ return name;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/jmx/ContextHandlerMBean.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/jmx/ContextHandlerMBean.java
new file mode 100644
index 0000000..e35dc29
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/jmx/ContextHandlerMBean.java
@@ -0,0 +1,72 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler.jmx;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.Attributes;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.annotation.Name;
+
+@ManagedObject("ContextHandler mbean wrapper")
+public class ContextHandlerMBean extends AbstractHandlerMBean
+{
+ public ContextHandlerMBean(Object managedObject)
+ {
+ super(managedObject);
+ }
+
+ @ManagedAttribute("Map of context attributes")
+ public Map<String, Object> getContextAttributes()
+ {
+ Map<String, Object> map = new HashMap<String, Object>();
+ Attributes attrs = ((ContextHandler)_managed).getAttributes();
+ for (String name : attrs.getAttributeNameSet())
+ {
+ Object value = attrs.getAttribute(name);
+ map.put(name, value);
+ }
+ return map;
+ }
+
+ @ManagedOperation(value = "Set context attribute", impact = "ACTION")
+ public void setContextAttribute(@Name(value = "name", description = "attribute name") String name, @Name(value = "value", description = "attribute value") Object value)
+ {
+ Attributes attrs = ((ContextHandler)_managed).getAttributes();
+ attrs.setAttribute(name, value);
+ }
+
+ @ManagedOperation(value = "Set context attribute", impact = "ACTION")
+ public void setContextAttribute(@Name(value = "name", description = "attribute name") String name, @Name(value = "value", description = "attribute value") String value)
+ {
+ Attributes attrs = ((ContextHandler)_managed).getAttributes();
+ attrs.setAttribute(name, value);
+ }
+
+ @ManagedOperation(value = "Remove context attribute", impact = "ACTION")
+ public void removeContextAttribute(@Name(value = "name", description = "attribute name") String name)
+ {
+ Attributes attrs = ((ContextHandler)_managed).getAttributes();
+ attrs.removeAttribute(name);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/jmx/package-info.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/jmx/package-info.java
new file mode 100644
index 0000000..5e85702
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/jmx/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Server : Handler JMX Integration
+ */
+package org.eclipse.jetty.server.handler.jmx;
+
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/package-info.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/package-info.java
new file mode 100644
index 0000000..48dcef7
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/handler/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Server : Core Handler API
+ */
+package org.eclipse.jetty.server.handler;
+
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/jmx/AbstractConnectorMBean.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/jmx/AbstractConnectorMBean.java
new file mode 100644
index 0000000..831a31c
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/jmx/AbstractConnectorMBean.java
@@ -0,0 +1,54 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.jmx;
+
+import org.eclipse.jetty.jmx.ObjectMBean;
+import org.eclipse.jetty.server.AbstractConnector;
+import org.eclipse.jetty.server.ConnectionFactory;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+
+@ManagedObject("MBean Wrapper for Connectors")
+public class AbstractConnectorMBean extends ObjectMBean
+{
+ final AbstractConnector _connector;
+
+ public AbstractConnectorMBean(Object managedObject)
+ {
+ super(managedObject);
+ _connector = (AbstractConnector)managedObject;
+ }
+
+ @Override
+ public String getObjectContextBasis()
+ {
+ StringBuilder buffer = new StringBuilder();
+ for (ConnectionFactory f : _connector.getConnectionFactories())
+ {
+ String protocol = f.getProtocol();
+ if (protocol != null)
+ {
+ if (buffer.length() > 0)
+ buffer.append("|");
+ buffer.append(protocol);
+ }
+ }
+
+ return String.format("%s@%x", buffer.toString(), _connector.hashCode());
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/jmx/ServerMBean.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/jmx/ServerMBean.java
new file mode 100644
index 0000000..ed242c6
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/jmx/ServerMBean.java
@@ -0,0 +1,55 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.jmx;
+
+import org.eclipse.jetty.jmx.ObjectMBean;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+
+/**
+ *
+ */
+@ManagedObject("MBean Wrapper for Server")
+public class ServerMBean extends ObjectMBean
+{
+ private final long startupTime;
+ private final Server server;
+
+ public ServerMBean(Object managedObject)
+ {
+ super(managedObject);
+ startupTime = System.currentTimeMillis();
+ server = (Server)managedObject;
+ }
+
+ @ManagedAttribute("contexts on this server")
+ public Handler[] getContexts()
+ {
+ return server.getChildHandlersByClass(ContextHandler.class);
+ }
+
+ @ManagedAttribute("the startup time since January 1st, 1970 (in ms)")
+ public long getStartupTime()
+ {
+ return startupTime;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/jmx/package-info.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/jmx/package-info.java
new file mode 100644
index 0000000..fea492b
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/jmx/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Server : Server JMX Integration
+ */
+package org.eclipse.jetty.server.jmx;
+
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/nio/NetworkTrafficSelectChannelConnector.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/nio/NetworkTrafficSelectChannelConnector.java
new file mode 100644
index 0000000..ced1646
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/nio/NetworkTrafficSelectChannelConnector.java
@@ -0,0 +1,60 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.nio;
+
+import java.util.concurrent.Executor;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.server.ConnectionFactory;
+import org.eclipse.jetty.server.NetworkTrafficServerConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * @deprecated use {@link org.eclipse.jetty.server.NetworkTrafficServerConnector} instead.
+ */
+@Deprecated
+public class NetworkTrafficSelectChannelConnector extends NetworkTrafficServerConnector
+{
+ public NetworkTrafficSelectChannelConnector(Server server)
+ {
+ super(server);
+ }
+
+ public NetworkTrafficSelectChannelConnector(Server server, ConnectionFactory connectionFactory, SslContextFactory sslContextFactory)
+ {
+ super(server, connectionFactory, sslContextFactory);
+ }
+
+ public NetworkTrafficSelectChannelConnector(Server server, ConnectionFactory connectionFactory)
+ {
+ super(server, connectionFactory);
+ }
+
+ public NetworkTrafficSelectChannelConnector(Server server, Executor executor, Scheduler scheduler, ByteBufferPool pool, int acceptors, int selectors, ConnectionFactory... factories)
+ {
+ super(server, executor, scheduler, pool, acceptors, selectors, factories);
+ }
+
+ public NetworkTrafficSelectChannelConnector(Server server, SslContextFactory sslContextFactory)
+ {
+ super(server, sslContextFactory);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/nio/package-info.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/nio/package-info.java
new file mode 100644
index 0000000..63f0248
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/nio/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Server : Core Server Connector
+ */
+package org.eclipse.jetty.server.nio;
+
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/package-info.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/package-info.java
new file mode 100644
index 0000000..09ace06
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Server : Core Server API
+ */
+package org.eclipse.jetty.server;
+
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/ByteBufferRangeWriter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/ByteBufferRangeWriter.java
new file mode 100644
index 0000000..1738080
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/ByteBufferRangeWriter.java
@@ -0,0 +1,64 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.resource;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.BufferUtil;
+
+/**
+ * ByteBuffer based RangeWriter
+ */
+public class ByteBufferRangeWriter implements RangeWriter
+{
+ private final ByteBuffer buffer;
+ private boolean closed = false;
+
+ public ByteBufferRangeWriter(ByteBuffer buffer)
+ {
+ this.buffer = buffer.asReadOnlyBuffer();
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ closed = true;
+ }
+
+ @Override
+ public void writeTo(OutputStream outputStream, long skipTo, long length) throws IOException
+ {
+ if (skipTo > Integer.MAX_VALUE)
+ {
+ throw new IllegalArgumentException("Unsupported skipTo " + skipTo + " > " + Integer.MAX_VALUE);
+ }
+
+ if (length > Integer.MAX_VALUE)
+ {
+ throw new IllegalArgumentException("Unsupported length " + skipTo + " > " + Integer.MAX_VALUE);
+ }
+
+ ByteBuffer src = buffer.slice();
+ src.position((int)skipTo);
+ src.limit(Math.addExact((int)skipTo, (int)length));
+ BufferUtil.writeTo(src, outputStream);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/HttpContentRangeWriter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/HttpContentRangeWriter.java
new file mode 100644
index 0000000..034525d
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/HttpContentRangeWriter.java
@@ -0,0 +1,83 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.resource;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.util.Objects;
+
+import org.eclipse.jetty.http.HttpContent;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Range Writer selection for HttpContent
+ */
+public class HttpContentRangeWriter
+{
+ private static final Logger LOG = Log.getLogger(HttpContentRangeWriter.class);
+
+ /**
+ * Obtain a new RangeWriter for the supplied HttpContent.
+ *
+ * @param content the HttpContent to base RangeWriter on
+ * @return the RangeWriter best suited for the supplied HttpContent
+ */
+ public static RangeWriter newRangeWriter(HttpContent content)
+ {
+ Objects.requireNonNull(content, "HttpContent");
+
+ // Try direct buffer
+ ByteBuffer buffer = content.getDirectBuffer();
+ if (buffer == null)
+ {
+ buffer = content.getIndirectBuffer();
+ }
+ if (buffer != null)
+ {
+ return new ByteBufferRangeWriter(buffer);
+ }
+
+ try
+ {
+ ReadableByteChannel channel = content.getReadableByteChannel();
+ if (channel != null)
+ {
+ if (channel instanceof SeekableByteChannel)
+ {
+ SeekableByteChannel seekableByteChannel = (SeekableByteChannel)channel;
+ return new SeekableByteChannelRangeWriter(seekableByteChannel, () -> (SeekableByteChannel)content.getReadableByteChannel());
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Skipping non-SeekableByteChannel option " + channel + " from content " + content);
+ channel.close();
+ }
+ }
+ catch (IOException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Skipping ReadableByteChannel option", e);
+ }
+
+ return new InputStreamRangeWriter(() -> content.getInputStream());
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/InputStreamRangeWriter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/InputStreamRangeWriter.java
new file mode 100644
index 0000000..b040b0b
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/InputStreamRangeWriter.java
@@ -0,0 +1,125 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.resource;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.eclipse.jetty.util.IO;
+
+/**
+ * Default Range Writer for InputStream
+ */
+public class InputStreamRangeWriter implements RangeWriter
+{
+
+ public static final int NO_PROGRESS_LIMIT = 3;
+
+ public interface InputStreamSupplier
+ {
+ InputStream newInputStream() throws IOException;
+ }
+
+ private final InputStreamSupplier inputStreamSupplier;
+ private boolean closed = false;
+ private InputStream inputStream;
+ private long pos;
+
+ /**
+ * Create InputStremRangeWriter
+ *
+ * @param inputStreamSupplier Supplier of the InputStream. If the stream needs to be regenerated, such as the next
+ * requested range being before the current position, then the current InputStream is closed and a new one obtained
+ * from this supplier.
+ */
+ public InputStreamRangeWriter(InputStreamSupplier inputStreamSupplier)
+ {
+ this.inputStreamSupplier = inputStreamSupplier;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ closed = true;
+ if (inputStream != null)
+ {
+ inputStream.close();
+ }
+ }
+
+ @Override
+ public void writeTo(OutputStream outputStream, long skipTo, long length) throws IOException
+ {
+ if (closed)
+ {
+ throw new IOException("RangeWriter is closed");
+ }
+
+ if (inputStream == null)
+ {
+ inputStream = inputStreamSupplier.newInputStream();
+ pos = 0;
+ }
+
+ if (skipTo < pos)
+ {
+ inputStream.close();
+ inputStream = inputStreamSupplier.newInputStream();
+ pos = 0;
+ }
+ if (pos < skipTo)
+ {
+ long skipSoFar = pos;
+ long actualSkipped;
+ int noProgressLoopLimit = NO_PROGRESS_LIMIT;
+ // loop till we reach desired point, break out on lack of progress.
+ while (noProgressLoopLimit > 0 && skipSoFar < skipTo)
+ {
+ actualSkipped = inputStream.skip(skipTo - skipSoFar);
+ if (actualSkipped == 0)
+ {
+ noProgressLoopLimit--;
+ }
+ else if (actualSkipped > 0)
+ {
+ skipSoFar += actualSkipped;
+ noProgressLoopLimit = NO_PROGRESS_LIMIT;
+ }
+ else
+ {
+ // negative values means the stream was closed or reached EOF
+ // either way, we've hit a state where we can no longer
+ // fulfill the requested range write.
+ throw new IOException("EOF reached before InputStream skip destination");
+ }
+ }
+
+ if (noProgressLoopLimit <= 0)
+ {
+ throw new IOException("No progress made to reach InputStream skip position " + (skipTo - pos));
+ }
+
+ pos = skipTo;
+ }
+
+ IO.copy(inputStream, outputStream, length);
+ pos += length;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/RangeWriter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/RangeWriter.java
new file mode 100644
index 0000000..cee24ae
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/RangeWriter.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.resource;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Interface for writing sections (ranges) of a single resource (SeekableByteChannel, Resource, etc) to an outputStream.
+ */
+public interface RangeWriter extends Closeable
+{
+ /**
+ * Write the specific range (start, size) to the outputStream.
+ *
+ * @param outputStream the stream to write to
+ * @param skipTo the offset / skip-to / seek-to / position in the resource to start the write from
+ * @param length the size of the section to write
+ */
+ void writeTo(OutputStream outputStream, long skipTo, long length) throws IOException;
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/SeekableByteChannelRangeWriter.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/SeekableByteChannelRangeWriter.java
new file mode 100644
index 0000000..aa713d1
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/resource/SeekableByteChannelRangeWriter.java
@@ -0,0 +1,166 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.resource;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.SeekableByteChannel;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.IO;
+
+public class SeekableByteChannelRangeWriter implements RangeWriter
+{
+ public static final int NO_PROGRESS_LIMIT = 3;
+
+ public interface ChannelSupplier
+ {
+ SeekableByteChannel newSeekableByteChannel() throws IOException;
+ }
+
+ private final ChannelSupplier channelSupplier;
+ private final int bufSize;
+ private final ByteBuffer buffer;
+ private SeekableByteChannel channel;
+ private long pos;
+ private boolean defaultSeekMode = true;
+
+ public SeekableByteChannelRangeWriter(SeekableByteChannelRangeWriter.ChannelSupplier channelSupplier)
+ {
+ this(null, channelSupplier);
+ }
+
+ public SeekableByteChannelRangeWriter(SeekableByteChannel initialChannel, SeekableByteChannelRangeWriter.ChannelSupplier channelSupplier)
+ {
+ this.channel = initialChannel;
+ this.channelSupplier = channelSupplier;
+ this.bufSize = IO.bufferSize;
+ this.buffer = BufferUtil.allocate(this.bufSize);
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ if (this.channel != null)
+ {
+ this.channel.close();
+ }
+ }
+
+ @Override
+ public void writeTo(OutputStream outputStream, long skipTo, long length) throws IOException
+ {
+ skipTo(skipTo);
+
+ // copy from channel to output stream
+ long readTotal = 0;
+ while (readTotal < length)
+ {
+ BufferUtil.clearToFill(buffer);
+ int size = (int)Math.min(bufSize, length - readTotal);
+ buffer.limit(size);
+ int readLen = channel.read(buffer);
+ BufferUtil.flipToFlush(buffer, 0);
+ BufferUtil.writeTo(buffer, outputStream);
+ readTotal += readLen;
+ pos += readLen;
+ }
+ }
+
+ private void skipTo(long skipTo) throws IOException
+ {
+ if (channel == null)
+ {
+ channel = channelSupplier.newSeekableByteChannel();
+ pos = 0;
+ }
+
+ if (defaultSeekMode)
+ {
+ try
+ {
+ if (channel.position() != skipTo)
+ {
+ channel.position(skipTo);
+ pos = skipTo;
+ return;
+ }
+ }
+ catch (UnsupportedOperationException e)
+ {
+ defaultSeekMode = false;
+ fallbackSkipTo(skipTo);
+ }
+ }
+ else
+ {
+ // Fallback mode
+ fallbackSkipTo(skipTo);
+ }
+ }
+
+ private void fallbackSkipTo(long skipTo) throws IOException
+ {
+ if (skipTo < pos)
+ {
+ channel.close();
+ channel = channelSupplier.newSeekableByteChannel();
+ pos = 0;
+ }
+
+ if (pos < skipTo)
+ {
+ long skipSoFar = pos;
+ long actualSkipped;
+ int noProgressLoopLimit = NO_PROGRESS_LIMIT;
+ // loop till we reach desired point, break out on lack of progress.
+ while (noProgressLoopLimit > 0 && skipSoFar < skipTo)
+ {
+ BufferUtil.clearToFill(buffer);
+ int len = (int)Math.min(bufSize, (skipTo - skipSoFar));
+ buffer.limit(len);
+ actualSkipped = channel.read(buffer);
+ if (actualSkipped == 0)
+ {
+ noProgressLoopLimit--;
+ }
+ else if (actualSkipped > 0)
+ {
+ skipSoFar += actualSkipped;
+ noProgressLoopLimit = NO_PROGRESS_LIMIT;
+ }
+ else
+ {
+ // negative values means the stream was closed or reached EOF
+ // either way, we've hit a state where we can no longer
+ // fulfill the requested range write.
+ throw new IOException("EOF reached before SeekableByteChannel skip destination");
+ }
+ }
+
+ if (noProgressLoopLimit <= 0)
+ {
+ throw new IOException("No progress made to reach SeekableByteChannel skip position " + (skipTo - pos));
+ }
+
+ pos = skipTo;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCache.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCache.java
new file mode 100644
index 0000000..5265b1e
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCache.java
@@ -0,0 +1,879 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Locker.Lock;
+
+/**
+ * AbstractSessionCache
+ *
+ * A base implementation of the {@link SessionCache} interface for managing a set of
+ * Session objects pertaining to a context in memory.
+ *
+ * This implementation ensures that multiple requests for the same session id
+ * always return the same Session object.
+ *
+ * It will delay writing out a session to the SessionDataStore until the
+ * last request exits the session. If the SessionDataStore supports passivation
+ * then the session passivation and activation listeners are called appropriately as
+ * the session is written.
+ *
+ * This implementation also supports evicting idle Session objects. An idle Session
+ * is one that is still valid, has not expired, but has not been accessed by a
+ * request for a configurable amount of time. An idle session will be first
+ * passivated before it is evicted from the cache.
+ */
+@ManagedObject
+public abstract class AbstractSessionCache extends ContainerLifeCycle implements SessionCache
+{
+ static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
+
+ /**
+ * The authoritative source of session data
+ */
+ protected SessionDataStore _sessionDataStore;
+
+ /**
+ * The SessionHandler related to this SessionCache
+ */
+ protected final SessionHandler _handler;
+
+ /**
+ * Information about the context to which this SessionCache pertains
+ */
+ protected SessionContext _context;
+
+ /**
+ * When, if ever, to evict sessions: never; only when the last request for them finishes; after inactivity time (expressed as secs)
+ */
+ protected int _evictionPolicy = SessionCache.NEVER_EVICT;
+
+ /**
+ * If true, as soon as a new session is created, it will be persisted to the SessionDataStore
+ */
+ protected boolean _saveOnCreate = false;
+
+ /**
+ * If true, a session that will be evicted from the cache because it has been
+ * inactive too long will be saved before being evicted.
+ */
+ protected boolean _saveOnInactiveEviction;
+
+ /**
+ * If true, a Session whose data cannot be read will be
+ * deleted from the SessionDataStore.
+ */
+ protected boolean _removeUnloadableSessions;
+
+ /**
+ * If true, when a response is about to be committed back to the client,
+ * a dirty session will be flushed to the session store.
+ */
+ protected boolean _flushOnResponseCommit;
+
+ /**
+ * If true, when the server shuts down, all sessions in the
+ * cache will be invalidated before being removed.
+ */
+ protected boolean _invalidateOnShutdown;
+
+ /**
+ * Create a new Session object from pre-existing session data
+ *
+ * @param data the session data
+ * @return a new Session object
+ */
+ @Override
+ public abstract Session newSession(SessionData data);
+
+ /**
+ * Create a new Session for a request.
+ *
+ * @param request the request
+ * @param data the session data
+ * @return the new session
+ */
+ public abstract Session newSession(HttpServletRequest request, SessionData data);
+
+ /**
+ * Get the session matching the key from the cache. Does not load
+ * the session.
+ *
+ * @param id session id
+ * @return the Session object matching the id
+ */
+ protected abstract Session doGet(String id);
+
+ /**
+ * Put the session into the map if it wasn't already there
+ *
+ * @param id the identity of the session
+ * @param session the session object
+ * @return null if the session wasn't already in the map, or the existing entry otherwise
+ */
+ protected abstract Session doPutIfAbsent(String id, Session session);
+
+ /**
+ * Compute the mappingFunction to create a Session object iff the session
+ * with the given id isn't already in the map, otherwise return the existing Session.
+ * This method is expected to have precisely the same behaviour as
+ * {@link java.util.concurrent.ConcurrentHashMap#computeIfAbsent}
+ *
+ * @param id the session id
+ * @param mappingFunction the function to load the data for the session
+ * @return an existing Session from the cache
+ */
+ protected abstract Session doComputeIfAbsent(String id, Function<String, Session> mappingFunction);
+
+ /**
+ * Replace the mapping from id to oldValue with newValue
+ *
+ * @param id the id
+ * @param oldValue the old value
+ * @param newValue the new value
+ * @return true if replacement was done
+ */
+ protected abstract boolean doReplace(String id, Session oldValue, Session newValue);
+
+ /**
+ * Remove the session with this identity from the store
+ *
+ * @param id the id
+ * @return Session that was removed or null
+ */
+ public abstract Session doDelete(String id);
+
+ /**
+ * @param handler the {@link SessionHandler} to use
+ */
+ public AbstractSessionCache(SessionHandler handler)
+ {
+ _handler = handler;
+ }
+
+ /**
+ * @return the SessionManger
+ */
+ @Override
+ public SessionHandler getSessionHandler()
+ {
+ return _handler;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionCache#initialize(org.eclipse.jetty.server.session.SessionContext)
+ */
+ @Override
+ public void initialize(SessionContext context)
+ {
+ if (isStarted())
+ throw new IllegalStateException("Context set after session store started");
+ _context = context;
+ }
+
+ /**
+ * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (_sessionDataStore == null)
+ throw new IllegalStateException("No session data store configured");
+
+ if (_handler == null)
+ throw new IllegalStateException("No session manager");
+
+ if (_context == null)
+ throw new IllegalStateException("No ContextId");
+
+ _sessionDataStore.initialize(_context);
+ super.doStart();
+ }
+
+ /**
+ * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
+ */
+ @Override
+ protected void doStop() throws Exception
+ {
+ _sessionDataStore.stop();
+ super.doStop();
+ }
+
+ /**
+ * @return the SessionDataStore or null if there isn't one
+ */
+ @Override
+ public SessionDataStore getSessionDataStore()
+ {
+ return _sessionDataStore;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionCache#setSessionDataStore(org.eclipse.jetty.server.session.SessionDataStore)
+ */
+ @Override
+ public void setSessionDataStore(SessionDataStore sessionStore)
+ {
+ updateBean(_sessionDataStore, sessionStore);
+ _sessionDataStore = sessionStore;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionCache#getEvictionPolicy()
+ */
+ @ManagedAttribute(value = "session eviction policy", readonly = true)
+ @Override
+ public int getEvictionPolicy()
+ {
+ return _evictionPolicy;
+ }
+
+ /**
+ * -1 means we never evict inactive sessions.
+ * 0 means we evict a session after the last request for it exits
+ * >0 is the number of seconds after which we evict inactive sessions from the cache
+ *
+ * @see org.eclipse.jetty.server.session.SessionCache#setEvictionPolicy(int)
+ */
+ @Override
+ public void setEvictionPolicy(int evictionTimeout)
+ {
+ _evictionPolicy = evictionTimeout;
+ }
+
+ @ManagedAttribute(value = "immediately save new sessions", readonly = true)
+ @Override
+ public boolean isSaveOnCreate()
+ {
+ return _saveOnCreate;
+ }
+
+ @Override
+ public void setSaveOnCreate(boolean saveOnCreate)
+ {
+ _saveOnCreate = saveOnCreate;
+ }
+
+ /**
+ * @return true if sessions that can't be loaded are deleted from the store
+ */
+ @ManagedAttribute(value = "delete unreadable stored sessions", readonly = true)
+ @Override
+ public boolean isRemoveUnloadableSessions()
+ {
+ return _removeUnloadableSessions;
+ }
+
+ /**
+ * If a session's data cannot be loaded from the store without error, remove
+ * it from the persistent store.
+ *
+ * @param removeUnloadableSessions if <code>true</code> unloadable sessions will be removed from session store
+ */
+ @Override
+ public void setRemoveUnloadableSessions(boolean removeUnloadableSessions)
+ {
+ _removeUnloadableSessions = removeUnloadableSessions;
+ }
+
+ @Override
+ public void setFlushOnResponseCommit(boolean flushOnResponseCommit)
+ {
+ _flushOnResponseCommit = flushOnResponseCommit;
+ }
+
+ @Override
+ public boolean isFlushOnResponseCommit()
+ {
+ return _flushOnResponseCommit;
+ }
+
+ /**
+ * Get a session object.
+ *
+ * If the session object is not in this session store, try getting
+ * the data for it from a SessionDataStore associated with the
+ * session manager. The usage count of the session is incremented.
+ *
+ * @see org.eclipse.jetty.server.session.SessionCache#get(java.lang.String)
+ */
+ @Override
+ public Session get(String id) throws Exception
+ {
+ return getAndEnter(id, true);
+ }
+
+ /** Get a session object.
+ *
+ * If the session object is not in this session store, try getting
+ * the data for it from a SessionDataStore associated with the
+ * session manager.
+ *
+ * @param id The session to retrieve
+ * @param enter if true, the usage count of the session will be incremented
+ * @return the session if it exists, null otherwise
+ * @throws Exception if the session cannot be loaded
+ */
+ protected Session getAndEnter(String id, boolean enter) throws Exception
+ {
+ Session session = null;
+ AtomicReference<Exception> exception = new AtomicReference<Exception>();
+
+ session = doComputeIfAbsent(id, k ->
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} not found locally in {}, attempting to load", id, this);
+
+ try
+ {
+ Session s = loadSession(k);
+ if (s != null)
+ {
+ try (Lock lock = s.lock())
+ {
+ s.setResident(true); //ensure freshly loaded session is resident
+ }
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} not loaded by store", id);
+ }
+ return s;
+ }
+ catch (Exception e)
+ {
+ exception.set(e);
+ return null;
+ }
+ });
+
+ Exception ex = exception.get();
+ if (ex != null)
+ throw ex;
+
+ if (session != null)
+ {
+ try (Lock lock = session.lock())
+ {
+ if (!session.isResident()) //session isn't marked as resident in cache
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Non-resident session {} in cache", id);
+ return null;
+ }
+ else if (enter)
+ {
+ session.use();
+ }
+ }
+ }
+
+ return session;
+ }
+
+ /**
+ * Load the info for the session from the session data store
+ *
+ * @param id the id
+ * @return a Session object filled with data or null if the session doesn't exist
+ */
+ private Session loadSession(String id)
+ throws Exception
+ {
+ SessionData data = null;
+ Session session = null;
+
+ if (_sessionDataStore == null)
+ return null; //can't load it
+
+ try
+ {
+ data = _sessionDataStore.load(id);
+
+ if (data == null) //session doesn't exist
+ return null;
+
+ data.setLastNode(_context.getWorkerName()); //we are going to manage the node
+ session = newSession(data);
+ return session;
+ }
+ catch (UnreadableSessionDataException e)
+ {
+ //can't load the session, delete it
+ if (isRemoveUnloadableSessions())
+ _sessionDataStore.delete(id);
+ throw e;
+ }
+ }
+
+ /**
+ * Add an entirely new session (created by the application calling Request.getSession(true))
+ * to the cache. The usage count of the fresh session is incremented.
+ *
+ * @param id the id
+ * @param session
+ */
+ @Override
+ public void add(String id, Session session) throws Exception
+ {
+ if (id == null || session == null)
+ throw new IllegalArgumentException("Add key=" + id + " session=" + (session == null ? "null" : session.getId()));
+
+ try (Lock lock = session.lock())
+ {
+ if (session.getSessionHandler() == null)
+ throw new IllegalStateException("Session " + id + " is not managed");
+
+ if (!session.isValid())
+ throw new IllegalStateException("Session " + id + " is not valid");
+
+ if (doPutIfAbsent(id, session) == null)
+ {
+ session.setResident(true); //its in the cache
+ session.use(); //the request is using it
+ }
+ else
+ throw new IllegalStateException("Session " + id + " already in cache");
+ }
+ }
+
+ /**
+ * A response that has accessed this session is about to
+ * be returned to the client. Pass the session to the store
+ * to persist, so that any changes will be visible to
+ * subsequent requests on the same node (if using NullSessionCache),
+ * or on other nodes.
+ */
+ @Override
+ public void commit(Session session) throws Exception
+ {
+ if (session == null)
+ return;
+
+ try (Lock lock = session.lock())
+ {
+ //only write the session out at this point if the attributes changed. If only
+ //the lastAccess/expiry time changed defer the write until the last request exits
+ if (session.isValid() && session.getSessionData().isDirty() && _flushOnResponseCommit)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Flush session {} on response commit", session);
+ //save the session
+ if (!_sessionDataStore.isPassivating())
+ {
+ _sessionDataStore.store(session.getId(), session.getSessionData());
+ }
+ else
+ {
+ session.willPassivate();
+ _sessionDataStore.store(session.getId(), session.getSessionData());
+ session.didActivate();
+ }
+ }
+ }
+ }
+
+ /**
+ * @deprecated use {@link #release(String, Session)} instead
+ */
+ @Override
+ @Deprecated
+ public void put(String id, Session session) throws Exception
+ {
+ release(id, session);
+ }
+
+ /**
+ * Finish using the Session object.
+ *
+ * This should be called when a request exists the session. Only when the last
+ * simultaneous request exists the session will any action be taken.
+ *
+ * If there is a SessionDataStore write the session data through to it.
+ *
+ * If the SessionDataStore supports passivation, call the passivate/active listeners.
+ *
+ * If the evictionPolicy == SessionCache.EVICT_ON_SESSION_EXIT then after we have saved
+ * the session, we evict it from the cache.
+ *
+ * @see org.eclipse.jetty.server.session.SessionCache#release(java.lang.String, org.eclipse.jetty.server.session.Session)
+ */
+ @Override
+ public void release(String id, Session session) throws Exception
+ {
+ if (id == null || session == null)
+ throw new IllegalArgumentException("Put key=" + id + " session=" + (session == null ? "null" : session.getId()));
+
+ try (Lock lock = session.lock())
+ {
+ if (session.getSessionHandler() == null)
+ throw new IllegalStateException("Session " + id + " is not managed");
+
+ if (session.isInvalid())
+ return;
+
+ session.complete();
+
+ //don't do anything with the session until the last request for it has finished
+ if ((session.getRequests() <= 0))
+ {
+ //save the session
+ if (!_sessionDataStore.isPassivating())
+ {
+ //if our backing datastore isn't the passivating kind, just save the session
+ _sessionDataStore.store(id, session.getSessionData());
+ //if we evict on session exit, boot it from the cache
+ if (getEvictionPolicy() == EVICT_ON_SESSION_EXIT)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Eviction on request exit id={}", id);
+ doDelete(session.getId());
+ session.setResident(false);
+ }
+ else
+ {
+ session.setResident(true);
+ doPutIfAbsent(id, session); //ensure it is in our map
+ if (LOG.isDebugEnabled())
+ LOG.debug("Non passivating SessionDataStore, session in SessionCache only id={}", id);
+ }
+ }
+ else
+ {
+ //backing store supports passivation, call the listeners
+ session.willPassivate();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session passivating id={}", id);
+ _sessionDataStore.store(id, session.getSessionData());
+
+ if (getEvictionPolicy() == EVICT_ON_SESSION_EXIT)
+ {
+ //throw out the passivated session object from the map
+ doDelete(id);
+ session.setResident(false);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Evicted on request exit id={}", id);
+ }
+ else
+ {
+ //reactivate the session
+ session.didActivate();
+ session.setResident(true);
+ doPutIfAbsent(id, session); //ensure it is in our map
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session reactivated id={}", id);
+ }
+ }
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Req count={} for id={}", session.getRequests(), id);
+ session.setResident(true);
+ doPutIfAbsent(id, session); //ensure it is the map, but don't save it to the backing store until the last request exists
+ }
+ }
+ }
+
+ /**
+ * Check to see if a session corresponding to the id exists.
+ *
+ * This method will first check with the object store. If it
+ * doesn't exist in the object store (might be passivated etc),
+ * it will check with the data store.
+ *
+ * @throws Exception the Exception
+ * @see org.eclipse.jetty.server.session.SessionCache#exists(java.lang.String)
+ */
+ @Override
+ public boolean exists(String id) throws Exception
+ {
+ //try the object store first
+ Session s = doGet(id);
+ if (s != null)
+ {
+ try (Lock lock = s.lock())
+ {
+ //wait for the lock and check the validity of the session
+ return s.isValid();
+ }
+ }
+
+ //not there, so find out if session data exists for it
+ return _sessionDataStore.exists(id);
+ }
+
+ /**
+ * Check to see if this cache contains an entry for the session
+ * corresponding to the session id.
+ *
+ * @see org.eclipse.jetty.server.session.SessionCache#contains(java.lang.String)
+ */
+ @Override
+ public boolean contains(String id) throws Exception
+ {
+ //just ask our object cache, not the store
+ return (doGet(id) != null);
+ }
+
+ /**
+ * Remove a session object from this store and from any backing store.
+ *
+ * @see org.eclipse.jetty.server.session.SessionCache#delete(java.lang.String)
+ */
+ @Override
+ public Session delete(String id) throws Exception
+ {
+ //get the session, if its not in memory, this will load it
+ Session session = getAndEnter(id, false);
+
+ //Always delete it from the backing data store
+ if (_sessionDataStore != null)
+ {
+ boolean dsdel = _sessionDataStore.delete(id);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} deleted in session data store {}", id, dsdel);
+ }
+
+ //delete it from the session object store
+ if (session != null)
+ {
+ session.setResident(false);
+ }
+
+ return doDelete(id);
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionCache#checkExpiration(Set)
+ */
+ @Override
+ public Set<String> checkExpiration(Set<String> candidates)
+ {
+ if (!isStarted())
+ return Collections.emptySet();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} checking expiration on {}", this, candidates);
+ Set<String> allCandidates = _sessionDataStore.getExpired(candidates);
+ Set<String> sessionsInUse = new HashSet<>();
+ if (allCandidates != null)
+ {
+ for (String c : allCandidates)
+ {
+ Session s = doGet(c);
+ if (s != null && s.getRequests() > 0) //if the session is in my cache, check its not in use first
+ sessionsInUse.add(c);
+ }
+ try
+ {
+ allCandidates.removeAll(sessionsInUse);
+ }
+ catch (UnsupportedOperationException e)
+ {
+ Set<String> tmp = new HashSet<>(allCandidates);
+ tmp.removeAll(sessionsInUse);
+ allCandidates = tmp;
+ }
+ }
+ return allCandidates;
+ }
+
+ /**
+ * Check a session for being inactive and
+ * thus being able to be evicted, if eviction
+ * is enabled.
+ *
+ * @param session session to check
+ */
+ @Override
+ public void checkInactiveSession(Session session)
+ {
+ if (session == null)
+ return;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Checking for idle {}", session.getId());
+ try (Lock s = session.lock())
+ {
+ if (getEvictionPolicy() > 0 && session.isIdleLongerThan(getEvictionPolicy()) &&
+ session.isValid() && session.isResident() && session.getRequests() <= 0)
+ {
+ //Be careful with saveOnInactiveEviction - you may be able to re-animate a session that was
+ //being managed on another node and has expired.
+ try
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Evicting idle session {}", session.getId());
+
+ //save before evicting
+ if (isSaveOnInactiveEviction() && _sessionDataStore != null)
+ {
+ if (_sessionDataStore.isPassivating())
+ session.willPassivate();
+
+ //Fake being dirty to force the write
+ session.getSessionData().setDirty(true);
+ _sessionDataStore.store(session.getId(), session.getSessionData());
+ }
+
+ doDelete(session.getId()); //detach from this cache
+ session.setResident(false);
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Passivation of idle session {} failed", session.getId());
+ LOG.warn(e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public Session renewSessionId(String oldId, String newId, String oldExtendedId, String newExtendedId)
+ throws Exception
+ {
+ if (StringUtil.isBlank(oldId))
+ throw new IllegalArgumentException("Old session id is null");
+ if (StringUtil.isBlank(newId))
+ throw new IllegalArgumentException("New session id is null");
+
+ Session session = getAndEnter(oldId, true);
+ renewSessionId(session, newId, newExtendedId);
+
+ return session;
+ }
+
+ /**
+ * Swap the id on a session.
+ *
+ * @param session the session for which to do the swap
+ * @param newId the new id
+ * @param newExtendedId the full id plus node id
+ * @throws Exception if there was a failure saving the change
+ */
+ protected void renewSessionId(Session session, String newId, String newExtendedId)
+ throws Exception
+ {
+ if (session == null)
+ return;
+
+ try (Lock lock = session.lock())
+ {
+ String oldId = session.getId();
+ session.checkValidForWrite(); //can't change id on invalid session
+ session.getSessionData().setId(newId);
+ session.getSessionData().setLastSaved(0); //pretend that the session has never been saved before to get a full save
+ session.getSessionData().setDirty(true); //ensure we will try to write the session out
+ session.setExtendedId(newExtendedId); //remember the new extended id
+ session.setIdChanged(true); //session id changed
+
+ doPutIfAbsent(newId, session); //put the new id into our map
+ doDelete(oldId); //take old out of map
+
+ if (_sessionDataStore != null)
+ {
+ _sessionDataStore.delete(oldId); //delete the session data with the old id
+ _sessionDataStore.store(newId, session.getSessionData()); //save the session data with the new id
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session id {} swapped for new id {}", oldId, newId);
+ }
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionCache#setSaveOnInactiveEviction(boolean)
+ */
+ @Override
+ public void setSaveOnInactiveEviction(boolean saveOnEvict)
+ {
+ _saveOnInactiveEviction = saveOnEvict;
+ }
+
+ @Override
+ public void setInvalidateOnShutdown(boolean invalidateOnShutdown)
+ {
+ _invalidateOnShutdown = invalidateOnShutdown;
+ }
+
+ @Override
+ public boolean isInvalidateOnShutdown()
+ {
+ return _invalidateOnShutdown;
+ }
+
+ /**
+ * Whether we should save a session that has been inactive before
+ * we boot it from the cache.
+ *
+ * @return true if an inactive session will be saved before being evicted
+ */
+ @ManagedAttribute(value = "save sessions before evicting from cache", readonly = true)
+ @Override
+ public boolean isSaveOnInactiveEviction()
+ {
+ return _saveOnInactiveEviction;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionCache#newSession(javax.servlet.http.HttpServletRequest, java.lang.String, long, long)
+ */
+ @Override
+ public Session newSession(HttpServletRequest request, String id, long time, long maxInactiveMs)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Creating new session id=" + id);
+ Session session = newSession(request, _sessionDataStore.newSessionData(id, time, time, time, maxInactiveMs));
+ session.getSessionData().setLastNode(_context.getWorkerName());
+ try
+ {
+ if (isSaveOnCreate() && _sessionDataStore != null)
+ _sessionDataStore.store(id, session.getSessionData());
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Save of new session {} failed", id);
+ LOG.warn(e);
+ }
+ return session;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x[evict=%d,removeUnloadable=%b,saveOnCreate=%b,saveOnInactiveEvict=%b]",
+ this.getClass().getName(), this.hashCode(), _evictionPolicy,
+ _removeUnloadableSessions, _saveOnCreate, _saveOnInactiveEviction);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCacheFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCacheFactory.java
new file mode 100644
index 0000000..639ecdb
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCacheFactory.java
@@ -0,0 +1,140 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+/**
+ * AbstractSessionCacheFactory
+ *
+ * Base class for SessionCacheFactories.
+ *
+ */
+public abstract class AbstractSessionCacheFactory implements SessionCacheFactory
+{
+ int _evictionPolicy;
+ boolean _saveOnInactiveEvict;
+ boolean _saveOnCreate;
+ boolean _removeUnloadableSessions;
+ boolean _flushOnResponseCommit;
+ boolean _invalidateOnShutdown;
+
+ public abstract SessionCache newSessionCache(SessionHandler handler);
+
+ public boolean isInvalidateOnShutdown()
+ {
+ return _invalidateOnShutdown;
+ }
+
+ public void setInvalidateOnShutdown(boolean invalidateOnShutdown)
+ {
+ _invalidateOnShutdown = invalidateOnShutdown;
+ }
+
+ /**
+ * @return the flushOnResponseCommit
+ */
+ public boolean isFlushOnResponseCommit()
+ {
+ return _flushOnResponseCommit;
+ }
+
+ /**
+ * @param flushOnResponseCommit the flushOnResponseCommit to set
+ */
+ public void setFlushOnResponseCommit(boolean flushOnResponseCommit)
+ {
+ _flushOnResponseCommit = flushOnResponseCommit;
+ }
+
+ /**
+ * @return the saveOnCreate
+ */
+ public boolean isSaveOnCreate()
+ {
+ return _saveOnCreate;
+ }
+
+ /**
+ * @param saveOnCreate the saveOnCreate to set
+ */
+ public void setSaveOnCreate(boolean saveOnCreate)
+ {
+ _saveOnCreate = saveOnCreate;
+ }
+
+ /**
+ * @return the removeUnloadableSessions
+ */
+ public boolean isRemoveUnloadableSessions()
+ {
+ return _removeUnloadableSessions;
+ }
+
+ /**
+ * @param removeUnloadableSessions the removeUnloadableSessions to set
+ */
+ public void setRemoveUnloadableSessions(boolean removeUnloadableSessions)
+ {
+ _removeUnloadableSessions = removeUnloadableSessions;
+ }
+
+ /**
+ * @return the evictionPolicy
+ */
+ public int getEvictionPolicy()
+ {
+ return _evictionPolicy;
+ }
+
+ /**
+ * @param evictionPolicy the evictionPolicy to set
+ */
+ public void setEvictionPolicy(int evictionPolicy)
+ {
+ _evictionPolicy = evictionPolicy;
+ }
+
+ /**
+ * @return the saveOnInactiveEvict
+ */
+ public boolean isSaveOnInactiveEvict()
+ {
+ return _saveOnInactiveEvict;
+ }
+
+ /**
+ * @param saveOnInactiveEvict the saveOnInactiveEvict to set
+ */
+ public void setSaveOnInactiveEvict(boolean saveOnInactiveEvict)
+ {
+ _saveOnInactiveEvict = saveOnInactiveEvict;
+ }
+
+ @Override
+ public SessionCache getSessionCache(SessionHandler handler)
+ {
+ SessionCache cache = newSessionCache(handler);
+ cache.setEvictionPolicy(getEvictionPolicy());
+ cache.setSaveOnInactiveEviction(isSaveOnInactiveEvict());
+ cache.setSaveOnCreate(isSaveOnCreate());
+ cache.setRemoveUnloadableSessions(isRemoveUnloadableSessions());
+ cache.setFlushOnResponseCommit(isFlushOnResponseCommit());
+ cache.setInvalidateOnShutdown(isInvalidateOnShutdown());
+ return cache;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java
new file mode 100644
index 0000000..a869cc0
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java
@@ -0,0 +1,243 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * AbstractSessionDataStore
+ */
+@ManagedObject
+public abstract class AbstractSessionDataStore extends ContainerLifeCycle implements SessionDataStore
+{
+ static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
+
+ protected SessionContext _context; //context associated with this session data store
+ protected int _gracePeriodSec = 60 * 60; //default of 1hr
+ protected long _lastExpiryCheckTime = 0; //last time in ms that getExpired was called
+ protected int _savePeriodSec = 0; //time in sec between saves
+
+ /**
+ * Store the session data persistently.
+ *
+ * @param id identity of session to store
+ * @param data info of the session
+ * @param lastSaveTime time of previous save or 0 if never saved
+ * @throws Exception if unable to store data
+ */
+ public abstract void doStore(String id, SessionData data, long lastSaveTime) throws Exception;
+
+ /**
+ * Load the session from persistent store.
+ *
+ * @param id the id of the session to load
+ * @return the re-inflated session
+ * @throws Exception if unable to load the session
+ */
+ public abstract SessionData doLoad(String id) throws Exception;
+
+ /**
+ * Implemented by subclasses to resolve which sessions this node
+ * should attempt to expire.
+ *
+ * @param candidates the ids of sessions the SessionDataStore thinks has expired
+ * @return the reconciled set of session ids that this node should attempt to expire
+ */
+ public abstract Set<String> doGetExpired(Set<String> candidates);
+
+ @Override
+ public void initialize(SessionContext context) throws Exception
+ {
+ if (isStarted())
+ throw new IllegalStateException("Context set after SessionDataStore started");
+ _context = context;
+ }
+
+ @Override
+ public SessionData load(String id) throws Exception
+ {
+ if (!isStarted())
+ throw new IllegalStateException("Not started");
+
+ final AtomicReference<SessionData> reference = new AtomicReference<SessionData>();
+ final AtomicReference<Exception> exception = new AtomicReference<Exception>();
+
+ Runnable r = () ->
+ {
+ try
+ {
+ reference.set(doLoad(id));
+ }
+ catch (Exception e)
+ {
+ exception.set(e);
+ }
+ };
+
+ _context.run(r);
+ if (exception.get() != null)
+ throw exception.get();
+
+ return reference.get();
+ }
+
+ @Override
+ public void store(String id, SessionData data) throws Exception
+ {
+ if (!isStarted())
+ throw new IllegalStateException("Not started");
+
+ if (data == null)
+ return;
+
+ final AtomicReference<Exception> exception = new AtomicReference<Exception>();
+
+ Runnable r = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ long lastSave = data.getLastSaved();
+ long savePeriodMs = (_savePeriodSec <= 0 ? 0 : TimeUnit.SECONDS.toMillis(_savePeriodSec));
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Store: id={}, mdirty={}, dirty={}, lsave={}, period={}, elapsed={}", id, data.isMetaDataDirty(),
+ data.isDirty(), data.getLastSaved(), savePeriodMs, (System.currentTimeMillis() - lastSave));
+ }
+
+ //save session if attribute changed, never been saved or metadata changed (eg expiry time) and save interval exceeded
+ if (data.isDirty() || (lastSave <= 0) ||
+ (data.isMetaDataDirty() && ((System.currentTimeMillis() - lastSave) >= savePeriodMs)))
+ {
+ //set the last saved time to now
+ data.setLastSaved(System.currentTimeMillis());
+ try
+ {
+ //call the specific store method, passing in previous save time
+ doStore(id, data, lastSave);
+ data.clean(); //unset all dirty flags
+ }
+ catch (Exception e)
+ {
+ //reset last save time if save failed
+ data.setLastSaved(lastSave);
+ exception.set(e);
+ }
+ }
+ }
+ };
+
+ _context.run(r);
+ if (exception.get() != null)
+ throw exception.get();
+ }
+
+ @Override
+ public Set<String> getExpired(Set<String> candidates)
+ {
+ if (!isStarted())
+ throw new IllegalStateException("Not started");
+
+ try
+ {
+ return doGetExpired(candidates);
+ }
+ finally
+ {
+ _lastExpiryCheckTime = System.currentTimeMillis();
+ }
+ }
+
+ @Override
+ public SessionData newSessionData(String id, long created, long accessed, long lastAccessed, long maxInactiveMs)
+ {
+ return new SessionData(id, _context.getCanonicalContextPath(), _context.getVhost(), created, accessed, lastAccessed, maxInactiveMs);
+ }
+
+ protected void checkStarted() throws IllegalStateException
+ {
+ if (isStarted())
+ throw new IllegalStateException("Already started");
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (_context == null)
+ throw new IllegalStateException("No SessionContext");
+
+ super.doStart();
+ }
+
+ @ManagedAttribute(value = "interval in secs to prevent too eager session scavenging", readonly = true)
+ public int getGracePeriodSec()
+ {
+ return _gracePeriodSec;
+ }
+
+ public void setGracePeriodSec(int sec)
+ {
+ _gracePeriodSec = sec;
+ }
+
+ /**
+ * @return the savePeriodSec
+ */
+ @ManagedAttribute(value = "min secs between saves", readonly = true)
+ public int getSavePeriodSec()
+ {
+ return _savePeriodSec;
+ }
+
+ /**
+ * The minimum time in seconds between save operations.
+ * Saves normally occur every time the last request
+ * exits as session. If nothing changes on the session
+ * except for the access time and the persistence technology
+ * is slow, this can cause delays.
+ * <p>
+ * By default the value is 0, which means we save
+ * after the last request exists. A non zero value
+ * means that we will skip doing the save if the
+ * session isn't dirty if the elapsed time since
+ * the session was last saved does not exceed this
+ * value.
+ *
+ * @param savePeriodSec the savePeriodSec to set
+ */
+ public void setSavePeriodSec(int savePeriodSec)
+ {
+ _savePeriodSec = savePeriodSec;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x[passivating=%b,graceSec=%d]", this.getClass().getName(), this.hashCode(), isPassivating(), getGracePeriodSec());
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStoreFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStoreFactory.java
new file mode 100644
index 0000000..9d214e5
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStoreFactory.java
@@ -0,0 +1,61 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+/**
+ * AbstractSessionDataStoreFactory
+ */
+public abstract class AbstractSessionDataStoreFactory implements SessionDataStoreFactory
+{
+
+ int _gracePeriodSec;
+ int _savePeriodSec;
+
+ /**
+ * @return the gracePeriodSec
+ */
+ public int getGracePeriodSec()
+ {
+ return _gracePeriodSec;
+ }
+
+ /**
+ * @param gracePeriodSec the gracePeriodSec to set
+ */
+ public void setGracePeriodSec(int gracePeriodSec)
+ {
+ _gracePeriodSec = gracePeriodSec;
+ }
+
+ /**
+ * @return the savePeriodSec
+ */
+ public int getSavePeriodSec()
+ {
+ return _savePeriodSec;
+ }
+
+ /**
+ * @param savePeriodSec the savePeriodSec to set
+ */
+ public void setSavePeriodSec(int savePeriodSec)
+ {
+ _savePeriodSec = savePeriodSec;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/CachingSessionDataStore.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/CachingSessionDataStore.java
new file mode 100644
index 0000000..889fd62
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/CachingSessionDataStore.java
@@ -0,0 +1,215 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.util.Set;
+
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * CachingSessionDataStore
+ *
+ * A SessionDataStore is a mechanism for (persistently) storing data associated with sessions.
+ * This implementation delegates to a pluggable SessionDataStore for actually storing the
+ * session data. It also uses a pluggable cache implementation in front of the
+ * delegate SessionDataStore to improve performance: accessing most persistent store
+ * technology can be expensive time-wise, so introducing a fronting cache
+ * can increase performance. The cache implementation can either be a local cache,
+ * a remote cache, or a clustered cache.
+ *
+ * The implementation here will try to read first from the cache and fallback to
+ * reading from the SessionDataStore if the session key is not found. On writes, the
+ * session data is written first to the SessionDataStore, and then to the cache. On
+ * deletes, the data is deleted first from the SessionDataStore, and then from the
+ * cache. There is no transaction manager ensuring atomic operations, so it is
+ * possible that failures can result in cache inconsistency.
+ */
+public class CachingSessionDataStore extends ContainerLifeCycle implements SessionDataStore
+{
+ private static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
+ /**
+ * The actual store for the session data
+ */
+ protected SessionDataStore _store;
+
+ /**
+ * The fronting cache
+ */
+ protected SessionDataMap _cache;
+
+ /**
+ * @param cache the front cache to use
+ * @param store the actual store for the the session data
+ */
+ public CachingSessionDataStore(SessionDataMap cache, SessionDataStore store)
+ {
+ _cache = cache;
+ addBean(_cache, true);
+ _store = store;
+ addBean(_store, true);
+ }
+
+ /**
+ * @return the delegate session store
+ */
+ public SessionDataStore getSessionStore()
+ {
+ return _store;
+ }
+
+ /**
+ * @return the fronting cache for session data
+ */
+ public SessionDataMap getSessionDataMap()
+ {
+ return _cache;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionDataStore#load(java.lang.String)
+ */
+ @Override
+ public SessionData load(String id) throws Exception
+ {
+ SessionData d = null;
+
+ try
+ {
+ //check to see if the session data is already in the cache
+ d = _cache.load(id);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+
+ if (d != null)
+ return d; //cache hit
+
+ //cache miss - go get it from the store
+ d = _store.load(id);
+
+ return d;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionDataStore#delete(java.lang.String)
+ */
+ @Override
+ public boolean delete(String id) throws Exception
+ {
+ //delete from the store
+ boolean deleted = _store.delete(id);
+ //and from the cache
+ _cache.delete(id);
+
+ return deleted;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionDataStore#getExpired(Set)
+ */
+ @Override
+ public Set<String> getExpired(Set<String> candidates)
+ {
+ //pass thru to the delegate store
+ return _store.getExpired(candidates);
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionDataStore#store(java.lang.String, org.eclipse.jetty.server.session.SessionData)
+ */
+ @Override
+ public void store(String id, SessionData data) throws Exception
+ {
+ long lastSaved = data.getLastSaved();
+
+ //write to the SessionDataStore first
+ _store.store(id, data);
+
+ //if the store saved it, then update the cache too
+ if (data.getLastSaved() != lastSaved)
+ _cache.store(id, data);
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ super.doStop();
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionDataStore#isPassivating()
+ */
+ @Override
+ public boolean isPassivating()
+ {
+ return _store.isPassivating();
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionDataStore#exists(java.lang.String)
+ */
+ @Override
+ public boolean exists(String id) throws Exception
+ {
+ try
+ {
+ //check the cache first
+ SessionData data = _cache.load(id);
+ if (data != null)
+ return true;
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+
+ //then the delegate store
+ return _store.exists(id);
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionDataStore#initialize(org.eclipse.jetty.server.session.SessionContext)
+ */
+ @Override
+ public void initialize(SessionContext context) throws Exception
+ {
+ //pass through
+ _store.initialize(context);
+ _cache.initialize(context);
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionDataStore#newSessionData(java.lang.String, long, long, long, long)
+ */
+ @Override
+ public SessionData newSessionData(String id, long created, long accessed, long lastAccessed, long maxInactiveMs)
+ {
+ return _store.newSessionData(id, created, accessed, lastAccessed, maxInactiveMs);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/CachingSessionDataStoreFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/CachingSessionDataStoreFactory.java
new file mode 100644
index 0000000..5524e68
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/CachingSessionDataStoreFactory.java
@@ -0,0 +1,67 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+/**
+ * CachingSessionDataStoreFactory
+ */
+public class CachingSessionDataStoreFactory extends AbstractSessionDataStoreFactory
+{
+
+ /**
+ * The SessionDataStore that will store session data.
+ */
+ protected SessionDataStoreFactory _sessionStoreFactory;
+
+ protected SessionDataMapFactory _mapFactory;
+
+ /**
+ * @return the SessionDataMapFactory
+ */
+ public SessionDataMapFactory getMapFactory()
+ {
+ return _mapFactory;
+ }
+
+ /**
+ * @param mapFactory the SessionDataMapFactory
+ */
+ public void setSessionDataMapFactory(SessionDataMapFactory mapFactory)
+ {
+ _mapFactory = mapFactory;
+ }
+
+ /**
+ * @param factory The factory for the actual SessionDataStore that the
+ * CachingSessionDataStore will delegate to
+ */
+ public void setSessionStoreFactory(SessionDataStoreFactory factory)
+ {
+ _sessionStoreFactory = factory;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionDataStoreFactory#getSessionDataStore(org.eclipse.jetty.server.session.SessionHandler)
+ */
+ @Override
+ public SessionDataStore getSessionDataStore(SessionHandler handler) throws Exception
+ {
+ return new CachingSessionDataStore(_mapFactory.getSessionDataMap(), _sessionStoreFactory.getSessionDataStore(handler));
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/DatabaseAdaptor.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/DatabaseAdaptor.java
new file mode 100644
index 0000000..5ad1e51
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/DatabaseAdaptor.java
@@ -0,0 +1,316 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.sql.Blob;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.Locale;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * DatabaseAdaptor
+ *
+ * Handles differences between databases.
+ *
+ * Postgres uses the getBytes and setBinaryStream methods to access
+ * a "bytea" datatype, which can be up to 1Gb of binary data. MySQL
+ * is happy to use the "blob" type and getBlob() methods instead.
+ */
+public class DatabaseAdaptor
+{
+ static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
+
+ String _dbName;
+ boolean _isLower;
+ boolean _isUpper;
+
+ protected String _blobType; //if not set, is deduced from the type of the database at runtime
+ protected String _longType; //if not set, is deduced from the type of the database at runtime
+ protected String _stringType; //if not set defaults to 'varchar'
+ private String _driverClassName;
+ private String _connectionUrl;
+ private Driver _driver;
+ private DataSource _datasource;
+
+ private String _jndiName;
+
+ public DatabaseAdaptor()
+ {
+ }
+
+ public void adaptTo(DatabaseMetaData dbMeta)
+ throws SQLException
+ {
+ _dbName = dbMeta.getDatabaseProductName().toLowerCase(Locale.ENGLISH);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Using database {}", _dbName);
+ _isLower = dbMeta.storesLowerCaseIdentifiers();
+ _isUpper = dbMeta.storesUpperCaseIdentifiers();
+ }
+
+ public void setBlobType(String blobType)
+ {
+ _blobType = blobType;
+ }
+
+ public String getBlobType()
+ {
+ if (_blobType != null)
+ return _blobType;
+
+ if (_dbName.startsWith("postgres"))
+ return "bytea";
+
+ return "blob";
+ }
+
+ public void setLongType(String longType)
+ {
+ _longType = longType;
+ }
+
+ public String getLongType()
+ {
+ if (_longType != null)
+ return _longType;
+
+ if (_dbName == null)
+ throw new IllegalStateException("DbAdaptor missing metadata");
+
+ if (_dbName.startsWith("oracle"))
+ return "number(20)";
+
+ return "bigint";
+ }
+
+ public void setStringType(String stringType)
+ {
+ _stringType = stringType;
+ }
+
+ public String getStringType()
+ {
+ if (_stringType != null)
+ return _stringType;
+
+ return "varchar";
+ }
+
+ /**
+ * Convert a camel case identifier into either upper or lower
+ * depending on the way the db stores identifiers.
+ *
+ * @param identifier the raw identifier
+ * @return the converted identifier
+ */
+ public String convertIdentifier(String identifier)
+ {
+ if (identifier == null)
+ return null;
+
+ if (_dbName == null)
+ throw new IllegalStateException("DbAdaptor missing metadata");
+
+ if (_isLower)
+ return identifier.toLowerCase(Locale.ENGLISH);
+ if (_isUpper)
+ return identifier.toUpperCase(Locale.ENGLISH);
+
+ return identifier;
+ }
+
+ public String getDBName()
+ {
+ return _dbName;
+ }
+
+ public InputStream getBlobInputStream(ResultSet result, String columnName)
+ throws SQLException
+ {
+ if (_dbName == null)
+ throw new IllegalStateException("DbAdaptor missing metadata");
+
+ if (_dbName.startsWith("postgres"))
+ {
+ byte[] bytes = result.getBytes(columnName);
+ return new ByteArrayInputStream(bytes);
+ }
+
+ try
+ {
+ Blob blob = result.getBlob(columnName);
+ return blob.getBinaryStream();
+ }
+ catch (SQLFeatureNotSupportedException ex)
+ {
+ byte[] bytes = result.getBytes(columnName);
+ return new ByteArrayInputStream(bytes);
+ }
+ }
+
+ public boolean isEmptyStringNull()
+ {
+ if (_dbName == null)
+ throw new IllegalStateException("DbAdaptor missing metadata");
+
+ return (_dbName.startsWith("oracle"));
+ }
+
+ /**
+ * rowId is a reserved word for Oracle, so change the name of this column
+ *
+ * @return true if db in use is oracle
+ */
+ public boolean isRowIdReserved()
+ {
+ if (_dbName == null)
+ throw new IllegalStateException("DbAdaptor missing metadata");
+
+ return (_dbName != null && _dbName.startsWith("oracle"));
+ }
+
+ /**
+ * Configure jdbc connection information via a jdbc Driver
+ *
+ * @param driverClassName the driver classname
+ * @param connectionUrl the driver connection url
+ */
+ public void setDriverInfo(String driverClassName, String connectionUrl)
+ {
+ _driverClassName = driverClassName;
+ _connectionUrl = connectionUrl;
+ }
+
+ /**
+ * Configure jdbc connection information via a jdbc Driver
+ *
+ * @param driverClass the driver class
+ * @param connectionUrl the driver connection url
+ */
+ public void setDriverInfo(Driver driverClass, String connectionUrl)
+ {
+ _driver = driverClass;
+ _connectionUrl = connectionUrl;
+ }
+
+ public void setDatasource(DataSource ds)
+ {
+ _datasource = ds;
+ }
+
+ public void setDatasourceName(String jndi)
+ {
+ _jndiName = jndi;
+ }
+
+ public String getDatasourceName()
+ {
+ return _jndiName;
+ }
+
+ public DataSource getDatasource()
+ {
+ return _datasource;
+ }
+
+ public String getDriverClassName()
+ {
+ return _driverClassName;
+ }
+
+ public Driver getDriver()
+ {
+ return _driver;
+ }
+
+ public String getConnectionUrl()
+ {
+ return _connectionUrl;
+ }
+
+ public void initialize()
+ throws Exception
+ {
+ if (_datasource != null)
+ return; //already set up
+
+ if (_jndiName != null)
+ {
+ InitialContext ic = new InitialContext();
+ _datasource = (DataSource)ic.lookup(_jndiName);
+ }
+ else if (_driver != null && _connectionUrl != null)
+ {
+ DriverManager.registerDriver(_driver);
+ }
+ else if (_driverClassName != null && _connectionUrl != null)
+ {
+ Class.forName(_driverClassName);
+ }
+ else
+ {
+ try
+ {
+ InitialContext ic = new InitialContext();
+ _datasource = (DataSource)ic.lookup("jdbc/sessions"); //last ditch effort
+ }
+ catch (NamingException e)
+ {
+ throw new IllegalStateException("No database configured for sessions");
+ }
+ }
+ }
+
+ /**
+ * Get a connection from the driver or datasource.
+ *
+ * @return the connection for the datasource
+ * @throws SQLException if unable to get the connection
+ */
+ protected Connection getConnection()
+ throws SQLException
+ {
+ if (_datasource != null)
+ return _datasource.getConnection();
+ else
+ return DriverManager.getConnection(_connectionUrl);
+ }
+
+ /**
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString()
+ {
+ return String.format("%s[jndi=%s,driver=%s]", super.toString(), _jndiName, _driverClassName);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionCache.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionCache.java
new file mode 100644
index 0000000..670f420
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionCache.java
@@ -0,0 +1,191 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.statistic.CounterStatistic;
+
+/**
+ * DefaultSessionCache
+ *
+ * A session store that keeps its sessions in memory in a hashmap
+ */
+@ManagedObject
+public class DefaultSessionCache extends AbstractSessionCache
+{
+ private static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
+
+ /**
+ * The cache of sessions in a hashmap
+ */
+ protected ConcurrentHashMap<String, Session> _sessions = new ConcurrentHashMap<>();
+
+ private final CounterStatistic _stats = new CounterStatistic();
+
+ /**
+ * @param manager The SessionHandler related to this SessionCache
+ */
+ public DefaultSessionCache(SessionHandler manager)
+ {
+ super(manager);
+ }
+
+ /**
+ * @return the number of sessions in the cache
+ */
+ @ManagedAttribute(value = "current sessions in cache", readonly = true)
+ public long getSessionsCurrent()
+ {
+ return _stats.getCurrent();
+ }
+
+ /**
+ * @return the max number of sessions in the cache
+ */
+ @ManagedAttribute(value = "max sessions in cache", readonly = true)
+ public long getSessionsMax()
+ {
+ return _stats.getMax();
+ }
+
+ /**
+ * @return a running total of sessions in the cache
+ */
+ @ManagedAttribute(value = "total sessions in cache", readonly = true)
+ public long getSessionsTotal()
+ {
+ return _stats.getTotal();
+ }
+
+ @ManagedOperation(value = "reset statistics", impact = "ACTION")
+ public void resetStats()
+ {
+ _stats.reset();
+ }
+
+ @Override
+ public Session doGet(String id)
+ {
+ if (id == null)
+ return null;
+ return _sessions.get(id);
+ }
+
+ @Override
+ public Session doPutIfAbsent(String id, Session session)
+ {
+ Session s = _sessions.putIfAbsent(id, session);
+ if (s == null)
+ _stats.increment();
+ return s;
+ }
+
+ @Override
+ protected Session doComputeIfAbsent(String id, Function<String, Session> mappingFunction)
+ {
+ return _sessions.computeIfAbsent(id, k ->
+ {
+ Session s = mappingFunction.apply(k);
+ if (s != null)
+ _stats.increment();
+ return s;
+ });
+ }
+
+ @Override
+ public Session doDelete(String id)
+ {
+ Session s = _sessions.remove(id);
+ if (s != null)
+ _stats.decrement();
+ return s;
+ }
+
+ @Override
+ public void shutdown()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Shutdown sessions, invalidating = {}", isInvalidateOnShutdown());
+
+ // loop over all the sessions in memory (a few times if necessary to catch sessions that have been
+ // added while we're running
+ int loop = 100;
+
+ while (!_sessions.isEmpty() && loop-- > 0)
+ {
+ for (Session session : _sessions.values())
+ {
+ if (isInvalidateOnShutdown())
+ {
+ //not preserving sessions on exit
+ try
+ {
+ session.invalidate();
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+ }
+ else
+ {
+ //write out the session and remove from the cache
+ if (_sessionDataStore.isPassivating())
+ session.willPassivate();
+ try
+ {
+ _sessionDataStore.store(session.getId(), session.getSessionData());
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ doDelete(session.getId()); //remove from memory
+ session.setResident(false);
+ }
+ }
+ }
+ }
+
+ @Override
+ public Session newSession(HttpServletRequest request, SessionData data)
+ {
+ return new Session(getSessionHandler(), request, data);
+ }
+
+ @Override
+ public Session newSession(SessionData data)
+ {
+ return new Session(getSessionHandler(), data);
+ }
+
+ @Override
+ public boolean doReplace(String id, Session oldValue, Session newValue)
+ {
+ return _sessions.replace(id, oldValue, newValue);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionCacheFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionCacheFactory.java
new file mode 100644
index 0000000..9da5f5c
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionCacheFactory.java
@@ -0,0 +1,33 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+/**
+ * DefaultSessionCacheFactory
+ *
+ * Factory for creating new DefaultSessionCaches.
+ */
+public class DefaultSessionCacheFactory extends AbstractSessionCacheFactory
+{
+ @Override
+ public SessionCache newSessionCache(SessionHandler handler)
+ {
+ return new DefaultSessionCache(handler);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java
new file mode 100644
index 0000000..ef28734
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java
@@ -0,0 +1,505 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.security.SecureRandom;
+import java.util.HashSet;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.SessionIdManager;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * DefaultSessionIdManager
+ *
+ * Manages session ids to ensure each session id within a context is unique, and that
+ * session ids can be shared across contexts (but not session contents).
+ *
+ * There is only 1 session id manager per Server instance.
+ *
+ * Runs a HouseKeeper thread to periodically check for expired Sessions.
+ *
+ * @see HouseKeeper
+ */
+@ManagedObject
+public class DefaultSessionIdManager extends ContainerLifeCycle implements SessionIdManager
+{
+ private static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
+
+ public static final String __NEW_SESSION_ID = "org.eclipse.jetty.server.newSessionId";
+
+ protected static final AtomicLong COUNTER = new AtomicLong();
+
+ protected Random _random;
+ protected boolean _weakRandom;
+ protected String _workerName;
+ protected String _workerAttr;
+ protected long _reseed = 100000L;
+ protected Server _server;
+ protected HouseKeeper _houseKeeper;
+ protected boolean _ownHouseKeeper;
+
+ /**
+ * @param server the server associated with the id manager
+ */
+ public DefaultSessionIdManager(Server server)
+ {
+ _server = server;
+ }
+
+ /**
+ * @param server the server associated with the id manager
+ * @param random a random number generator to use for ids
+ */
+ public DefaultSessionIdManager(Server server, Random random)
+ {
+ this(server);
+ _random = random;
+ }
+
+ /**
+ * @param server the server associated with this id manager
+ */
+ public void setServer(Server server)
+ {
+ _server = server;
+ }
+
+ /**
+ * @return the server associated with this id manager
+ */
+ public Server getServer()
+ {
+ return _server;
+ }
+
+ /**
+ * @param houseKeeper the housekeeper
+ */
+ @Override
+ public void setSessionHouseKeeper(HouseKeeper houseKeeper)
+ {
+ updateBean(_houseKeeper, houseKeeper);
+ _houseKeeper = houseKeeper;
+ _houseKeeper.setSessionIdManager(this);
+ }
+
+ /**
+ * @return the housekeeper
+ */
+ @Override
+ public HouseKeeper getSessionHouseKeeper()
+ {
+ return _houseKeeper;
+ }
+
+ /**
+ * Get the workname. If set, the workername is dot appended to the session
+ * ID and can be used to assist session affinity in a load balancer.
+ *
+ * @return name or null
+ */
+ @Override
+ @ManagedAttribute(value = "unique name for this node", readonly = true)
+ public String getWorkerName()
+ {
+ return _workerName;
+ }
+
+ /**
+ * Set the workername. If set, the workername is dot appended to the session
+ * ID and can be used to assist session affinity in a load balancer.
+ * A worker name starting with $ is used as a request attribute name to
+ * lookup the worker name that can be dynamically set by a request
+ * Customizer.
+ *
+ * @param workerName the name of the worker, if null it is coerced to empty string
+ */
+ public void setWorkerName(String workerName)
+ {
+ if (isRunning())
+ throw new IllegalStateException(getState());
+ if (workerName == null)
+ _workerName = "";
+ else
+ {
+ if (workerName.contains("."))
+ throw new IllegalArgumentException("Name cannot contain '.'");
+ _workerName = workerName;
+ }
+ }
+
+ /**
+ * @return the random number generator
+ */
+ public Random getRandom()
+ {
+ return _random;
+ }
+
+ /**
+ * @param random a random number generator for generating ids
+ */
+ public synchronized void setRandom(Random random)
+ {
+ _random = random;
+ _weakRandom = false;
+ }
+
+ /**
+ * @return the reseed probability
+ */
+ public long getReseed()
+ {
+ return _reseed;
+ }
+
+ /**
+ * Set the reseed probability.
+ *
+ * @param reseed If non zero then when a random long modulo the reseed value == 1, the {@link SecureRandom} will be reseeded.
+ */
+ public void setReseed(long reseed)
+ {
+ _reseed = reseed;
+ }
+
+ /**
+ * Create a new session id if necessary.
+ *
+ * @see org.eclipse.jetty.server.SessionIdManager#newSessionId(javax.servlet.http.HttpServletRequest, long)
+ */
+ @Override
+ public String newSessionId(HttpServletRequest request, long created)
+ {
+ if (request == null)
+ return newSessionId(created);
+
+ // A requested session ID can only be used if it is in use already.
+ String requestedId = request.getRequestedSessionId();
+ if (requestedId != null)
+ {
+ String clusterId = getId(requestedId);
+ if (isIdInUse(clusterId))
+ return clusterId;
+ }
+
+ // Else reuse any new session ID already defined for this request.
+ String newId = (String)request.getAttribute(__NEW_SESSION_ID);
+ if (newId != null && isIdInUse(newId))
+ return newId;
+
+ // pick a new unique ID!
+ String id = newSessionId(request.hashCode());
+
+ request.setAttribute(__NEW_SESSION_ID, id);
+ return id;
+ }
+
+ /**
+ * @param seedTerm the seed for RNG
+ * @return a new unique session id
+ */
+ public String newSessionId(long seedTerm)
+ {
+ // pick a new unique ID!
+ String id = null;
+
+ synchronized (_random)
+ {
+ while (id == null || id.length() == 0)
+ {
+ long r0 = _weakRandom
+ ? (hashCode() ^ Runtime.getRuntime().freeMemory() ^ _random.nextInt() ^ ((seedTerm) << 32))
+ : _random.nextLong();
+ if (r0 < 0)
+ r0 = -r0;
+
+ // random chance to reseed
+ if (_reseed > 0 && (r0 % _reseed) == 1L)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Reseeding {}", this);
+ if (_random instanceof SecureRandom)
+ {
+ SecureRandom secure = (SecureRandom)_random;
+ secure.setSeed(secure.generateSeed(8));
+ }
+ else
+ {
+ _random.setSeed(_random.nextLong() ^ System.currentTimeMillis() ^ seedTerm ^ Runtime.getRuntime().freeMemory());
+ }
+ }
+
+ long r1 = _weakRandom
+ ? (hashCode() ^ Runtime.getRuntime().freeMemory() ^ _random.nextInt() ^ ((seedTerm) << 32))
+ : _random.nextLong();
+ if (r1 < 0)
+ r1 = -r1;
+
+ id = Long.toString(r0, 36) + Long.toString(r1, 36);
+
+ //add in the id of the node to ensure unique id across cluster
+ //NOTE this is different to the node suffix which denotes which node the request was received on
+ if (!StringUtil.isBlank(_workerName))
+ id = _workerName + id;
+
+ id = id + COUNTER.getAndIncrement();
+ }
+ }
+ return id;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.SessionIdManager#isIdInUse(java.lang.String)
+ */
+ @Override
+ public boolean isIdInUse(String id)
+ {
+ if (id == null)
+ return false;
+
+ boolean inUse = false;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Checking {} is in use by at least one context", id);
+
+ try
+ {
+ for (SessionHandler manager : getSessionHandlers())
+ {
+ if (manager.isIdInUse(id))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Context {} reports id in use", manager);
+ inUse = true;
+ break;
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Checked {}, in use: {}", id, inUse);
+ return inUse;
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Problem checking if id {} is in use", id);
+ LOG.warn(e);
+ return false;
+ }
+ }
+
+ /**
+ * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (_server == null)
+ throw new IllegalStateException("No Server for SessionIdManager");
+
+ initRandom();
+
+ if (_workerName == null)
+ {
+ String inst = System.getenv("JETTY_WORKER_INSTANCE");
+ _workerName = "node" + (inst == null ? "0" : inst);
+ }
+
+ LOG.info("DefaultSessionIdManager workerName={}", _workerName);
+ _workerAttr = (_workerName != null && _workerName.startsWith("$")) ? _workerName.substring(1) : null;
+
+ if (_houseKeeper == null)
+ {
+ LOG.info("No SessionScavenger set, using defaults");
+ _ownHouseKeeper = true;
+ _houseKeeper = new HouseKeeper();
+ _houseKeeper.setSessionIdManager(this);
+ addBean(_houseKeeper, true);
+ }
+
+ _houseKeeper.start();
+ }
+
+ /**
+ * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
+ */
+ @Override
+ protected void doStop() throws Exception
+ {
+ _houseKeeper.stop();
+ if (_ownHouseKeeper)
+ {
+ _houseKeeper = null;
+ }
+ _random = null;
+ }
+
+ /**
+ * Set up a random number generator for the sessionids.
+ *
+ * By preference, use a SecureRandom but allow to be injected.
+ */
+ public void initRandom()
+ {
+ if (_random == null)
+ {
+ try
+ {
+ _random = new SecureRandom();
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Could not generate SecureRandom for session-id randomness", e);
+ _random = new Random();
+ _weakRandom = true;
+ }
+ }
+ else
+ _random.setSeed(_random.nextLong() ^ System.currentTimeMillis() ^ hashCode() ^ Runtime.getRuntime().freeMemory());
+ }
+
+ /**
+ * Get the session ID with any worker ID.
+ *
+ * @param clusterId the cluster id
+ * @param request the request
+ * @return sessionId plus any worker ID.
+ */
+ @Override
+ public String getExtendedId(String clusterId, HttpServletRequest request)
+ {
+ if (!StringUtil.isBlank(_workerName))
+ {
+ if (_workerAttr == null)
+ return clusterId + '.' + _workerName;
+
+ String worker = (String)request.getAttribute(_workerAttr);
+ if (worker != null)
+ return clusterId + '.' + worker;
+ }
+
+ return clusterId;
+ }
+
+ /**
+ * Get the session ID without any worker ID.
+ *
+ * @param extendedId the session id with the worker extension
+ * @return sessionId without any worker ID.
+ */
+ @Override
+ public String getId(String extendedId)
+ {
+ int dot = extendedId.lastIndexOf('.');
+ return (dot > 0) ? extendedId.substring(0, dot) : extendedId;
+ }
+
+ /**
+ * Remove an id from use by telling all contexts to remove a session with this id.
+ *
+ * @see org.eclipse.jetty.server.SessionIdManager#expireAll(java.lang.String)
+ */
+ @Override
+ public void expireAll(String id)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Expiring {}", id);
+
+ for (SessionHandler manager : getSessionHandlers())
+ {
+ manager.invalidate(id);
+ }
+ }
+
+ @Override
+ public void invalidateAll(String id)
+ {
+ //tell all contexts that may have a session object with this id to
+ //get rid of them
+ for (SessionHandler manager : getSessionHandlers())
+ {
+ manager.invalidate(id);
+ }
+ }
+
+ /**
+ * Generate a new id for a session and update across
+ * all SessionManagers.
+ *
+ * @see org.eclipse.jetty.server.SessionIdManager#renewSessionId(java.lang.String, java.lang.String, javax.servlet.http.HttpServletRequest)
+ */
+ @Override
+ public String renewSessionId(String oldClusterId, String oldNodeId, HttpServletRequest request)
+ {
+ //generate a new id
+ String newClusterId = newSessionId(request.hashCode());
+
+ //TODO how to handle request for old id whilst id change is happening?
+
+ //tell all contexts to update the id
+ for (SessionHandler manager : getSessionHandlers())
+ {
+ manager.renewSessionId(oldClusterId, oldNodeId, newClusterId, getExtendedId(newClusterId, request));
+ }
+
+ return newClusterId;
+ }
+
+ /**
+ * Get SessionHandler for every context.
+ *
+ * @return all SessionHandlers that are running
+ */
+ @Override
+ public Set<SessionHandler> getSessionHandlers()
+ {
+ Set<SessionHandler> handlers = new HashSet<>();
+ Handler[] tmp = _server.getChildHandlersByClass(SessionHandler.class);
+ if (tmp != null)
+ {
+ for (Handler h : tmp)
+ {
+ //This method can be called on shutdown when the handlers are STOPPING, so only
+ //check that they are not already stopped
+ if (!h.isStopped() && !h.isFailed())
+ handlers.add((SessionHandler)h);
+ }
+ }
+ return handlers;
+ }
+
+ /**
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString()
+ {
+ return String.format("%s[worker=%s]", super.toString(), _workerName);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/FileSessionDataStore.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/FileSessionDataStore.java
new file mode 100644
index 0000000..ae2b04c
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/FileSessionDataStore.java
@@ -0,0 +1,634 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.nio.file.FileVisitOption;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.util.ClassLoadingObjectInputStream;
+import org.eclipse.jetty.util.MultiException;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * FileSessionDataStore
+ *
+ * A file-based store of session data.
+ */
+@ManagedObject
+public class FileSessionDataStore extends AbstractSessionDataStore
+{
+ private static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
+ protected File _storeDir;
+ protected boolean _deleteUnrestorableFiles = false;
+ protected Map<String, String> _sessionFileMap = new ConcurrentHashMap<>();
+ protected String _contextString;
+ protected long _lastSweepTime = 0L;
+
+ @Override
+ public void initialize(SessionContext context) throws Exception
+ {
+ super.initialize(context);
+ _contextString = _context.getCanonicalContextPath() + "_" + _context.getVhost();
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ initializeStore();
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ _sessionFileMap.clear();
+ _lastSweepTime = 0;
+ super.doStop();
+ }
+
+ @ManagedAttribute(value = "dir where sessions are stored", readonly = true)
+ public File getStoreDir()
+ {
+ return _storeDir;
+ }
+
+ public void setStoreDir(File storeDir)
+ {
+ checkStarted();
+ _storeDir = storeDir;
+ }
+
+ public boolean isDeleteUnrestorableFiles()
+ {
+ return _deleteUnrestorableFiles;
+ }
+
+ public void setDeleteUnrestorableFiles(boolean deleteUnrestorableFiles)
+ {
+ checkStarted();
+ _deleteUnrestorableFiles = deleteUnrestorableFiles;
+ }
+
+ /**
+ * Delete a session
+ *
+ * @param id session id
+ */
+ @Override
+ public boolean delete(String id) throws Exception
+ {
+ if (_storeDir != null)
+ {
+ //remove from our map
+ String filename = _sessionFileMap.remove(getIdWithContext(id));
+ if (filename == null)
+ return false;
+
+ //remove the file
+ return deleteFile(filename);
+ }
+
+ return false;
+ }
+
+ /**
+ * Delete the file associated with a session
+ *
+ * @param filename name of the file containing the session's information
+ * @return true if file was deleted, false otherwise
+ * @throws Exception indicating delete failure
+ */
+ public boolean deleteFile(String filename) throws Exception
+ {
+ if (filename == null)
+ return false;
+ File file = new File(_storeDir, filename);
+ return Files.deleteIfExists(file.toPath());
+ }
+
+ /**
+ * Check to see which sessions have expired.
+ *
+ * @param candidates the set of session ids that the SessionCache believes
+ * have expired
+ * @return the complete set of sessions that have expired, including those
+ * that are not currently loaded into the SessionCache
+ */
+ @Override
+ public Set<String> doGetExpired(final Set<String> candidates)
+ {
+ final long now = System.currentTimeMillis();
+ HashSet<String> expired = new HashSet<>();
+
+ //iterate over the files and work out which have expired
+ for (String filename : _sessionFileMap.values())
+ {
+ try
+ {
+ long expiry = getExpiryFromFilename(filename);
+ if (expiry > 0 && expiry < now)
+ expired.add(getIdFromFilename(filename));
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ //check candidates that were not found to be expired, perhaps
+ //because they no longer exist and they should be expired
+ for (String c : candidates)
+ {
+ if (!expired.contains(c))
+ {
+ //if it doesn't have a file then the session doesn't exist
+ String filename = _sessionFileMap.get(getIdWithContext(c));
+ if (filename == null)
+ expired.add(c);
+ }
+ }
+
+ //Infrequently iterate over all files in the store, and delete those
+ //that expired a long time ago, even if they belong to
+ //another context. This ensures that files that
+ //belong to defunct contexts are cleaned up.
+ //If the graceperiod is disabled, don't do the sweep!
+ if ((_gracePeriodSec > 0) && ((_lastSweepTime == 0) || ((now - _lastSweepTime) >= (5 * TimeUnit.SECONDS.toMillis(_gracePeriodSec)))))
+ {
+ _lastSweepTime = now;
+ sweepDisk();
+ }
+ return expired;
+ }
+
+ /**
+ * Check all session files that do not belong to this context and
+ * remove any that expired long ago (ie at least 5 gracePeriods ago).
+ */
+ public void sweepDisk()
+ {
+ //iterate over the files in the store dir and check expiry times
+ long now = System.currentTimeMillis();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Sweeping {} for old session files", _storeDir);
+ try (Stream<Path> stream = Files.walk(_storeDir.toPath(), 1, FileVisitOption.FOLLOW_LINKS))
+ {
+ stream
+ .filter(p -> !Files.isDirectory(p)).filter(p -> !isOurContextSessionFilename(p.getFileName().toString()))
+ .filter(p -> isSessionFilename(p.getFileName().toString()))
+ .forEach(p -> sweepFile(now, p));
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ /**
+ * Check to see if the expiry on the file is very old, and
+ * delete the file if so. "Old" means that it expired at least
+ * 5 gracePeriods ago. The session can belong to any context.
+ *
+ * @param now the time now in msec
+ * @param p the file to check
+ */
+ public void sweepFile(long now, Path p)
+ {
+ if (p != null)
+ {
+ try
+ {
+ long expiry = getExpiryFromFilename(p.getFileName().toString());
+ //files with 0 expiry never expire
+ if (expiry > 0 && ((now - expiry) >= (5 * TimeUnit.SECONDS.toMillis(_gracePeriodSec))))
+ {
+ try
+ {
+ if (!Files.deleteIfExists(p))
+ LOG.warn("Could not delete {}", p.getFileName());
+ else if (LOG.isDebugEnabled())
+ LOG.debug("Deleted {}", p.getFileName());
+ }
+ catch (IOException e)
+ {
+ LOG.warn("Could not delete {}", p.getFileName(), e);
+ }
+ }
+ }
+ catch (NumberFormatException e)
+ {
+ LOG.warn("Not valid session filename {}", p.getFileName());
+ LOG.warn(e);
+ }
+ }
+ }
+
+ @Override
+ public SessionData doLoad(String id) throws Exception
+ {
+ //load session info from its file
+ String idWithContext = getIdWithContext(id);
+ String filename = _sessionFileMap.get(idWithContext);
+ if (filename == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Unknown file {}", idWithContext);
+ return null;
+ }
+ File file = new File(_storeDir, filename);
+ if (!file.exists())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("No such file {}", filename);
+ return null;
+ }
+
+ try (FileInputStream in = new FileInputStream(file))
+ {
+ SessionData data = load(in, id);
+ data.setLastSaved(file.lastModified());
+ return data;
+ }
+ catch (UnreadableSessionDataException e)
+ {
+ if (isDeleteUnrestorableFiles() && file.exists() && file.getParentFile().equals(_storeDir))
+ {
+ try
+ {
+ delete(id);
+ LOG.warn("Deleted unrestorable file for session {}", id);
+ }
+ catch (Exception x)
+ {
+ LOG.warn("Unable to delete unrestorable file {} for session {}", filename, id);
+ LOG.warn(x);
+ }
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public void doStore(String id, SessionData data, long lastSaveTime) throws Exception
+ {
+ File file;
+ if (_storeDir != null)
+ {
+ delete(id);
+
+ //make a fresh file using the latest session expiry
+ String filename = getIdWithContextAndExpiry(data);
+ String idWithContext = getIdWithContext(id);
+ file = new File(_storeDir, filename);
+
+ try (FileOutputStream fos = new FileOutputStream(file, false))
+ {
+ save(fos, id, data);
+ _sessionFileMap.put(idWithContext, filename);
+ }
+ catch (Exception e)
+ {
+ // No point keeping the file if we didn't save the whole session
+ if (!file.delete())
+ e.addSuppressed(new IOException("Could not delete " + file));
+ throw new UnwriteableSessionDataException(id, _context, e);
+ }
+ }
+ }
+
+ /**
+ * Read the names of the existing session files and build a map of
+ * fully qualified session ids (ie with context) to filename. If there
+ * is more than one file for the same session, only the most recently modified will
+ * be kept and the rest deleted. At the same time, any files - for any context -
+ * that expired a long time ago will be cleaned up.
+ *
+ * @throws Exception if storeDir doesn't exist, isn't readable/writeable
+ * or contains 2 files with the same lastmodify time for the same session. Throws IOException
+ * if the lastmodifytimes can't be read.
+ */
+ public void initializeStore()
+ throws Exception
+ {
+ if (_storeDir == null)
+ throw new IllegalStateException("No file store specified");
+
+ if (!_storeDir.exists())
+ {
+ if (!_storeDir.mkdirs())
+ throw new IllegalStateException("Could not create " + _storeDir);
+ }
+ else
+ {
+ if (!(_storeDir.isDirectory() && _storeDir.canWrite() && _storeDir.canRead()))
+ throw new IllegalStateException(_storeDir.getAbsolutePath() + " must be readable/writeable dir");
+
+ //iterate over files in _storeDir and build map of session id to filename.
+ //if we come across files for sessions in other contexts, check if they're
+ //ancient and remove if necessary.
+ MultiException me = new MultiException();
+ long now = System.currentTimeMillis();
+
+ // Build session file map by walking directory
+ try (Stream<Path> stream = Files.walk(_storeDir.toPath(), 1, FileVisitOption.FOLLOW_LINKS))
+ {
+ stream
+ .filter(p -> !Files.isDirectory(p))
+ .filter(p -> isSessionFilename(p.getFileName().toString()))
+ .forEach(p ->
+ {
+ // first get rid of all ancient files
+ sweepFile(now, p);
+
+ String filename = p.getFileName().toString();
+ String context = getContextFromFilename(filename);
+ //now process it if it wasn't deleted, and it is for our context
+ if (Files.exists(p) && _contextString.equals(context))
+ {
+ //the session is for our context, populate the map with it
+ String sessionIdWithContext = getIdWithContextFromFilename(filename);
+ if (sessionIdWithContext != null)
+ {
+ //handle multiple session files existing for the same session: remove all
+ //but the file with the most recent expiry time
+ String existing = _sessionFileMap.putIfAbsent(sessionIdWithContext, filename);
+ if (existing != null)
+ {
+ //if there was a prior filename, work out which has the most
+ //recent modify time
+ try
+ {
+ long existingExpiry = getExpiryFromFilename(existing);
+ long thisExpiry = getExpiryFromFilename(filename);
+
+ if (thisExpiry > existingExpiry)
+ {
+ //replace with more recent file
+ Path existingPath = _storeDir.toPath().resolve(existing);
+ //update the file we're keeping
+ _sessionFileMap.put(sessionIdWithContext, filename);
+ //delete the old file
+ Files.delete(existingPath);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Replaced {} with {}", existing, filename);
+ }
+ else
+ {
+ //we found an older file, delete it
+ Files.delete(p);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Deleted expired session file {}", filename);
+ }
+ }
+ catch (IOException e)
+ {
+ me.add(e);
+ }
+ }
+ }
+ }
+ });
+ me.ifExceptionThrow();
+ }
+ }
+ }
+
+ @Override
+ @ManagedAttribute(value = "are sessions serialized by this store", readonly = true)
+ public boolean isPassivating()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean exists(String id) throws Exception
+ {
+ String idWithContext = getIdWithContext(id);
+ String filename = _sessionFileMap.get(idWithContext);
+
+ if (filename == null)
+ return false;
+
+ //check the expiry
+ long expiry = getExpiryFromFilename(filename);
+ if (expiry <= 0)
+ return true; //never expires
+ else
+ return (expiry > System.currentTimeMillis()); //hasn't yet expired
+ }
+
+ /**
+ * Save the session data.
+ *
+ * @param os the output stream to save to
+ * @param id identity of the session
+ * @param data the info of the session
+ */
+ protected void save(OutputStream os, String id, SessionData data) throws IOException
+ {
+ DataOutputStream out = new DataOutputStream(os);
+ out.writeUTF(id);
+ out.writeUTF(_context.getCanonicalContextPath());
+ out.writeUTF(_context.getVhost());
+ out.writeUTF(data.getLastNode());
+ out.writeLong(data.getCreated());
+ out.writeLong(data.getAccessed());
+ out.writeLong(data.getLastAccessed());
+ out.writeLong(data.getCookieSet());
+ out.writeLong(data.getExpiry());
+ out.writeLong(data.getMaxInactiveMs());
+
+ ObjectOutputStream oos = new ObjectOutputStream(out);
+ SessionData.serializeAttributes(data, oos);
+ }
+
+ /**
+ * Get the session id with its context.
+ *
+ * @param id identity of session
+ * @return the session id plus context
+ */
+ protected String getIdWithContext(String id)
+ {
+ return _contextString + "_" + id;
+ }
+
+ /**
+ * Get the session id with its context and its expiry time
+ *
+ * @return the session id plus context and expiry
+ */
+ protected String getIdWithContextAndExpiry(SessionData data)
+ {
+ return "" + data.getExpiry() + "_" + getIdWithContext(data.getId());
+ }
+
+ protected String getIdFromFilename(String filename)
+ {
+ if (filename == null)
+ return null;
+ return filename.substring(filename.lastIndexOf('_') + 1);
+ }
+
+ protected long getExpiryFromFilename(String filename)
+ {
+ if (StringUtil.isBlank(filename) || !filename.contains("_"))
+ throw new IllegalStateException("Invalid or missing filename");
+
+ String s = filename.substring(0, filename.indexOf('_'));
+ return Long.parseLong(s);
+ }
+
+ protected String getContextFromFilename(String filename)
+ {
+ if (StringUtil.isBlank(filename))
+ return null;
+
+ int start = filename.indexOf('_');
+ int end = filename.lastIndexOf('_');
+ return filename.substring(start + 1, end);
+ }
+
+ /**
+ * Extract the session id and context from the filename
+ *
+ * @param filename the name of the file to use
+ * @return the session id plus context
+ */
+ protected String getIdWithContextFromFilename(String filename)
+ {
+ if (StringUtil.isBlank(filename) || filename.indexOf('_') < 0)
+ return null;
+
+ return filename.substring(filename.indexOf('_') + 1);
+ }
+
+ /**
+ * Check if the filename is a session filename.
+ *
+ * @param filename the filename to check
+ * @return true if the filename has the correct filename format
+ */
+ protected boolean isSessionFilename(String filename)
+ {
+ if (StringUtil.isBlank(filename))
+ return false;
+ String[] parts = filename.split("_");
+
+ //Need at least 4 parts for a valid filename
+ return parts.length >= 4;
+ }
+
+ /**
+ * Check if the filename matches our session pattern
+ * and is a session for our context.
+ *
+ * @param filename the filename to check
+ * @return true if the filename has the correct filename format and is for this context
+ */
+ protected boolean isOurContextSessionFilename(String filename)
+ {
+ if (StringUtil.isBlank(filename))
+ return false;
+ String[] parts = filename.split("_");
+
+ //Need at least 4 parts for a valid filename
+ if (parts.length < 4)
+ return false;
+
+ //Also needs to be for our context
+ String context = getContextFromFilename(filename);
+ if (context == null)
+ return false;
+ return (_contextString.equals(context));
+ }
+
+ /**
+ * Load the session data from a file.
+ *
+ * @param is file input stream containing session data
+ * @param expectedId the id we've been told to load
+ * @return the session data
+ */
+ protected SessionData load(InputStream is, String expectedId)
+ throws Exception
+ {
+ String id; //the actual id from inside the file
+
+ try
+ {
+ SessionData data;
+ DataInputStream di = new DataInputStream(is);
+
+ id = di.readUTF();
+ String contextPath = di.readUTF();
+ String vhost = di.readUTF();
+ String lastNode = di.readUTF();
+ long created = di.readLong();
+ long accessed = di.readLong();
+ long lastAccessed = di.readLong();
+ long cookieSet = di.readLong();
+ long expiry = di.readLong();
+ long maxIdle = di.readLong();
+
+ data = newSessionData(id, created, accessed, lastAccessed, maxIdle);
+ data.setContextPath(contextPath);
+ data.setVhost(vhost);
+ data.setLastNode(lastNode);
+ data.setCookieSet(cookieSet);
+ data.setExpiry(expiry);
+ data.setMaxInactiveMs(maxIdle);
+
+ // Attributes
+ ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(is);
+ SessionData.deserializeAttributes(data, ois);
+ return data;
+ }
+ catch (Exception e)
+ {
+ throw new UnreadableSessionDataException(expectedId, _context, e);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[dir=%s,deleteUnrestorableFiles=%b]", super.toString(), _storeDir, _deleteUnrestorableFiles);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/FileSessionDataStoreFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/FileSessionDataStoreFactory.java
new file mode 100644
index 0000000..e24408c
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/FileSessionDataStoreFactory.java
@@ -0,0 +1,76 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.io.File;
+
+/**
+ * FileSessionDataStoreFactory
+ */
+public class FileSessionDataStoreFactory extends AbstractSessionDataStoreFactory
+{
+ boolean _deleteUnrestorableFiles;
+ File _storeDir;
+
+ /**
+ * @return the deleteUnrestorableFiles
+ */
+ public boolean isDeleteUnrestorableFiles()
+ {
+ return _deleteUnrestorableFiles;
+ }
+
+ /**
+ * @param deleteUnrestorableFiles the deleteUnrestorableFiles to set
+ */
+ public void setDeleteUnrestorableFiles(boolean deleteUnrestorableFiles)
+ {
+ _deleteUnrestorableFiles = deleteUnrestorableFiles;
+ }
+
+ /**
+ * @return the storeDir
+ */
+ public File getStoreDir()
+ {
+ return _storeDir;
+ }
+
+ /**
+ * @param storeDir the storeDir to set
+ */
+ public void setStoreDir(File storeDir)
+ {
+ _storeDir = storeDir;
+ }
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionDataStoreFactory#getSessionDataStore(org.eclipse.jetty.server.session.SessionHandler)
+ */
+ @Override
+ public SessionDataStore getSessionDataStore(SessionHandler handler)
+ {
+ FileSessionDataStore fsds = new FileSessionDataStore();
+ fsds.setDeleteUnrestorableFiles(isDeleteUnrestorableFiles());
+ fsds.setStoreDir(getStoreDir());
+ fsds.setGracePeriodSec(getGracePeriodSec());
+ fsds.setSavePeriodSec(getSavePeriodSec());
+ return fsds;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/HouseKeeper.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/HouseKeeper.java
new file mode 100644
index 0000000..b2b13ff
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/HouseKeeper.java
@@ -0,0 +1,268 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.server.SessionIdManager;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * HouseKeeper
+ *
+ * There is 1 session HouseKeeper per SessionIdManager instance.
+ */
+@ManagedObject
+public class HouseKeeper extends AbstractLifeCycle
+{
+ private static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
+
+ public static final long DEFAULT_PERIOD_MS = 1000L * 60 * 10;
+
+ protected SessionIdManager _sessionIdManager;
+ protected Scheduler _scheduler;
+ protected Scheduler.Task _task; //scavenge task
+ protected Runner _runner;
+ protected boolean _ownScheduler = false;
+ private long _intervalMs = DEFAULT_PERIOD_MS;
+
+ /**
+ * Runner
+ */
+ protected class Runner implements Runnable
+ {
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ scavenge();
+ }
+ finally
+ {
+ synchronized (HouseKeeper.this)
+ {
+ if (_scheduler != null && _scheduler.isRunning())
+ _task = _scheduler.schedule(this, _intervalMs, TimeUnit.MILLISECONDS);
+ }
+ }
+ }
+ }
+
+ /**
+ * SessionIdManager associated with this scavenger
+ *
+ * @param sessionIdManager the session id manager
+ */
+ public void setSessionIdManager(SessionIdManager sessionIdManager)
+ {
+ if (isStarted())
+ throw new IllegalStateException("HouseKeeper started");
+ _sessionIdManager = sessionIdManager;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (_sessionIdManager == null)
+ throw new IllegalStateException("No SessionIdManager for Housekeeper");
+
+ setIntervalSec(getIntervalSec());
+
+ super.doStart();
+ }
+
+ /**
+ * If scavenging is not scheduled, schedule it.
+ *
+ * @throws Exception if any error during scheduling the scavenging
+ */
+ protected void startScavenging() throws Exception
+ {
+ synchronized (this)
+ {
+ if (_scheduler == null)
+ {
+ if (_sessionIdManager instanceof DefaultSessionIdManager)
+ {
+ //try and use a common scheduler, fallback to own
+ _scheduler = ((DefaultSessionIdManager)_sessionIdManager).getServer().getBean(Scheduler.class);
+ }
+
+ if (_scheduler == null)
+ {
+ _scheduler = new ScheduledExecutorScheduler(String.format("Session-HouseKeeper-%x", hashCode()), false);
+ _ownScheduler = true;
+ _scheduler.start();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Using own scheduler for scavenging");
+ }
+ else if (!_scheduler.isStarted())
+ throw new IllegalStateException("Shared scheduler not started");
+ }
+
+ //cancel any previous task
+ if (_task != null)
+ _task.cancel();
+ if (_runner == null)
+ _runner = new Runner();
+ LOG.info("{} Scavenging every {}ms", _sessionIdManager.getWorkerName(), _intervalMs);
+ _task = _scheduler.schedule(_runner, _intervalMs, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ /**
+ * If scavenging is scheduled, stop it.
+ *
+ * @throws Exception if any error during stopping the scavenging
+ */
+ protected void stopScavenging() throws Exception
+ {
+ synchronized (this)
+ {
+ if (_task != null)
+ {
+ _task.cancel();
+ LOG.info("{} Stopped scavenging", _sessionIdManager.getWorkerName());
+ }
+ _task = null;
+ if (_ownScheduler && _scheduler != null)
+ {
+ _ownScheduler = false;
+ _scheduler.stop();
+ _scheduler = null;
+ }
+ _runner = null;
+ }
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ synchronized (this)
+ {
+ stopScavenging();
+ _scheduler = null;
+ }
+ super.doStop();
+ }
+
+ /**
+ * Set the period between scavenge cycles
+ *
+ * @param sec the interval (in seconds)
+ * @throws Exception if any error during restarting the scavenging
+ */
+ public void setIntervalSec(long sec) throws Exception
+ {
+ synchronized (this)
+ {
+ if (isStarted() || isStarting())
+ {
+ if (sec <= 0)
+ {
+ _intervalMs = 0L;
+ LOG.info("{} Scavenging disabled", _sessionIdManager.getWorkerName());
+ stopScavenging();
+ }
+ else
+ {
+ if (sec < 10)
+ LOG.warn("{} Short interval of {}sec for session scavenging.", _sessionIdManager.getWorkerName(), sec);
+
+ _intervalMs = sec * 1000L;
+
+ //add a bit of variability into the scavenge time so that not all
+ //nodes with the same scavenge interval sync up
+ long tenPercent = _intervalMs / 10;
+ if ((System.currentTimeMillis() % 2) == 0)
+ _intervalMs += tenPercent;
+
+ if (isStarting() || isStarted())
+ {
+ startScavenging();
+ }
+ }
+ }
+ else
+ {
+ _intervalMs = sec * 1000L;
+ }
+ }
+ }
+
+ /**
+ * Get the period between scavenge cycles.
+ *
+ * @return the interval (in seconds)
+ */
+ @ManagedAttribute(value = "secs between scavenge cycles", readonly = true)
+ public long getIntervalSec()
+ {
+ synchronized (this)
+ {
+ return _intervalMs / 1000;
+ }
+ }
+
+ /**
+ * Periodically do session housekeeping
+ */
+ public void scavenge()
+ {
+ //don't attempt to scavenge if we are shutting down
+ if (isStopping() || isStopped())
+ return;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} scavenging sessions", _sessionIdManager.getWorkerName());
+
+ //find the session managers
+ for (SessionHandler manager : _sessionIdManager.getSessionHandlers())
+ {
+ if (manager != null)
+ {
+ try
+ {
+ manager.scavenge();
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ synchronized (this)
+ {
+ return super.toString() + "[interval=" + _intervalMs + ", ownscheduler=" + _ownScheduler + "]";
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionDataStore.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionDataStore.java
new file mode 100644
index 0000000..6bfb3ca
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionDataStore.java
@@ -0,0 +1,959 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.ObjectOutputStream;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.jetty.util.ClassLoadingObjectInputStream;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * JDBCSessionDataStore
+ *
+ * Session data stored in database
+ */
+@ManagedObject
+public class JDBCSessionDataStore extends AbstractSessionDataStore
+{
+ static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
+
+ /**
+ * Used for Oracle and other databases where "" is treated as NULL
+ */
+ public static final String NULL_CONTEXT_PATH = "/";
+
+ protected boolean _initialized = false;
+ protected DatabaseAdaptor _dbAdaptor;
+ protected SessionTableSchema _sessionTableSchema;
+ protected boolean _schemaProvided;
+
+ private static final ByteArrayInputStream EMPTY = new ByteArrayInputStream(new byte[0]);
+
+ /**
+ * SessionTableSchema
+ */
+ public static class SessionTableSchema
+ {
+ public static final int MAX_INTERVAL_NOT_SET = -999;
+ public static final String INFERRED = "INFERRED";
+
+ protected DatabaseAdaptor _dbAdaptor;
+ protected String _schemaName = null;
+ protected String _catalogName = null;
+ protected String _tableName = "JettySessions";
+ protected String _idColumn = "sessionId";
+ protected String _contextPathColumn = "contextPath";
+ protected String _virtualHostColumn = "virtualHost";
+ protected String _lastNodeColumn = "lastNode";
+ protected String _accessTimeColumn = "accessTime";
+ protected String _lastAccessTimeColumn = "lastAccessTime";
+ protected String _createTimeColumn = "createTime";
+ protected String _cookieTimeColumn = "cookieTime";
+ protected String _lastSavedTimeColumn = "lastSavedTime";
+ protected String _expiryTimeColumn = "expiryTime";
+ protected String _maxIntervalColumn = "maxInterval";
+ protected String _mapColumn = "map";
+
+ protected void setDatabaseAdaptor(DatabaseAdaptor dbadaptor)
+ {
+ _dbAdaptor = dbadaptor;
+ }
+
+ public void setCatalogName(String catalogName)
+ {
+ if (catalogName != null && StringUtil.isBlank(catalogName))
+ _catalogName = null;
+ else
+ _catalogName = catalogName;
+ }
+
+ public String getCatalogName()
+ {
+ return _catalogName;
+ }
+
+ public String getSchemaName()
+ {
+ return _schemaName;
+ }
+
+ public void setSchemaName(String schemaName)
+ {
+ if (schemaName != null && StringUtil.isBlank(schemaName))
+ _schemaName = null;
+ else
+ _schemaName = schemaName;
+ }
+
+ public String getTableName()
+ {
+ return _tableName;
+ }
+
+ public void setTableName(String tableName)
+ {
+ checkNotNull(tableName);
+ _tableName = tableName;
+ }
+
+ private String getSchemaTableName()
+ {
+ return (getSchemaName() != null ? getSchemaName() + "." : "") + getTableName();
+ }
+
+ public String getIdColumn()
+ {
+ return _idColumn;
+ }
+
+ public void setIdColumn(String idColumn)
+ {
+ checkNotNull(idColumn);
+ _idColumn = idColumn;
+ }
+
+ public String getContextPathColumn()
+ {
+ return _contextPathColumn;
+ }
+
+ public void setContextPathColumn(String contextPathColumn)
+ {
+ checkNotNull(contextPathColumn);
+ _contextPathColumn = contextPathColumn;
+ }
+
+ public String getVirtualHostColumn()
+ {
+ return _virtualHostColumn;
+ }
+
+ public void setVirtualHostColumn(String virtualHostColumn)
+ {
+ checkNotNull(virtualHostColumn);
+ _virtualHostColumn = virtualHostColumn;
+ }
+
+ public String getLastNodeColumn()
+ {
+ return _lastNodeColumn;
+ }
+
+ public void setLastNodeColumn(String lastNodeColumn)
+ {
+ checkNotNull(lastNodeColumn);
+ _lastNodeColumn = lastNodeColumn;
+ }
+
+ public String getAccessTimeColumn()
+ {
+ return _accessTimeColumn;
+ }
+
+ public void setAccessTimeColumn(String accessTimeColumn)
+ {
+ checkNotNull(accessTimeColumn);
+ _accessTimeColumn = accessTimeColumn;
+ }
+
+ public String getLastAccessTimeColumn()
+ {
+ return _lastAccessTimeColumn;
+ }
+
+ public void setLastAccessTimeColumn(String lastAccessTimeColumn)
+ {
+ checkNotNull(lastAccessTimeColumn);
+ _lastAccessTimeColumn = lastAccessTimeColumn;
+ }
+
+ public String getCreateTimeColumn()
+ {
+ return _createTimeColumn;
+ }
+
+ public void setCreateTimeColumn(String createTimeColumn)
+ {
+ checkNotNull(createTimeColumn);
+ _createTimeColumn = createTimeColumn;
+ }
+
+ public String getCookieTimeColumn()
+ {
+ return _cookieTimeColumn;
+ }
+
+ public void setCookieTimeColumn(String cookieTimeColumn)
+ {
+ checkNotNull(cookieTimeColumn);
+ _cookieTimeColumn = cookieTimeColumn;
+ }
+
+ public String getLastSavedTimeColumn()
+ {
+ return _lastSavedTimeColumn;
+ }
+
+ public void setLastSavedTimeColumn(String lastSavedTimeColumn)
+ {
+ checkNotNull(lastSavedTimeColumn);
+ _lastSavedTimeColumn = lastSavedTimeColumn;
+ }
+
+ public String getExpiryTimeColumn()
+ {
+ return _expiryTimeColumn;
+ }
+
+ public void setExpiryTimeColumn(String expiryTimeColumn)
+ {
+ checkNotNull(expiryTimeColumn);
+ _expiryTimeColumn = expiryTimeColumn;
+ }
+
+ public String getMaxIntervalColumn()
+ {
+ return _maxIntervalColumn;
+ }
+
+ public void setMaxIntervalColumn(String maxIntervalColumn)
+ {
+ checkNotNull(maxIntervalColumn);
+ _maxIntervalColumn = maxIntervalColumn;
+ }
+
+ public String getMapColumn()
+ {
+ return _mapColumn;
+ }
+
+ public void setMapColumn(String mapColumn)
+ {
+ checkNotNull(mapColumn);
+ _mapColumn = mapColumn;
+ }
+
+ public String getCreateStatementAsString()
+ {
+ if (_dbAdaptor == null)
+ throw new IllegalStateException("No DBAdaptor");
+
+ String blobType = _dbAdaptor.getBlobType();
+ String longType = _dbAdaptor.getLongType();
+ String stringType = _dbAdaptor.getStringType();
+
+ return "create table " + _tableName + " (" + _idColumn + " " + stringType + "(120), " +
+ _contextPathColumn + " " + stringType + "(60), " + _virtualHostColumn + " " + stringType + "(60), " + _lastNodeColumn + " " + stringType + "(60), " + _accessTimeColumn + " " + longType + ", " +
+ _lastAccessTimeColumn + " " + longType + ", " + _createTimeColumn + " " + longType + ", " + _cookieTimeColumn + " " + longType + ", " +
+ _lastSavedTimeColumn + " " + longType + ", " + _expiryTimeColumn + " " + longType + ", " + _maxIntervalColumn + " " + longType + ", " +
+ _mapColumn + " " + blobType + ", primary key(" + _idColumn + ", " + _contextPathColumn + "," + _virtualHostColumn + "))";
+ }
+
+ public String getCreateIndexOverExpiryStatementAsString(String indexName)
+ {
+ return "create index " + indexName + " on " + getSchemaTableName() + " (" + getExpiryTimeColumn() + ")";
+ }
+
+ public String getCreateIndexOverSessionStatementAsString(String indexName)
+ {
+ return "create index " + indexName + " on " + getSchemaTableName() + " (" + getIdColumn() + ", " + getContextPathColumn() + ")";
+ }
+
+ public String getAlterTableForMaxIntervalAsString()
+ {
+ if (_dbAdaptor == null)
+ throw new IllegalStateException("No DBAdaptor");
+ String longType = _dbAdaptor.getLongType();
+ String stem = "alter table " + getSchemaTableName() + " add " + getMaxIntervalColumn() + " " + longType;
+ if (_dbAdaptor.getDBName().contains("oracle"))
+ return stem + " default " + MAX_INTERVAL_NOT_SET + " not null";
+ else
+ return stem + " not null default " + MAX_INTERVAL_NOT_SET;
+ }
+
+ private void checkNotNull(String s)
+ {
+ if (s == null)
+ throw new IllegalArgumentException(s);
+ }
+
+ public String getInsertSessionStatementAsString()
+ {
+ return "insert into " + getSchemaTableName() +
+ " (" + getIdColumn() + ", " + getContextPathColumn() + ", " + getVirtualHostColumn() + ", " + getLastNodeColumn() +
+ ", " + getAccessTimeColumn() + ", " + getLastAccessTimeColumn() + ", " + getCreateTimeColumn() + ", " + getCookieTimeColumn() +
+ ", " + getLastSavedTimeColumn() + ", " + getExpiryTimeColumn() + ", " + getMaxIntervalColumn() + ", " + getMapColumn() + ") " +
+ " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ }
+
+ public PreparedStatement getUpdateSessionStatement(Connection connection, String id, SessionContext context)
+ throws SQLException
+ {
+ String s = "update " + getSchemaTableName() +
+ " set " + getLastNodeColumn() + " = ?, " + getAccessTimeColumn() + " = ?, " +
+ getLastAccessTimeColumn() + " = ?, " + getLastSavedTimeColumn() + " = ?, " + getExpiryTimeColumn() + " = ?, " +
+ getMaxIntervalColumn() + " = ?, " + getMapColumn() + " = ? where " + getIdColumn() + " = ? and " + getContextPathColumn() +
+ " = ? and " + getVirtualHostColumn() + " = ?";
+
+ String cp = context.getCanonicalContextPath();
+ if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
+ cp = NULL_CONTEXT_PATH;
+
+ PreparedStatement statement = connection.prepareStatement(s);
+ statement.setString(8, id);
+ statement.setString(9, cp);
+ statement.setString(10, context.getVhost());
+ return statement;
+ }
+
+ public PreparedStatement getExpiredSessionsStatement(Connection connection, String canonicalContextPath, String vhost, long expiry)
+ throws SQLException
+ {
+ // TODO expiry should be a delay rather than an absolute time.
+
+ if (_dbAdaptor == null)
+ throw new IllegalStateException("No DB adaptor");
+
+ String cp = canonicalContextPath;
+ if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
+ cp = NULL_CONTEXT_PATH;
+
+ PreparedStatement statement = connection.prepareStatement("select " + getIdColumn() + ", " + getExpiryTimeColumn() +
+ " from " + getSchemaTableName() + " where " + getContextPathColumn() + " = ? and " +
+ getVirtualHostColumn() + " = ? and " +
+ getExpiryTimeColumn() + " >0 and " + getExpiryTimeColumn() + " <= ?");
+
+ statement.setString(1, cp);
+ statement.setString(2, vhost);
+ statement.setLong(3, expiry);
+ return statement;
+ }
+
+ public PreparedStatement getMyExpiredSessionsStatement(Connection connection, SessionContext sessionContext, long expiry)
+ throws SQLException
+ {
+ // TODO expiry should be a delay rather than an absolute time.
+
+ if (_dbAdaptor == null)
+ throw new IllegalStateException("No DB adaptor");
+
+ String cp = sessionContext.getCanonicalContextPath();
+ if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
+ cp = NULL_CONTEXT_PATH;
+
+ PreparedStatement statement = connection.prepareStatement("select " + getIdColumn() + ", " + getExpiryTimeColumn() +
+ " from " + getSchemaTableName() + " where " +
+ getLastNodeColumn() + " = ? and " +
+ getContextPathColumn() + " = ? and " +
+ getVirtualHostColumn() + " = ? and " +
+ getExpiryTimeColumn() + " >0 and " + getExpiryTimeColumn() + " <= ?");
+
+ statement.setString(1, sessionContext.getWorkerName());
+ statement.setString(2, cp);
+ statement.setString(3, sessionContext.getVhost());
+ statement.setLong(4, expiry);
+ return statement;
+ }
+
+ public PreparedStatement getAllAncientExpiredSessionsStatement(Connection connection)
+ throws SQLException
+ {
+ if (_dbAdaptor == null)
+ throw new IllegalStateException("No DB adaptor");
+
+ PreparedStatement statement = connection.prepareStatement("select " + getIdColumn() + ", " + getContextPathColumn() + ", " + getVirtualHostColumn() +
+ " from " + getSchemaTableName() +
+ " where " + getExpiryTimeColumn() + " >0 and " + getExpiryTimeColumn() + " <= ?");
+ return statement;
+ }
+
+ public PreparedStatement getCheckSessionExistsStatement(Connection connection, SessionContext context)
+ throws SQLException
+ {
+ if (_dbAdaptor == null)
+ throw new IllegalStateException("No DB adaptor");
+
+ String cp = context.getCanonicalContextPath();
+ if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
+ cp = NULL_CONTEXT_PATH;
+
+ PreparedStatement statement = connection.prepareStatement("select " + getIdColumn() + ", " + getExpiryTimeColumn() +
+ " from " + getSchemaTableName() +
+ " where " + getIdColumn() + " = ? and " +
+ getContextPathColumn() + " = ? and " +
+ getVirtualHostColumn() + " = ?");
+ statement.setString(2, cp);
+ statement.setString(3, context.getVhost());
+ return statement;
+ }
+
+ public PreparedStatement getLoadStatement(Connection connection, String id, SessionContext contextId)
+ throws SQLException
+ {
+ if (_dbAdaptor == null)
+ throw new IllegalStateException("No DB adaptor");
+
+ String cp = contextId.getCanonicalContextPath();
+ if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
+ cp = NULL_CONTEXT_PATH;
+
+ PreparedStatement statement = connection.prepareStatement("select * from " + getSchemaTableName() +
+ " where " + getIdColumn() + " = ? and " + getContextPathColumn() +
+ " = ? and " + getVirtualHostColumn() + " = ?");
+ statement.setString(1, id);
+ statement.setString(2, cp);
+ statement.setString(3, contextId.getVhost());
+
+ return statement;
+ }
+
+ public PreparedStatement getUpdateStatement(Connection connection, String id, SessionContext contextId)
+ throws SQLException
+ {
+ if (_dbAdaptor == null)
+ throw new IllegalStateException("No DB adaptor");
+
+ String cp = contextId.getCanonicalContextPath();
+ if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
+ cp = NULL_CONTEXT_PATH;
+
+ String s = "update " + getSchemaTableName() +
+ " set " + getLastNodeColumn() + " = ?, " + getAccessTimeColumn() + " = ?, " +
+ getLastAccessTimeColumn() + " = ?, " + getLastSavedTimeColumn() + " = ?, " + getExpiryTimeColumn() + " = ?, " +
+ getMaxIntervalColumn() + " = ?, " + getMapColumn() + " = ? where " + getIdColumn() + " = ? and " + getContextPathColumn() +
+ " = ? and " + getVirtualHostColumn() + " = ?";
+
+ PreparedStatement statement = connection.prepareStatement(s);
+ statement.setString(8, id);
+ statement.setString(9, cp);
+ statement.setString(10, contextId.getVhost());
+
+ return statement;
+ }
+
+ public PreparedStatement getDeleteStatement(Connection connection, String id, SessionContext contextId)
+ throws Exception
+ {
+ if (_dbAdaptor == null)
+
+ throw new IllegalStateException("No DB adaptor");
+
+ String cp = contextId.getCanonicalContextPath();
+ if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
+ cp = NULL_CONTEXT_PATH;
+
+ PreparedStatement statement = connection.prepareStatement("delete from " + getSchemaTableName() +
+ " where " + getIdColumn() + " = ? and " + getContextPathColumn() +
+ " = ? and " + getVirtualHostColumn() + " = ?");
+ statement.setString(1, id);
+ statement.setString(2, cp);
+ statement.setString(3, contextId.getVhost());
+
+ return statement;
+ }
+
+ /**
+ * Set up the tables in the database
+ *
+ * @throws SQLException if unable to prepare tables
+ */
+ public void prepareTables()
+ throws SQLException
+ {
+ try (Connection connection = _dbAdaptor.getConnection();
+ Statement statement = connection.createStatement())
+ {
+ //make the id table
+ connection.setAutoCommit(true);
+ DatabaseMetaData metaData = connection.getMetaData();
+ _dbAdaptor.adaptTo(metaData);
+
+ //make the session table if necessary
+ String tableName = _dbAdaptor.convertIdentifier(getTableName());
+
+ String schemaName = _dbAdaptor.convertIdentifier(getSchemaName());
+ if (INFERRED.equalsIgnoreCase(schemaName))
+ {
+ //use the value from the connection -
+ //NOTE that this value will also now be prepended to ALL
+ //table names in queries/updates.
+ schemaName = connection.getSchema();
+ setSchemaName(schemaName);
+ }
+ String catalogName = _dbAdaptor.convertIdentifier(getCatalogName());
+ if (INFERRED.equalsIgnoreCase(catalogName))
+ {
+ //use the value from the connection
+ catalogName = connection.getCatalog();
+ setCatalogName(catalogName);
+ }
+
+ try (ResultSet result = metaData.getTables(catalogName, schemaName, tableName, null))
+ {
+ if (!result.next())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Creating table {} schema={} catalog={}", tableName, schemaName, catalogName);
+ //table does not exist, so create it
+ statement.executeUpdate(getCreateStatementAsString());
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Not creating table {} schema={} catalog={}", tableName, schemaName, catalogName);
+ //session table exists, check it has maxinterval column
+ ResultSet colResult = null;
+ try
+ {
+ colResult = metaData.getColumns(catalogName, schemaName, tableName,
+ _dbAdaptor.convertIdentifier(getMaxIntervalColumn()));
+ }
+ catch (SQLException sqlEx)
+ {
+ LOG.warn("Problem checking if {} table contains {} column. Ensure table contains column with definition: long not null default -999",
+ getTableName(), getMaxIntervalColumn());
+ throw sqlEx;
+ }
+ try
+ {
+ if (!colResult.next())
+ {
+ try
+ {
+ //add the maxinterval column
+ statement.executeUpdate(getAlterTableForMaxIntervalAsString());
+ }
+ catch (SQLException sqlEx)
+ {
+ LOG.warn("Problem adding {} column. Ensure table contains column definition: long not null default -999", getMaxIntervalColumn());
+ throw sqlEx;
+ }
+ }
+ }
+ finally
+ {
+ colResult.close();
+ }
+ }
+ }
+ //make some indexes on the JettySessions table
+ String index1 = "idx_" + getTableName() + "_expiry";
+ String index2 = "idx_" + getTableName() + "_session";
+
+ boolean index1Exists = false;
+ boolean index2Exists = false;
+ try (ResultSet result = metaData.getIndexInfo(catalogName, schemaName, tableName, false, true))
+ {
+ while (result.next())
+ {
+ String idxName = result.getString("INDEX_NAME");
+ if (index1.equalsIgnoreCase(idxName))
+ index1Exists = true;
+ else if (index2.equalsIgnoreCase(idxName))
+ index2Exists = true;
+ }
+ }
+ if (!index1Exists)
+ statement.executeUpdate(getCreateIndexOverExpiryStatementAsString(index1));
+ if (!index2Exists)
+ statement.executeUpdate(getCreateIndexOverSessionStatementAsString(index2));
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s]", super.toString(),
+ _catalogName, _schemaName, _tableName, _idColumn, _contextPathColumn, _virtualHostColumn, _cookieTimeColumn, _createTimeColumn,
+ _expiryTimeColumn, _accessTimeColumn, _lastAccessTimeColumn, _lastNodeColumn, _lastSavedTimeColumn, _maxIntervalColumn);
+ }
+ }
+
+ public JDBCSessionDataStore()
+ {
+ super();
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (_dbAdaptor == null)
+ throw new IllegalStateException("No jdbc config");
+
+ initialize();
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ super.doStop();
+ _initialized = false;
+ if (!_schemaProvided)
+ _sessionTableSchema = null;
+ }
+
+ public void initialize() throws Exception
+ {
+ if (!_initialized)
+ {
+ _initialized = true;
+
+ //taking the defaults if one not set
+ if (_sessionTableSchema == null)
+ {
+ _sessionTableSchema = new SessionTableSchema();
+ addBean(_sessionTableSchema, true);
+ }
+
+ _dbAdaptor.initialize();
+ _sessionTableSchema.setDatabaseAdaptor(_dbAdaptor);
+ _sessionTableSchema.prepareTables();
+ }
+ }
+
+ @Override
+ public SessionData doLoad(String id) throws Exception
+ {
+ try (Connection connection = _dbAdaptor.getConnection();
+ PreparedStatement statement = _sessionTableSchema.getLoadStatement(connection, id, _context);
+ ResultSet result = statement.executeQuery())
+ {
+ SessionData data = null;
+ if (result.next())
+ {
+ data = newSessionData(id,
+ result.getLong(_sessionTableSchema.getCreateTimeColumn()),
+ result.getLong(_sessionTableSchema.getAccessTimeColumn()),
+ result.getLong(_sessionTableSchema.getLastAccessTimeColumn()),
+ result.getLong(_sessionTableSchema.getMaxIntervalColumn()));
+ data.setCookieSet(result.getLong(_sessionTableSchema.getCookieTimeColumn()));
+ data.setLastNode(result.getString(_sessionTableSchema.getLastNodeColumn()));
+ data.setLastSaved(result.getLong(_sessionTableSchema.getLastSavedTimeColumn()));
+ data.setExpiry(result.getLong(_sessionTableSchema.getExpiryTimeColumn()));
+ data.setContextPath(_context.getCanonicalContextPath());
+ data.setVhost(_context.getVhost());
+
+ try (InputStream is = _dbAdaptor.getBlobInputStream(result, _sessionTableSchema.getMapColumn());
+ ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(is))
+ {
+ SessionData.deserializeAttributes(data, ois);
+ }
+ catch (Exception e)
+ {
+ throw new UnreadableSessionDataException(id, _context, e);
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("LOADED session {}", data);
+ }
+ else if (LOG.isDebugEnabled())
+ LOG.debug("No session {}", id);
+
+ return data;
+ }
+ }
+
+ @Override
+ public boolean delete(String id) throws Exception
+ {
+ try (Connection connection = _dbAdaptor.getConnection();
+ PreparedStatement statement = _sessionTableSchema.getDeleteStatement(connection, id, _context))
+ {
+ connection.setAutoCommit(true);
+ int rows = statement.executeUpdate();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Deleted Session {}:{}", id, (rows > 0));
+
+ return rows > 0;
+ }
+ }
+
+ @Override
+ public void doStore(String id, SessionData data, long lastSaveTime) throws Exception
+ {
+ if (data == null || id == null)
+ return;
+
+ if (lastSaveTime <= 0)
+ {
+ doInsert(id, data);
+ }
+ else
+ {
+ doUpdate(id, data);
+ }
+ }
+
+ protected void doInsert(String id, SessionData data)
+ throws Exception
+ {
+ String s = _sessionTableSchema.getInsertSessionStatementAsString();
+
+ try (Connection connection = _dbAdaptor.getConnection())
+ {
+ connection.setAutoCommit(true);
+ try (PreparedStatement statement = connection.prepareStatement(s))
+ {
+ statement.setString(1, id); //session id
+
+ String cp = _context.getCanonicalContextPath();
+ if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
+ cp = NULL_CONTEXT_PATH;
+
+ statement.setString(2, cp); //context path
+
+ statement.setString(3, _context.getVhost()); //first vhost
+ statement.setString(4, data.getLastNode()); //my node id
+ statement.setLong(5, data.getAccessed()); //accessTime
+ statement.setLong(6, data.getLastAccessed()); //lastAccessTime
+ statement.setLong(7, data.getCreated()); //time created
+ statement.setLong(8, data.getCookieSet()); //time cookie was set
+ statement.setLong(9, data.getLastSaved()); //last saved time
+ statement.setLong(10, data.getExpiry());
+ statement.setLong(11, data.getMaxInactiveMs());
+
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(baos))
+ {
+ SessionData.serializeAttributes(data, oos);
+ byte[] bytes = baos.toByteArray();
+ ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+ statement.setBinaryStream(12, bais, bytes.length); //attribute map as blob
+ }
+
+ statement.executeUpdate();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Inserted session " + data);
+ }
+ }
+ }
+
+ protected void doUpdate(String id, SessionData data)
+ throws Exception
+ {
+ try (Connection connection = _dbAdaptor.getConnection())
+ {
+ connection.setAutoCommit(true);
+ try (PreparedStatement statement = _sessionTableSchema.getUpdateSessionStatement(connection, data.getId(), _context))
+ {
+ statement.setString(1, data.getLastNode()); //should be my node id
+ statement.setLong(2, data.getAccessed()); //accessTime
+ statement.setLong(3, data.getLastAccessed()); //lastAccessTime
+ statement.setLong(4, data.getLastSaved()); //last saved time
+ statement.setLong(5, data.getExpiry());
+ statement.setLong(6, data.getMaxInactiveMs());
+
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(baos))
+ {
+ SessionData.serializeAttributes(data, oos);
+ byte[] bytes = baos.toByteArray();
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes))
+ {
+ statement.setBinaryStream(7, bais, bytes.length); //attribute map as blob
+ }
+ }
+
+ statement.executeUpdate();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Updated session " + data);
+ }
+ }
+ }
+
+ @Override
+ public Set<String> doGetExpired(Set<String> candidates)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Getting expired sessions at time {}", System.currentTimeMillis());
+
+ long now = System.currentTimeMillis();
+
+ Set<String> expiredSessionKeys = new HashSet<>();
+ try (Connection connection = _dbAdaptor.getConnection())
+ {
+ connection.setAutoCommit(true);
+
+ /*
+ * 1. Select sessions managed by this node for our context that have expired
+ */
+ long upperBound = now;
+ if (LOG.isDebugEnabled())
+ LOG.debug("{}- Pass 1: Searching for sessions for context {} managed by me and expired before {}", _context.getWorkerName(), _context.getCanonicalContextPath(), upperBound);
+
+ try (PreparedStatement statement = _sessionTableSchema.getExpiredSessionsStatement(connection, _context.getCanonicalContextPath(), _context.getVhost(), upperBound))
+ {
+ try (ResultSet result = statement.executeQuery())
+ {
+ while (result.next())
+ {
+ String sessionId = result.getString(_sessionTableSchema.getIdColumn());
+ long exp = result.getLong(_sessionTableSchema.getExpiryTimeColumn());
+ expiredSessionKeys.add(sessionId);
+ if (LOG.isDebugEnabled())
+ LOG.debug(_context.getCanonicalContextPath() + "- Found expired sessionId=" + sessionId);
+ }
+ }
+ }
+
+ /*
+ * 2. Select sessions for any node or context that have expired
+ * at least 1 graceperiod since the last expiry check. If we haven't done previous expiry checks, then check
+ * those that have expired at least 3 graceperiod ago.
+ */
+ try (PreparedStatement selectExpiredSessions = _sessionTableSchema.getAllAncientExpiredSessionsStatement(connection))
+ {
+ if (_lastExpiryCheckTime <= 0)
+ upperBound = (now - (3 * (1000L * _gracePeriodSec)));
+ else
+ upperBound = _lastExpiryCheckTime - (1000L * _gracePeriodSec);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{}- Pass 2: Searching for sessions expired before {}", _context.getWorkerName(), upperBound);
+
+ selectExpiredSessions.setLong(1, upperBound);
+ try (ResultSet result = selectExpiredSessions.executeQuery())
+ {
+ while (result.next())
+ {
+ String sessionId = result.getString(_sessionTableSchema.getIdColumn());
+ String ctxtpth = result.getString(_sessionTableSchema.getContextPathColumn());
+ String vh = result.getString(_sessionTableSchema.getVirtualHostColumn());
+ expiredSessionKeys.add(sessionId);
+ if (LOG.isDebugEnabled())
+ LOG.debug("{}- Found expired sessionId=", _context.getWorkerName(), sessionId);
+ }
+ }
+ }
+
+ Set<String> notExpiredInDB = new HashSet<>();
+ for (String k : candidates)
+ {
+ //there are some keys that the session store thought had expired, but were not
+ //found in our sweep either because it is no longer in the db, or its
+ //expiry time was updated
+ if (!expiredSessionKeys.contains(k))
+ notExpiredInDB.add(k);
+ }
+
+ if (!notExpiredInDB.isEmpty())
+ {
+ //we have some sessions to check
+ try (PreparedStatement checkSessionExists = _sessionTableSchema.getCheckSessionExistsStatement(connection, _context))
+ {
+ for (String k : notExpiredInDB)
+ {
+ checkSessionExists.setString(1, k);
+ try (ResultSet result = checkSessionExists.executeQuery())
+ {
+ if (!result.next())
+ {
+ //session doesn't exist any more, can be expired
+ expiredSessionKeys.add(k);
+ }
+ //else its expiry time has not been reached
+ }
+ catch (Exception e)
+ {
+ LOG.warn("{} Problem checking if potentially expired session {} exists in db", _context.getWorkerName(), k);
+ LOG.warn(e);
+ }
+ }
+ }
+ }
+
+ return expiredSessionKeys;
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ return expiredSessionKeys; //return whatever we got
+ }
+ }
+
+ public void setDatabaseAdaptor(DatabaseAdaptor dbAdaptor)
+ {
+ checkStarted();
+ updateBean(_dbAdaptor, dbAdaptor);
+ _dbAdaptor = dbAdaptor;
+ }
+
+ public void setSessionTableSchema(SessionTableSchema schema)
+ {
+ checkStarted();
+ updateBean(_sessionTableSchema, schema);
+ _sessionTableSchema = schema;
+ _schemaProvided = true;
+ }
+
+ @Override
+ @ManagedAttribute(value = "does this store serialize sessions", readonly = true)
+ public boolean isPassivating()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean exists(String id)
+ throws Exception
+ {
+ try (Connection connection = _dbAdaptor.getConnection())
+ {
+ connection.setAutoCommit(true);
+
+ //non-expired session exists?
+ try (PreparedStatement checkSessionExists = _sessionTableSchema.getCheckSessionExistsStatement(connection, _context))
+ {
+ checkSessionExists.setString(1, id);
+ try (ResultSet result = checkSessionExists.executeQuery())
+ {
+ if (!result.next())
+ {
+ return false; //no such session
+ }
+ else
+ {
+ long expiry = result.getLong(_sessionTableSchema.getExpiryTimeColumn());
+ if (expiry <= 0) //never expires
+ return true;
+ else
+ return (expiry > System.currentTimeMillis()); //hasn't already expired
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionDataStoreFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionDataStoreFactory.java
new file mode 100644
index 0000000..12c0678
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionDataStoreFactory.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+/**
+ * JDBCSessionDataStoreFactory
+ */
+public class JDBCSessionDataStoreFactory extends AbstractSessionDataStoreFactory
+{
+
+ /**
+ *
+ */
+ DatabaseAdaptor _adaptor;
+
+ /**
+ *
+ */
+ JDBCSessionDataStore.SessionTableSchema _schema;
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionDataStoreFactory#getSessionDataStore(org.eclipse.jetty.server.session.SessionHandler)
+ */
+ @Override
+ public SessionDataStore getSessionDataStore(SessionHandler handler)
+ {
+ JDBCSessionDataStore ds = new JDBCSessionDataStore();
+ ds.setDatabaseAdaptor(_adaptor);
+ ds.setSessionTableSchema(_schema);
+ ds.setGracePeriodSec(getGracePeriodSec());
+ ds.setSavePeriodSec(getSavePeriodSec());
+ return ds;
+ }
+
+ /**
+ * @param adaptor the {@link DatabaseAdaptor} to set
+ */
+ public void setDatabaseAdaptor(DatabaseAdaptor adaptor)
+ {
+ _adaptor = adaptor;
+ }
+
+ /**
+ * @param schema the {@link JDBCSessionDataStoreFactory} to set
+ */
+ public void setSessionTableSchema(JDBCSessionDataStore.SessionTableSchema schema)
+ {
+ _schema = schema;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java
new file mode 100644
index 0000000..76c6a93
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java
@@ -0,0 +1,99 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.util.function.Function;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * NullSessionCache
+ *
+ * Does not actually cache any Session objects. Useful for testing.
+ * Also useful if you do not want to share Session objects with the same id between
+ * simultaneous requests: note that this means that context forwarding can't share
+ * the same id either.
+ */
+public class NullSessionCache extends AbstractSessionCache
+{
+ /**
+ * @param handler The SessionHandler related to this SessionCache
+ */
+ public NullSessionCache(SessionHandler handler)
+ {
+ super(handler);
+ super.setEvictionPolicy(EVICT_ON_SESSION_EXIT);
+ }
+
+ @Override
+ public void shutdown()
+ {
+ }
+
+ @Override
+ public Session newSession(SessionData data)
+ {
+ return new Session(getSessionHandler(), data);
+ }
+
+ @Override
+ public Session newSession(HttpServletRequest request, SessionData data)
+ {
+ return new Session(getSessionHandler(), request, data);
+ }
+
+ @Override
+ public Session doGet(String id)
+ {
+ //do not cache anything
+ return null;
+ }
+
+ @Override
+ public Session doPutIfAbsent(String id, Session session)
+ {
+ //nothing was stored previously
+ return null;
+ }
+
+ @Override
+ public boolean doReplace(String id, Session oldValue, Session newValue)
+ {
+ //always accept new value
+ return true;
+ }
+
+ @Override
+ public Session doDelete(String id)
+ {
+ return null;
+ }
+
+ @Override
+ public void setEvictionPolicy(int evictionTimeout)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Ignoring eviction setting: {}", evictionTimeout);
+ }
+
+ @Override
+ protected Session doComputeIfAbsent(String id, Function<String, Session> mappingFunction)
+ {
+ return mappingFunction.apply(id);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java
new file mode 100644
index 0000000..faa8898
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java
@@ -0,0 +1,77 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * NullSessionCacheFactory
+ *
+ * Factory for NullSessionCaches.
+ */
+public class NullSessionCacheFactory extends AbstractSessionCacheFactory
+{
+ private static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
+
+ @Override
+ public int getEvictionPolicy()
+ {
+ return SessionCache.EVICT_ON_SESSION_EXIT; //never actually stored
+ }
+
+ @Override
+ public void setEvictionPolicy(int evictionPolicy)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Ignoring eviction policy setting for NullSessionCaches");
+ }
+
+ @Override
+ public boolean isSaveOnInactiveEvict()
+ {
+ return false; //never kept in cache
+ }
+
+ @Override
+ public void setSaveOnInactiveEvict(boolean saveOnInactiveEvict)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Ignoring eviction policy setting for NullSessionCaches");
+ }
+
+ @Override
+ public boolean isInvalidateOnShutdown()
+ {
+ return false; //meaningless for NullSessionCache
+ }
+
+ @Override
+ public void setInvalidateOnShutdown(boolean invalidateOnShutdown)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Ignoring invalidateOnShutdown setting for NullSessionCaches");
+ }
+
+ @Override
+ public SessionCache newSessionCache(SessionHandler handler)
+ {
+ return new NullSessionCache(handler);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionDataStore.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionDataStore.java
new file mode 100644
index 0000000..632f115
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionDataStore.java
@@ -0,0 +1,77 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.util.Set;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+
+/**
+ * NullSessionDataStore
+ *
+ * Does not actually store anything, useful for testing.
+ */
+@ManagedObject
+public class NullSessionDataStore extends AbstractSessionDataStore
+{
+
+ @Override
+ public SessionData doLoad(String id) throws Exception
+ {
+ return null;
+ }
+
+ @Override
+ public SessionData newSessionData(String id, long created, long accessed, long lastAccessed, long maxInactiveMs)
+ {
+ return new SessionData(id, _context.getCanonicalContextPath(), _context.getVhost(), created, accessed, lastAccessed, maxInactiveMs);
+ }
+
+ @Override
+ public boolean delete(String id) throws Exception
+ {
+ return true;
+ }
+
+ @Override
+ public void doStore(String id, SessionData data, long lastSaveTime) throws Exception
+ {
+ //noop
+ }
+
+ @Override
+ public Set<String> doGetExpired(Set<String> candidates)
+ {
+ return candidates; //whatever is suggested we accept
+ }
+
+ @ManagedAttribute(value = "does this store serialize sessions", readonly = true)
+ @Override
+ public boolean isPassivating()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean exists(String id)
+ {
+ return false;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionDataStoreFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionDataStoreFactory.java
new file mode 100644
index 0000000..0498ad4
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionDataStoreFactory.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+/**
+ * NullSessionDataStoreFactory
+ */
+public class NullSessionDataStoreFactory extends AbstractSessionDataStoreFactory
+{
+
+ /**
+ * @see org.eclipse.jetty.server.session.SessionDataStoreFactory#getSessionDataStore(org.eclipse.jetty.server.session.SessionHandler)
+ */
+ @Override
+ public SessionDataStore getSessionDataStore(SessionHandler handler) throws Exception
+ {
+ return new NullSessionDataStore();
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java
new file mode 100644
index 0000000..4db2b40
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java
@@ -0,0 +1,1157 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSessionActivationListener;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionBindingListener;
+import javax.servlet.http.HttpSessionContext;
+import javax.servlet.http.HttpSessionEvent;
+
+import org.eclipse.jetty.io.CyclicTimeout;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Locker;
+import org.eclipse.jetty.util.thread.Locker.Lock;
+
+/**
+ * Session
+ *
+ * A heavy-weight Session object representing an HttpSession. Session objects
+ * relating to a context are kept in a {@link SessionCache}. The purpose of the
+ * SessionCache is to keep the working set of Session objects in memory so that
+ * they may be accessed quickly, and facilitate the sharing of a Session object
+ * amongst multiple simultaneous requests referring to the same session id.
+ *
+ * The {@link SessionHandler} coordinates the lifecycle of Session objects with
+ * the help of the SessionCache.
+ *
+ * @see SessionHandler
+ * @see org.eclipse.jetty.server.SessionIdManager
+ */
+public class Session implements SessionHandler.SessionIf
+{
+ private static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
+
+ /**
+ *
+ */
+ public static final String SESSION_CREATED_SECURE = "org.eclipse.jetty.security.sessionCreatedSecure";
+
+ /**
+ * State
+ *
+ * Validity states of a session
+ */
+ public enum State
+ {
+ VALID, INVALID, INVALIDATING, CHANGING
+ }
+
+ public enum IdState
+ {
+ SET, CHANGING
+ }
+
+ protected final SessionData _sessionData; // the actual data associated with
+ // a session
+
+ protected final SessionHandler _handler; // the manager of the session
+
+ protected String _extendedId; // the _id plus the worker name
+
+ protected long _requests;
+
+ protected boolean _idChanged;
+
+ protected boolean _newSession;
+
+ protected State _state = State.VALID; // state of the session:valid,invalid
+ // or being invalidated
+
+ protected Locker _lock = new Locker(); // sync lock
+ protected Condition _stateChangeCompleted = _lock.newCondition();
+ protected boolean _resident = false;
+ protected final SessionInactivityTimer _sessionInactivityTimer;
+
+ /**
+ * SessionInactivityTimer
+ *
+ * Each Session has a timer associated with it that fires whenever it has
+ * been idle (ie not accessed by a request) for a configurable amount of
+ * time, or the Session expires.
+ *
+ * @see SessionCache
+ */
+ public class SessionInactivityTimer
+ {
+ protected final CyclicTimeout _timer;
+
+ public SessionInactivityTimer()
+ {
+ _timer = new CyclicTimeout((getSessionHandler().getScheduler()))
+ {
+ @Override
+ public void onTimeoutExpired()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Timer expired for session {}", getId());
+ long now = System.currentTimeMillis();
+ //handle what to do with the session after the timer expired
+ getSessionHandler().sessionInactivityTimerExpired(Session.this, now);
+ try (Lock lock = Session.this.lock())
+ {
+ //grab the lock and check what happened to the session: if it didn't get evicted and
+ //it hasn't expired, we need to reset the timer
+ if (Session.this.isResident() && Session.this.getRequests() <= 0 && Session.this.isValid() &&
+ !Session.this.isExpiredAt(now))
+ {
+ //session wasn't expired or evicted, we need to reset the timer
+ SessionInactivityTimer.this.schedule(Session.this.calculateInactivityTimeout(now));
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * For backward api compatibility only.
+ *
+ * @see #schedule(long)
+ */
+ @Deprecated
+ public void schedule()
+ {
+ schedule(calculateInactivityTimeout(System.currentTimeMillis()));
+ }
+
+ /**
+ * @param time the timeout to set; -1 means that the timer will not be
+ * scheduled
+ */
+ public void schedule(long time)
+ {
+ if (time >= 0)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("(Re)starting timer for session {} at {}ms", getId(), time);
+ _timer.schedule(time, TimeUnit.MILLISECONDS);
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Not starting timer for session {}", getId());
+ }
+ }
+
+ public void cancel()
+ {
+ _timer.cancel();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Cancelled timer for session {}", getId());
+ }
+
+ public void destroy()
+ {
+ _timer.destroy();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Destroyed timer for session {}", getId());
+ }
+ }
+
+ /**
+ * Create a new session
+ *
+ * @param handler the SessionHandler that manages this session
+ * @param request the request the session should be based on
+ * @param data the session data
+ */
+ public Session(SessionHandler handler, HttpServletRequest request, SessionData data)
+ {
+ _handler = handler;
+ _sessionData = data;
+ _newSession = true;
+ _sessionData.setDirty(true);
+ _sessionInactivityTimer = new SessionInactivityTimer();
+ }
+
+ /**
+ * Re-inflate an existing session from some eg persistent store.
+ *
+ * @param handler the SessionHandler managing the session
+ * @param data the session data
+ */
+ public Session(SessionHandler handler, SessionData data)
+ {
+ _handler = handler;
+ _sessionData = data;
+ _sessionInactivityTimer = new SessionInactivityTimer();
+ }
+
+ /**
+ * Returns the current number of requests that are active in the Session.
+ *
+ * @return the number of active requests for this session
+ */
+ public long getRequests()
+ {
+ try (Lock lock = _lock.lock())
+ {
+ return _requests;
+ }
+ }
+
+ public void setExtendedId(String extendedId)
+ {
+ _extendedId = extendedId;
+ }
+
+ protected void cookieSet()
+ {
+ try (Lock lock = _lock.lock())
+ {
+ _sessionData.setCookieSet(_sessionData.getAccessed());
+ }
+ }
+
+ protected void use()
+ {
+ try (Lock lock = _lock.lock())
+ {
+ _requests++;
+
+ // temporarily stop the idle timer
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} in use, stopping timer, active requests={}", getId(), _requests);
+ _sessionInactivityTimer.cancel();
+ }
+ }
+
+ protected boolean access(long time)
+ {
+ try (Lock lock = _lock.lock())
+ {
+ if (!isValid() || !isResident())
+ return false;
+ _newSession = false;
+ long lastAccessed = _sessionData.getAccessed();
+ _sessionData.setAccessed(time);
+ _sessionData.setLastAccessed(lastAccessed);
+ _sessionData.calcAndSetExpiry(time);
+ if (isExpiredAt(time))
+ {
+ invalidate();
+ return false;
+ }
+ return true;
+ }
+ }
+
+ protected void complete()
+ {
+ try (Lock lock = _lock.lock())
+ {
+ _requests--;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} complete, active requests={}", getId(), _requests);
+
+ // start the inactivity timer if necessary
+ if (_requests == 0)
+ {
+ //update the expiry time to take account of the time all requests spent inside of the
+ //session.
+ long now = System.currentTimeMillis();
+ _sessionData.calcAndSetExpiry(now);
+ _sessionInactivityTimer.schedule(calculateInactivityTimeout(now));
+ }
+ }
+ }
+
+ /**
+ * Check to see if session has expired as at the time given.
+ *
+ * @param time the time since the epoch in ms
+ * @return true if expired
+ */
+ protected boolean isExpiredAt(long time)
+ {
+ try (Lock lock = _lock.lock())
+ {
+ return _sessionData.isExpiredAt(time);
+ }
+ }
+
+ /**
+ * Check if the Session has been idle longer than a number of seconds.
+ *
+ * @param sec the number of seconds
+ * @return true if the session has been idle longer than the interval
+ */
+ protected boolean isIdleLongerThan(int sec)
+ {
+ long now = System.currentTimeMillis();
+ try (Lock lock = _lock.lock())
+ {
+ return ((_sessionData.getAccessed() + (sec * 1000)) <= now);
+ }
+ }
+
+ /**
+ * Call binding and attribute listeners based on the new and old values of
+ * the attribute.
+ *
+ * @param name name of the attribute
+ * @param newValue new value of the attribute
+ * @param oldValue previous value of the attribute
+ * @throws IllegalStateException if no session manager can be find
+ */
+ protected void callSessionAttributeListeners(String name, Object newValue, Object oldValue)
+ {
+ if (newValue == null || !newValue.equals(oldValue))
+ {
+ if (oldValue != null)
+ unbindValue(name, oldValue);
+ if (newValue != null)
+ bindValue(name, newValue);
+
+ if (_handler == null)
+ throw new IllegalStateException("No session manager for session " + _sessionData.getId());
+
+ _handler.doSessionAttributeListeners(this, name, oldValue, newValue);
+ }
+ }
+
+ /**
+ * Unbind value if value implements {@link HttpSessionBindingListener}
+ * (calls
+ * {@link HttpSessionBindingListener#valueUnbound(HttpSessionBindingEvent)})
+ *
+ * @param name the name with which the object is bound or unbound
+ * @param value the bound value
+ */
+ public void unbindValue(java.lang.String name, Object value)
+ {
+ if (value != null && value instanceof HttpSessionBindingListener)
+ ((HttpSessionBindingListener)value).valueUnbound(new HttpSessionBindingEvent(this, name));
+ }
+
+ /**
+ * Bind value if value implements {@link HttpSessionBindingListener} (calls
+ * {@link HttpSessionBindingListener#valueBound(HttpSessionBindingEvent)})
+ *
+ * @param name the name with which the object is bound or unbound
+ * @param value the bound value
+ */
+ public void bindValue(java.lang.String name, Object value)
+ {
+ if (value != null && value instanceof HttpSessionBindingListener)
+ ((HttpSessionBindingListener)value).valueBound(new HttpSessionBindingEvent(this, name));
+ }
+
+ /**
+ * Call the activation listeners. This must be called holding the lock.
+ */
+ public void didActivate()
+ {
+ //A passivate listener might remove a non-serializable attribute that
+ //the activate listener might put back in again, which would spuriously
+ //set the dirty bit to true, causing another round of passivate/activate
+ //when the request exits. The store clears the dirty bit if it does a
+ //save, so ensure dirty flag is set to the value determined by the store,
+ //not a passivation listener.
+ boolean dirty = getSessionData().isDirty();
+
+ try
+ {
+ HttpSessionEvent event = new HttpSessionEvent(this);
+ for (Iterator<String> iter = _sessionData.getKeys().iterator(); iter.hasNext();)
+ {
+ Object value = _sessionData.getAttribute(iter.next());
+ if (value instanceof HttpSessionActivationListener)
+ {
+ HttpSessionActivationListener listener = (HttpSessionActivationListener)value;
+ listener.sessionDidActivate(event);
+ }
+ }
+ }
+ finally
+ {
+ getSessionData().setDirty(dirty);
+ }
+ }
+
+ /**
+ * Call the passivation listeners. This must be called holding the lock
+ */
+ public void willPassivate()
+ {
+ HttpSessionEvent event = new HttpSessionEvent(this);
+ for (Iterator<String> iter = _sessionData.getKeys().iterator(); iter.hasNext();)
+ {
+ Object value = _sessionData.getAttribute(iter.next());
+ if (value instanceof HttpSessionActivationListener)
+ {
+ HttpSessionActivationListener listener = (HttpSessionActivationListener)value;
+ listener.sessionWillPassivate(event);
+ }
+ }
+ }
+
+ public boolean isValid()
+ {
+ try (Lock lock = _lock.lock())
+ {
+ return _state == State.VALID;
+ }
+ }
+
+ public boolean isInvalid()
+ {
+ try (Lock lock = _lock.lock())
+ {
+ return _state == State.INVALID || _state == State.INVALIDATING;
+ }
+ }
+
+ public boolean isChanging()
+ {
+ checkLocked();
+ return _state == State.CHANGING;
+ }
+
+ public long getCookieSetTime()
+ {
+ try (Lock lock = _lock.lock())
+ {
+ return _sessionData.getCookieSet();
+ }
+ }
+
+ @Override
+ public long getCreationTime() throws IllegalStateException
+ {
+ try (Lock lock = _lock.lock())
+ {
+ checkValidForRead();
+ return _sessionData.getCreated();
+ }
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSession#getId()
+ */
+ @Override
+ public String getId()
+ {
+ try (Lock lock = _lock.lock())
+ {
+ return _sessionData.getId();
+ }
+ }
+
+ public String getExtendedId()
+ {
+ return _extendedId;
+ }
+
+ public String getContextPath()
+ {
+ return _sessionData.getContextPath();
+ }
+
+ public String getVHost()
+ {
+ return _sessionData.getVhost();
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSession#getLastAccessedTime()
+ */
+ @Override
+ public long getLastAccessedTime()
+ {
+ try (Lock lock = _lock.lock())
+ {
+ checkValidForRead();
+ return _sessionData.getLastAccessed();
+ }
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSession#getServletContext()
+ */
+ @Override
+ public ServletContext getServletContext()
+ {
+ if (_handler == null)
+ throw new IllegalStateException("No session manager for session " + _sessionData.getId());
+ return _handler._context;
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSession#setMaxInactiveInterval(int)
+ */
+ @Override
+ public void setMaxInactiveInterval(int secs)
+ {
+ try (Lock lock = _lock.lock())
+ {
+ _sessionData.setMaxInactiveMs((long)secs * 1000L);
+ _sessionData.calcAndSetExpiry();
+ //dirty metadata writes can be skipped, but changing the
+ //maxinactiveinterval should write the session out because
+ //it may affect the session on other nodes, or on the same
+ //node in the case of the nullsessioncache
+ _sessionData.setDirty(true);
+
+ if (LOG.isDebugEnabled())
+ {
+ if (secs <= 0)
+ LOG.debug("Session {} is now immortal (maxInactiveInterval={})", _sessionData.getId(), secs);
+ else
+ LOG.debug("Session {} maxInactiveInterval={}", _sessionData.getId(), secs);
+ }
+ }
+ }
+
+ @Deprecated
+ public void updateInactivityTimer()
+ {
+ //for backward api compatibility only
+ }
+
+ /**
+ * Calculate what the session timer setting should be based on:
+ * the time remaining before the session expires
+ * and any idle eviction time configured.
+ * The timer value will be the lesser of the above.
+ *
+ * @param now the time at which to calculate remaining expiry
+ * @return the time remaining before expiry or inactivity timeout
+ */
+ public long calculateInactivityTimeout(long now)
+ {
+ long time = 0;
+
+ try (Lock lock = _lock.lock())
+ {
+ long remaining = _sessionData.getExpiry() - now;
+ long maxInactive = _sessionData.getMaxInactiveMs();
+ int evictionPolicy = getSessionHandler().getSessionCache().getEvictionPolicy();
+
+ if (maxInactive <= 0)
+ {
+ // sessions are immortal, they never expire
+ if (evictionPolicy < SessionCache.EVICT_ON_INACTIVITY)
+ {
+ // we do not want to evict inactive sessions
+ time = -1;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} is immortal && no inactivity eviction", getId());
+ }
+ else
+ {
+ // sessions are immortal but we want to evict after
+ // inactivity
+ time = TimeUnit.SECONDS.toMillis(evictionPolicy);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} is immortal; evict after {} sec inactivity", getId(), evictionPolicy);
+ }
+ }
+ else
+ {
+ // sessions are not immortal
+ if (evictionPolicy == SessionCache.NEVER_EVICT)
+ {
+ // timeout is the time remaining until its expiry
+ time = (remaining > 0 ? remaining : 0);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} no eviction", getId());
+ }
+ else if (evictionPolicy == SessionCache.EVICT_ON_SESSION_EXIT)
+ {
+ // session will not remain in the cache, so no timeout
+ time = -1;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} evict on exit", getId());
+ }
+ else
+ {
+ // want to evict on idle: timer is lesser of the session's
+ // expiration remaining and the time to evict
+ time = (remaining > 0 ? (Math.min(maxInactive, TimeUnit.SECONDS.toMillis(evictionPolicy))) : 0);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} timer set to lesser of maxInactive={} and inactivityEvict={}", getId(),
+ maxInactive, evictionPolicy);
+ }
+ }
+ }
+
+ return time;
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSession#getMaxInactiveInterval()
+ */
+ @Override
+ public int getMaxInactiveInterval()
+ {
+ try (Lock lock = _lock.lock())
+ {
+ long maxInactiveMs = _sessionData.getMaxInactiveMs();
+ return (int)(maxInactiveMs < 0 ? -1 : maxInactiveMs / 1000);
+ }
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSession#getSessionContext()
+ */
+ @Override
+ @Deprecated
+ public HttpSessionContext getSessionContext()
+ {
+ checkValidForRead();
+ return SessionHandler.__nullSessionContext;
+ }
+
+ public SessionHandler getSessionHandler()
+ {
+ return _handler;
+ }
+
+ /**
+ * Check that the session can be modified.
+ *
+ * @throws IllegalStateException if the session is invalid
+ */
+ protected void checkValidForWrite() throws IllegalStateException
+ {
+ checkLocked();
+
+ if (_state == State.INVALID)
+ throw new IllegalStateException("Not valid for write: id=" + _sessionData.getId() +
+ " created=" + _sessionData.getCreated() +
+ " accessed=" + _sessionData.getAccessed() +
+ " lastaccessed=" + _sessionData.getLastAccessed() +
+ " maxInactiveMs=" + _sessionData.getMaxInactiveMs() +
+ " expiry=" + _sessionData.getExpiry());
+
+ if (_state == State.INVALIDATING)
+ return; // in the process of being invalidated, listeners may try to
+ // remove attributes
+
+ if (!isResident())
+ throw new IllegalStateException("Not valid for write: id=" + _sessionData.getId() + " not resident");
+ }
+
+ /**
+ * Chech that the session data can be read.
+ *
+ * @throws IllegalStateException if the session is invalid
+ */
+ protected void checkValidForRead() throws IllegalStateException
+ {
+ checkLocked();
+
+ if (_state == State.INVALID)
+ throw new IllegalStateException("Invalid for read: id=" + _sessionData.getId() +
+ " created=" + _sessionData.getCreated() +
+ " accessed=" + _sessionData.getAccessed() +
+ " lastaccessed=" + _sessionData.getLastAccessed() +
+ " maxInactiveMs=" + _sessionData.getMaxInactiveMs() +
+ " expiry=" + _sessionData.getExpiry());
+
+ if (_state == State.INVALIDATING)
+ return;
+
+ if (!isResident())
+ throw new IllegalStateException("Invalid for read: id=" + _sessionData.getId() + " not resident");
+ }
+
+ protected void checkLocked() throws IllegalStateException
+ {
+ if (!_lock.isLocked())
+ throw new IllegalStateException("Session not locked");
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSession#getAttribute(java.lang.String)
+ */
+ @Override
+ public Object getAttribute(String name)
+ {
+ try (Lock lock = _lock.lock())
+ {
+ checkValidForRead();
+ return _sessionData.getAttribute(name);
+ }
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSession#getValue(java.lang.String)
+ */
+ @Override
+ @Deprecated
+ public Object getValue(String name)
+ {
+ try (Lock lock = _lock.lock())
+ {
+ checkValidForRead();
+ return _sessionData.getAttribute(name);
+ }
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSession#getAttributeNames()
+ */
+ @Override
+ public Enumeration<String> getAttributeNames()
+ {
+ try (Lock lock = _lock.lock())
+ {
+ checkValidForRead();
+ final Iterator<String> itor = _sessionData.getKeys().iterator();
+ return new Enumeration<String>()
+ {
+
+ @Override
+ public boolean hasMoreElements()
+ {
+ return itor.hasNext();
+ }
+
+ @Override
+ public String nextElement()
+ {
+ return itor.next();
+ }
+ };
+ }
+ }
+
+ public int getAttributes()
+ {
+ return _sessionData.getKeys().size();
+ }
+
+ public Set<String> getNames()
+ {
+ return Collections.unmodifiableSet(_sessionData.getKeys());
+ }
+
+ /**
+ * @deprecated As of Version 2.2, this method is replaced by
+ * {@link #getAttributeNames}
+ */
+ @Deprecated
+ @Override
+ public String[] getValueNames() throws IllegalStateException
+ {
+ try (Lock lock = _lock.lock())
+ {
+ checkValidForRead();
+ Iterator<String> itor = _sessionData.getKeys().iterator();
+ if (!itor.hasNext())
+ return new String[0];
+ ArrayList<String> names = new ArrayList<>();
+ while (itor.hasNext())
+ {
+ names.add(itor.next());
+ }
+ return names.toArray(new String[names.size()]);
+ }
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSession#setAttribute(java.lang.String,
+ * java.lang.Object)
+ */
+ @Override
+ public void setAttribute(String name, Object value)
+ {
+ Object old = null;
+ try (Lock lock = _lock.lock())
+ {
+ // if session is not valid, don't accept the set
+ checkValidForWrite();
+ old = _sessionData.setAttribute(name, value);
+ }
+ if (value == null && old == null)
+ return; // if same as remove attribute but attribute was already
+ // removed, no change
+ callSessionAttributeListeners(name, value, old);
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSession#putValue(java.lang.String,
+ * java.lang.Object)
+ */
+ @Override
+ @Deprecated
+ public void putValue(String name, Object value)
+ {
+ setAttribute(name, value);
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSession#removeAttribute(java.lang.String)
+ */
+ @Override
+ public void removeAttribute(String name)
+ {
+ setAttribute(name, null);
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSession#removeValue(java.lang.String)
+ */
+ @Override
+ @Deprecated
+ public void removeValue(String name)
+ {
+ setAttribute(name, null);
+ }
+
+ /**
+ * Force a change to the id of a session.
+ *
+ * @param request the Request associated with the call to change id.
+ */
+ public void renewId(HttpServletRequest request)
+ {
+ if (_handler == null)
+ throw new IllegalStateException("No session manager for session " + _sessionData.getId());
+
+ String id = null;
+ String extendedId = null;
+ try (Lock lock = _lock.lock())
+ {
+ while (true)
+ {
+ switch (_state)
+ {
+ case INVALID:
+ case INVALIDATING:
+ throw new IllegalStateException();
+
+ case CHANGING:
+ try
+ {
+ _stateChangeCompleted.await();
+ }
+ catch (InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ continue;
+
+ case VALID:
+ _state = State.CHANGING;
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ break;
+ }
+
+ id = _sessionData.getId(); // grab the values as they are now
+ extendedId = getExtendedId();
+ }
+
+ String newId = _handler._sessionIdManager.renewSessionId(id, extendedId, request);
+
+ try (Lock lock = _lock.lock())
+ {
+ switch (_state)
+ {
+ case CHANGING:
+ if (id.equals(newId))
+ throw new IllegalStateException("Unable to change session id");
+
+ // this shouldn't be necessary to do here EXCEPT that when a
+ // null session cache is
+ // used, a new Session object will be created during the
+ // call to renew, so this
+ // Session object will not have been modified.
+ _sessionData.setId(newId);
+ setExtendedId(_handler._sessionIdManager.getExtendedId(newId, request));
+ setIdChanged(true);
+
+ _state = State.VALID;
+ _stateChangeCompleted.signalAll();
+ break;
+
+ case INVALID:
+ case INVALIDATING:
+ throw new IllegalStateException("Session invalid");
+
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ /**
+ * Called by users to invalidate a session, or called by the access method
+ * as a request enters the session if the session has expired, or called by
+ * manager as a result of scavenger expiring session
+ *
+ * @see javax.servlet.http.HttpSession#invalidate()
+ */
+ @Override
+ public void invalidate()
+ {
+ if (_handler == null)
+ throw new IllegalStateException("No session manager for session " + _sessionData.getId());
+
+ boolean result = beginInvalidate();
+
+ try
+ {
+ // if the session was not already invalid, or in process of being
+ // invalidated, do invalidate
+ if (result)
+ {
+ try
+ {
+ // do the invalidation
+ _handler.callSessionDestroyedListeners(this);
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Error during Session destroy listener", e);
+ }
+ finally
+ {
+ // call the attribute removed listeners and finally mark it
+ // as invalid
+ finishInvalidate();
+ // tell id mgr to remove sessions with same id from all contexts
+ _handler.getSessionIdManager().invalidateAll(_sessionData.getId());
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ /**
+ * Grab the lock on the session
+ *
+ * @return the lock
+ */
+ public Lock lock()
+ {
+ return _lock.lock();
+ }
+
+ /**
+ * @return true if the session is not already invalid or being invalidated.
+ */
+ protected boolean beginInvalidate()
+ {
+ boolean result = false;
+
+ try (Lock lock = _lock.lock())
+ {
+
+ while (true)
+ {
+ switch (_state)
+ {
+ case INVALID:
+ {
+ throw new IllegalStateException(); // spec does not
+ // allow invalidate
+ // of already invalid
+ // session
+ }
+ case INVALIDATING:
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} already being invalidated", _sessionData.getId());
+ break;
+ }
+ case CHANGING:
+ {
+ try
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} waiting for id change to complete", _sessionData.getId());
+ _stateChangeCompleted.await();
+ }
+ catch (InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ continue;
+ }
+ case VALID:
+ {
+ // only first change from valid to invalidating should
+ // be actionable
+ result = true;
+ _state = State.INVALIDATING;
+ break;
+ }
+ default:
+ throw new IllegalStateException();
+ }
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Call HttpSessionAttributeListeners as part of invalidating a Session.
+ *
+ * @throws IllegalStateException if no session manager can be find
+ */
+ @Deprecated
+ protected void doInvalidate() throws IllegalStateException
+ {
+ finishInvalidate();
+ }
+
+ /**
+ * Call HttpSessionAttributeListeners as part of invalidating a Session.
+ *
+ * @throws IllegalStateException if no session manager can be find
+ */
+ protected void finishInvalidate() throws IllegalStateException
+ {
+ try (Lock lock = _lock.lock())
+ {
+ try
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("invalidate {}", _sessionData.getId());
+ if (_state == State.VALID || _state == State.INVALIDATING)
+ {
+ Set<String> keys = null;
+ do
+ {
+ keys = _sessionData.getKeys();
+ for (String key : keys)
+ {
+ Object old = _sessionData.setAttribute(key, null);
+ // if same as remove attribute but attribute was
+ // already removed, no change
+ if (old == null)
+ continue;
+ callSessionAttributeListeners(key, null, old);
+ }
+ }
+ while (!keys.isEmpty());
+ }
+ }
+ finally
+ {
+ // mark as invalid
+ _state = State.INVALID;
+ _handler.recordSessionTime(this);
+ _stateChangeCompleted.signalAll();
+ }
+ }
+ }
+
+ @Override
+ public boolean isNew() throws IllegalStateException
+ {
+ try (Lock lock = _lock.lock())
+ {
+ checkValidForRead();
+ return _newSession;
+ }
+ }
+
+ public void setIdChanged(boolean changed)
+ {
+ try (Lock lock = _lock.lock())
+ {
+ _idChanged = changed;
+ }
+ }
+
+ public boolean isIdChanged()
+ {
+ try (Lock lock = _lock.lock())
+ {
+ return _idChanged;
+ }
+ }
+
+ @Override
+ public Session getSession()
+ {
+ // TODO why is this used
+ return this;
+ }
+
+ protected SessionData getSessionData()
+ {
+ return _sessionData;
+ }
+
+ /**
+ *
+ */
+ public void setResident(boolean resident)
+ {
+ _resident = resident;
+
+ if (!_resident)
+ _sessionInactivityTimer.destroy();
+ }
+
+ public boolean isResident()
+ {
+ return _resident;
+ }
+
+ @Override
+ public String toString()
+ {
+ try (Lock lock = _lock.lock())
+ {
+ return String.format("%s@%x{id=%s,x=%s,req=%d,res=%b}",
+ getClass().getSimpleName(),
+ hashCode(),
+ _sessionData.getId(),
+ _extendedId,
+ _requests,
+ _resident);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionCache.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionCache.java
new file mode 100644
index 0000000..4547d3f
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionCache.java
@@ -0,0 +1,324 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.util.Set;
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.util.component.LifeCycle;
+
+/**
+ * SessionCache
+ *
+ * A working set of {@link Session} objects for a context.
+ *
+ * Ideally, multiple requests for the same session id in the same context will always
+ * share the same Session object from the SessionCache, but it would be possible
+ * for implementations of SessionCache to create a fresh object for each request.
+ *
+ * The SessionData pertaining to the Session objects is obtained from/written to a SessionDataStore.
+ * The SessionDataStore is the authoritative source of session data:
+ * <ul>
+ * <li>if the session data is not present in the SessionDataStore the session does not exist.</li>
+ * <li>if the session data is present in the SessionDataStore but its expiry time has passed then
+ * the session is deemed to have expired and is therefore invalid</li>
+ * </ul>
+ *
+ * A SessionCache can passivate a valid Session to the SessionDataStore and
+ * evict it from the cache according to various strategies:
+ * <ul>
+ * <li>whenever the last request exits a Session</li>
+ * <li>whenever the Session has not been accessed for a configurable number of seconds</li>
+ * </ul>.
+ *
+ * Eviction can save memory, and can also help mitigate
+ * some of the problems of a non-sticky load balancer by forcing the session data to
+ * be re-read from the SessionDataStore more frequently.
+ */
+public interface SessionCache extends LifeCycle
+{
+ int NEVER_EVICT = -1;
+ int EVICT_ON_SESSION_EXIT = 0;
+ int EVICT_ON_INACTIVITY = 1; //any number equal or greater is time in seconds
+
+ /**
+ * @param context the {@link SessionContext} to use for this cache
+ */
+ void initialize(SessionContext context);
+
+ void shutdown();
+
+ SessionHandler getSessionHandler();
+
+ /**
+ * Create an entirely new Session.
+ *
+ * @param request the request
+ * @param id the unique id associated to the session
+ * @param time the timestamp of the session creation
+ * @param maxInactiveMs the max inactive time in milliseconds
+ * @return a new Session
+ */
+ Session newSession(HttpServletRequest request, String id, long time, long maxInactiveMs);
+
+ /**
+ * Re-materialize a Session that has previously existed.
+ *
+ * @param data the data associated with the session
+ * @return a Session object for the data supplied
+ */
+ Session newSession(SessionData data);
+
+ /**
+ * Change the id of a session.
+ *
+ * This method has been superceded by the 4 arg renewSessionId method and
+ * should no longer be called.
+ *
+ * @param oldId the old id
+ * @param newId the new id
+ * @return the changed Session
+ * @throws Exception if anything went wrong
+ * @deprecated use
+ * {@link #renewSessionId(String oldId, String newId, String oldExtendedId, String newExtendedId)}
+ */
+ @Deprecated
+ default Session renewSessionId(String oldId, String newId) throws Exception
+ {
+ return null;
+ }
+
+ /**
+ * Change the id of a Session.
+ *
+ * @param oldId the current session id
+ * @param newId the new session id
+ * @param oldExtendedId the current extended session id
+ * @param newExtendedId the new extended session id
+ * @return the Session after changing its id
+ * @throws Exception if any error occurred
+ */
+ default Session renewSessionId(String oldId, String newId, String oldExtendedId, String newExtendedId) throws Exception
+ {
+ return renewSessionId(oldId, newId);
+ }
+
+ /**
+ * Adds a new Session, with a never-before-used id,
+ * to the cache.
+ *
+ * @param id
+ * @param session
+ * @throws Exception
+ */
+ void add(String id, Session session) throws Exception;
+
+ /**
+ * Get an existing Session. If necessary, the cache will load the data for
+ * the session from the configured SessionDataStore.
+ *
+ * @param id the session id
+ * @return the Session if one exists, null otherwise
+ * @throws Exception if any error occurred
+ */
+ Session get(String id) throws Exception;
+
+ /**
+ * Finish using a Session. This is called by the SessionHandler
+ * once a request is finished with a Session. SessionCache
+ * implementations may want to delay writing out Session contents
+ * until the last request exits a Session.
+ *
+ * @param id the session id
+ * @param session the current session object
+ * @throws Exception if any error occurred
+ * @deprecated @see release
+ */
+ void put(String id, Session session) throws Exception;
+
+
+ /**
+ * Finish using a Session. This is called by the SessionHandler
+ * once a request is finished with a Session. SessionCache
+ * implementations may want to delay writing out Session contents
+ * until the last request exits a Session.
+ *
+ * @param id the session id
+ * @param session the current session object
+ * @throws Exception if any error occurred
+ */
+ void release(String id, Session session) throws Exception;
+
+ /**
+ * Called when a response is about to be committed. The
+ * cache can write the session to ensure that the
+ * SessionDataStore contains changes to the session
+ * that occurred during the lifetime of the request. This
+ * can help ensure that if a subsequent request goes to a
+ * different server, it will be able to see the session
+ * changes via the shared store.
+ */
+ void commit(Session session) throws Exception;
+
+ /**
+ * Check to see if a Session is in the cache. Does NOT consult
+ * the SessionDataStore.
+ *
+ * @param id the session id
+ * @return true if a Session object matching the id is present
+ * in the cache, false otherwise
+ * @throws Exception if any error occurred
+ */
+ boolean contains(String id) throws Exception;
+
+ /**
+ * Check to see if a session exists: WILL consult the
+ * SessionDataStore.
+ *
+ * @param id the session id
+ * @return true if the session exists, false otherwise
+ * @throws Exception if any error occurred
+ */
+ boolean exists(String id) throws Exception;
+
+ /**
+ * Remove a Session completely: from both this
+ * cache and the SessionDataStore.
+ *
+ * @param id the session id
+ * @return the Session that was removed, null otherwise
+ * @throws Exception if any error occurred
+ */
+ Session delete(String id) throws Exception;
+
+ /**
+ * Check a list of session ids that belong to potentially expired
+ * sessions. The Session in the cache should be checked,
+ * but also the SessionDataStore, as that is the authoritative
+ * source of all session information.
+ *
+ * @param candidates the session ids to check
+ * @return the set of session ids that have actually expired: this can
+ * be a superset of the original candidate list.
+ */
+ Set<String> checkExpiration(Set<String> candidates);
+
+ /**
+ * Check a Session to see if it might be appropriate to
+ * evict or expire.
+ *
+ * @param session the session to check
+ */
+ void checkInactiveSession(Session session);
+
+ /**
+ * A SessionDataStore that is the authoritative source
+ * of session information.
+ *
+ * @param sds the {@link SessionDataStore} to use
+ */
+ void setSessionDataStore(SessionDataStore sds);
+
+ /**
+ * @return the {@link SessionDataStore} used
+ */
+ SessionDataStore getSessionDataStore();
+
+ /**
+ * Sessions in this cache can be:
+ * <ul>
+ * <li>never evicted</li>
+ * <li>evicted once the last request exits</li>
+ * <li>evicted after a configurable period of inactivity</li>
+ * </ul>
+ *
+ * @param policy -1 is never evict; 0 is evict-on-exit; and any other positive
+ * value is the time in seconds that a session can be idle before it can
+ * be evicted.
+ */
+ void setEvictionPolicy(int policy);
+
+ /**
+ * @return the eviction policy
+ */
+ int getEvictionPolicy();
+
+ /**
+ * Whether or not a a session that is about to be evicted should
+ * be saved before being evicted.
+ *
+ * @param saveOnEvict <code>true</code> if the session should be saved before being evicted
+ */
+ void setSaveOnInactiveEviction(boolean saveOnEvict);
+
+ /**
+ * @return <code>true</code> if the session should be saved before being evicted
+ */
+ boolean isSaveOnInactiveEviction();
+
+ /**
+ * Whether or not a session that is newly created should be
+ * immediately saved. If false, a session that is created and
+ * invalidated within a single request is never persisted.
+ *
+ * @param saveOnCreate <code>true</code> to immediately save the newly created session
+ */
+ void setSaveOnCreate(boolean saveOnCreate);
+
+ /**
+ * @return if <code>true</code> the newly created session will be saved immediately
+ */
+ boolean isSaveOnCreate();
+
+ /**
+ * If the data for a session exists but is unreadable,
+ * the SessionCache can instruct the SessionDataStore to delete it.
+ *
+ * @param removeUnloadableSessions <code>true</code> to delete session which cannot be loaded
+ */
+ void setRemoveUnloadableSessions(boolean removeUnloadableSessions);
+
+ /**
+ * @return if <code>true</code> unloadable session will be deleted
+ */
+ boolean isRemoveUnloadableSessions();
+
+ /**
+ * If true, a dirty session will be written to the SessionDataStore
+ * just before a response is returned to the client. This ensures
+ * that subsequent requests to either the same node or a different
+ * node see the changed session data.
+ */
+ void setFlushOnResponseCommit(boolean flushOnResponse);
+
+ /**
+ * @return <code>true</code> if dirty sessions should be written
+ * before the response is committed.
+ */
+ boolean isFlushOnResponseCommit();
+
+ /**
+ * If true, all existing sessions in the cache will be invalidated when
+ * the server shuts down. Default is false.
+ * @param invalidateOnShutdown
+ */
+ void setInvalidateOnShutdown(boolean invalidateOnShutdown);
+
+ boolean isInvalidateOnShutdown();
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionCacheFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionCacheFactory.java
new file mode 100644
index 0000000..63df9b5
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionCacheFactory.java
@@ -0,0 +1,27 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+/**
+ * SessionCacheFactory
+ */
+public interface SessionCacheFactory
+{
+ SessionCache getSessionCache(SessionHandler handler);
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionContext.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionContext.java
new file mode 100644
index 0000000..3f83cb1
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionContext.java
@@ -0,0 +1,138 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandler.Context;
+import org.eclipse.jetty.util.StringUtil;
+
+/**
+ * SessionContext
+ *
+ * Information about the context to which sessions belong: the Context,
+ * the SessionHandler of the context, and the unique name of the node.
+ *
+ * A SessionHandler is 1:1 with a SessionContext.
+ */
+public class SessionContext
+{
+ public static final String NULL_VHOST = "0.0.0.0";
+ private ContextHandler.Context _context;
+ private SessionHandler _sessionHandler;
+ private String _workerName;
+ private String _canonicalContextPath;
+ private String _vhost;
+
+ public SessionContext(String workerName, ContextHandler.Context context)
+ {
+ if (context != null)
+ _sessionHandler = context.getContextHandler().getChildHandlerByClass(SessionHandler.class);
+ _workerName = workerName;
+ _context = context;
+ _canonicalContextPath = canonicalizeContextPath(_context);
+ _vhost = canonicalizeVHost(_context);
+ }
+
+ public String getWorkerName()
+ {
+ return _workerName;
+ }
+
+ public SessionHandler getSessionHandler()
+ {
+ return _sessionHandler;
+ }
+
+ public Context getContext()
+ {
+ return _context;
+ }
+
+ public String getCanonicalContextPath()
+ {
+ return _canonicalContextPath;
+ }
+
+ public String getVhost()
+ {
+ return _vhost;
+ }
+
+ @Override
+ public String toString()
+ {
+ return _workerName + "_" + _canonicalContextPath + "_" + _vhost;
+ }
+
+ /**
+ * Run a runnable in the context (with context classloader set) if
+ * there is one, otherwise just run it.
+ *
+ * @param r the runnable
+ */
+ public void run(Runnable r)
+ {
+ if (_context != null)
+ _context.getContextHandler().handle(r);
+ else
+ r.run();
+ }
+
+ private String canonicalizeContextPath(Context context)
+ {
+ if (context == null)
+ return "";
+ return canonicalize(context.getContextPath());
+ }
+
+ /**
+ * Get the first virtual host for the context.
+ *
+ * Used to help identify the exact session/contextPath.
+ *
+ * @return 0.0.0.0 if no virtual host is defined
+ */
+ private String canonicalizeVHost(Context context)
+ {
+ String vhost = NULL_VHOST;
+
+ if (context == null)
+ return vhost;
+
+ String[] vhosts = context.getContextHandler().getVirtualHosts();
+ if (vhosts == null || vhosts.length == 0 || vhosts[0] == null)
+ return vhost;
+
+ return vhosts[0];
+ }
+
+ /**
+ * Make an acceptable name from a context path.
+ *
+ * @param path the path to normalize/fix
+ * @return the clean/acceptable form of the path
+ */
+ private String canonicalize(String path)
+ {
+ if (path == null)
+ return "";
+
+ return StringUtil.sanitizeFileSystemName(path);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionData.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionData.java
new file mode 100644
index 0000000..978d3d8
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionData.java
@@ -0,0 +1,518 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jetty.util.ClassLoadingObjectInputStream;
+import org.eclipse.jetty.util.ClassVisibilityChecker;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * SessionData
+ *
+ * The data associated with a session. A Session object has a 1:1 relationship
+ * with a SessionData object. The behaviour of sessions is implemented in the
+ * Session object (eg calling listeners, keeping timers etc). A Session's
+ * associated SessionData is the object which can be persisted, serialized etc.
+ */
+public class SessionData implements Serializable
+{
+ private static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
+
+ private static final long serialVersionUID = 1L;
+
+ protected String _id;
+ protected String _contextPath;
+ protected String _vhost;
+ protected String _lastNode;
+ protected long _expiry; //precalculated time of expiry in ms since epoch
+ protected long _created;
+ protected long _cookieSet;
+ protected long _accessed; // the time of the last access
+ protected long _lastAccessed; // the time of the last access excluding this one
+ protected long _maxInactiveMs;
+ protected Map<String, Object> _attributes;
+ protected boolean _dirty;
+ protected long _lastSaved; //time in msec since last save
+ protected boolean _metaDataDirty; //non-attribute data has changed
+
+ /**
+ * Serialize the attribute map of the session.
+ *
+ * This special handling allows us to record which classloader should be used to load the value of the
+ * attribute: either the container classloader (which could be the application loader ie null, or jetty's
+ * startjar loader) or the webapp's classloader.
+ *
+ * @param data the SessionData for which to serialize the attributes
+ * @param out the stream to which to serialize
+ */
+ public static void serializeAttributes(SessionData data, java.io.ObjectOutputStream out)
+ throws IOException
+ {
+ int entries = data._attributes.size();
+ out.writeObject(entries);
+ for (Entry<String, Object> entry : data._attributes.entrySet())
+ {
+ out.writeUTF(entry.getKey());
+
+ Class<?> clazz = entry.getValue().getClass();
+ ClassLoader loader = clazz.getClassLoader();
+ ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
+ boolean isContextLoader;
+
+ if (loader == contextLoader) //is it the context classloader?
+ isContextLoader = true;
+ else if (contextLoader == null) //not context classloader
+ isContextLoader = false;
+ else if (contextLoader instanceof ClassVisibilityChecker)
+ {
+ //Clazz not loaded by context classloader, but ask if loadable by context classloader,
+ //because preferable to use context classloader if possible (eg for deep structures).
+ ClassVisibilityChecker checker = (ClassVisibilityChecker)(contextLoader);
+ isContextLoader = (checker.isSystemClass(clazz) && !(checker.isServerClass(clazz)));
+ }
+ else
+ {
+ //Class wasn't loaded by context classloader, but try loading from context loader,
+ //because preferable to use context classloader if possible (eg for deep structures).
+ try
+ {
+ Class<?> result = contextLoader.loadClass(clazz.getName());
+ isContextLoader = (result == clazz); //only if TTCL loaded this instance of the class
+ }
+ catch (Throwable e)
+ {
+ isContextLoader = false; //TCCL can't see the class
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Attribute {} class={} isServerLoader={}", entry.getKey(), clazz.getName(), (!isContextLoader));
+ out.writeBoolean(!isContextLoader);
+ out.writeObject(entry.getValue());
+ }
+ }
+
+ /**
+ * De-serialize the attribute map of a session.
+ *
+ * When the session was serialized, we will have recorded which classloader should be used to
+ * recover the attribute value. The classloader could be the container classloader, or the
+ * webapp classloader.
+ *
+ * @param data the SessionData for which to deserialize the attribute map
+ * @param in the serialized stream
+ */
+ public static void deserializeAttributes(SessionData data, java.io.ObjectInputStream in)
+ throws IOException, ClassNotFoundException
+ {
+ Object o = in.readObject();
+ if (o instanceof Integer)
+ {
+ //new serialization was used
+ if (!(ClassLoadingObjectInputStream.class.isAssignableFrom(in.getClass())))
+ throw new IOException("Not ClassLoadingObjectInputStream");
+
+ data._attributes = new ConcurrentHashMap<>();
+ int entries = ((Integer)o).intValue();
+ ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
+ ClassLoader serverLoader = SessionData.class.getClassLoader();
+ for (int i = 0; i < entries; i++)
+ {
+ String name = in.readUTF(); //attribute name
+ boolean isServerClassLoader = in.readBoolean(); //use server or webapp classloader to load
+ if (LOG.isDebugEnabled())
+ LOG.debug("Deserialize {} isServerLoader={} serverLoader={} tccl={}", name, isServerClassLoader, serverLoader, contextLoader);
+ Object value = ((ClassLoadingObjectInputStream)in).readObject(isServerClassLoader ? serverLoader : contextLoader);
+ data._attributes.put(name, value);
+ }
+ }
+ else
+ {
+ LOG.info("Legacy serialization detected for {}", data.getId());
+ //legacy serialization was used, we have just deserialized the
+ //entire attribute map
+ data._attributes = new ConcurrentHashMap<>();
+ data.putAllAttributes((Map<String, Object>)o);
+ }
+ }
+
+ public SessionData(String id, String cpath, String vhost, long created, long accessed, long lastAccessed, long maxInactiveMs)
+ {
+ _id = id;
+ setContextPath(cpath);
+ setVhost(vhost);
+ _created = created;
+ _accessed = accessed;
+ _lastAccessed = lastAccessed;
+ _maxInactiveMs = maxInactiveMs;
+ calcAndSetExpiry();
+ _attributes = new ConcurrentHashMap<>();
+ }
+
+ public SessionData(String id, String cpath, String vhost, long created, long accessed, long lastAccessed, long maxInactiveMs, Map<String, Object> attributes)
+ {
+ this(id, cpath, vhost, created, accessed, lastAccessed, maxInactiveMs);
+ putAllAttributes(attributes);
+ }
+
+ /**
+ * Copy the info from the given sessiondata
+ *
+ * @param data the sessiondata to be copied
+ */
+ public void copy(SessionData data)
+ {
+ if (data == null)
+ return; //don't copy if no data
+
+ if (data.getId() == null || !(getId().equals(data.getId())))
+ throw new IllegalStateException("Can only copy data for same session id");
+
+ if (data == this)
+ return; //don't copy ourself
+
+ setLastNode(data.getLastNode());
+ setContextPath(data.getContextPath());
+ setVhost(data.getVhost());
+ setCookieSet(data.getCookieSet());
+ setCreated(data.getCreated());
+ setAccessed(data.getAccessed());
+ setLastAccessed(data.getLastAccessed());
+ setMaxInactiveMs(data.getMaxInactiveMs());
+ setExpiry(data.getExpiry());
+ setLastSaved(data.getLastSaved());
+ clearAllAttributes();
+ putAllAttributes(data.getAllAttributes());
+ }
+
+ /**
+ * @return time at which session was last written out
+ */
+ public long getLastSaved()
+ {
+ return _lastSaved;
+ }
+
+ public void setLastSaved(long lastSaved)
+ {
+ _lastSaved = lastSaved;
+ }
+
+ /**
+ * @return true if a session needs to be written out
+ */
+ public boolean isDirty()
+ {
+ return _dirty;
+ }
+
+ public void setDirty(boolean dirty)
+ {
+ _dirty = dirty;
+ }
+
+ /**
+ * @return the metaDataDirty
+ */
+ public boolean isMetaDataDirty()
+ {
+ return _metaDataDirty;
+ }
+
+ /**
+ * @param metaDataDirty true means non-attribute data has changed
+ */
+ public void setMetaDataDirty(boolean metaDataDirty)
+ {
+ _metaDataDirty = metaDataDirty;
+ }
+
+ /**
+ * @param name the name of the attribute
+ * @return the value of the attribute named
+ */
+ public Object getAttribute(String name)
+ {
+ return _attributes.get(name);
+ }
+
+ /**
+ * @return a Set of attribute names
+ */
+ public Set<String> getKeys()
+ {
+ return _attributes.keySet();
+ }
+
+ public Object setAttribute(String name, Object value)
+ {
+ Object old = (value == null ? _attributes.remove(name) : _attributes.put(name, value));
+ if (value == null && old == null)
+ return old; //if same as remove attribute but attribute was already removed, no change
+
+ setDirty(name);
+ return old;
+ }
+
+ public void setDirty(String name)
+ {
+ setDirty(true);
+ }
+
+ /**
+ * Clear all dirty flags.
+ */
+ public void clean()
+ {
+ setDirty(false);
+ setMetaDataDirty(false);
+ }
+
+ public void putAllAttributes(Map<String, Object> attributes)
+ {
+ _attributes.putAll(attributes);
+ }
+
+ /**
+ * Remove all attributes
+ */
+ public void clearAllAttributes()
+ {
+ _attributes.clear();
+ }
+
+ /**
+ * @return an unmodifiable map of the attributes
+ */
+ public Map<String, Object> getAllAttributes()
+ {
+ return Collections.unmodifiableMap(_attributes);
+ }
+
+ /**
+ * @return the id of the session
+ */
+ public String getId()
+ {
+ return _id;
+ }
+
+ public void setId(String id)
+ {
+ _id = id;
+ }
+
+ /**
+ * @return the context path associated with this session
+ */
+ public String getContextPath()
+ {
+ return _contextPath;
+ }
+
+ public void setContextPath(String contextPath)
+ {
+ _contextPath = contextPath;
+ }
+
+ /**
+ * @return virtual host of context associated with session
+ */
+ public String getVhost()
+ {
+ return _vhost;
+ }
+
+ public void setVhost(String vhost)
+ {
+ _vhost = vhost;
+ }
+
+ /**
+ * @return last node to manage the session
+ */
+ public String getLastNode()
+ {
+ return _lastNode;
+ }
+
+ public void setLastNode(String lastNode)
+ {
+ _lastNode = lastNode;
+ }
+
+ /**
+ * @return time at which session expires
+ */
+ public long getExpiry()
+ {
+ return _expiry;
+ }
+
+ public void setExpiry(long expiry)
+ {
+ _expiry = expiry;
+ }
+
+ public long calcExpiry()
+ {
+ return calcExpiry(System.currentTimeMillis());
+ }
+
+ public long calcExpiry(long time)
+ {
+ return (getMaxInactiveMs() <= 0 ? 0 : (time + getMaxInactiveMs()));
+ }
+
+ public void calcAndSetExpiry(long time)
+ {
+ setExpiry(calcExpiry(time));
+ setMetaDataDirty(true);
+ }
+
+ public void calcAndSetExpiry()
+ {
+ setExpiry(calcExpiry());
+ setMetaDataDirty(true);
+ }
+
+ public long getCreated()
+ {
+ return _created;
+ }
+
+ public void setCreated(long created)
+ {
+ _created = created;
+ }
+
+ /**
+ * @return time cookie was set
+ */
+ public long getCookieSet()
+ {
+ return _cookieSet;
+ }
+
+ public void setCookieSet(long cookieSet)
+ {
+ _cookieSet = cookieSet;
+ }
+
+ /**
+ * @return time session was accessed
+ */
+ public long getAccessed()
+ {
+ return _accessed;
+ }
+
+ public void setAccessed(long accessed)
+ {
+ _accessed = accessed;
+ }
+
+ /**
+ * @return previous time session was accessed
+ */
+ public long getLastAccessed()
+ {
+ return _lastAccessed;
+ }
+
+ public void setLastAccessed(long lastAccessed)
+ {
+ _lastAccessed = lastAccessed;
+ }
+
+ public long getMaxInactiveMs()
+ {
+ return _maxInactiveMs;
+ }
+
+ public void setMaxInactiveMs(long maxInactive)
+ {
+ _maxInactiveMs = maxInactive;
+ }
+
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException
+ {
+ out.writeUTF(_id); //session id
+ out.writeUTF(_contextPath); //context path
+ out.writeUTF(_vhost); //first vhost
+ out.writeLong(_accessed); //accessTime
+ out.writeLong(_lastAccessed); //lastAccessTime
+ out.writeLong(_created); //time created
+ out.writeLong(_cookieSet); //time cookie was set
+ out.writeUTF(_lastNode); //name of last node managing
+ out.writeLong(_expiry);
+ out.writeLong(_maxInactiveMs);
+ serializeAttributes(this, out);
+ }
+
+ private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
+ {
+ _id = in.readUTF();
+ _contextPath = in.readUTF();
+ _vhost = in.readUTF();
+ _accessed = in.readLong(); //accessTime
+ _lastAccessed = in.readLong(); //lastAccessTime
+ _created = in.readLong(); //time created
+ _cookieSet = in.readLong(); //time cookie was set
+ _lastNode = in.readUTF(); //last managing node
+ _expiry = in.readLong();
+ _maxInactiveMs = in.readLong();
+ deserializeAttributes(this, in);
+ }
+
+ public boolean isExpiredAt(long time)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Testing expiry on session {}: expires at {} now {} maxIdle {}", _id, getExpiry(), time, getMaxInactiveMs());
+ if (getMaxInactiveMs() <= 0)
+ return false; //never expires
+ return (getExpiry() <= time);
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append("id=" + _id);
+ builder.append(", contextpath=" + _contextPath);
+ builder.append(", vhost=" + _vhost);
+ builder.append(", accessed=" + _accessed);
+ builder.append(", lastaccessed=" + _lastAccessed);
+ builder.append(", created=" + _created);
+ builder.append(", cookieset=" + _cookieSet);
+ builder.append(", lastnode=" + _lastNode);
+ builder.append(", expiry=" + _expiry);
+ builder.append(", maxinactive=" + _maxInactiveMs);
+ return builder.toString();
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionDataMap.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionDataMap.java
new file mode 100644
index 0000000..d82e2f9
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionDataMap.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import org.eclipse.jetty.util.component.LifeCycle;
+
+/**
+ * SessionDataMap
+ *
+ * A map style access to SessionData keyed by the session id.
+ */
+public interface SessionDataMap extends LifeCycle
+{
+ /**
+ * Initialize this data map for the
+ * given context. A SessionDataMap can only
+ * be used by one context(/session manager).
+ *
+ * @param context context associated
+ * @throws Exception if unable to initialize the
+ */
+ void initialize(SessionContext context) throws Exception;
+
+ /**
+ * Read in session data.
+ *
+ * @param id identity of session to load
+ * @return the SessionData matching the id
+ * @throws Exception if unable to load session data
+ */
+ SessionData load(String id) throws Exception;
+
+ /**
+ * Store the session data.
+ *
+ * @param id identity of session to store
+ * @param data info of session to store
+ * @throws Exception if unable to write session data
+ */
+ void store(String id, SessionData data) throws Exception;
+
+ /**
+ * Delete session data
+ *
+ * @param id identity of session to delete
+ * @return true if the session was deleted
+ * @throws Exception if unable to delete session data
+ */
+ boolean delete(String id) throws Exception;
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionDataMapFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionDataMapFactory.java
new file mode 100644
index 0000000..ec82b82
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionDataMapFactory.java
@@ -0,0 +1,27 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+/**
+ * SessionDataMapFactory
+ */
+public interface SessionDataMapFactory
+{
+ SessionDataMap getSessionDataMap();
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionDataStore.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionDataStore.java
new file mode 100644
index 0000000..4c7019d
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionDataStore.java
@@ -0,0 +1,71 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.util.Set;
+
+/**
+ * SessionDataStore
+ *
+ * A store for the data contained in a Session object. The store
+ * would usually be persistent.
+ */
+public interface SessionDataStore extends SessionDataMap
+{
+
+ /**
+ * Create a new SessionData
+ *
+ * @param id the id
+ * @param created the timestamp when created
+ * @param accessed the timestamp when accessed
+ * @param lastAccessed the timestamp when last accessed
+ * @param maxInactiveMs the max inactive time in milliseconds
+ * @return a new SessionData object
+ */
+ SessionData newSessionData(String id, long created, long accessed, long lastAccessed, long maxInactiveMs);
+
+ /**
+ * Called periodically, this method should search the data store
+ * for sessions that have been expired for a 'reasonable' amount
+ * of time.
+ *
+ * @param candidates if provided, these are keys of sessions that
+ * the SessionDataStore thinks has expired and should be verified by the
+ * SessionDataStore
+ * @return set of session ids
+ */
+ Set<String> getExpired(Set<String> candidates);
+
+ /**
+ * True if this type of datastore will passivate session objects
+ *
+ * @return true if this store can passivate sessions, false otherwise
+ */
+ boolean isPassivating();
+
+ /**
+ * Test if data exists for a given session id.
+ *
+ * @param id Identity of session whose existence should be checked
+ * @return true if valid, non-expired session exists
+ * @throws Exception if problem checking existence with persistence layer
+ */
+ boolean exists(String id) throws Exception;
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionDataStoreFactory.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionDataStoreFactory.java
new file mode 100644
index 0000000..88aed8f
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionDataStoreFactory.java
@@ -0,0 +1,27 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+/**
+ * SessionDataStoreFactory
+ */
+public interface SessionDataStoreFactory
+{
+ SessionDataStore getSessionDataStore(SessionHandler handler) throws Exception;
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java
new file mode 100644
index 0000000..63591ef
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java
@@ -0,0 +1,1774 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Enumeration;
+import java.util.EventListener;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletException;
+import javax.servlet.SessionCookieConfig;
+import javax.servlet.SessionTrackingMode;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionAttributeListener;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionContext;
+import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionIdListener;
+import javax.servlet.http.HttpSessionListener;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HttpCookie;
+import org.eclipse.jetty.http.Syntax;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.SessionIdManager;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ScopedHandler;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.statistic.CounterStatistic;
+import org.eclipse.jetty.util.statistic.SampleStatistic;
+import org.eclipse.jetty.util.thread.Locker.Lock;
+import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+import static java.lang.Math.round;
+
+/**
+ * SessionHandler.
+ */
+@ManagedObject
+public class SessionHandler extends ScopedHandler
+{
+ static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
+
+ public static final EnumSet<SessionTrackingMode> DEFAULT_TRACKING = EnumSet.of(SessionTrackingMode.COOKIE,
+ SessionTrackingMode.URL);
+
+ /**
+ * Session cookie name.
+ * Defaults to <code>JSESSIONID</code>, but can be set with the
+ * <code>org.eclipse.jetty.servlet.SessionCookie</code> context init parameter.
+ */
+ public static final String __SessionCookieProperty = "org.eclipse.jetty.servlet.SessionCookie";
+ public static final String __DefaultSessionCookie = "JSESSIONID";
+
+ /**
+ * Session id path parameter name.
+ * Defaults to <code>jsessionid</code>, but can be set with the
+ * <code>org.eclipse.jetty.servlet.SessionIdPathParameterName</code> context init parameter.
+ * If context init param is "none", or setSessionIdPathParameterName is called with null or "none",
+ * no URL rewriting will be done.
+ */
+ public static final String __SessionIdPathParameterNameProperty = "org.eclipse.jetty.servlet.SessionIdPathParameterName";
+ public static final String __DefaultSessionIdPathParameterName = "jsessionid";
+ public static final String __CheckRemoteSessionEncoding = "org.eclipse.jetty.servlet.CheckingRemoteSessionIdEncoding";
+
+ /**
+ * Session Domain.
+ * If this property is set as a ServletContext InitParam, then it is
+ * used as the domain for session cookies. If it is not set, then
+ * no domain is specified for the session cookie.
+ */
+ public static final String __SessionDomainProperty = "org.eclipse.jetty.servlet.SessionDomain";
+ public static final String __DefaultSessionDomain = null;
+
+ /**
+ * Session Path.
+ * If this property is set as a ServletContext InitParam, then it is
+ * used as the path for the session cookie. If it is not set, then
+ * the context path is used as the path for the cookie.
+ */
+ public static final String __SessionPathProperty = "org.eclipse.jetty.servlet.SessionPath";
+
+ /**
+ * Session Max Age.
+ * If this property is set as a ServletContext InitParam, then it is
+ * used as the max age for the session cookie. If it is not set, then
+ * a max age of -1 is used.
+ */
+ public static final String __MaxAgeProperty = "org.eclipse.jetty.servlet.MaxAge";
+
+ public static final Set<SessionTrackingMode> DEFAULT_SESSION_TRACKING_MODES =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(SessionTrackingMode.COOKIE, SessionTrackingMode.URL)));
+
+ @SuppressWarnings("unchecked")
+ public static final Class<? extends EventListener>[] SESSION_LISTENER_TYPES =
+ new Class[]
+ {
+ HttpSessionAttributeListener.class,
+ HttpSessionIdListener.class,
+ HttpSessionListener.class
+ };
+
+ /**
+ * Web.xml session-timeout is set in minutes, but is stored as an int in seconds by HttpSession and
+ * the sessionmanager. Thus MAX_INT is the max number of seconds that can be set, and MAX_INT/60 is the
+ * max number of minutes that you can set.
+ */
+ public static final java.math.BigDecimal MAX_INACTIVE_MINUTES = new java.math.BigDecimal(Integer.MAX_VALUE / 60);
+
+ static final HttpSessionContext __nullSessionContext = new HttpSessionContext()
+ {
+ @Override
+ public HttpSession getSession(String sessionId)
+ {
+ return null;
+ }
+
+ @Override
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public Enumeration getIds()
+ {
+ return Collections.enumeration(Collections.EMPTY_LIST);
+ }
+ };
+
+ /**
+ * Setting of max inactive interval for new sessions
+ * -1 means no timeout
+ */
+ protected int _dftMaxIdleSecs = -1;
+ protected boolean _httpOnly = false;
+ protected SessionIdManager _sessionIdManager;
+ protected boolean _secureCookies = false;
+ protected boolean _secureRequestOnly = true;
+
+ protected final List<HttpSessionAttributeListener> _sessionAttributeListeners = new CopyOnWriteArrayList<>();
+ protected final List<HttpSessionListener> _sessionListeners = new CopyOnWriteArrayList<>();
+ protected final List<HttpSessionIdListener> _sessionIdListeners = new CopyOnWriteArrayList<>();
+
+ protected ClassLoader _loader;
+ protected ContextHandler.Context _context;
+ protected SessionContext _sessionContext;
+ protected String _sessionCookie = __DefaultSessionCookie;
+ protected String _sessionIdPathParameterName = __DefaultSessionIdPathParameterName;
+ protected String _sessionIdPathParameterNamePrefix = ";" + _sessionIdPathParameterName + "=";
+ protected String _sessionDomain;
+ protected String _sessionPath;
+ protected int _maxCookieAge = -1;
+ protected int _refreshCookieAge;
+ protected boolean _nodeIdInSessionId;
+ protected boolean _checkingRemoteSessionIdEncoding;
+ protected String _sessionComment;
+ protected SessionCache _sessionCache;
+ protected final SampleStatistic _sessionTimeStats = new SampleStatistic();
+ protected final CounterStatistic _sessionsCreatedStats = new CounterStatistic();
+ public Set<SessionTrackingMode> _sessionTrackingModes;
+
+ protected boolean _usingURLs;
+ protected boolean _usingCookies = true;
+
+ protected Set<String> _candidateSessionIdsForExpiry = ConcurrentHashMap.newKeySet();
+
+ protected Scheduler _scheduler;
+ protected boolean _ownScheduler = false;
+
+ /**
+ * Constructor.
+ */
+ public SessionHandler()
+ {
+ setSessionTrackingModes(DEFAULT_SESSION_TRACKING_MODES);
+ }
+
+ @ManagedAttribute("path of the session cookie, or null for default")
+ public String getSessionPath()
+ {
+ return _sessionPath;
+ }
+
+ @ManagedAttribute("if greater the zero, the time in seconds a session cookie will last for")
+ public int getMaxCookieAge()
+ {
+ return _maxCookieAge;
+ }
+
+ /**
+ * Called by the {@link SessionHandler} when a session is first accessed by a request.
+ *
+ * @param session the session object
+ * @param secure whether the request is secure or not
+ * @return the session cookie. If not null, this cookie should be set on the response to either migrate
+ * the session or to refresh a session cookie that may expire.
+ * @see #complete(HttpSession)
+ */
+ public HttpCookie access(HttpSession session, boolean secure)
+ {
+ long now = System.currentTimeMillis();
+
+ Session s = ((SessionIf)session).getSession();
+
+ if (s.access(now))
+ {
+ // Do we need to refresh the cookie?
+ if (isUsingCookies() &&
+ (s.isIdChanged() ||
+ (getSessionCookieConfig().getMaxAge() > 0 && getRefreshCookieAge() > 0 &&
+ ((now - s.getCookieSetTime()) / 1000 > getRefreshCookieAge()))))
+ {
+ HttpCookie cookie = getSessionCookie(session, _context == null ? "/" : (_context.getContextPath()), secure);
+ s.cookieSet();
+ s.setIdChanged(false);
+ return cookie;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds an event listener for session-related events.
+ *
+ * @param listener the session event listener to add
+ * Individual SessionManagers implementations may accept arbitrary listener types,
+ * but they are expected to at least handle HttpSessionActivationListener,
+ * HttpSessionAttributeListener, HttpSessionBindingListener and HttpSessionListener.
+ * @see #removeEventListener(EventListener)
+ */
+ public void addEventListener(EventListener listener)
+ {
+ if (listener instanceof HttpSessionAttributeListener)
+ _sessionAttributeListeners.add((HttpSessionAttributeListener)listener);
+ if (listener instanceof HttpSessionListener)
+ _sessionListeners.add((HttpSessionListener)listener);
+ if (listener instanceof HttpSessionIdListener)
+ _sessionIdListeners.add((HttpSessionIdListener)listener);
+ addBean(listener, false);
+ }
+
+ /**
+ * Removes all event listeners for session-related events.
+ *
+ * @see #removeEventListener(EventListener)
+ */
+ public void clearEventListeners()
+ {
+ for (EventListener e : getBeans(EventListener.class))
+ {
+ removeBean(e);
+ }
+ _sessionAttributeListeners.clear();
+ _sessionListeners.clear();
+ _sessionIdListeners.clear();
+ }
+
+ /**
+ * Call the session lifecycle listeners
+ *
+ * @param session the session on which to call the lifecycle listeners
+ */
+ protected void callSessionDestroyedListeners(Session session)
+ {
+ if (session == null)
+ return;
+
+ if (_sessionListeners != null)
+ {
+ //We annoint the calling thread with
+ //the webapp's classloader because the calling thread may
+ //come from the scavenger, rather than a request thread
+ Runnable r = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ HttpSessionEvent event = new HttpSessionEvent(session);
+ for (int i = _sessionListeners.size() - 1; i >= 0; i--)
+ {
+ _sessionListeners.get(i).sessionDestroyed(event);
+ }
+ }
+ };
+ _sessionContext.run(r);
+ }
+ }
+
+ /**
+ * Call the session lifecycle listeners
+ *
+ * @param session the session on which to call the lifecycle listeners
+ */
+ protected void callSessionCreatedListeners(Session session)
+ {
+ if (session == null)
+ return;
+
+ if (_sessionListeners != null)
+ {
+ HttpSessionEvent event = new HttpSessionEvent(session);
+ for (int i = _sessionListeners.size() - 1; i >= 0; i--)
+ {
+ _sessionListeners.get(i).sessionCreated(event);
+ }
+ }
+ }
+
+ protected void callSessionIdListeners(Session session, String oldId)
+ {
+ //inform the listeners
+ if (!_sessionIdListeners.isEmpty())
+ {
+ HttpSessionEvent event = new HttpSessionEvent(session);
+ for (HttpSessionIdListener l : _sessionIdListeners)
+ {
+ l.sessionIdChanged(event, oldId);
+ }
+ }
+ }
+
+ /**
+ * Called when a request is finally leaving a session.
+ *
+ * @param session the session object
+ */
+ public void complete(HttpSession session)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Complete called with session {}", session);
+
+ if (session == null)
+ return;
+
+ Session s = ((SessionIf)session).getSession();
+ try
+ {
+ _sessionCache.release(s.getId(), s);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ /**
+ * Called when a response is about to be committed.
+ * We might take this opportunity to persist the session
+ * so that any subsequent requests to other servers
+ * will see the modifications.
+ */
+ public void commit(HttpSession session)
+ {
+ if (session == null)
+ return;
+
+ Session s = ((SessionIf)session).getSession();
+ try
+ {
+ _sessionCache.commit(s);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ @Deprecated
+ public void complete(Session session, Request baseRequest)
+ {
+ //not used
+ }
+
+ /*
+ * @see org.eclipse.thread.AbstractLifeCycle#doStart()
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ //check if session management is set up, if not set up HashSessions
+ final Server server = getServer();
+
+ _context = ContextHandler.getCurrentContext();
+ _loader = Thread.currentThread().getContextClassLoader();
+
+ synchronized (server)
+ {
+ //Get a SessionDataStore and a SessionDataStore, falling back to in-memory sessions only
+ if (_sessionCache == null)
+ {
+ SessionCacheFactory ssFactory = server.getBean(SessionCacheFactory.class);
+ setSessionCache(ssFactory != null ? ssFactory.getSessionCache(this) : new DefaultSessionCache(this));
+ SessionDataStore sds = null;
+ SessionDataStoreFactory sdsFactory = server.getBean(SessionDataStoreFactory.class);
+ if (sdsFactory != null)
+ sds = sdsFactory.getSessionDataStore(this);
+ else
+ sds = new NullSessionDataStore();
+
+ _sessionCache.setSessionDataStore(sds);
+ }
+
+ if (_sessionIdManager == null)
+ {
+ _sessionIdManager = server.getSessionIdManager();
+ if (_sessionIdManager == null)
+ {
+ //create a default SessionIdManager and set it as the shared
+ //SessionIdManager for the Server, being careful NOT to use
+ //the webapp context's classloader, otherwise if the context
+ //is stopped, the classloader is leaked.
+ ClassLoader serverLoader = server.getClass().getClassLoader();
+ try
+ {
+ Thread.currentThread().setContextClassLoader(serverLoader);
+ _sessionIdManager = new DefaultSessionIdManager(server);
+ server.setSessionIdManager(_sessionIdManager);
+ server.manage(_sessionIdManager);
+ _sessionIdManager.start();
+ }
+ finally
+ {
+ Thread.currentThread().setContextClassLoader(_loader);
+ }
+ }
+
+ // server session id is never managed by this manager
+ addBean(_sessionIdManager, false);
+ }
+
+ _scheduler = server.getBean(Scheduler.class);
+ if (_scheduler == null)
+ {
+ _scheduler = new ScheduledExecutorScheduler(String.format("Session-Scheduler-%x", hashCode()), false);
+ _ownScheduler = true;
+ _scheduler.start();
+ }
+ }
+
+ // Look for a session cookie name
+ if (_context != null)
+ {
+ String tmp = _context.getInitParameter(__SessionCookieProperty);
+ if (tmp != null)
+ _sessionCookie = tmp;
+
+ tmp = _context.getInitParameter(__SessionIdPathParameterNameProperty);
+ if (tmp != null)
+ setSessionIdPathParameterName(tmp);
+
+ // set up the max session cookie age if it isn't already
+ if (_maxCookieAge == -1)
+ {
+ tmp = _context.getInitParameter(__MaxAgeProperty);
+ if (tmp != null)
+ _maxCookieAge = Integer.parseInt(tmp.trim());
+ }
+
+ // set up the session domain if it isn't already
+ if (_sessionDomain == null)
+ _sessionDomain = _context.getInitParameter(__SessionDomainProperty);
+
+ // set up the sessionPath if it isn't already
+ if (_sessionPath == null)
+ _sessionPath = _context.getInitParameter(__SessionPathProperty);
+
+ tmp = _context.getInitParameter(__CheckRemoteSessionEncoding);
+ if (tmp != null)
+ _checkingRemoteSessionIdEncoding = Boolean.parseBoolean(tmp);
+ }
+
+ _sessionContext = new SessionContext(_sessionIdManager.getWorkerName(), _context);
+ _sessionCache.initialize(_sessionContext);
+ super.doStart();
+ }
+
+ /*
+ * @see org.eclipse.thread.AbstractLifeCycle#doStop()
+ */
+ @Override
+ protected void doStop() throws Exception
+ {
+ // Destroy sessions before destroying servlets/filters see JETTY-1266
+ shutdownSessions();
+ _sessionCache.stop();
+ if (_ownScheduler && _scheduler != null)
+ _scheduler.stop();
+ _scheduler = null;
+ super.doStop();
+ _loader = null;
+ }
+
+ /**
+ * @return true if session cookies should be HTTP-only (Microsoft extension)
+ * @see org.eclipse.jetty.http.HttpCookie#isHttpOnly()
+ */
+ @ManagedAttribute("true if cookies use the http only flag")
+ public boolean getHttpOnly()
+ {
+ return _httpOnly;
+ }
+
+ /**
+ * @return The sameSite setting for session cookies or null for no setting
+ * @see HttpCookie#getSameSite()
+ */
+ @ManagedAttribute("SameSite setting for session cookies")
+ public HttpCookie.SameSite getSameSite()
+ {
+ return HttpCookie.getSameSiteFromComment(_sessionComment);
+ }
+
+ /**
+ * Returns the <code>HttpSession</code> with the given session id
+ *
+ * @param extendedId the session id
+ * @return the <code>HttpSession</code> with the corresponding id or null if no session with the given id exists
+ */
+ protected HttpSession getHttpSession(String extendedId)
+ {
+ String id = getSessionIdManager().getId(extendedId);
+ Session session = getSession(id);
+
+ if (session != null && !session.getExtendedId().equals(extendedId))
+ session.setIdChanged(true);
+ return session;
+ }
+
+ /**
+ * Gets the cross context session id manager
+ *
+ * @return the session id manager
+ */
+ @ManagedAttribute("Session ID Manager")
+ public SessionIdManager getSessionIdManager()
+ {
+ return _sessionIdManager;
+ }
+
+ /**
+ * @return the max period of inactivity, after which the session is invalidated, in seconds.
+ * @see #setMaxInactiveInterval(int)
+ */
+ @ManagedAttribute("default maximum time a session may be idle for (in s)")
+ public int getMaxInactiveInterval()
+ {
+ return _dftMaxIdleSecs;
+ }
+
+ @ManagedAttribute("time before a session cookie is re-set (in s)")
+ public int getRefreshCookieAge()
+ {
+ return _refreshCookieAge;
+ }
+
+ /**
+ * @return same as SessionCookieConfig.getSecure(). If true, session
+ * cookies are ALWAYS marked as secure. If false, a session cookie is
+ * ONLY marked as secure if _secureRequestOnly == true and it is an HTTPS request.
+ */
+ @ManagedAttribute("if true, secure cookie flag is set on session cookies")
+ public boolean getSecureCookies()
+ {
+ return _secureCookies;
+ }
+
+ /**
+ * @return true if session cookie is to be marked as secure only on HTTPS requests
+ */
+ public boolean isSecureRequestOnly()
+ {
+ return _secureRequestOnly;
+ }
+
+ /**
+ * HTTPS request. Can be overridden by setting SessionCookieConfig.setSecure(true),
+ * in which case the session cookie will be marked as secure on both HTTPS and HTTP.
+ *
+ * @param secureRequestOnly true to set Session Cookie Config as secure
+ */
+ public void setSecureRequestOnly(boolean secureRequestOnly)
+ {
+ _secureRequestOnly = secureRequestOnly;
+ }
+
+ @ManagedAttribute("the set session cookie")
+ public String getSessionCookie()
+ {
+ return _sessionCookie;
+ }
+
+ /**
+ * A session cookie is marked as secure IFF any of the following conditions are true:
+ * <ol>
+ * <li>SessionCookieConfig.setSecure == true</li>
+ * <li>SessionCookieConfig.setSecure == false && _secureRequestOnly==true && request is HTTPS</li>
+ * </ol>
+ * According to SessionCookieConfig javadoc, case 1 can be used when:
+ * "... even though the request that initiated the session came over HTTP,
+ * is to support a topology where the web container is front-ended by an
+ * SSL offloading load balancer. In this case, the traffic between the client
+ * and the load balancer will be over HTTPS, whereas the traffic between the
+ * load balancer and the web container will be over HTTP."
+ * <p>
+ * For case 2, you can use _secureRequestOnly to determine if you want the
+ * Servlet Spec 3.0 default behavior when SessionCookieConfig.setSecure==false,
+ * which is:
+ * <cite>
+ * "they shall be marked as secure only if the request that initiated the
+ * corresponding session was also secure"
+ * </cite>
+ * <p>
+ * The default for _secureRequestOnly is true, which gives the above behavior. If
+ * you set it to false, then a session cookie is NEVER marked as secure, even if
+ * the initiating request was secure.
+ *
+ * @param session the session to which the cookie should refer.
+ * @param contextPath the context to which the cookie should be linked.
+ * The client will only send the cookie value when requesting resources under this path.
+ * @param requestIsSecure whether the client is accessing the server over a secure protocol (i.e. HTTPS).
+ * @return if this <code>SessionManager</code> uses cookies, then this method will return a new
+ * {@link Cookie cookie object} that should be set on the client in order to link future HTTP requests
+ * with the <code>session</code>. If cookies are not in use, this method returns <code>null</code>.
+ */
+ public HttpCookie getSessionCookie(HttpSession session, String contextPath, boolean requestIsSecure)
+ {
+ if (isUsingCookies())
+ {
+ String sessionPath = (_cookieConfig.getPath() == null) ? contextPath : _cookieConfig.getPath();
+ sessionPath = (StringUtil.isEmpty(sessionPath)) ? "/" : sessionPath;
+ String id = getExtendedId(session);
+ HttpCookie cookie = null;
+
+ cookie = new HttpCookie(
+ getSessionCookieName(_cookieConfig),
+ id,
+ _cookieConfig.getDomain(),
+ sessionPath,
+ _cookieConfig.getMaxAge(),
+ _cookieConfig.isHttpOnly(),
+ _cookieConfig.isSecure() || (isSecureRequestOnly() && requestIsSecure),
+ HttpCookie.getCommentWithoutAttributes(_cookieConfig.getComment()),
+ 0,
+ HttpCookie.getSameSiteFromComment(_cookieConfig.getComment()));
+
+ return cookie;
+ }
+ return null;
+ }
+
+ @ManagedAttribute("domain of the session cookie, or null for the default")
+ public String getSessionDomain()
+ {
+ return _sessionDomain;
+ }
+
+ @ManagedAttribute("number of sessions created by this node")
+ public int getSessionsCreated()
+ {
+ return (int)_sessionsCreatedStats.getCurrent();
+ }
+
+ /**
+ * @return the URL path parameter name for session id URL rewriting, by default "jsessionid".
+ * @see #setSessionIdPathParameterName(String)
+ */
+ @ManagedAttribute("name of use for URL session tracking")
+ public String getSessionIdPathParameterName()
+ {
+ return _sessionIdPathParameterName;
+ }
+
+ /**
+ * @return a formatted version of {@link #getSessionIdPathParameterName()}, by default
+ * ";" + sessionIdParameterName + "=", for easier lookup in URL strings.
+ * @see #getSessionIdPathParameterName()
+ */
+ public String getSessionIdPathParameterNamePrefix()
+ {
+ return _sessionIdPathParameterNamePrefix;
+ }
+
+ /**
+ * @return whether the session management is handled via cookies.
+ */
+ public boolean isUsingCookies()
+ {
+ return _usingCookies;
+ }
+
+ /**
+ * @param session the session to test for validity
+ * @return whether the given session is valid, that is, it has not been invalidated.
+ */
+ public boolean isValid(HttpSession session)
+ {
+ Session s = ((SessionIf)session).getSession();
+ return s.isValid();
+ }
+
+ /**
+ * @param session the session object
+ * @return the unique id of the session within the cluster (without a node id extension)
+ * @see #getExtendedId(HttpSession)
+ */
+ public String getId(HttpSession session)
+ {
+ Session s = ((SessionIf)session).getSession();
+ return s.getId();
+ }
+
+ /**
+ * @param session the session object
+ * @return the unique id of the session within the cluster, extended with an optional node id.
+ * @see #getId(HttpSession)
+ */
+ public String getExtendedId(HttpSession session)
+ {
+ Session s = ((SessionIf)session).getSession();
+ return s.getExtendedId();
+ }
+
+ /**
+ * Creates a new <code>HttpSession</code>.
+ *
+ * @param request the HttpServletRequest containing the requested session id
+ * @return the new <code>HttpSession</code>
+ */
+ public HttpSession newHttpSession(HttpServletRequest request)
+ {
+ long created = System.currentTimeMillis();
+ String id = _sessionIdManager.newSessionId(request, created);
+ Session session = _sessionCache.newSession(request, id, created, (_dftMaxIdleSecs > 0 ? _dftMaxIdleSecs * 1000L : -1));
+ session.setExtendedId(_sessionIdManager.getExtendedId(id, request));
+ session.getSessionData().setLastNode(_sessionIdManager.getWorkerName());
+
+ try
+ {
+ _sessionCache.add(id, session);
+ Request baseRequest = Request.getBaseRequest(request);
+ baseRequest.setSession(session);
+ baseRequest.enterSession(session);
+ _sessionsCreatedStats.increment();
+
+ if (request != null && request.isSecure())
+ session.setAttribute(Session.SESSION_CREATED_SECURE, Boolean.TRUE);
+
+ callSessionCreatedListeners(session);
+
+ return session;
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ return null;
+ }
+ }
+
+ /**
+ * Removes an event listener for for session-related events.
+ *
+ * @param listener the session event listener to remove
+ * @see #addEventListener(EventListener)
+ */
+ public void removeEventListener(EventListener listener)
+ {
+ if (listener instanceof HttpSessionAttributeListener)
+ _sessionAttributeListeners.remove(listener);
+ if (listener instanceof HttpSessionListener)
+ _sessionListeners.remove(listener);
+ if (listener instanceof HttpSessionIdListener)
+ _sessionIdListeners.remove(listener);
+ removeBean(listener);
+ }
+
+ /**
+ * Reset statistics values
+ */
+ @ManagedOperation(value = "reset statistics", impact = "ACTION")
+ public void statsReset()
+ {
+ _sessionsCreatedStats.reset();
+ _sessionTimeStats.reset();
+ }
+
+ /**
+ * Set if Session cookies should use HTTP Only
+ * @param httpOnly True if cookies should be HttpOnly.
+ * @see HttpCookie
+ */
+ public void setHttpOnly(boolean httpOnly)
+ {
+ _httpOnly = httpOnly;
+ }
+
+ /**
+ * Set Session cookie sameSite mode.
+ * Currently this is encoded in the session comment until sameSite is supported by {@link SessionCookieConfig}
+ * @param sameSite The sameSite setting for Session cookies (or null for no sameSite setting)
+ */
+ public void setSameSite(HttpCookie.SameSite sameSite)
+ {
+ // Encode in comment whilst not supported by SessionConfig, so that it can be set/saved in
+ // web.xml and quickstart.
+ // Always pass false for httpOnly as it has it's own setter.
+ _sessionComment = HttpCookie.getCommentWithAttributes(_sessionComment, false, sameSite);
+ }
+
+ /**
+ * @param metaManager The metaManager used for cross context session management.
+ */
+ public void setSessionIdManager(SessionIdManager metaManager)
+ {
+ updateBean(_sessionIdManager, metaManager);
+ _sessionIdManager = metaManager;
+ }
+
+ /**
+ * Sets the max period of inactivity, after which the session is invalidated, in seconds.
+ *
+ * @param seconds the max inactivity period, in seconds.
+ * @see #getMaxInactiveInterval()
+ */
+ public void setMaxInactiveInterval(int seconds)
+ {
+ _dftMaxIdleSecs = seconds;
+ if (LOG.isDebugEnabled())
+ {
+ if (_dftMaxIdleSecs <= 0)
+ LOG.debug("Sessions created by this manager are immortal (default maxInactiveInterval={})", _dftMaxIdleSecs);
+ else
+ LOG.debug("SessionManager default maxInactiveInterval={}", _dftMaxIdleSecs);
+ }
+ }
+
+ public void setRefreshCookieAge(int ageInSeconds)
+ {
+ _refreshCookieAge = ageInSeconds;
+ }
+
+ public void setSessionCookie(String cookieName)
+ {
+ _sessionCookie = cookieName;
+ }
+
+ /**
+ * Sets the session id URL path parameter name.
+ *
+ * @param param the URL path parameter name for session id URL rewriting (null or "none" for no rewriting).
+ * @see #getSessionIdPathParameterName()
+ * @see #getSessionIdPathParameterNamePrefix()
+ */
+ public void setSessionIdPathParameterName(String param)
+ {
+ _sessionIdPathParameterName = (param == null || "none".equals(param)) ? null : param;
+ _sessionIdPathParameterNamePrefix = (param == null || "none".equals(param))
+ ? null : (";" + _sessionIdPathParameterName + "=");
+ }
+
+ /**
+ * @param usingCookies The usingCookies to set.
+ */
+ public void setUsingCookies(boolean usingCookies)
+ {
+ _usingCookies = usingCookies;
+ }
+
+ /**
+ * Get a known existing session
+ *
+ * @param id The session ID stripped of any worker name.
+ * @return A Session or null if none exists.
+ */
+ public Session getSession(String id)
+ {
+ try
+ {
+ Session session = _sessionCache.get(id);
+ if (session != null)
+ {
+ //If the session we got back has expired
+ if (session.isExpiredAt(System.currentTimeMillis()))
+ {
+ //Expire the session
+ try
+ {
+ session.invalidate();
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Invalidating session {} found to be expired when requested", id);
+ LOG.warn(e);
+ }
+
+ return null;
+ }
+
+ session.setExtendedId(_sessionIdManager.getExtendedId(id, null));
+ }
+ return session;
+ }
+ catch (UnreadableSessionDataException e)
+ {
+ LOG.warn("Error loading session {}", id);
+ LOG.warn(e);
+ try
+ {
+ //tell id mgr to remove session from all other contexts
+ getSessionIdManager().invalidateAll(id);
+ }
+ catch (Exception x)
+ {
+ LOG.warn("Error cross-context invalidating unreadable session {}", id);
+ LOG.warn(x);
+ }
+ return null;
+ }
+ catch (Exception other)
+ {
+ LOG.warn(other);
+ return null;
+ }
+ }
+
+ /**
+ * Prepare sessions for session manager shutdown
+ *
+ * @throws Exception if unable to shutdown sesssions
+ */
+ protected void shutdownSessions() throws Exception
+ {
+ _sessionCache.shutdown();
+ }
+
+ /**
+ * @return the session store
+ */
+ public SessionCache getSessionCache()
+ {
+ return _sessionCache;
+ }
+
+ /**
+ * @param cache the session store to use
+ */
+ public void setSessionCache(SessionCache cache)
+ {
+ updateBean(_sessionCache, cache);
+ _sessionCache = cache;
+ }
+
+ /**
+ * @return true if the cluster node id (worker id) is returned as part of the session id by {@link HttpSession#getId()}. Default is false.
+ */
+ public boolean isNodeIdInSessionId()
+ {
+ return _nodeIdInSessionId;
+ }
+
+ /**
+ * @param nodeIdInSessionId true if the cluster node id (worker id) will be returned as part of the session id by {@link HttpSession#getId()}. Default is false.
+ */
+ public void setNodeIdInSessionId(boolean nodeIdInSessionId)
+ {
+ _nodeIdInSessionId = nodeIdInSessionId;
+ }
+
+ /**
+ * Remove session from manager
+ *
+ * @param id The session to remove
+ * @param invalidate True if {@link HttpSessionListener#sessionDestroyed(HttpSessionEvent)} and
+ * {@link SessionIdManager#expireAll(String)} should be called.
+ * @return if the session was removed
+ */
+ public Session removeSession(String id, boolean invalidate)
+ {
+ try
+ {
+ //Remove the Session object from the session store and any backing data store
+ Session session = _sessionCache.delete(id);
+ if (session != null)
+ {
+ if (invalidate)
+ {
+ session.beginInvalidate();
+
+ if (_sessionListeners != null)
+ {
+ HttpSessionEvent event = new HttpSessionEvent(session);
+ for (int i = _sessionListeners.size() - 1; i >= 0; i--)
+ {
+ _sessionListeners.get(i).sessionDestroyed(event);
+ }
+ }
+ }
+ }
+ //TODO if session object is not known to this node, how to get rid of it if no other
+ //node knows about it?
+
+ return session;
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ return null;
+ }
+ }
+
+ /**
+ * @return maximum amount of time session remained valid
+ */
+ @ManagedAttribute("maximum amount of time sessions have remained active (in s)")
+ public long getSessionTimeMax()
+ {
+ return _sessionTimeStats.getMax();
+ }
+
+ public Set<SessionTrackingMode> getDefaultSessionTrackingModes()
+ {
+ return DEFAULT_SESSION_TRACKING_MODES;
+ }
+
+ public Set<SessionTrackingMode> getEffectiveSessionTrackingModes()
+ {
+ return Collections.unmodifiableSet(_sessionTrackingModes);
+ }
+
+ public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes)
+ {
+ if (sessionTrackingModes != null &&
+ sessionTrackingModes.size() > 1 &&
+ sessionTrackingModes.contains(SessionTrackingMode.SSL))
+ {
+ throw new IllegalArgumentException("sessionTrackingModes specifies a combination of SessionTrackingMode.SSL with a session tracking mode other than SessionTrackingMode.SSL");
+ }
+ _sessionTrackingModes = new HashSet<>(sessionTrackingModes);
+ _usingCookies = _sessionTrackingModes.contains(SessionTrackingMode.COOKIE);
+ _usingURLs = _sessionTrackingModes.contains(SessionTrackingMode.URL);
+ }
+
+ /**
+ * @return whether the session management is handled via URLs.
+ */
+ public boolean isUsingURLs()
+ {
+ return _usingURLs;
+ }
+
+ public SessionCookieConfig getSessionCookieConfig()
+ {
+ return _cookieConfig;
+ }
+
+ private SessionCookieConfig _cookieConfig =
+ new CookieConfig();
+
+ /**
+ * @return total amount of time all sessions remained valid
+ */
+ @ManagedAttribute("total time sessions have remained valid")
+ public long getSessionTimeTotal()
+ {
+ return _sessionTimeStats.getTotal();
+ }
+
+ /**
+ * @return mean amount of time session remained valid
+ */
+ @ManagedAttribute("mean time sessions remain valid (in s)")
+ public double getSessionTimeMean()
+ {
+ return _sessionTimeStats.getMean();
+ }
+
+ /**
+ * @return standard deviation of amount of time session remained valid
+ */
+ @ManagedAttribute("standard deviation a session remained valid (in s)")
+ public double getSessionTimeStdDev()
+ {
+ return _sessionTimeStats.getStdDev();
+ }
+
+ /**
+ * @return True if absolute URLs are check for remoteness before being session encoded.
+ */
+ @ManagedAttribute("check remote session id encoding")
+ public boolean isCheckingRemoteSessionIdEncoding()
+ {
+ return _checkingRemoteSessionIdEncoding;
+ }
+
+ /**
+ * @param remote True if absolute URLs are check for remoteness before being session encoded.
+ */
+ public void setCheckingRemoteSessionIdEncoding(boolean remote)
+ {
+ _checkingRemoteSessionIdEncoding = remote;
+ }
+
+ /**
+ * Change the existing session id.
+ *
+ * @param oldId the old session id
+ * @param oldExtendedId the session id including worker suffix
+ * @param newId the new session id
+ * @param newExtendedId the new session id including worker suffix
+ */
+ public void renewSessionId(String oldId, String oldExtendedId, String newId, String newExtendedId)
+ {
+ Session session = null;
+ try
+ {
+ //the use count for the session will be incremented in renewSessionId
+ session = _sessionCache.renewSessionId(oldId, newId, oldExtendedId, newExtendedId); //swap the id over
+ if (session == null)
+ {
+ //session doesn't exist on this context
+ return;
+ }
+
+ //inform the listeners
+ callSessionIdListeners(session, oldId);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ finally
+ {
+ if (session != null)
+ {
+ try
+ {
+ _sessionCache.release(newId, session);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Record length of time session has been active. Called when the
+ * session is about to be invalidated.
+ *
+ * @param session the session whose time to record
+ */
+ protected void recordSessionTime(Session session)
+ {
+ _sessionTimeStats.record(round((System.currentTimeMillis() - session.getSessionData().getCreated()) / 1000.0));
+ }
+
+ /**
+ * Called by SessionIdManager to remove a session that has been invalidated,
+ * either by this context or another context. Also called by
+ * SessionIdManager when a session has expired in either this context or
+ * another context.
+ *
+ * @param id the session id to invalidate
+ */
+ public void invalidate(String id)
+ {
+
+ if (StringUtil.isBlank(id))
+ return;
+
+ try
+ {
+ // Remove the Session object from the session cache and any backing
+ // data store
+ Session session = _sessionCache.delete(id);
+ if (session != null)
+ {
+ //start invalidating if it is not already begun, and call the listeners
+ try
+ {
+ if (session.beginInvalidate())
+ {
+ try
+ {
+ callSessionDestroyedListeners(session);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ //call the attribute removed listeners and finally mark it as invalid
+ session.finishInvalidate();
+ }
+ }
+ catch (IllegalStateException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} already invalid", session);
+ LOG.ignore(e);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ /**
+ * Called periodically by the HouseKeeper to handle the list of
+ * sessions that have expired since the last call to scavenge.
+ */
+ public void scavenge()
+ {
+ //don't attempt to scavenge if we are shutting down
+ if (isStopping() || isStopped())
+ return;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} scavenging sessions", this);
+ //Get a snapshot of the candidates as they are now. Others that
+ //arrive during this processing will be dealt with on
+ //subsequent call to scavenge
+ String[] ss = _candidateSessionIdsForExpiry.toArray(new String[0]);
+ Set<String> candidates = new HashSet<>(Arrays.asList(ss));
+ _candidateSessionIdsForExpiry.removeAll(candidates);
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} scavenging session ids {}", this, candidates);
+ try
+ {
+ candidates = _sessionCache.checkExpiration(candidates);
+ for (String id : candidates)
+ {
+ try
+ {
+ getSessionIdManager().expireAll(id);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ /**
+ * @see #sessionInactivityTimerExpired(Session, long)
+ */
+ @Deprecated
+ public void sessionInactivityTimerExpired(Session session)
+ {
+ //for backwards compilation compatibility only
+ sessionInactivityTimerExpired(session, System.currentTimeMillis());
+ }
+
+ /**
+ * Each session has a timer that is configured to go off
+ * when either the session has not been accessed for a
+ * configurable amount of time, or the session itself
+ * has passed its expiry.
+ *
+ * If it has passed its expiry, then we will mark it for
+ * scavenging by next run of the HouseKeeper; if it has
+ * been idle longer than the configured eviction period,
+ * we evict from the cache.
+ *
+ * If none of the above are true, then the System timer
+ * is inconsistent and the caller of this method will
+ * need to reset the timer.
+ *
+ * @param session the session
+ * @param now the time at which to check for expiry
+ */
+ public void sessionInactivityTimerExpired(Session session, long now)
+ {
+ if (session == null)
+ return;
+
+ //check if the session is:
+ //1. valid
+ //2. expired
+ //3. idle
+ try (Lock lock = session.lock())
+ {
+ if (session.getRequests() > 0)
+ return; //session can't expire or be idle if there is a request in it
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Inspecting session {}, valid={}", session.getId(), session.isValid());
+
+ if (!session.isValid())
+ return; //do nothing, session is no longer valid
+
+ if (session.isExpiredAt(now))
+ {
+ //instead of expiring the session directly here, accumulate a list of
+ //session ids that need to be expired. This is an efficiency measure: as
+ //the expiration involves the SessionDataStore doing a delete, it is
+ //most efficient if it can be done as a bulk operation to eg reduce
+ //roundtrips to the persistent store. Only do this if the HouseKeeper that
+ //does the scavenging is configured to actually scavenge
+ if (_sessionIdManager.getSessionHouseKeeper() != null &&
+ _sessionIdManager.getSessionHouseKeeper().getIntervalSec() > 0)
+ {
+ _candidateSessionIdsForExpiry.add(session.getId());
+ if (LOG.isDebugEnabled())
+ LOG.debug("Session {} is candidate for expiry", session.getId());
+ }
+ }
+ else
+ {
+ //possibly evict the session
+ _sessionCache.checkInactiveSession(session);
+ }
+ }
+ }
+
+ /**
+ * Check if id is in use by this context
+ *
+ * @param id identity of session to check
+ * @return <code>true</code> if this manager knows about this id
+ * @throws Exception if any error occurred
+ */
+ public boolean isIdInUse(String id) throws Exception
+ {
+ //Ask the session store
+ return _sessionCache.exists(id);
+ }
+
+ public Scheduler getScheduler()
+ {
+ return _scheduler;
+ }
+
+ /**
+ * SessionIf
+ *
+ * Interface that any session wrapper should implement so that
+ * SessionManager may access the Jetty session implementation.
+ */
+ public interface SessionIf extends HttpSession
+ {
+ Session getSession();
+ }
+
+ public static String getSessionCookieName(SessionCookieConfig config)
+ {
+ if (config == null || config.getName() == null)
+ return __DefaultSessionCookie;
+ return config.getName();
+ }
+
+ /**
+ * CookieConfig
+ *
+ * Implementation of the javax.servlet.SessionCookieConfig.
+ * SameSite configuration can be achieved by using setComment
+ * @see HttpCookie
+ */
+ public final class CookieConfig implements SessionCookieConfig
+ {
+ @Override
+ public String getComment()
+ {
+ return _sessionComment;
+ }
+
+ @Override
+ public String getDomain()
+ {
+ return _sessionDomain;
+ }
+
+ @Override
+ public int getMaxAge()
+ {
+ return _maxCookieAge;
+ }
+
+ @Override
+ public String getName()
+ {
+ return _sessionCookie;
+ }
+
+ @Override
+ public String getPath()
+ {
+ return _sessionPath;
+ }
+
+ @Override
+ public boolean isHttpOnly()
+ {
+ return _httpOnly;
+ }
+
+ @Override
+ public boolean isSecure()
+ {
+ return _secureCookies;
+ }
+
+ @Override
+ public void setComment(String comment)
+ {
+ if (_context != null && _context.getContextHandler().isAvailable())
+ throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started");
+ _sessionComment = comment;
+ }
+
+ @Override
+ public void setDomain(String domain)
+ {
+ if (_context != null && _context.getContextHandler().isAvailable())
+ throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started");
+ _sessionDomain = domain;
+ }
+
+ @Override
+ public void setHttpOnly(boolean httpOnly)
+ {
+ if (_context != null && _context.getContextHandler().isAvailable())
+ throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started");
+ _httpOnly = httpOnly;
+ }
+
+ @Override
+ public void setMaxAge(int maxAge)
+ {
+ if (_context != null && _context.getContextHandler().isAvailable())
+ throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started");
+ _maxCookieAge = maxAge;
+ }
+
+ @Override
+ public void setName(String name)
+ {
+ if (_context != null && _context.getContextHandler().isAvailable())
+ throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started");
+ if ("".equals(name))
+ throw new IllegalArgumentException("Blank cookie name");
+ if (name != null)
+ Syntax.requireValidRFC2616Token(name, "Bad Session cookie name");
+ _sessionCookie = name;
+ }
+
+ @Override
+ public void setPath(String path)
+ {
+ if (_context != null && _context.getContextHandler().isAvailable())
+ throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started");
+ _sessionPath = path;
+ }
+
+ @Override
+ public void setSecure(boolean secure)
+ {
+ if (_context != null && _context.getContextHandler().isAvailable())
+ throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started");
+ _secureCookies = secure;
+ }
+ }
+
+ public void doSessionAttributeListeners(Session session, String name, Object old, Object value)
+ {
+ if (!_sessionAttributeListeners.isEmpty())
+ {
+ HttpSessionBindingEvent event = new HttpSessionBindingEvent(session, name, old == null ? value : old);
+
+ for (HttpSessionAttributeListener l : _sessionAttributeListeners)
+ {
+ if (old == null)
+ l.attributeAdded(event);
+ else if (value == null)
+ l.attributeRemoved(event);
+ else
+ l.attributeReplaced(event);
+ }
+ }
+ }
+
+ /*
+ * @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
+ */
+ @Override
+ public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ SessionHandler oldSessionHandler = null;
+ HttpSession oldSession = null;
+ HttpSession existingSession = null;
+
+ try
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Entering scope {}, dispatch={} asyncstarted={}", this, baseRequest.getDispatcherType(), baseRequest
+ .isAsyncStarted());
+
+ switch (baseRequest.getDispatcherType())
+ {
+ case REQUEST:
+ {
+ //there are no previous sessionhandlers or sessions for dispatch=REQUEST
+ //look for a session for this context
+ baseRequest.setSession(null);
+ checkRequestedSessionId(baseRequest, request);
+ existingSession = baseRequest.getSession(false);
+ baseRequest.setSessionHandler(this);
+ baseRequest.setSession(existingSession); //can be null
+ break;
+ }
+ case ASYNC:
+ case ERROR:
+ case FORWARD:
+ case INCLUDE:
+ {
+ //remember previous sessionhandler and session
+ oldSessionHandler = baseRequest.getSessionHandler();
+ oldSession = baseRequest.getSession(false);
+
+ if (oldSessionHandler != this)
+ {
+ //find any existing session for this request that has already been accessed
+ existingSession = baseRequest.getSession(this);
+ if (existingSession == null)
+ {
+ //session for this context has not been visited previously,
+ //try getting it
+ baseRequest.setSession(null);
+ checkRequestedSessionId(baseRequest, request);
+ existingSession = baseRequest.getSession(false);
+ }
+
+ baseRequest.setSession(existingSession);
+ baseRequest.setSessionHandler(this);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ if ((existingSession != null) && (oldSessionHandler != this))
+ {
+ HttpCookie cookie = access(existingSession, request.isSecure());
+ // Handle changed ID or max-age refresh, but only if this is not a redispatched request
+ if ((cookie != null) &&
+ (request.getDispatcherType() == DispatcherType.ASYNC ||
+ request.getDispatcherType() == DispatcherType.REQUEST))
+ baseRequest.getResponse().replaceCookie(cookie);
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("sessionHandler={} session={}", this, existingSession);
+
+ if (_nextScope != null)
+ _nextScope.doScope(target, baseRequest, request, response);
+ else if (_outerScope != null)
+ _outerScope.doHandle(target, baseRequest, request, response);
+ else
+ doHandle(target, baseRequest, request, response);
+ }
+ finally
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Leaving scope {} dispatch={}, async={}, session={}, oldsession={}, oldsessionhandler={}",
+ this, baseRequest.getDispatcherType(), baseRequest.isAsyncStarted(), baseRequest.getSession(false),
+ oldSession, oldSessionHandler);
+
+ // revert the session handler to the previous, unless it was null, in which case remember it as
+ // the first session handler encountered.
+ if (oldSessionHandler != null && oldSessionHandler != this)
+ {
+ baseRequest.setSessionHandler(oldSessionHandler);
+ baseRequest.setSession(oldSession);
+ }
+ }
+ }
+
+ /*
+ * @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
+ */
+ @Override
+ public void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ nextHandle(target, baseRequest, request, response);
+ }
+
+ /**
+ * Look for a requested session ID in cookies and URI parameters
+ *
+ * @param baseRequest the request to check
+ * @param request the request to check
+ */
+ protected void checkRequestedSessionId(Request baseRequest, HttpServletRequest request)
+ {
+ String requestedSessionId = request.getRequestedSessionId();
+
+ if (requestedSessionId != null)
+ {
+ HttpSession session = getHttpSession(requestedSessionId);
+
+ if (session != null && isValid(session))
+ {
+ baseRequest.enterSession(session); //enter session for first time
+ baseRequest.setSession(session);
+ }
+ return;
+ }
+ else if (!DispatcherType.REQUEST.equals(baseRequest.getDispatcherType()))
+ return;
+
+ boolean requestedSessionIdFromCookie = false;
+ HttpSession session = null;
+
+ //first try getting id from a cookie
+ if (isUsingCookies())
+ {
+ Cookie[] cookies = request.getCookies();
+ if (cookies != null && cookies.length > 0)
+ {
+ final String sessionCookie = getSessionCookieName(getSessionCookieConfig());
+ for (Cookie cookie : cookies)
+ {
+ if (sessionCookie.equalsIgnoreCase(cookie.getName()))
+ {
+ String id = cookie.getValue();
+ requestedSessionIdFromCookie = true;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Got Session ID {} from cookie {}", id, sessionCookie);
+
+ if (session == null)
+ {
+ //we currently do not have a session selected, use this one if it is valid
+ HttpSession s = getHttpSession(id);
+ if (s != null && isValid(s))
+ {
+ //associate it with the request so its reference count is decremented as the
+ //request exits
+ requestedSessionId = id;
+ session = s;
+ baseRequest.enterSession(session);
+ baseRequest.setSession(session);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Selected session {}", session);
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("No session found for session cookie id {}", id);
+
+ //if we don't have a valid session id yet, just choose the current id
+ if (requestedSessionId == null)
+ requestedSessionId = id;
+ }
+ }
+ else
+ {
+ //we currently have a valid session selected. We will throw an error
+ //if there is a _different_ valid session id cookie. Duplicate ids, or
+ //invalid session ids are ignored
+ if (!session.getId().equals(getSessionIdManager().getId(id)))
+ {
+ //load the session to see if it is valid or not
+ HttpSession s = getHttpSession(id);
+ if (s != null && isValid(s))
+ {
+ //associate it with the request so its reference count is decremented as the
+ //request exits
+ baseRequest.enterSession(s);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Multiple different valid session ids: {}, {}", requestedSessionId, id);
+ throw new BadMessageException("Duplicate valid session cookies: " + requestedSessionId + " ," + id);
+ }
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Duplicate valid session cookie id: {}", id);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ //try getting id from a url
+ if (isUsingURLs() && (requestedSessionId == null))
+ {
+ String uri = request.getRequestURI();
+ String prefix = getSessionIdPathParameterNamePrefix();
+ if (prefix != null)
+ {
+ int s = uri.indexOf(prefix);
+ if (s >= 0)
+ {
+ s += prefix.length();
+ int i = s;
+ while (i < uri.length())
+ {
+ char c = uri.charAt(i);
+ if (c == ';' || c == '#' || c == '?' || c == '/')
+ break;
+ i++;
+ }
+
+ requestedSessionId = uri.substring(s, i);
+ requestedSessionIdFromCookie = false;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Got Session ID {} from URL", requestedSessionId);
+
+ session = getHttpSession(requestedSessionId);
+ if (session != null && isValid(session))
+ {
+ baseRequest.enterSession(session); //request enters this session for first time
+ baseRequest.setSession(session); //associate the session with the request
+ }
+ }
+ }
+ }
+
+ baseRequest.setRequestedSessionId(requestedSessionId);
+ baseRequest.setRequestedSessionIdFromCookie(requestedSessionId != null && requestedSessionIdFromCookie);
+ }
+
+ /**
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString()
+ {
+ return String.format("%s%d==dftMaxIdleSec=%d", this.getClass().getName(), this.hashCode(), _dftMaxIdleSecs);
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/UnreadableSessionDataException.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/UnreadableSessionDataException.java
new file mode 100644
index 0000000..b13478f
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/UnreadableSessionDataException.java
@@ -0,0 +1,60 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+/**
+ * UnreadableSessionDataException
+ */
+public class UnreadableSessionDataException extends Exception
+{
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1806303483488966566L;
+ private String _id;
+ private SessionContext _sessionContext;
+
+ /**
+ * @return the session id
+ */
+ public String getId()
+ {
+ return _id;
+ }
+
+ /**
+ * @return the SessionContext to which the unreadable session belongs
+ */
+ public SessionContext getSessionContext()
+ {
+ return _sessionContext;
+ }
+
+ /**
+ * @param id the session id
+ * @param sessionContext the sessionContext
+ * @param t the cause of the exception
+ */
+ public UnreadableSessionDataException(String id, SessionContext sessionContext, Throwable t)
+ {
+ super("Unreadable session " + id + " for " + sessionContext, t);
+ _sessionContext = sessionContext;
+ _id = id;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/UnwriteableSessionDataException.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/UnwriteableSessionDataException.java
new file mode 100644
index 0000000..9079b60
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/UnwriteableSessionDataException.java
@@ -0,0 +1,44 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+/**
+ * UnwriteableSessionDataException
+ */
+public class UnwriteableSessionDataException extends Exception
+{
+ private String _id;
+ private SessionContext _sessionContext;
+
+ public UnwriteableSessionDataException(String id, SessionContext contextId, Throwable t)
+ {
+ super("Unwriteable session " + id + " for " + contextId, t);
+ _id = id;
+ }
+
+ public String getId()
+ {
+ return _id;
+ }
+
+ public SessionContext getSessionContext()
+ {
+ return _sessionContext;
+ }
+}
diff --git a/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/package-info.java b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/package-info.java
new file mode 100644
index 0000000..6a7a1c7
--- /dev/null
+++ b/third_party/jetty-server/src/main/java/org/eclipse/jetty/server/session/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Server : Session Management Implementations
+ */
+package org.eclipse.jetty.server.session;
+
diff --git a/third_party/jetty-server/src/main/resources/jetty-dir.css b/third_party/jetty-server/src/main/resources/jetty-dir.css
new file mode 100644
index 0000000..cbcc88d
--- /dev/null
+++ b/third_party/jetty-server/src/main/resources/jetty-dir.css
@@ -0,0 +1,49 @@
+body {
+ background-color: #FFFFFF;
+ margin: 10px;
+ padding: 5px;
+ font-family: sans-serif;
+}
+
+h1.title {
+ text-shadow: #000000 -1px -1px 1px;
+ color: #FC390E;
+ font-weight: bold;
+}
+
+table.listing {
+ border: 0px;
+}
+
+thead a {
+ color: blue;
+}
+
+thead th {
+ border-bottom: black 1px solid;
+}
+
+.name, .lastmodified {
+ text-align: left;
+ padding-right: 15px;
+}
+
+.size {
+ text-align: right;
+}
+
+a {
+ color: #7036be;
+ font-weight: bold;
+ font-style: normal;
+ text-decoration: none;
+ font-size:inherit;
+}
+
+td {
+ font-style: italic;
+ padding: 2px;
+}
+
+
+
diff --git a/third_party/jetty-server/src/main/resources/org/eclipse/jetty/favicon.ico b/third_party/jetty-server/src/main/resources/org/eclipse/jetty/favicon.ico
new file mode 100644
index 0000000..ea9e174
--- /dev/null
+++ b/third_party/jetty-server/src/main/resources/org/eclipse/jetty/favicon.ico
Binary files differ
diff --git a/third_party/jetty-server/src/test/config/etc/keystore b/third_party/jetty-server/src/test/config/etc/keystore
new file mode 100644
index 0000000..d6592f9
--- /dev/null
+++ b/third_party/jetty-server/src/test/config/etc/keystore
Binary files differ
diff --git a/third_party/jetty-server/src/test/config/etc/keystore.pkf b/third_party/jetty-server/src/test/config/etc/keystore.pkf
new file mode 100644
index 0000000..443818e
--- /dev/null
+++ b/third_party/jetty-server/src/test/config/etc/keystore.pkf
@@ -0,0 +1,20 @@
+Bag Attributes
+ friendlyName: jetty
+ localKeyID: 54 69 6D 65 20 31 34 32 33 31 39 38 30 39 33 31 31 35
+Key Attributes: <No Attributes>
+-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAIPh4Q0t4xklXTzX
+N2VAb47r5n7idAupp4CTNEhhT6lS70iA+A8i4+0lSEHWAogvd9jl3H7SvScr30QM
+4ieC0JCGSOwGc8f+yqKrO56PPd5OuqW380BJ0r74jJczU9CcsuavHD7e6mRLUnmj
+xM20NSxrcicMiPUHY1mJZtN9swtxAgMBAAECgYADS9P6Jll0uXBZIu/pgfDH27GJ
+HlPULstW9VbrMDNzgfUlFMQebLrRpIrnyleJ29Xc//HA4beEkR4lb0T/w88+pEkt
+7fhYeqRLPIfpDOgzloynnsoPcd8f/PypbimQrNLmBiG1178nVcy4Yoh5lYVIJwtU
+3VriqDlvAfTLrrx8AQJBAMLWuh27Hb8xs3LRg4UD7hcv8tJejstm08Y+czRz7cO0
+RENa3aDjGFSegc+IUfdez7BP8uDw+PwE+jybmTvaliECQQCtR/anCY1WS28/bKvy
+lmIwoI15eraBdVFkN0Hfxh+9PfR3rMD5uyvukT5GgTtY/XxADyafSTaipDJiZHJI
+EitRAkBjeCBYYVjUbVlBuvi8Bb+dktsSzzdzXDGtueAy3SR7jyJyiIcxRf775Fg9
+TUkbUwoQ5yAF+sACWcAvBPj796JBAkAEZEeHEkHnxv+pztpIyrDwZJFRW9/WRh/q
+90+PGVlilXhltBYr/idt43Z9mPblGX+VrAyhitx8oMa6IauX0gYRAkEAgnyVeXrD
+jDLUZRA3P8Gu27k1k6GjbTYiUz3HKCz2/6+MZ2MK2qqwafgqocji029Q6dHdPD7a
+4QnRlvraUnyQLA==
+-----END PRIVATE KEY-----
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java
new file mode 100644
index 0000000..11a1978
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java
@@ -0,0 +1,134 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.io.ArrayByteBufferPool;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public abstract class AbstractHttpTest
+{
+ private static final Set<String> __noBodyCodes = new HashSet<>(Arrays.asList("100", "101", "102", "204", "304"));
+
+ protected static Server server;
+ protected static ServerConnector connector;
+ private StacklessLogging stacklessChannelLogging;
+
+ @BeforeEach
+ public void setUp() throws Exception
+ {
+ server = new Server();
+ connector = new ServerConnector(server, null, null, new ArrayByteBufferPool(64, 2048, 64 * 1024), 1, 1, new HttpConnectionFactory());
+ connector.setIdleTimeout(100000);
+
+ server.addConnector(connector);
+ stacklessChannelLogging = new StacklessLogging(HttpChannel.class);
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception
+ {
+ server.stop();
+ stacklessChannelLogging.close();
+ }
+
+ protected HttpTester.Response executeRequest(HttpVersion httpVersion) throws URISyntaxException, IOException
+ {
+ try (Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ socket.setSoTimeout((int)connector.getIdleTimeout());
+
+ try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream())))
+ {
+ writer.write("GET / " + httpVersion.asString() + "\r\n");
+ writer.write("Host: localhost\r\n");
+ writer.write("\r\n");
+ writer.flush();
+
+ HttpTester.Response response = new HttpTester.Response();
+ HttpTester.Input input = HttpTester.from(socket.getInputStream());
+ HttpTester.parseResponse(input, response);
+
+ if (httpVersion.is("HTTP/1.1") &&
+ response.isComplete() &&
+ response.get("content-length") == null &&
+ response.get("transfer-encoding") == null &&
+ !__noBodyCodes.contains(response.getStatus()))
+ assertThat("If HTTP/1.1 response doesn't contain transfer-encoding or content-length headers, " +
+ "it should contain connection:close", response.get("connection"), is("close"));
+ return response;
+ }
+ }
+ }
+
+ protected static class TestCommitException extends IllegalStateException
+ {
+ public TestCommitException()
+ {
+ super("Thrown by test");
+ }
+ }
+
+ protected class ThrowExceptionOnDemandHandler extends AbstractHandler
+ {
+ private final boolean throwException;
+ private volatile Throwable failure;
+
+ protected ThrowExceptionOnDemandHandler(boolean throwException)
+ {
+ this.throwException = throwException;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (throwException)
+ throw new TestCommitException();
+ }
+
+ protected void markFailed(Throwable x)
+ {
+ this.failure = x;
+ }
+
+ public Throwable failure()
+ {
+ return failure;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java
new file mode 100644
index 0000000..180ec9d
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java
@@ -0,0 +1,767 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Exchanger;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Stream;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpCompliance;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.io.ChannelEndPoint;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.ManagedSelector;
+import org.eclipse.jetty.io.SocketChannelEndPoint;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Extended Server Tester.
+ */
+public class AsyncCompletionTest extends HttpServerTestFixture
+{
+ private static final int POLL = 10; // milliseconds
+ private static final int WAIT = 10; // seconds
+ private static final String SMALL = "Now is the time for all good men to come to the aid of the party. ";
+ private static final String LARGE = SMALL + SMALL + SMALL + SMALL + SMALL;
+ private static final int BUFFER_SIZE = SMALL.length() * 3 / 2;
+ private static final BlockingQueue<PendingCallback> __queue = new BlockingArrayQueue<>();
+ private static final AtomicBoolean __transportComplete = new AtomicBoolean();
+
+ private static class PendingCallback extends Callback.Nested
+ {
+ private CompletableFuture<Void> _pending = new CompletableFuture<>();
+
+ public PendingCallback(Callback callback)
+ {
+ super(callback);
+ }
+
+ @Override
+ public void succeeded()
+ {
+ _pending.complete(null);
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ _pending.completeExceptionally(x);
+ }
+
+ public void proceed()
+ {
+ try
+ {
+ _pending.get(WAIT, TimeUnit.SECONDS);
+ getCallback().succeeded();
+ }
+ catch (Throwable th)
+ {
+ th.printStackTrace();
+ getCallback().failed(th);
+ }
+ }
+ }
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ __transportComplete.set(false);
+
+ startServer(new ServerConnector(_server, new HttpConnectionFactory()
+ {
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endPoint)
+ {
+ getHttpConfiguration().setOutputBufferSize(BUFFER_SIZE);
+ getHttpConfiguration().setOutputAggregationSize(BUFFER_SIZE);
+ return configure(new ExtendedHttpConnection(getHttpConfiguration(), connector, endPoint), connector, endPoint);
+ }
+ })
+ {
+ @Override
+ protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException
+ {
+ return new ExtendedEndPoint(channel, selectSet, key, getScheduler());
+ }
+ });
+ }
+
+ private static class ExtendedEndPoint extends SocketChannelEndPoint
+ {
+ public ExtendedEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler)
+ {
+ super(channel, selector, key, scheduler);
+ }
+
+ @Override
+ public void write(Callback callback, ByteBuffer... buffers) throws IllegalStateException
+ {
+ PendingCallback delay = new PendingCallback(callback);
+ super.write(delay, buffers);
+ __queue.offer(delay);
+ }
+ }
+
+ private static class ExtendedHttpConnection extends HttpConnection
+ {
+ public ExtendedHttpConnection(HttpConfiguration config, Connector connector, EndPoint endPoint)
+ {
+ super(config, connector, endPoint, HttpCompliance.RFC7230_LEGACY, false);
+ }
+
+ @Override
+ public void onCompleted()
+ {
+ __transportComplete.compareAndSet(false, true);
+ super.onCompleted();
+ }
+ }
+
+ enum WriteStyle
+ {
+ ARRAY, BUFFER, BYTE, BYTE_THEN_ARRAY, PRINT
+ }
+
+ public static Stream<Arguments> asyncIOWriteTests()
+ {
+ List<Object[]> tests = new ArrayList<>();
+ for (WriteStyle w : WriteStyle.values())
+ {
+ for (boolean contentLength : new Boolean[]{true, false})
+ {
+ for (boolean isReady : new Boolean[]{true, false})
+ {
+ for (boolean flush : new Boolean[]{true, false})
+ {
+ for (boolean close : new Boolean[]{true, false})
+ {
+ for (String data : new String[]{SMALL, LARGE})
+ {
+ tests.add(new Object[]{new AsyncIOWriteHandler(w, contentLength, isReady, flush, close, data)});
+ }
+ }
+ }
+ }
+ }
+ }
+ return tests.stream().map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("asyncIOWriteTests")
+ public void testAsyncIOWrite(AsyncIOWriteHandler handler) throws Exception
+ {
+ configureServer(handler);
+
+ int base = _threadPool.getBusyThreads();
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+ InputStream in = client.getInputStream();
+
+ // write the request
+ os.write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
+ os.flush();
+
+ // wait for OWP to execute (proves we do not block in write APIs)
+ boolean completeCalled = handler.waitForOWPExit();
+
+ while (true)
+ {
+ // wait for threads to return to base level (proves we are really async)
+ long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(WAIT);
+ while (_threadPool.getBusyThreads() != base)
+ {
+ if (System.nanoTime() > end)
+ throw new TimeoutException();
+ Thread.sleep(POLL);
+ }
+
+ if (completeCalled)
+ break;
+
+ // We are now asynchronously waiting!
+ assertThat(__transportComplete.get(), is(false));
+
+ // If we are not complete, we must be waiting for one or more writes to complete
+ while (true)
+ {
+ PendingCallback delay = __queue.poll(POLL, TimeUnit.MILLISECONDS);
+ if (delay != null)
+ {
+ delay.proceed();
+ continue;
+ }
+ // No delay callback found, have we finished OWP again?
+ Boolean c = handler.pollForOWPExit();
+
+ if (c == null)
+ // No we haven't, so look for another delay callback
+ continue;
+
+ // We have a OWP result, so let's handle it.
+ completeCalled = c;
+ break;
+ }
+ }
+
+ // Wait for full completion
+ long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(WAIT);
+ while (!__transportComplete.get())
+ {
+ if (System.nanoTime() > end)
+ throw new TimeoutException();
+
+ // proceed with any delayCBs needed for completion
+ PendingCallback delay = __queue.poll(POLL, TimeUnit.MILLISECONDS);
+ if (delay != null)
+ delay.proceed();
+ }
+
+ // Check we got a response!
+ HttpTester.Response response = HttpTester.parseResponse(in);
+ assertThat(response, Matchers.notNullValue());
+ assertThat(response.getStatus(), is(200));
+ String content = response.getContent();
+ assertThat(content, containsString(handler.getExpectedMessage()));
+ }
+ }
+
+ private static class AsyncIOWriteHandler extends AbstractHandler
+ {
+ final WriteStyle _write;
+ final boolean _contentLength;
+ final boolean _isReady;
+ final boolean _flush;
+ final boolean _close;
+ final String _data;
+ final Exchanger<Boolean> _ready = new Exchanger<>();
+ int _toWrite;
+ boolean _flushed;
+ boolean _closed;
+
+ AsyncIOWriteHandler(WriteStyle write, boolean contentLength, boolean isReady, boolean flush, boolean close, String data)
+ {
+ _write = write;
+ _contentLength = contentLength;
+ _isReady = isReady;
+ _flush = flush;
+ _close = close;
+ _data = data;
+ _toWrite = data.length();
+ }
+
+ public String getExpectedMessage()
+ {
+ return SMALL;
+ }
+
+ boolean waitForOWPExit()
+ {
+ try
+ {
+ return _ready.exchange(null);
+ }
+ catch (InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ Boolean pollForOWPExit()
+ {
+ try
+ {
+ return _ready.exchange(null, POLL, TimeUnit.MILLISECONDS);
+ }
+ catch (InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ catch (TimeoutException e)
+ {
+ return null;
+ }
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ AsyncContext context = request.startAsync();
+ ServletOutputStream out = response.getOutputStream();
+ response.setContentType("text/plain");
+ byte[] bytes = _data.getBytes(StandardCharsets.ISO_8859_1);
+ if (_contentLength)
+ response.setContentLength(bytes.length);
+
+ out.setWriteListener(new WriteListener()
+ {
+ @Override
+ public void onWritePossible() throws IOException
+ {
+ try
+ {
+ if (out.isReady())
+ {
+ if (_toWrite > 0)
+ {
+ switch (_write)
+ {
+ case ARRAY:
+ _toWrite = 0;
+ out.write(bytes, 0, bytes.length);
+ break;
+
+ case BUFFER:
+ _toWrite = 0;
+ ((HttpOutput)out).write(BufferUtil.toBuffer(bytes));
+ break;
+
+ case BYTE:
+ for (int i = bytes.length - _toWrite; i < bytes.length; i++)
+ {
+ _toWrite--;
+ out.write(bytes[i]);
+ boolean ready = out.isReady();
+ if (!ready)
+ {
+ _ready.exchange(Boolean.FALSE);
+ return;
+ }
+ }
+ break;
+
+ case BYTE_THEN_ARRAY:
+ _toWrite = 0;
+ out.write(bytes[0]); // This should always aggregate
+ assertThat(out.isReady(), is(true));
+ out.write(bytes, 1, bytes.length - 1);
+ break;
+
+ case PRINT:
+ _toWrite = 0;
+ out.print(_data);
+ break;
+ }
+ }
+
+ if (_flush && !_flushed)
+ {
+ boolean ready = out.isReady();
+ if (!ready)
+ {
+ _ready.exchange(Boolean.FALSE);
+ return;
+ }
+ _flushed = true;
+ out.flush();
+ }
+
+ if (_close && !_closed)
+ {
+ if (_isReady)
+ {
+ boolean ready = out.isReady();
+ if (!ready)
+ {
+ _ready.exchange(Boolean.FALSE);
+ return;
+ }
+ }
+ _closed = true;
+ out.close();
+ }
+
+ if (_isReady)
+ {
+ boolean ready = out.isReady();
+ if (!ready)
+ {
+ _ready.exchange(Boolean.FALSE);
+ return;
+ }
+ }
+ context.complete();
+ _ready.exchange(Boolean.TRUE);
+ }
+ }
+ catch (InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void onError(Throwable t)
+ {
+ t.printStackTrace();
+ }
+ });
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("AWCH{w=%s,cl=%b,ir=%b,f=%b,c=%b,d=%d}", _write, _contentLength, _isReady, _flush, _close, _data.length());
+ }
+ }
+
+ public static Stream<Arguments> blockingWriteTests()
+ {
+ List<Object[]> tests = new ArrayList<>();
+ for (WriteStyle w : WriteStyle.values())
+ {
+ for (boolean contentLength : new Boolean[]{true, false})
+ {
+ for (boolean flush : new Boolean[]{true, false})
+ {
+ for (boolean close : new Boolean[]{true, false})
+ {
+ for (String data : new String[]{SMALL, LARGE})
+ {
+ tests.add(new Object[]{new BlockingWriteHandler(w, contentLength, flush, close, data)});
+ }
+ }
+ }
+ }
+ }
+ return tests.stream().map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("blockingWriteTests")
+ public void testBlockingWrite(BlockingWriteHandler handler) throws Exception
+ {
+ configureServer(handler);
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+ InputStream in = client.getInputStream();
+
+ // write the request
+ os.write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
+ os.flush();
+
+ handler.wait4handle();
+
+ // Wait for full completion
+ long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(WAIT);
+ while (!__transportComplete.get())
+ {
+ if (System.nanoTime() > end)
+ throw new TimeoutException();
+
+ // proceed with any delayCBs needed for completion
+ try
+ {
+ PendingCallback delay = __queue.poll(POLL, TimeUnit.MILLISECONDS);
+ if (delay != null)
+ delay.proceed();
+ }
+ catch (Exception e)
+ {
+ // ignored
+ }
+ }
+
+ // Check we got a response!
+ HttpTester.Response response = HttpTester.parseResponse(in);
+ assertThat(response, Matchers.notNullValue());
+ assertThat(response.getStatus(), is(200));
+ String content = response.getContent();
+ assertThat(content, containsString(handler.getExpectedMessage()));
+ }
+ }
+
+ private static class BlockingWriteHandler extends AbstractHandler
+ {
+ final WriteStyle _write;
+ final boolean _contentLength;
+ final boolean _flush;
+ final boolean _close;
+ final String _data;
+ final CountDownLatch _wait = new CountDownLatch(1);
+
+ BlockingWriteHandler(WriteStyle write, boolean contentLength, boolean flush, boolean close, String data)
+ {
+ _write = write;
+ _contentLength = contentLength;
+ _flush = flush;
+ _close = close;
+ _data = data;
+ }
+
+ public String getExpectedMessage()
+ {
+ return SMALL;
+ }
+
+ public void wait4handle()
+ {
+ try
+ {
+ Assertions.assertTrue(_wait.await(WAIT, TimeUnit.SECONDS));
+ }
+ catch (InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ AsyncContext context = request.startAsync();
+ ServletOutputStream out = response.getOutputStream();
+
+ context.start(() ->
+ {
+ try
+ {
+ _wait.countDown();
+
+ response.setContentType("text/plain");
+ byte[] bytes = _data.getBytes(StandardCharsets.ISO_8859_1);
+ if (_contentLength)
+ response.setContentLength(bytes.length);
+
+ switch (_write)
+ {
+ case ARRAY:
+ out.write(bytes, 0, bytes.length);
+ break;
+
+ case BUFFER:
+ ((HttpOutput)out).write(BufferUtil.toBuffer(bytes));
+ break;
+
+ case BYTE:
+ for (byte b : bytes)
+ {
+ out.write(b);
+ }
+ break;
+
+ case BYTE_THEN_ARRAY:
+ out.write(bytes[0]); // This should always aggregate
+ out.write(bytes, 1, bytes.length - 1);
+ break;
+
+ case PRINT:
+ out.print(_data);
+ break;
+ }
+
+ if (_flush)
+ out.flush();
+
+ if (_close)
+ out.close();
+
+ context.complete();
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("BWCH{w=%s,cl=%b,f=%b,c=%b,d=%d}", _write, _contentLength, _flush, _close, _data.length());
+ }
+ }
+
+ public static Stream<Arguments> sendContentTests()
+ {
+ List<Object[]> tests = new ArrayList<>();
+ for (ContentStyle style : ContentStyle.values())
+ {
+ for (String data : new String[]{SMALL, LARGE})
+ {
+ tests.add(new Object[]{new SendContentHandler(style, data)});
+ }
+ }
+ return tests.stream().map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("sendContentTests")
+ public void testSendContent(SendContentHandler handler) throws Exception
+ {
+ configureServer(handler);
+
+ int base = _threadPool.getBusyThreads();
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+ InputStream in = client.getInputStream();
+
+ // write the request
+ os.write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
+ os.flush();
+
+ handler.wait4handle();
+
+ long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(WAIT);
+ while (_threadPool.getBusyThreads() != base)
+ {
+ if (System.nanoTime() > end)
+ throw new TimeoutException();
+ Thread.sleep(POLL);
+ }
+
+ // Wait for full completion
+ end = System.nanoTime() + TimeUnit.SECONDS.toNanos(WAIT);
+ while (!__transportComplete.get())
+ {
+ if (System.nanoTime() > end)
+ throw new TimeoutException();
+
+ // proceed with any delayCBs needed for completion
+ try
+ {
+ PendingCallback delay = __queue.poll(POLL, TimeUnit.MILLISECONDS);
+ if (delay != null)
+ delay.proceed();
+ }
+ catch (Exception e)
+ {
+ // ignored
+ }
+ }
+
+ // Check we got a response!
+ HttpTester.Response response = HttpTester.parseResponse(in);
+ assertThat(response, Matchers.notNullValue());
+ assertThat(response.getStatus(), is(200));
+ String content = response.getContent();
+ assertThat(content, containsString(handler.getExpectedMessage()));
+ }
+ }
+
+ enum ContentStyle
+ {
+ BUFFER, STREAM
+ // TODO more types needed here
+ }
+
+ private static class SendContentHandler extends AbstractHandler
+ {
+ final ContentStyle _style;
+ final String _data;
+ final CountDownLatch _wait = new CountDownLatch(1);
+
+ SendContentHandler(ContentStyle style, String data)
+ {
+ _style = style;
+ _data = data;
+ }
+
+ public String getExpectedMessage()
+ {
+ return SMALL;
+ }
+
+ public void wait4handle()
+ {
+ try
+ {
+ Assertions.assertTrue(_wait.await(WAIT, TimeUnit.SECONDS));
+ }
+ catch (InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ AsyncContext context = request.startAsync();
+ HttpOutput out = (HttpOutput)response.getOutputStream();
+
+ response.setContentType("text/plain");
+ byte[] bytes = _data.getBytes(StandardCharsets.ISO_8859_1);
+
+ switch (_style)
+ {
+ case BUFFER:
+ out.sendContent(BufferUtil.toBuffer(bytes), Callback.from(context::complete));
+ break;
+
+ case STREAM:
+ out.sendContent(new ByteArrayInputStream(bytes), Callback.from(context::complete));
+ break;
+ }
+
+ _wait.countDown();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("SCCH{w=%s,d=%d}", _style, _data.length());
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncRequestReadTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncRequestReadTest.java
new file mode 100644
index 0000000..66fc369
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncRequestReadTest.java
@@ -0,0 +1,364 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.IO;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class AsyncRequestReadTest
+{
+ private static Server server;
+ private static ServerConnector connector;
+ private static final BlockingQueue<Long> __total = new BlockingArrayQueue<>();
+
+ @BeforeEach
+ public void startServer() throws Exception
+ {
+ server = new Server();
+ connector = new ServerConnector(server);
+ connector.setIdleTimeout(10000);
+ connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setSendDateHeader(false);
+ server.addConnector(connector);
+ }
+
+ @AfterEach
+ public void stopServer() throws Exception
+ {
+ server.stop();
+ server.join();
+ }
+
+ @Test
+ public void testPipelined() throws Exception
+ {
+ server.setHandler(new AsyncStreamHandler());
+ server.start();
+
+ try (final Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ socket.setSoTimeout(1000);
+
+ byte[] content = new byte[32 * 4096];
+ Arrays.fill(content, (byte)120);
+
+ OutputStream out = socket.getOutputStream();
+ String header =
+ "POST / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: " + content.length + "\r\n" +
+ "Content-Type: bytes\r\n" +
+ "\r\n";
+ byte[] h = header.getBytes(StandardCharsets.ISO_8859_1);
+ out.write(h);
+ out.write(content);
+
+ header =
+ "POST / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: " + content.length + "\r\n" +
+ "Content-Type: bytes\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ h = header.getBytes(StandardCharsets.ISO_8859_1);
+ out.write(h);
+ out.write(content);
+ out.flush();
+
+ InputStream in = socket.getInputStream();
+ String response = IO.toString(in);
+ assertTrue(response.indexOf("200 OK") > 0);
+
+ long total = __total.poll(5, TimeUnit.SECONDS);
+ assertEquals(content.length, total);
+ total = __total.poll(5, TimeUnit.SECONDS);
+ assertEquals(content.length, total);
+ }
+ }
+
+ @Test
+ public void testAsyncReadsWithDelays() throws Exception
+ {
+ server.setHandler(new AsyncStreamHandler());
+ server.start();
+
+ asyncReadTest(64, 4, 4, 20);
+ asyncReadTest(256, 16, 16, 50);
+ asyncReadTest(256, 1, 128, 10);
+ asyncReadTest(128 * 1024, 1, 64, 10);
+ asyncReadTest(256 * 1024, 5321, 10, 100);
+ asyncReadTest(512 * 1024, 32 * 1024, 10, 10);
+ }
+
+ public void asyncReadTest(int contentSize, int chunkSize, int chunks, int delayMS) throws Exception
+ {
+ String tst = contentSize + "," + chunkSize + "," + chunks + "," + delayMS;
+ //System.err.println(tst);
+
+ try (final Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+
+ byte[] content = new byte[contentSize];
+ Arrays.fill(content, (byte)120);
+
+ OutputStream out = socket.getOutputStream();
+ out.write("POST / HTTP/1.1\r\n".getBytes());
+ out.write("Host: localhost\r\n".getBytes());
+ out.write(("Content-Length: " + content.length + "\r\n").getBytes());
+ out.write("Content-Type: bytes\r\n".getBytes());
+ out.write("Connection: close\r\n".getBytes());
+ out.write("\r\n".getBytes());
+ out.flush();
+
+ int offset = 0;
+ for (int i = 0; i < chunks; i++)
+ {
+ out.write(content, offset, chunkSize);
+ offset += chunkSize;
+ Thread.sleep(delayMS);
+ }
+ out.write(content, offset, content.length - offset);
+
+ out.flush();
+
+ InputStream in = socket.getInputStream();
+ String response = IO.toString(in);
+ assertThat(response, containsString("200 OK"));
+
+ long total = __total.poll(30, TimeUnit.SECONDS);
+ assertEquals(content.length, total, tst);
+ }
+ }
+
+ private static class AsyncStreamHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String path, final Request request, HttpServletRequest httpRequest, final HttpServletResponse httpResponse) throws IOException, ServletException
+ {
+ httpResponse.setStatus(500);
+ request.setHandled(true);
+
+ final AsyncContext async = request.startAsync();
+ // System.err.println("handle "+request.getContentLength());
+
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ long total = 0;
+ try (InputStream in = request.getInputStream();)
+ {
+ // System.err.println("reading...");
+
+ byte[] b = new byte[4 * 4096];
+ int read;
+ while ((read = in.read(b)) >= 0)
+ {
+ total += read;
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ total = -1;
+ }
+ finally
+ {
+ httpResponse.setStatus(200);
+ async.complete();
+ // System.err.println("read "+total);
+ __total.offer(total);
+ }
+ }
+ }.start();
+ }
+ }
+
+ @Test
+ public void testPartialRead() throws Exception
+ {
+ server.setHandler(new PartialReaderHandler());
+ server.start();
+
+ try (final Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ socket.setSoTimeout(10000);
+
+ byte[] content = new byte[32 * 4096];
+ Arrays.fill(content, (byte)88);
+
+ OutputStream out = socket.getOutputStream();
+ String header =
+ "POST /?read=10 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: " + content.length + "\r\n" +
+ "Content-Type: bytes\r\n" +
+ "\r\n";
+ byte[] h = header.getBytes(StandardCharsets.ISO_8859_1);
+ out.write(h);
+ out.write(content);
+
+ header = "POST /?read=10 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: " + content.length + "\r\n" +
+ "Content-Type: bytes\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ h = header.getBytes(StandardCharsets.ISO_8859_1);
+ out.write(h);
+ out.write(content);
+ out.flush();
+
+ BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ assertThat(in.readLine(), containsString("HTTP/1.1 200 OK"));
+ assertThat(in.readLine(), containsString("Content-Length: 11"));
+ assertThat(in.readLine(), containsString("Server:"));
+ in.readLine();
+ assertThat(in.readLine(), containsString("XXXXXXX"));
+ assertThat(in.readLine(), containsString("HTTP/1.1 200 OK"));
+ assertThat(in.readLine(), containsString("Connection: close"));
+ assertThat(in.readLine(), containsString("Content-Length: 11"));
+ assertThat(in.readLine(), containsString("Server:"));
+ in.readLine();
+ assertThat(in.readLine(), containsString("XXXXXXX"));
+ }
+ }
+
+ @Test
+ public void testPartialReadThenShutdown() throws Exception
+ {
+ server.setHandler(new PartialReaderHandler());
+ server.start();
+
+ try (final Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ socket.setSoTimeout(10000);
+
+ byte[] content = new byte[32 * 4096];
+ Arrays.fill(content, (byte)88);
+
+ OutputStream out = socket.getOutputStream();
+ String header =
+ "POST /?read=10 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: " + content.length + "\r\n" +
+ "Content-Type: bytes\r\n" +
+ "\r\n";
+ byte[] h = header.getBytes(StandardCharsets.ISO_8859_1);
+ out.write(h);
+ out.write(content, 0, 4096);
+ out.flush();
+ socket.shutdownOutput();
+
+ BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ assertThat(in.readLine(), containsString("HTTP/1.1 200 OK"));
+ assertThat(in.readLine(), containsString("Content-Length:"));
+ assertThat(in.readLine(), containsString("Connection: close"));
+ assertThat(in.readLine(), containsString("Server:"));
+ in.readLine();
+ assertThat(in.readLine(), containsString("XXXXXXX"));
+ }
+ }
+
+ @Test
+ public void testPartialReadThenClose() throws Exception
+ {
+ server.setHandler(new PartialReaderHandler());
+ server.start();
+
+ try (final Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ socket.setSoTimeout(1000);
+
+ byte[] content = new byte[32 * 4096];
+ Arrays.fill(content, (byte)88);
+
+ OutputStream out = socket.getOutputStream();
+ String header =
+ "POST /?read=10 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: " + content.length + "\r\n" +
+ "Content-Type: bytes\r\n" +
+ "\r\n";
+ byte[] h = header.getBytes(StandardCharsets.ISO_8859_1);
+ out.write(h);
+ out.write(content, 0, 4096);
+ out.flush();
+
+ BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ assertThat(in.readLine(), containsString("HTTP/1.1 200 OK"));
+ assertThat(in.readLine(), containsString("Connection: close"));
+ assertThat(in.readLine(), containsString("Content-Length:"));
+ assertThat(in.readLine(), containsString("Server:"));
+ in.readLine();
+ assertThat(in.readLine(), containsString("XXXXXXX"));
+
+ socket.close();
+ }
+ }
+
+ private static class PartialReaderHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String path, final Request request, HttpServletRequest httpRequest, final HttpServletResponse httpResponse) throws IOException, ServletException
+ {
+ httpResponse.setStatus(200);
+ request.setHandled(true);
+
+ BufferedReader in = request.getReader();
+ PrintWriter out = httpResponse.getWriter();
+ int read = Integer.parseInt(request.getParameter("read"));
+ // System.err.println("read="+read);
+ for (int i = read; i-- > 0; )
+ {
+ int c = in.read();
+ // System.err.println("in="+c);
+ if (c < 0)
+ break;
+ out.write(c);
+ }
+ out.write('\n');
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncStressTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncStressTest.java
new file mode 100644
index 0000000..eeae3b4
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncStressTest.java
@@ -0,0 +1,353 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Random;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.AsyncContext;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.handler.HandlerWrapper;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@Disabled
+@Tag("stress")
+public class AsyncStressTest
+{
+ private static final Logger LOG = Log.getLogger(AsyncStressTest.class);
+
+ protected QueuedThreadPool _threads = new QueuedThreadPool();
+ protected Server _server = new Server(_threads);
+ protected SuspendHandler _handler = new SuspendHandler();
+ protected ServerConnector _connector;
+ protected InetAddress _addr;
+ protected int _port;
+ protected Random _random = new Random();
+ private static final String[][] __paths =
+ {
+ {"/path", "NORMAL"},
+ {"/path/info", "NORMAL"},
+ {"/path?sleep=<PERIOD>", "SLEPT"},
+ {"/path?suspend=<PERIOD>", "TIMEOUT"},
+ {"/path?suspend=60000&resume=<PERIOD>", "RESUMED"},
+ {"/path?suspend=60000&complete=<PERIOD>", "COMPLETED"},
+ };
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ _server.manage(_threads);
+ _threads.setMaxThreads(50);
+ _connector = new ServerConnector(_server);
+ _connector.setIdleTimeout(120000);
+ _server.setConnectors(new Connector[]{_connector});
+ _server.setHandler(_handler);
+ _server.start();
+ _port = _connector.getLocalPort();
+ _addr = InetAddress.getLocalHost();
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ _server.stop();
+ _server.join();
+ }
+
+ @Test
+ public void testAsync() throws Throwable
+ {
+ doConnections(1600, 240);
+ }
+
+ private void doConnections(int connections, final int loops) throws Throwable
+ {
+ Socket[] socket = new Socket[connections];
+ int[][] path = new int[connections][loops];
+ for (int i = 0; i < connections; i++)
+ {
+ socket[i] = new Socket(_addr, _port);
+ socket[i].setSoTimeout(30000);
+ if (i % 10 == 0)
+ Thread.sleep(50);
+ if (i % 80 == 0)
+ System.err.println();
+ System.err.print('+');
+ }
+ System.err.println();
+ LOG.info("Bound " + connections);
+
+ for (int l = 0; l < loops; l++)
+ {
+ for (int i = 0; i < connections; i++)
+ {
+ int p = path[i][l] = _random.nextInt(__paths.length);
+
+ int period = _random.nextInt(290) + 10;
+ String uri = StringUtil.replace(__paths[p][0], "<PERIOD>", Integer.toString(period));
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ String request =
+ "GET " + uri + " HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "start: " + start + "\r\n" +
+ "result: " + __paths[p][1] + "\r\n" +
+ ((l + 1 < loops) ? "" : "Connection: close\r\n") +
+ "\r\n";
+ socket[i].getOutputStream().write(request.getBytes(StandardCharsets.UTF_8));
+ socket[i].getOutputStream().flush();
+ }
+ if (l % 80 == 0)
+ System.err.println();
+ System.err.print('.');
+ Thread.sleep(_random.nextInt(290) + 10);
+ }
+
+ System.err.println();
+ LOG.info("Sent " + (loops * __paths.length) + " requests");
+
+ String[] results = new String[connections];
+ for (int i = 0; i < connections; i++)
+ {
+ results[i] = IO.toString(socket[i].getInputStream(), StandardCharsets.UTF_8);
+ if (i % 80 == 0)
+ System.err.println();
+ System.err.print('-');
+ }
+ System.err.println();
+
+ LOG.info("Read " + connections + " connections");
+
+ for (int i = 0; i < connections; i++)
+ {
+ int offset = 0;
+ String result = results[i];
+ for (int l = 0; l < loops; l++)
+ {
+ String expect = __paths[path[i][l]][1];
+ expect = expect + " " + expect;
+
+ offset = result.indexOf("200 OK", offset) + 6;
+ offset = result.indexOf("\r\n\r\n", offset) + 4;
+ int end = result.indexOf("\n", offset);
+ String r = result.substring(offset, end).trim();
+ assertEquals(i + "," + l, expect, r);
+ offset = end;
+ }
+ }
+ }
+
+ private static class SuspendHandler extends HandlerWrapper
+ {
+ private final Timer _timer;
+
+ private SuspendHandler()
+ {
+ _timer = new Timer();
+ }
+
+ @Override
+ public void handle(String target, final Request baseRequest, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException
+ {
+ int readBefore = 0;
+ long sleepFor = -1;
+ long suspendFor = -1;
+ long resumeAfter = -1;
+ long completeAfter = -1;
+
+ final String uri = baseRequest.getHttpURI().toString();
+
+ if (request.getParameter("read") != null)
+ readBefore = Integer.parseInt(request.getParameter("read"));
+ if (request.getParameter("sleep") != null)
+ sleepFor = Integer.parseInt(request.getParameter("sleep"));
+ if (request.getParameter("suspend") != null)
+ suspendFor = Integer.parseInt(request.getParameter("suspend"));
+ if (request.getParameter("resume") != null)
+ resumeAfter = Integer.parseInt(request.getParameter("resume"));
+ if (request.getParameter("complete") != null)
+ completeAfter = Integer.parseInt(request.getParameter("complete"));
+
+ if (DispatcherType.REQUEST.equals(baseRequest.getDispatcherType()))
+ {
+ if (readBefore > 0)
+ {
+ byte[] buf = new byte[readBefore];
+ request.getInputStream().read(buf);
+ }
+ else if (readBefore < 0)
+ {
+ InputStream in = request.getInputStream();
+ int b = in.read();
+ while (b != -1)
+ {
+ b = in.read();
+ }
+ }
+
+ if (suspendFor >= 0)
+ {
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ asyncContext.addListener(__asyncListener);
+ if (suspendFor > 0)
+ asyncContext.setTimeout(suspendFor);
+ if (completeAfter > 0)
+ {
+ TimerTask complete = new TimerTask()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ response.setStatus(200);
+ response.getOutputStream().println("COMPLETED " + request.getHeader("result"));
+ baseRequest.setHandled(true);
+ asyncContext.complete();
+ }
+ catch (Exception e)
+ {
+ Request br = (Request)asyncContext.getRequest();
+ System.err.println("\n" + e.toString());
+ System.err.println(baseRequest + "==" + br);
+ System.err.println(uri + "==" + br.getHttpURI());
+ System.err.println(asyncContext + "==" + br.getHttpChannelState());
+
+ LOG.warn(e);
+ System.exit(1);
+ }
+ }
+ };
+ synchronized (_timer)
+ {
+ _timer.schedule(complete, completeAfter);
+ }
+ }
+ else if (completeAfter == 0)
+ {
+ response.setStatus(200);
+ response.getOutputStream().println("COMPLETED " + request.getHeader("result"));
+ baseRequest.setHandled(true);
+ asyncContext.complete();
+ }
+ else if (resumeAfter > 0)
+ {
+ TimerTask resume = new TimerTask()
+ {
+ @Override
+ public void run()
+ {
+ asyncContext.dispatch();
+ }
+ };
+ synchronized (_timer)
+ {
+ _timer.schedule(resume, resumeAfter);
+ }
+ }
+ else if (resumeAfter == 0)
+ {
+ asyncContext.dispatch();
+ }
+ }
+ else if (sleepFor >= 0)
+ {
+ try
+ {
+ Thread.sleep(sleepFor);
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ response.setStatus(200);
+ response.getOutputStream().println("SLEPT " + request.getHeader("result"));
+ baseRequest.setHandled(true);
+ }
+ else
+ {
+ response.setStatus(200);
+ response.getOutputStream().println("NORMAL " + request.getHeader("result"));
+ baseRequest.setHandled(true);
+ }
+ }
+ else if (request.getAttribute("TIMEOUT") != null)
+ {
+ response.setStatus(200);
+ response.getOutputStream().println("TIMEOUT " + request.getHeader("result"));
+ baseRequest.setHandled(true);
+ }
+ else
+ {
+ response.setStatus(200);
+ response.getOutputStream().println("RESUMED " + request.getHeader("result"));
+ baseRequest.setHandled(true);
+ }
+ }
+ }
+
+ private static AsyncListener __asyncListener = new AsyncListener()
+ {
+ @Override
+ public void onComplete(AsyncEvent event) throws IOException
+ {
+ }
+
+ @Override
+ public void onTimeout(AsyncEvent event) throws IOException
+ {
+ event.getSuppliedRequest().setAttribute("TIMEOUT", Boolean.TRUE);
+ event.getSuppliedRequest().getAsyncContext().dispatch();
+ }
+
+ @Override
+ public void onError(AsyncEvent event) throws IOException
+ {
+
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event) throws IOException
+ {
+ }
+ };
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/BlockingTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/BlockingTest.java
new file mode 100644
index 0000000..2ce8800
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/BlockingTest.java
@@ -0,0 +1,504 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.servlet.AsyncContext;
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.DefaultHandler;
+import org.eclipse.jetty.server.handler.HandlerList;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.startsWith;
+import static org.hamcrest.core.Is.is;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class BlockingTest
+{
+ private Server server;
+ private ServerConnector connector;
+ private ContextHandler context;
+
+ @BeforeEach
+ public void setUp()
+ {
+ server = new Server();
+ connector = new ServerConnector(server);
+ connector.setPort(0);
+ server.addConnector(connector);
+
+ context = new ContextHandler("/ctx");
+
+ HandlerList handlers = new HandlerList();
+ handlers.setHandlers(new Handler[]{context, new DefaultHandler()});
+ server.setHandler(handlers);
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception
+ {
+ server.stop();
+ }
+
+ @Test
+ public void testBlockingReadThenNormalComplete() throws Exception
+ {
+ CountDownLatch started = new CountDownLatch(1);
+ CountDownLatch stopped = new CountDownLatch(1);
+ AtomicReference<Throwable> readException = new AtomicReference<>();
+ AbstractHandler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ new Thread(() ->
+ {
+ try
+ {
+ int b = baseRequest.getHttpInput().read();
+ if (b == '1')
+ {
+ started.countDown();
+ if (baseRequest.getHttpInput().read() > Integer.MIN_VALUE)
+ throw new IllegalStateException();
+ }
+ }
+ catch (Throwable t)
+ {
+ readException.set(t);
+ stopped.countDown();
+ }
+ }).start();
+
+ try
+ {
+ // wait for thread to start and read first byte
+ started.await(10, TimeUnit.SECONDS);
+ // give it time to block on second byte
+ Thread.sleep(1000);
+ }
+ catch (Throwable e)
+ {
+ throw new ServletException(e);
+ }
+
+ response.setStatus(200);
+ response.setContentType("text/plain");
+ response.getOutputStream().print("OK\r\n");
+ }
+ };
+ context.setHandler(handler);
+ server.start();
+
+ StringBuilder request = new StringBuilder();
+ request.append("POST /ctx/path/info HTTP/1.1\r\n")
+ .append("Host: localhost\r\n")
+ .append("Content-Type: test/data\r\n")
+ .append("Content-Length: 2\r\n")
+ .append("\r\n")
+ .append("1");
+
+ int port = connector.getLocalPort();
+ try (Socket socket = new Socket("localhost", port))
+ {
+ socket.setSoTimeout(10000);
+ OutputStream out = socket.getOutputStream();
+ out.write(request.toString().getBytes(StandardCharsets.ISO_8859_1));
+
+ HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
+ assertThat(response, notNullValue());
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), containsString("OK"));
+
+ // Async thread should have stopped
+ assertTrue(stopped.await(10, TimeUnit.SECONDS));
+ assertThat(readException.get(), instanceOf(IOException.class));
+ }
+ }
+
+ @Test
+ public void testNormalCompleteThenBlockingRead() throws Exception
+ {
+ CountDownLatch started = new CountDownLatch(1);
+ CountDownLatch completed = new CountDownLatch(1);
+ CountDownLatch stopped = new CountDownLatch(1);
+ AtomicReference<Throwable> readException = new AtomicReference<>();
+ AbstractHandler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ new Thread(() ->
+ {
+ try
+ {
+ int b = baseRequest.getHttpInput().read();
+ if (b == '1')
+ {
+ started.countDown();
+ completed.await(10, TimeUnit.SECONDS);
+ Thread.sleep(500);
+ if (baseRequest.getHttpInput().read() > Integer.MIN_VALUE)
+ throw new IllegalStateException();
+ }
+ }
+ catch (Throwable t)
+ {
+ readException.set(t);
+ stopped.countDown();
+ }
+ }).start();
+
+ try
+ {
+ // wait for thread to start and read first byte
+ started.await(10, TimeUnit.SECONDS);
+ // give it time to block on second byte
+ Thread.sleep(1000);
+ }
+ catch (Throwable e)
+ {
+ throw new ServletException(e);
+ }
+
+ response.setStatus(200);
+ response.setContentType("text/plain");
+ response.getOutputStream().print("OK\r\n");
+ }
+ };
+ context.setHandler(handler);
+ server.start();
+
+ StringBuilder request = new StringBuilder();
+ request.append("POST /ctx/path/info HTTP/1.1\r\n")
+ .append("Host: localhost\r\n")
+ .append("Content-Type: test/data\r\n")
+ .append("Content-Length: 2\r\n")
+ .append("\r\n")
+ .append("1");
+
+ int port = connector.getLocalPort();
+ try (Socket socket = new Socket("localhost", port))
+ {
+ socket.setSoTimeout(10000);
+ OutputStream out = socket.getOutputStream();
+ out.write(request.toString().getBytes(StandardCharsets.ISO_8859_1));
+
+ HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
+ assertThat(response, notNullValue());
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), containsString("OK"));
+
+ completed.countDown();
+ Thread.sleep(1000);
+
+ // Async thread should have stopped
+ assertTrue(stopped.await(10, TimeUnit.SECONDS));
+ assertThat(readException.get(), instanceOf(IOException.class));
+ }
+ }
+
+ @Test
+ public void testStartAsyncThenBlockingReadThenTimeout() throws Exception
+ {
+ CountDownLatch started = new CountDownLatch(1);
+ CountDownLatch completed = new CountDownLatch(1);
+ CountDownLatch stopped = new CountDownLatch(1);
+ AtomicReference<Throwable> readException = new AtomicReference<>();
+ AbstractHandler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws ServletException
+ {
+ baseRequest.setHandled(true);
+ if (baseRequest.getDispatcherType() != DispatcherType.ERROR)
+ {
+ AsyncContext async = request.startAsync();
+ async.setTimeout(100);
+
+ new Thread(() ->
+ {
+ try
+ {
+ int b = baseRequest.getHttpInput().read();
+ if (b == '1')
+ {
+ started.countDown();
+ completed.await(10, TimeUnit.SECONDS);
+ Thread.sleep(500);
+ if (baseRequest.getHttpInput().read() > Integer.MIN_VALUE)
+ throw new IllegalStateException();
+ }
+ }
+ catch (Throwable t)
+ {
+ readException.set(t);
+ stopped.countDown();
+ }
+ }).start();
+
+ try
+ {
+ // wait for thread to start and read first byte
+ started.await(10, TimeUnit.SECONDS);
+ // give it time to block on second byte
+ Thread.sleep(1000);
+ }
+ catch (Throwable e)
+ {
+ throw new ServletException(e);
+ }
+ }
+ }
+ };
+ context.setHandler(handler);
+ server.start();
+
+ StringBuilder request = new StringBuilder();
+ request.append("POST /ctx/path/info HTTP/1.1\r\n")
+ .append("Host: localhost\r\n")
+ .append("Content-Type: test/data\r\n")
+ .append("Content-Length: 2\r\n")
+ .append("\r\n")
+ .append("1");
+
+ int port = connector.getLocalPort();
+ try (Socket socket = new Socket("localhost", port))
+ {
+ socket.setSoTimeout(10000);
+ OutputStream out = socket.getOutputStream();
+ out.write(request.toString().getBytes(StandardCharsets.ISO_8859_1));
+
+ HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
+ assertThat(response, notNullValue());
+ assertThat(response.getStatus(), is(500));
+ assertThat(response.getContent(), containsString("AsyncContext timeout"));
+
+ completed.countDown();
+ Thread.sleep(1000);
+
+ // Async thread should have stopped
+ assertTrue(stopped.await(10, TimeUnit.SECONDS));
+ assertThat(readException.get(), instanceOf(IOException.class));
+ }
+ }
+
+ @Test
+ public void testBlockingReadThenSendError() throws Exception
+ {
+ CountDownLatch started = new CountDownLatch(1);
+ CountDownLatch stopped = new CountDownLatch(1);
+ AtomicReference<Throwable> readException = new AtomicReference<>();
+ AbstractHandler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ if (baseRequest.getDispatcherType() != DispatcherType.ERROR)
+ {
+ new Thread(() ->
+ {
+ try
+ {
+ int b = baseRequest.getHttpInput().read();
+ if (b == '1')
+ {
+ started.countDown();
+ if (baseRequest.getHttpInput().read() > Integer.MIN_VALUE)
+ throw new IllegalStateException();
+ }
+ }
+ catch (Throwable t)
+ {
+ readException.set(t);
+ stopped.countDown();
+ }
+ }).start();
+
+ try
+ {
+ // wait for thread to start and read first byte
+ started.await(10, TimeUnit.SECONDS);
+ // give it time to block on second byte
+ Thread.sleep(1000);
+ }
+ catch (Throwable e)
+ {
+ throw new ServletException(e);
+ }
+
+ response.sendError(499);
+ }
+ }
+ };
+ context.setHandler(handler);
+ server.start();
+
+ StringBuilder request = new StringBuilder();
+ request.append("POST /ctx/path/info HTTP/1.1\r\n")
+ .append("Host: localhost\r\n")
+ .append("Content-Type: test/data\r\n")
+ .append("Content-Length: 2\r\n")
+ .append("\r\n")
+ .append("1");
+
+ int port = connector.getLocalPort();
+ try (Socket socket = new Socket("localhost", port))
+ {
+ socket.setSoTimeout(10000);
+ OutputStream out = socket.getOutputStream();
+ out.write(request.toString().getBytes(StandardCharsets.ISO_8859_1));
+
+ HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
+ assertThat(response, notNullValue());
+ assertThat(response.getStatus(), is(499));
+
+ // Async thread should have stopped
+ assertTrue(stopped.await(10, TimeUnit.SECONDS));
+ assertThat(readException.get(), instanceOf(IOException.class));
+ }
+ }
+
+ @Test
+ public void testBlockingWriteThenNormalComplete() throws Exception
+ {
+ CountDownLatch started = new CountDownLatch(1);
+ CountDownLatch stopped = new CountDownLatch(1);
+ AtomicReference<Throwable> readException = new AtomicReference<>();
+ AbstractHandler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.setContentType("text/plain");
+ new Thread(() ->
+ {
+ try
+ {
+ byte[] data = new byte[16 * 1024];
+ Arrays.fill(data, (byte)'X');
+ data[data.length - 2] = '\r';
+ data[data.length - 1] = '\n';
+ OutputStream out = response.getOutputStream();
+ started.countDown();
+ while (true)
+ out.write(data);
+ }
+ catch (Throwable t)
+ {
+ readException.set(t);
+ stopped.countDown();
+ }
+ }).start();
+
+ try
+ {
+ // wait for thread to start and read first byte
+ started.await(10, TimeUnit.SECONDS);
+ // give it time to block on write
+ Thread.sleep(1000);
+ }
+ catch (Throwable e)
+ {
+ throw new ServletException(e);
+ }
+ }
+ };
+ context.setHandler(handler);
+ server.start();
+
+ StringBuilder request = new StringBuilder();
+ request.append("GET /ctx/path/info HTTP/1.1\r\n")
+ .append("Host: localhost\r\n")
+ .append("\r\n");
+
+ int port = connector.getLocalPort();
+ try (Socket socket = new Socket("localhost", port))
+ {
+ socket.setSoTimeout(10000);
+ OutputStream out = socket.getOutputStream();
+ out.write(request.toString().getBytes(StandardCharsets.ISO_8859_1));
+
+ BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.ISO_8859_1));
+
+ // Read the header
+ List<String> header = new ArrayList<>();
+ while (true)
+ {
+ String line = in.readLine();
+ if (line.length() == 0)
+ break;
+ header.add(line);
+ }
+ assertThat(header.get(0), containsString("200 OK"));
+
+ // read one line of content
+ String content = in.readLine();
+ assertThat(content, is("4000"));
+ content = in.readLine();
+ assertThat(content, startsWith("XXXXXXXX"));
+
+ // check that writing thread is stopped by end of request handling
+ assertTrue(stopped.await(10, TimeUnit.SECONDS));
+
+ // read until last line
+ String last = null;
+ while (true)
+ {
+ String line = in.readLine();
+ if (line == null)
+ break;
+
+ last = line;
+ }
+
+ // last line is not empty chunk, ie abnormal completion
+ assertThat(last, startsWith("XXXXX"));
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ClassLoaderDumpTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ClassLoaderDumpTest.java
new file mode 100644
index 0000000..66814f1
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ClassLoaderDumpTest.java
@@ -0,0 +1,196 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLClassLoader;
+
+import org.eclipse.jetty.util.component.Dumpable;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+
+public class ClassLoaderDumpTest
+{
+ @Test
+ public void testSimple() throws Exception
+ {
+ Server server = new Server();
+ ClassLoader loader = new ClassLoader()
+ {
+ public String toString()
+ {
+ return "SimpleLoader";
+ }
+ };
+
+ server.addBean(new ClassLoaderDump(loader));
+
+ StringBuilder out = new StringBuilder();
+ server.dump(out);
+ String dump = out.toString();
+ assertThat(dump, containsString("+- SimpleLoader"));
+ assertThat(dump, containsString("+> " + Server.class.getClassLoader()));
+ }
+
+ @Test
+ public void testParent() throws Exception
+ {
+ Server server = new Server();
+ ClassLoader loader = new ClassLoader(Server.class.getClassLoader())
+ {
+ public String toString()
+ {
+ return "ParentedLoader";
+ }
+ };
+
+ server.addBean(new ClassLoaderDump(loader));
+
+ StringBuilder out = new StringBuilder();
+ server.dump(out);
+ String dump = out.toString();
+ assertThat(dump, containsString("+- ParentedLoader"));
+ assertThat(dump, containsString("| +> " + Server.class.getClassLoader()));
+ assertThat(dump, containsString("+> " + Server.class.getClassLoader()));
+ }
+
+ @Test
+ public void testNested() throws Exception
+ {
+ Server server = new Server();
+ ClassLoader middleLoader = new ClassLoader(Server.class.getClassLoader())
+ {
+ public String toString()
+ {
+ return "MiddleLoader";
+ }
+ };
+ ClassLoader loader = new ClassLoader(middleLoader)
+ {
+ public String toString()
+ {
+ return "TopLoader";
+ }
+ };
+
+ server.addBean(new ClassLoaderDump(loader));
+
+ StringBuilder out = new StringBuilder();
+ server.dump(out);
+ String dump = out.toString();
+ assertThat(dump, containsString("+- TopLoader"));
+ assertThat(dump, containsString("| +> MiddleLoader"));
+ assertThat(dump, containsString("| +> " + Server.class.getClassLoader()));
+ assertThat(dump, containsString("+> " + Server.class.getClassLoader()));
+ }
+
+ @Test
+ public void testDumpable() throws Exception
+ {
+ Server server = new Server();
+ ClassLoader middleLoader = new DumpableClassLoader(Server.class.getClassLoader());
+ ClassLoader loader = new ClassLoader(middleLoader)
+ {
+ public String toString()
+ {
+ return "TopLoader";
+ }
+ };
+
+ server.addBean(new ClassLoaderDump(loader));
+
+ StringBuilder out = new StringBuilder();
+ server.dump(out);
+ String dump = out.toString();
+ assertThat(dump, containsString("+- TopLoader"));
+ assertThat(dump, containsString("| +> DumpableClassLoader"));
+ assertThat(dump, not(containsString("| +> " + Server.class.getClassLoader())));
+ assertThat(dump, containsString("+> " + Server.class.getClassLoader()));
+ }
+
+ public static class DumpableClassLoader extends ClassLoader implements Dumpable
+ {
+ public DumpableClassLoader(ClassLoader parent)
+ {
+ super(parent);
+ }
+
+ @Override
+ public String dump()
+ {
+ return "DumpableClassLoader";
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ out.append(dump()).append('\n');
+ }
+
+ public String toString()
+ {
+ return "DumpableClassLoader";
+ }
+ }
+
+ @Test
+ public void testUrlClassLoaders() throws Exception
+ {
+ Server server = new Server();
+ ClassLoader middleLoader = new URLClassLoader(new URL[]
+ {new URL("file:/one"), new URL("file:/two"), new URL("file:/three")},
+ Server.class.getClassLoader())
+ {
+ public String toString()
+ {
+ return "MiddleLoader";
+ }
+ };
+ ClassLoader loader = new URLClassLoader(new URL[]
+ {new URL("file:/ONE"), new URL("file:/TWO"), new URL("file:/THREE")},
+ middleLoader)
+ {
+ public String toString()
+ {
+ return "TopLoader";
+ }
+ };
+
+ server.addBean(new ClassLoaderDump(loader));
+
+ StringBuilder out = new StringBuilder();
+ server.dump(out);
+ String dump = out.toString();
+ // System.err.println(dump);
+ assertThat(dump, containsString("+- TopLoader"));
+ assertThat(dump, containsString("| | +> file:/ONE"));
+ assertThat(dump, containsString("| | +> file:/TWO"));
+ assertThat(dump, containsString("| | +> file:/THREE"));
+ assertThat(dump, containsString("| +> MiddleLoader"));
+ assertThat(dump, containsString("| | +> file:/one"));
+ assertThat(dump, containsString("| | +> file:/two"));
+ assertThat(dump, containsString("| | +> file:/three"));
+ assertThat(dump, containsString("| +> " + Server.class.getClassLoader()));
+ assertThat(dump, containsString("+> " + Server.class.getClassLoader()));
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectionOpenCloseTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectionOpenCloseTest.java
new file mode 100644
index 0000000..8460a67
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectionOpenCloseTest.java
@@ -0,0 +1,236 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ConnectionOpenCloseTest extends AbstractHttpTest
+{
+ @Test
+ @Tag("Slow")
+ @DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review
+ public void testOpenClose() throws Exception
+ {
+ server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ {
+ throw new IllegalStateException();
+ }
+ });
+ server.start();
+
+ final AtomicInteger callbacks = new AtomicInteger();
+ final CountDownLatch openLatch = new CountDownLatch(1);
+ final CountDownLatch closeLatch = new CountDownLatch(1);
+ connector.addBean(new Connection.Listener.Adapter()
+ {
+ @Override
+ public void onOpened(Connection connection)
+ {
+ callbacks.incrementAndGet();
+ openLatch.countDown();
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ callbacks.incrementAndGet();
+ closeLatch.countDown();
+ }
+ });
+
+ try (Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ socket.setSoTimeout((int)connector.getIdleTimeout());
+
+ assertTrue(openLatch.await(5, TimeUnit.SECONDS));
+ socket.shutdownOutput();
+ assertTrue(closeLatch.await(5, TimeUnit.SECONDS));
+ String response = IO.toString(socket.getInputStream());
+ assertEquals(0, response.length());
+
+ // Wait some time to see if the callbacks are called too many times
+ TimeUnit.MILLISECONDS.sleep(200);
+ assertEquals(2, callbacks.get());
+ }
+ }
+
+ @Test
+ @Tag("Slow")
+ @DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review
+ public void testOpenRequestClose() throws Exception
+ {
+ server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ {
+ baseRequest.setHandled(true);
+ }
+ });
+ server.start();
+
+ final AtomicInteger callbacks = new AtomicInteger();
+ final CountDownLatch openLatch = new CountDownLatch(1);
+ final CountDownLatch closeLatch = new CountDownLatch(1);
+ connector.addBean(new Connection.Listener.Adapter()
+ {
+ @Override
+ public void onOpened(Connection connection)
+ {
+ callbacks.incrementAndGet();
+ openLatch.countDown();
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ callbacks.incrementAndGet();
+ closeLatch.countDown();
+ }
+ });
+
+ try (Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ socket.setSoTimeout((int)connector.getIdleTimeout());
+ OutputStream output = socket.getOutputStream();
+ output.write((
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost:" + connector.getLocalPort() + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n").getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ InputStream inputStream = socket.getInputStream();
+ HttpTester.Response response = HttpTester.parseResponse(inputStream);
+ assertThat("Status Code", response.getStatus(), is(200));
+
+ assertEquals(-1, inputStream.read());
+ socket.close();
+
+ assertTrue(openLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(closeLatch.await(5, TimeUnit.SECONDS));
+
+ // Wait some time to see if the callbacks are called too many times
+ TimeUnit.SECONDS.sleep(1);
+
+ assertEquals(2, callbacks.get());
+ }
+ }
+
+ @Test
+ @Tag("Slow")
+ @DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review
+ public void testSSLOpenRequestClose() throws Exception
+ {
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ File keystore = MavenTestingUtils.getTestResourceFile("keystore");
+ sslContextFactory.setKeyStoreResource(Resource.newResource(keystore));
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+ server.addBean(sslContextFactory);
+
+ server.removeConnector(connector);
+ connector = new ServerConnector(server, sslContextFactory);
+ server.addConnector(connector);
+
+ server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ {
+ baseRequest.setHandled(true);
+ }
+ });
+ server.start();
+
+ final AtomicInteger callbacks = new AtomicInteger();
+ final CountDownLatch openLatch = new CountDownLatch(2);
+ final CountDownLatch closeLatch = new CountDownLatch(2);
+ connector.addBean(new Connection.Listener.Adapter()
+ {
+ @Override
+ public void onOpened(Connection connection)
+ {
+ callbacks.incrementAndGet();
+ openLatch.countDown();
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ callbacks.incrementAndGet();
+ closeLatch.countDown();
+ }
+ });
+
+ Socket socket = sslContextFactory.getSslContext().getSocketFactory().createSocket("localhost", connector.getLocalPort());
+ socket.setSoTimeout((int)connector.getIdleTimeout());
+ OutputStream output = socket.getOutputStream();
+ output.write((
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost:" + connector.getLocalPort() + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n").getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ // Read to EOF
+ String response = BufferUtil.toString(ByteBuffer.wrap(IO.readBytes(socket.getInputStream())));
+ assertThat(response, Matchers.containsString("200 OK"));
+ socket.close();
+
+ assertTrue(openLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(closeLatch.await(5, TimeUnit.SECONDS));
+
+ // Wait some time to see if the callbacks are called too many times
+ TimeUnit.SECONDS.sleep(1);
+
+ assertEquals(4, callbacks.get());
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorCloseTestBase.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorCloseTestBase.java
new file mode 100644
index 0000000..4cc3c6b
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorCloseTestBase.java
@@ -0,0 +1,257 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * HttpServer Tester.
+ */
+public abstract class ConnectorCloseTestBase extends HttpServerTestFixture
+{
+ private static String __content =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In quis felis nunc. " +
+ "Quisque suscipit mauris et ante auctor ornare rhoncus lacus aliquet. Pellentesque " +
+ "habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. " +
+ "Vestibulum sit amet felis augue, vel convallis dolor. Cras accumsan vehicula diam " +
+ "at faucibus. Etiam in urna turpis, sed congue mi. Morbi et lorem eros. Donec vulputate " +
+ "velit in risus suscipit lobortis. Aliquam id urna orci, nec sollicitudin ipsum. " +
+ "Cras a orci turpis. Donec suscipit vulputate cursus. Mauris nunc tellus, fermentum " +
+ "eu auctor ut, mollis at diam. Quisque porttitor ultrices metus, vitae tincidunt massa " +
+ "sollicitudin a. Vivamus porttitor libero eget purus hendrerit cursus. Integer aliquam " +
+ "consequat mauris quis luctus. Cras enim nibh, dignissim eu faucibus ac, mollis nec neque. " +
+ "Aliquam purus mauris, consectetur nec convallis lacinia, porta sed ante. Suspendisse " +
+ "et cursus magna. Donec orci enim, molestie a lobortis eu, imperdiet vitae neque.";
+ private static int __length = __content.length();
+
+ @Test
+ public void testCloseBetweenRequests() throws Exception
+ {
+ final int requestCount = 32;
+ final CountDownLatch latch = new CountDownLatch(requestCount);
+
+ configureServer(new HelloWorldHandler());
+ URI uri = _server.getURI();
+
+ try (Socket client = newSocket(uri.getHost(), uri.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ ResponseReader reader = new ResponseReader(client)
+ {
+ private int _index = 0;
+
+ @Override
+ protected int doRead() throws IOException, InterruptedException
+ {
+ int count = super.doRead();
+ if (count > 0)
+ {
+ int idx;
+ while ((idx = _response.indexOf("HTTP/1.1 200 OK", _index)) >= 0)
+ {
+ latch.countDown();
+ _index = idx + 15;
+ }
+ }
+
+ return count;
+ }
+ };
+
+ Thread runner = new Thread(reader);
+ runner.start();
+
+ for (int pipeline = 1; pipeline <= requestCount; pipeline++)
+ {
+ if (pipeline == requestCount / 2)
+ {
+ // wait for at least 1 request to have been received
+ if (latch.getCount() == requestCount)
+ Thread.sleep(1);
+ _connector.close();
+ }
+
+ String request =
+ "GET /data?writes=1&block=16&id=" + pipeline + " HTTP/1.1\r\n" +
+ "host: " + uri.getHost() + ":" + uri.getPort() + "\r\n" +
+ "user-agent: testharness/1.0 (blah foo/bar)\r\n" +
+ "accept-encoding: nothing\r\n" +
+ "cookie: aaa=1234567890\r\n" +
+ "\r\n";
+ os.write(request.getBytes());
+ os.flush();
+
+ Thread.sleep(25);
+ }
+
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+
+ reader.setDone();
+ runner.join();
+ }
+ }
+
+ private int iterations(int cnt)
+ {
+ return cnt > 0 ? iterations(--cnt) + cnt : 0;
+ }
+
+ @Test
+ public void testCloseBetweenChunks() throws Exception
+ {
+ configureServer(new EchoHandler());
+
+ URI uri = _server.getURI();
+
+ try (Socket client = newSocket(uri.getHost(), uri.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ ResponseReader reader = new ResponseReader(client);
+ Thread runner = new Thread(reader);
+ runner.start();
+
+ byte[] bytes = __content.getBytes("utf-8");
+
+ os.write((
+ "POST /echo?charset=utf-8 HTTP/1.1\r\n" +
+ "host: " + uri.getHost() + ":" + uri.getPort() + "\r\n" +
+ "content-type: text/plain; charset=utf-8\r\n" +
+ "content-length: " + bytes.length + "\r\n" +
+ "\r\n"
+ ).getBytes(StandardCharsets.ISO_8859_1));
+
+ int len = bytes.length;
+ int offset = 0;
+ int stop = len / 2;
+ while (offset < stop)
+ {
+ os.write(bytes, offset, 64);
+ offset += 64;
+ Thread.sleep(25);
+ }
+
+ _connector.close();
+
+ while (offset < len)
+ {
+ os.write(bytes, offset, len - offset <= 64 ? len - offset : 64);
+ offset += 64;
+ Thread.sleep(25);
+ }
+ os.flush();
+
+ reader.setDone();
+ runner.join();
+
+ String in = reader.getResponse().toString();
+ assertTrue(in.indexOf(__content.substring(__length - 64)) > 0);
+ }
+ }
+
+ public class ResponseReader implements Runnable
+ {
+ private boolean _done = false;
+
+ protected char[] _buffer;
+ protected StringBuffer _response;
+ protected BufferedReader _reader;
+
+ public ResponseReader(Socket client) throws IOException
+ {
+ _buffer = new char[256];
+ _response = new StringBuffer();
+ _reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
+ }
+
+ public void setDone()
+ {
+ _done = true;
+ }
+
+ public StringBuffer getResponse()
+ {
+ return _response;
+ }
+
+ /**
+ * @see java.lang.Runnable#run()
+ */
+ @Override
+ public void run()
+ {
+ try
+ {
+ int count = 0;
+ while (!_done || count > 0)
+ {
+ count = doRead();
+ }
+ }
+ catch (IOException | InterruptedException e)
+ {
+ // ignore
+ }
+ finally
+ {
+ try
+ {
+ _reader.close();
+ }
+ catch (IOException e)
+ {
+ // ignore
+ }
+ }
+ }
+
+ protected int doRead() throws IOException, InterruptedException
+ {
+ if (!_reader.ready())
+ {
+ Thread.sleep(25);
+ }
+
+ int count = 0;
+ if (_reader.ready())
+ {
+ count = _reader.read(_buffer);
+ if (count > 0)
+ {
+ _response.append(_buffer, 0, count);
+ }
+ }
+
+ return count;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java
new file mode 100644
index 0000000..354d5a1
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java
@@ -0,0 +1,806 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channel;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.Exchanger;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.io.AbstractConnection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.ssl.SslConnection;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import static java.time.Duration.ofSeconds;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThan;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
+{
+ protected static final Logger LOG = Log.getLogger(ConnectorTimeoutTest.class);
+
+ protected static final int MAX_IDLE_TIME = 2000;
+ private int sleepTime = MAX_IDLE_TIME + MAX_IDLE_TIME / 5;
+ private int minimumTestRuntime = MAX_IDLE_TIME - MAX_IDLE_TIME / 5;
+ private int maximumTestRuntime = MAX_IDLE_TIME * 10;
+
+ static
+ {
+ System.setProperty("org.eclipse.jetty.io.nio.IDLE_TICK", "500");
+ }
+
+ @BeforeEach
+ @Override
+ public void before()
+ {
+ super.before();
+ if (_httpConfiguration != null)
+ {
+ _httpConfiguration.setBlockingTimeout(-1L);
+ _httpConfiguration.setMinRequestDataRate(-1);
+ _httpConfiguration.setIdleTimeout(-1);
+ }
+ }
+
+ @Test
+ public void testMaxIdleWithRequest10() throws Exception
+ {
+ configureServer(new HelloWorldHandler());
+
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+
+ assertFalse(client.isClosed());
+
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ os.write((
+ "GET / HTTP/1.0\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: keep-alive\r\n" +
+ "\r\n").getBytes("utf-8"));
+ os.flush();
+
+ IO.toString(is);
+
+ Thread.sleep(sleepTime);
+ assertEquals(-1, is.read());
+ });
+
+ assertTrue(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start > minimumTestRuntime);
+ assertTrue(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start < maximumTestRuntime);
+ }
+
+ @Test
+ public void testMaxIdleWithRequest11() throws Exception
+ {
+ configureServer(new EchoHandler());
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+
+ assertFalse(client.isClosed());
+
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ String content = "Wibble";
+ byte[] contentB = content.getBytes("utf-8");
+ os.write((
+ "POST /echo HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "content-type: text/plain; charset=utf-8\r\n" +
+ "content-length: " + contentB.length + "\r\n" +
+ "\r\n").getBytes("utf-8"));
+ os.write(contentB);
+ os.flush();
+
+ IO.toString(is);
+
+ Thread.sleep(sleepTime);
+ assertEquals(-1, is.read());
+ });
+
+ assertTrue(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start > minimumTestRuntime);
+ assertTrue(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start < maximumTestRuntime);
+ }
+
+ @Test
+ public void testMaxIdleWithRequest10NoClientClose() throws Exception
+ {
+ final Exchanger<EndPoint> exchanger = new Exchanger<>();
+ configureServer(new HelloWorldHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException,
+ ServletException
+ {
+ try
+ {
+ exchanger.exchange(baseRequest.getHttpChannel().getEndPoint());
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ super.handle(target, baseRequest, request, response);
+ }
+ });
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+
+ assertFalse(client.isClosed());
+
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ os.write((
+ "GET / HTTP/1.0\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: close\r\n" +
+ "\r\n").getBytes("utf-8"));
+ os.flush();
+
+ // Get the server side endpoint
+ EndPoint endPoint = exchanger.exchange(null, 10, TimeUnit.SECONDS);
+ if (endPoint instanceof SslConnection.DecryptedEndPoint)
+ endPoint = ((SslConnection.DecryptedEndPoint)endPoint).getSslConnection().getEndPoint();
+
+ // read the response
+ String result = IO.toString(is);
+ assertThat("OK", result, containsString("200 OK"));
+
+ // check client reads EOF
+ assertEquals(-1, is.read());
+ assertTrue(endPoint.isOutputShutdown());
+
+ // wait for idle timeout
+ TimeUnit.MILLISECONDS.sleep(2 * MAX_IDLE_TIME);
+
+ // check the server side is closed
+ assertFalse(endPoint.isOpen());
+ Object transport = endPoint.getTransport();
+ if (transport instanceof Channel)
+ assertFalse(((Channel)transport).isOpen());
+ }
+
+ @Test
+ public void testMaxIdleWithRequest11NoClientClose() throws Exception
+ {
+ final Exchanger<EndPoint> exchanger = new Exchanger<>();
+ configureServer(new EchoHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException,
+ ServletException
+ {
+ try
+ {
+ exchanger.exchange(baseRequest.getHttpChannel().getEndPoint());
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ super.handle(target, baseRequest, request, response);
+ }
+ });
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+
+ assertFalse(client.isClosed());
+
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ String content = "Wibble";
+ byte[] contentB = content.getBytes("utf-8");
+ os.write((
+ "POST /echo HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "content-type: text/plain; charset=utf-8\r\n" +
+ "content-length: " + contentB.length + "\r\n" +
+ "connection: close\r\n" +
+ "\r\n").getBytes("utf-8"));
+ os.write(contentB);
+ os.flush();
+
+ // Get the server side endpoint
+ EndPoint endPoint = exchanger.exchange(null, 10, TimeUnit.SECONDS);
+ if (endPoint instanceof SslConnection.DecryptedEndPoint)
+ endPoint = ((SslConnection.DecryptedEndPoint)endPoint).getSslConnection().getEndPoint();
+
+ // read the response
+ IO.toString(is);
+
+ // check client reads EOF
+ assertEquals(-1, is.read());
+ assertTrue(endPoint.isOutputShutdown());
+
+ // The server has shutdown the output, the client does not close,
+ // the server should idle timeout and close the connection.
+ TimeUnit.MILLISECONDS.sleep(2 * MAX_IDLE_TIME);
+
+ assertFalse(endPoint.isOpen());
+ Object transport = endPoint.getTransport();
+ if (transport instanceof Channel)
+ assertFalse(((Channel)transport).isOpen());
+ }
+
+ @Test
+ @Tag("Unstable")
+ @Disabled // TODO make more stable
+ public void testNoBlockingTimeoutRead() throws Exception
+ {
+ _httpConfiguration.setBlockingTimeout(-1L);
+
+ configureServer(new EchoHandler());
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+ InputStream is = client.getInputStream();
+ assertFalse(client.isClosed());
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+
+ OutputStream os = client.getOutputStream();
+ os.write(("GET / HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "5\r\n" +
+ "LMNOP\r\n")
+ .getBytes("utf-8"));
+ os.flush();
+
+ try
+ {
+ Thread.sleep(250);
+ os.write("1".getBytes("utf-8"));
+ os.flush();
+ Thread.sleep(250);
+ os.write("0".getBytes("utf-8"));
+ os.flush();
+ Thread.sleep(250);
+ os.write("\r".getBytes("utf-8"));
+ os.flush();
+ Thread.sleep(250);
+ os.write("\n".getBytes("utf-8"));
+ os.flush();
+ Thread.sleep(250);
+ os.write("0123456789ABCDEF\r\n".getBytes("utf-8"));
+ os.write("0\r\n".getBytes("utf-8"));
+ os.write("\r\n".getBytes("utf-8"));
+ os.flush();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start;
+ assertThat(duration, greaterThan(500L));
+
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ // read the response
+ String response = IO.toString(is);
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("LMNOP0123456789ABCDEF"));
+ });
+ }
+
+ @Test
+ @Tag("Unstable")
+ @Disabled // TODO make more stable
+ public void testBlockingTimeoutRead() throws Exception
+ {
+ _httpConfiguration.setBlockingTimeout(750L);
+
+ configureServer(new EchoHandler());
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+ InputStream is = client.getInputStream();
+ assertFalse(client.isClosed());
+
+ OutputStream os = client.getOutputStream();
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ os.write(("GET / HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "5\r\n" +
+ "LMNOP\r\n")
+ .getBytes("utf-8"));
+ os.flush();
+
+ try (StacklessLogging stackless = new StacklessLogging(HttpChannel.class))
+ {
+ Thread.sleep(300);
+ os.write("1".getBytes("utf-8"));
+ os.flush();
+ Thread.sleep(300);
+ os.write("0".getBytes("utf-8"));
+ os.flush();
+ Thread.sleep(300);
+ os.write("\r".getBytes("utf-8"));
+ os.flush();
+ Thread.sleep(300);
+ os.write("\n".getBytes("utf-8"));
+ os.flush();
+ Thread.sleep(300);
+ os.write("0123456789ABCDEF\r\n".getBytes("utf-8"));
+ os.write("0\r\n".getBytes("utf-8"));
+ os.write("\r\n".getBytes("utf-8"));
+ os.flush();
+ }
+
+ long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start;
+ assertThat(duration, greaterThan(500L));
+
+ // read the response
+ String response = IO.toString(is);
+ assertThat(response, startsWith("HTTP/1.1 500 "));
+ assertThat(response, containsString("InterruptedIOException"));
+
+ }
+
+ @Test
+ @Tag("Unstable")
+ @Disabled // TODO make more stable
+ public void testNoBlockingTimeoutWrite() throws Exception
+ {
+ configureServer(new HugeResponseHandler());
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+
+ assertFalse(client.isClosed());
+
+ OutputStream os = client.getOutputStream();
+ BufferedReader is = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.ISO_8859_1), 2048);
+
+ os.write((
+ "GET / HTTP/1.0\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: keep-alive\r\n" +
+ "Connection: close\r\n" +
+ "\r\n").getBytes("utf-8"));
+ os.flush();
+
+ // read the header
+ String line = is.readLine();
+ assertThat(line, startsWith("HTTP/1.1 200 OK"));
+ while (line.length() != 0)
+ {
+ line = is.readLine();
+ }
+
+ for (int i = 0; i < (128 * 1024); i++)
+ {
+ if (i % 1028 == 0)
+ {
+ Thread.sleep(20);
+ // System.err.println("read "+TimeUnit.NANOSECONDS.toMillis(System.nanoTime()));
+ }
+ line = is.readLine();
+ assertThat(line, notNullValue());
+ assertEquals(1022, line.length());
+ }
+ }
+
+ @Test
+ @Tag("Unstable")
+ @Disabled // TODO make more stable
+ public void testBlockingTimeoutWrite() throws Exception
+ {
+ _httpConfiguration.setBlockingTimeout(750L);
+ configureServer(new HugeResponseHandler());
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+
+ assertFalse(client.isClosed());
+
+ OutputStream os = client.getOutputStream();
+ BufferedReader is = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.ISO_8859_1), 2048);
+
+ os.write((
+ "GET / HTTP/1.0\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: keep-alive\r\n" +
+ "Connection: close\r\n" +
+ "\r\n").getBytes("utf-8"));
+ os.flush();
+
+ // read the header
+ String line = is.readLine();
+ assertThat(line, startsWith("HTTP/1.1 200 OK"));
+ while (line.length() != 0)
+ {
+ line = is.readLine();
+ }
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ try (StacklessLogging stackless = new StacklessLogging(HttpChannel.class, AbstractConnection.class))
+ {
+ for (int i = 0; i < (128 * 1024); i++)
+ {
+ if (i % 1028 == 0)
+ {
+ Thread.sleep(20);
+ // System.err.println("read "+TimeUnit.NANOSECONDS.toMillis(System.nanoTime()));
+ }
+ line = is.readLine();
+ if (line == null)
+ break;
+ }
+ }
+ long end = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ long duration = end - start;
+ assertThat(duration, lessThan(20L * 128L));
+ }
+
+ @Test
+ public void testMaxIdleNoRequest() throws Exception
+ {
+ configureServer(new EchoHandler());
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+ InputStream is = client.getInputStream();
+ assertFalse(client.isClosed());
+
+ OutputStream os = client.getOutputStream();
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ os.write("GET ".getBytes("utf-8"));
+ os.flush();
+
+ Thread.sleep(sleepTime);
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ try
+ {
+ String response = IO.toString(is);
+ assertThat(response, is(""));
+ assertEquals(-1, is.read());
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e.getMessage());
+ }
+ });
+ assertTrue(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start < maximumTestRuntime);
+ }
+
+ @Test
+ public void testMaxIdleNothingSent() throws Exception
+ {
+ configureServer(new EchoHandler());
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+ InputStream is = client.getInputStream();
+ assertFalse(client.isClosed());
+
+ Thread.sleep(sleepTime);
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ try
+ {
+ String response = IO.toString(is);
+ assertThat(response, is(""));
+ assertEquals(-1, is.read());
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ });
+ assertTrue(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start < maximumTestRuntime);
+ }
+
+ @Test
+ public void testMaxIdleDelayedDispatch() throws Exception
+ {
+ configureServer(new EchoHandler());
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+ InputStream is = client.getInputStream();
+ assertFalse(client.isClosed());
+
+ OutputStream os = client.getOutputStream();
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ os.write((
+ "GET / HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: keep-alive\r\n" +
+ "Content-Length: 20\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n").getBytes("utf-8"));
+ os.flush();
+
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ try
+ {
+ String response = IO.toString(is);
+ assertThat(response, containsString("500"));
+ assertEquals(-1, is.read());
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ });
+ int duration = (int)(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start);
+ assertThat(duration, greaterThanOrEqualTo(MAX_IDLE_TIME));
+ assertThat(duration, lessThan(maximumTestRuntime));
+ }
+
+ @Test
+ public void testMaxIdleDispatch() throws Exception
+ {
+ configureServer(new EchoHandler());
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+ InputStream is = client.getInputStream();
+ assertFalse(client.isClosed());
+
+ OutputStream os = client.getOutputStream();
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ os.write((
+ "GET / HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: keep-alive\r\n" +
+ "Content-Length: 20\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "1234567890").getBytes("utf-8"));
+ os.flush();
+
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ try
+ {
+ String response = IO.toString(is);
+ assertThat(response, containsString("500"));
+ assertEquals(-1, is.read());
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ });
+ int duration = (int)(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start);
+ assertThat(duration + 100, greaterThanOrEqualTo(MAX_IDLE_TIME));
+ assertThat(duration - 100, lessThan(maximumTestRuntime));
+ }
+
+ @Test
+ public void testMaxIdleWithSlowRequest() throws Exception
+ {
+ configureServer(new EchoHandler());
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+
+ assertFalse(client.isClosed());
+
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ String content = "Wibble\r\n";
+ byte[] contentB = content.getBytes("utf-8");
+ os.write((
+ "GET / HTTP/1.0\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: keep-alive\r\n" +
+ "Content-Length: " + (contentB.length * 20) + "\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n").getBytes("utf-8"));
+ os.flush();
+
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ for (int i = 0; i < 20; i++)
+ {
+ Thread.sleep(50);
+ os.write(contentB);
+ os.flush();
+ }
+
+ String in = IO.toString(is);
+ int offset = 0;
+ for (int i = 0; i < 20; i++)
+ {
+ offset = in.indexOf("Wibble", offset + 1);
+ assertTrue(offset > 0, "" + i);
+ }
+ });
+ }
+
+ @Test
+ public void testMaxIdleWithSlowResponse() throws Exception
+ {
+ configureServer(new SlowResponseHandler());
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+
+ assertFalse(client.isClosed());
+
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ os.write((
+ "GET / HTTP/1.0\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: keep-alive\r\n" +
+ "Connection: close\r\n" +
+ "\r\n").getBytes("utf-8"));
+ os.flush();
+
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ String in = IO.toString(is);
+ int offset = 0;
+ for (int i = 0; i < 20; i++)
+ {
+ offset = in.indexOf("Hello World", offset + 1);
+ assertTrue(offset > 0, "" + i);
+ }
+ });
+ }
+
+ @Test
+ public void testMaxIdleWithWait() throws Exception
+ {
+ configureServer(new WaitHandler());
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+
+ assertFalse(client.isClosed());
+
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ os.write((
+ "GET / HTTP/1.0\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: keep-alive\r\n" +
+ "Connection: close\r\n" +
+ "\r\n").getBytes("utf-8"));
+ os.flush();
+
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ String in = IO.toString(is);
+ assertThat(in, containsString("Hello World"));
+ });
+ }
+
+ protected static class SlowResponseHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ OutputStream out = response.getOutputStream();
+
+ for (int i = 0; i < 20; i++)
+ {
+ out.write("Hello World\r\n".getBytes());
+ out.flush();
+ try
+ {
+ Thread.sleep(50);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ out.close();
+ }
+ }
+
+ protected static class HugeResponseHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ OutputStream out = response.getOutputStream();
+ byte[] buffer = new byte[128 * 1024 * 1024];
+ Arrays.fill(buffer, (byte)'x');
+ for (int i = 0; i < 128 * 1024; i++)
+ {
+ buffer[i * 1024 + 1022] = '\r';
+ buffer[i * 1024 + 1023] = '\n';
+ }
+ ByteBuffer bb = ByteBuffer.wrap(buffer);
+ ((HttpOutput)out).sendContent(bb);
+ out.close();
+ }
+ }
+
+ protected static class WaitHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ OutputStream out = response.getOutputStream();
+ try
+ {
+ Thread.sleep(2000);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ out.write("Hello World\r\n".getBytes());
+ out.flush();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterLenientTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterLenientTest.java
new file mode 100644
index 0000000..516ab98
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterLenientTest.java
@@ -0,0 +1,177 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.stream.Stream;
+import javax.servlet.http.Cookie;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Tests of poor various name=value scenarios and expectations of results
+ * due to our efforts at being lenient with what we receive.
+ */
+public class CookieCutterLenientTest
+{
+ public static Stream<Arguments> data()
+ {
+ return Stream.of(
+ // Simple test to verify behavior
+ Arguments.of("key=value", "key", "value"),
+
+ // Tests that conform to RFC2109
+ // RFC2109 - token values
+ // token = 1*<any CHAR except CTLs or tspecials>
+ // CHAR = <any US-ASCII character (octets 0 - 127)>
+ // CTL = <any US-ASCII control character
+ // (octets 0 - 31) and DEL (127)>
+ // SP = <US-ASCII SP, space (32)>
+ // HT = <US-ASCII HT, horizontal-tab (9)>
+ // tspecials = "(" | ")" | "<" | ">" | "@"
+ // | "," | ";" | ":" | "\" | <">
+ // | "/" | "[" | "]" | "?" | "="
+ // | "{" | "}" | SP | HT
+ Arguments.of("abc=xyz", "abc", "xyz"),
+ Arguments.of("abc=!bar", "abc", "!bar"),
+ Arguments.of("abc=#hash", "abc", "#hash"),
+ Arguments.of("abc=#hash", "abc", "#hash"),
+ // RFC2109 - quoted-string values
+ // quoted-string = ( <"> *(qdtext) <"> )
+ // qdtext = <any TEXT except <">>
+
+ // rejected, as name cannot be DQUOTED
+ Arguments.of("\"a\"=bcd", null, null),
+ Arguments.of("\"a\"=\"b c d\"", null, null),
+
+ // lenient with spaces and EOF
+ Arguments.of("abc=", "abc", ""),
+ Arguments.of("abc = ", "abc", ""),
+ Arguments.of("abc = ;", "abc", ""),
+ Arguments.of("abc = ; ", "abc", ""),
+ Arguments.of("abc = x ", "abc", "x"),
+ Arguments.of("abc = e f g ", "abc", "e f g"),
+ Arguments.of("abc=\"\"", "abc", ""),
+ Arguments.of("abc= \"\" ", "abc", ""),
+ Arguments.of("abc= \"x\" ", "abc", "x"),
+ Arguments.of("abc= \"x\" ;", "abc", "x"),
+ Arguments.of("abc= \"x\" ; ", "abc", "x"),
+
+ // The backslash character ("\") may be used as a single-character quoting
+ // mechanism only within quoted-string and comment constructs.
+ // quoted-pair = "\" CHAR
+ Arguments.of("abc=\"xyz\"", "abc", "xyz"),
+ Arguments.of("abc=\"!bar\"", "abc", "!bar"),
+ Arguments.of("abc=\"#hash\"", "abc", "#hash"),
+ // RFC2109 - other valid name types that conform to 'attr'/'token' syntax
+ Arguments.of("!f!o!o!=wat", "!f!o!o!", "wat"),
+ Arguments.of("__MyHost=Foo", "__MyHost", "Foo"),
+ Arguments.of("some-thing-else=to-parse", "some-thing-else", "to-parse"),
+ // RFC2109 - names with attr/token syntax starting with '$' (and not a cookie reserved word)
+ // See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-prefixes-00#section-5.2
+ // Cannot pass names through as javax.servlet.http.Cookie class does not allow them
+ Arguments.of("$foo=bar", null, null),
+
+ // Tests that conform to RFC6265
+ Arguments.of("abc=foobar!", "abc", "foobar!"),
+ Arguments.of("abc=\"foobar!\"", "abc", "foobar!"),
+
+ // Internal quotes
+ Arguments.of("foo=bar\"baz", "foo", "bar\"baz"),
+ Arguments.of("foo=\"bar\"baz\"", "foo", "bar\"baz"),
+ Arguments.of("foo=\"bar\"-\"baz\"", "foo", "bar\"-\"baz"),
+ Arguments.of("foo=\"bar'-\"baz\"", "foo", "bar'-\"baz"),
+ Arguments.of("foo=\"bar''-\"baz\"", "foo", "bar''-\"baz"),
+ // These seem dubious until you realize the "lots of equals signs" below works
+ Arguments.of("foo=\"bar\"=\"baz\"", "foo", "bar\"=\"baz"),
+ Arguments.of("query=\"?b=c\"&\"d=e\"", "query", "?b=c\"&\"d=e"),
+ // Escaped quotes
+ Arguments.of("foo=\"bar\\\"=\\\"baz\"", "foo", "bar\"=\"baz"),
+
+ // Unterminated Quotes
+ Arguments.of("x=\"abc", "x", "\"abc"),
+ // Unterminated Quotes with valid cookie params after it
+ Arguments.of("x=\"abc $Path=/", "x", "\"abc $Path=/"),
+ // Unterminated Quotes with trailing escape
+ Arguments.of("x=\"abc\\", "x", "\"abc\\"),
+
+ // UTF-8 raw values (not encoded) - VIOLATION of RFC6265
+ Arguments.of("2sides=\u262F", null, null), // 2 byte (YIN YANG) - rejected due to not being DQUOTED
+ Arguments.of("currency=\"\u20AC\"", "currency", "\u20AC"), // 3 byte (EURO SIGN)
+ Arguments.of("gothic=\"\uD800\uDF48\"", "gothic", "\uD800\uDF48"), // 4 byte (GOTHIC LETTER HWAIR)
+
+ // Spaces
+ Arguments.of("foo=bar baz", "foo", "bar baz"),
+ Arguments.of("foo=\"bar baz\"", "foo", "bar baz"),
+ Arguments.of("z=a b c d e f g", "z", "a b c d e f g"),
+
+ // Bad tspecials usage - VIOLATION of RFC6265
+ Arguments.of("foo=bar;baz", "foo", "bar"),
+ Arguments.of("foo=\"bar;baz\"", "foo", "bar;baz"),
+ Arguments.of("z=a;b,c:d;e/f[g]", "z", "a"),
+ Arguments.of("z=\"a;b,c:d;e/f[g]\"", "z", "a;b,c:d;e/f[g]"),
+ Arguments.of("name=quoted=\"\\\"badly\\\"\"", "name", "quoted=\"\\\"badly\\\"\""), // someone attempting to escape a DQUOTE from within a DQUOTED pair)
+
+ // Quoted with other Cookie keywords
+ Arguments.of("x=\"$Version=0\"", "x", "$Version=0"),
+ Arguments.of("x=\"$Path=/\"", "x", "$Path=/"),
+ Arguments.of("x=\"$Path=/ $Domain=.foo.com\"", "x", "$Path=/ $Domain=.foo.com"),
+ Arguments.of("x=\" $Path=/ $Domain=.foo.com \"", "x", " $Path=/ $Domain=.foo.com "),
+ Arguments.of("a=\"b; $Path=/a; c=d; $PATH=/c; e=f\"; $Path=/e/", "a", "b; $Path=/a; c=d; $PATH=/c; e=f"), // VIOLATES RFC6265
+
+ // Lots of equals signs
+ Arguments.of("query=b=c&d=e", "query", "b=c&d=e"),
+
+ // Escaping
+ Arguments.of("query=%7B%22sessionCount%22%3A5%2C%22sessionTime%22%3A14151%7D", "query", "%7B%22sessionCount%22%3A5%2C%22sessionTime%22%3A14151%7D"),
+
+ // Google cookies (seen in wild, has `tspecials` of ':' in value)
+ Arguments.of("GAPS=1:A1aaaAaAA1aaAAAaa1a11a:aAaaAa-aaA1-", "GAPS", "1:A1aaaAaAA1aaAAAaa1a11a:aAaaAa-aaA1-"),
+
+ // Strong abuse of cookie spec (lots of tspecials) - VIOLATION of RFC6265
+ Arguments.of("$Version=0; rToken=F_TOKEN''!--\"</a>=&{()}", "rToken", "F_TOKEN''!--\"</a>=&{()}"),
+
+ // Commas that were not commas
+ Arguments.of("name=foo,bar", "name", "foo,bar"),
+ Arguments.of("name=foo , bar", "name", "foo , bar"),
+ Arguments.of("name=foo , bar, bob", "name", "foo , bar, bob")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testLenientBehavior(String rawHeader, String expectedName, String expectedValue)
+ {
+ CookieCutter cutter = new CookieCutter();
+ cutter.addCookieField(rawHeader);
+ Cookie[] cookies = cutter.getCookies();
+ if (expectedName == null)
+ assertThat("Cookies.length", cookies.length, is(0));
+ else
+ {
+ assertThat("Cookies.length", cookies.length, is(1));
+ assertThat("Cookie.name", cookies[0].getName(), is(expectedName));
+ assertThat("Cookie.value", cookies[0].getValue(), is(expectedValue));
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java
new file mode 100644
index 0000000..4ba3db1
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java
@@ -0,0 +1,277 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.Arrays;
+import javax.servlet.http.Cookie;
+
+import org.eclipse.jetty.http.CookieCompliance;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class CookieCutterTest
+{
+ private Cookie[] parseCookieHeaders(CookieCompliance compliance, String... headers)
+ {
+ CookieCutter cutter = new CookieCutter(compliance);
+ for (String header : headers)
+ {
+ cutter.addCookieField(header);
+ }
+ return cutter.getCookies();
+ }
+
+ private void assertCookie(String prefix, Cookie cookie,
+ String expectedName,
+ String expectedValue,
+ int expectedVersion,
+ String expectedPath)
+ {
+ assertThat(prefix + ".name", cookie.getName(), is(expectedName));
+ assertThat(prefix + ".value", cookie.getValue(), is(expectedValue));
+ assertThat(prefix + ".version", cookie.getVersion(), is(expectedVersion));
+ assertThat(prefix + ".path", cookie.getPath(), is(expectedPath));
+ }
+
+ /**
+ * Example from RFC2109 and RFC2965
+ */
+ @Test
+ public void testRFCSingle()
+ {
+ String rawCookie = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(1));
+ assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
+ }
+
+ /**
+ * Example from RFC2109 and RFC2965.
+ * <p>
+ * Lenient parsing, input has no spaces after ';' token.
+ * </p>
+ */
+ @Test
+ public void testRFCSingleLenientNoSpaces()
+ {
+ String rawCookie = "$Version=\"1\";Customer=\"WILE_E_COYOTE\";$Path=\"/acme\"";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(1));
+ assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
+ }
+
+ /**
+ * Example from RFC2109 and RFC2965
+ */
+ @Test
+ public void testRFCDouble()
+ {
+ String rawCookie = "$Version=\"1\"; " +
+ "Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; " +
+ "Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(2));
+ assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
+ assertCookie("Cookies[1]", cookies[1], "Part_Number", "Rocket_Launcher_0001", 1, "/acme");
+ }
+
+ /**
+ * Example from RFC2109 and RFC2965
+ */
+ @Test
+ public void testRFCTriple()
+ {
+ String rawCookie = "$Version=\"1\"; " +
+ "Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; " +
+ "Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"; " +
+ "Shipping=\"FedEx\"; $Path=\"/acme\"";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(3));
+ assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
+ assertCookie("Cookies[1]", cookies[1], "Part_Number", "Rocket_Launcher_0001", 1, "/acme");
+ assertCookie("Cookies[2]", cookies[2], "Shipping", "FedEx", 1, "/acme");
+ }
+
+ /**
+ * Example from RFC2109 and RFC2965
+ */
+ @Test
+ public void testRFCPathExample()
+ {
+ String rawCookie = "$Version=\"1\"; " +
+ "Part_Number=\"Riding_Rocket_0023\"; $Path=\"/acme/ammo\"; " +
+ "Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(2));
+ assertCookie("Cookies[0]", cookies[0], "Part_Number", "Riding_Rocket_0023", 1, "/acme/ammo");
+ assertCookie("Cookies[1]", cookies[1], "Part_Number", "Rocket_Launcher_0001", 1, "/acme");
+ }
+
+ /**
+ * Example from RFC2109
+ */
+ @Test
+ public void testRFC2109CookieSpoofingExample()
+ {
+ String rawCookie = "$Version=\"1\"; " +
+ "session_id=\"1234\"; " +
+ "session_id=\"1111\"; $Domain=\".cracker.edu\"";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(2));
+ assertCookie("Cookies[0]", cookies[0], "session_id", "1234", 1, null);
+ assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 1, null);
+ }
+
+ /**
+ * Example from RFC2965
+ */
+ @Test
+ public void testRFC2965CookieSpoofingExample()
+ {
+ String rawCookie = "$Version=\"1\"; session_id=\"1234\", " +
+ "$Version=\"1\"; session_id=\"1111\"; $Domain=\".cracker.edu\"";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(2));
+ assertCookie("Cookies[0]", cookies[0], "session_id", "1234", 1, null);
+ assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 1, null);
+
+ cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
+ assertThat("Cookies.length", cookies.length, is(2));
+ assertCookie("Cookies[0]", cookies[0], "session_id", "1234\", $Version=\"1", 0, null);
+ assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 0, null);
+ }
+
+ /**
+ * Example from RFC6265
+ */
+ @Test
+ public void testRFC6265SidExample()
+ {
+ String rawCookie = "SID=31d4d96e407aad42";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(1));
+ assertCookie("Cookies[0]", cookies[0], "SID", "31d4d96e407aad42", 0, null);
+ }
+
+ /**
+ * Example from RFC6265
+ */
+ @Test
+ public void testRFC6265SidLangExample()
+ {
+ String rawCookie = "SID=31d4d96e407aad42; lang=en-US";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(2));
+ assertCookie("Cookies[0]", cookies[0], "SID", "31d4d96e407aad42", 0, null);
+ assertCookie("Cookies[1]", cookies[1], "lang", "en-US", 0, null);
+ }
+
+ /**
+ * Example from RFC6265.
+ * <p>
+ * Lenient parsing, input has no spaces after ';' token.
+ * </p>
+ */
+ @Test
+ public void testRFC6265SidLangExampleLenient()
+ {
+ String rawCookie = "SID=31d4d96e407aad42;lang=en-US";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(2));
+ assertCookie("Cookies[0]", cookies[0], "SID", "31d4d96e407aad42", 0, null);
+ assertCookie("Cookies[1]", cookies[1], "lang", "en-US", 0, null);
+ }
+
+ /**
+ * Basic name=value, following RFC6265 rules
+ */
+ @Test
+ public void testKeyValue()
+ {
+ String rawCookie = "key=value";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(1));
+ assertCookie("Cookies[0]", cookies[0], "key", "value", 0, null);
+ }
+
+ /**
+ * Basic name=value, following RFC6265 rules
+ */
+ @Test
+ public void testDollarName()
+ {
+ String rawCookie = "$key=value";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(0));
+ }
+
+ @Test
+ public void testMultipleCookies()
+ {
+ String rawCookie = "testcookie; server.id=abcd; server.detail=cfg";
+
+ // The first cookie "testcookie" should be ignored, per RFC6265, as it's missing the "=" sign.
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(2));
+ assertCookie("Cookies[0]", cookies[0], "server.id", "abcd", 0, null);
+ assertCookie("Cookies[1]", cookies[1], "server.detail", "cfg", 0, null);
+ }
+
+ @Test
+ public void testExcessiveSemicolons()
+ {
+ char[] excessive = new char[65535];
+ Arrays.fill(excessive, ';');
+ String rawCookie = "foo=bar; " + excessive + "; xyz=pdq";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(2));
+ assertCookie("Cookies[0]", cookies[0], "foo", "bar", 0, null);
+ assertCookie("Cookies[1]", cookies[1], "xyz", "pdq", 0, null);
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/CustomResourcesMonitorTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/CustomResourcesMonitorTest.java
new file mode 100644
index 0000000..233e9c3
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/CustomResourcesMonitorTest.java
@@ -0,0 +1,177 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.TimerScheduler;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class CustomResourcesMonitorTest
+{
+ Server _server;
+ ServerConnector _connector;
+ FileOnDirectoryMonitor _fileOnDirectoryMonitor;
+ Path _monitoredPath;
+ LowResourceMonitor _lowResourceMonitor;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ _server = new Server();
+
+ _server.addBean(new TimerScheduler());
+
+ _connector = new ServerConnector(_server);
+ _connector.setPort(0);
+ _connector.setIdleTimeout(35000);
+ _server.addConnector(_connector);
+
+ _server.setHandler(new DumpHandler());
+
+ _monitoredPath = Files.createTempDirectory("jetty_test");
+ _fileOnDirectoryMonitor = new FileOnDirectoryMonitor(_monitoredPath);
+ _lowResourceMonitor = new LowResourceMonitor(_server);
+ _server.addBean(_lowResourceMonitor);
+ _lowResourceMonitor.addLowResourceCheck(_fileOnDirectoryMonitor);
+ _server.start();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _server.stop();
+ }
+
+ @Test
+ public void testFileOnDirectoryMonitor() throws Exception
+ {
+ int monitorPeriod = _lowResourceMonitor.getPeriod();
+ int lowResourcesIdleTimeout = _lowResourceMonitor.getLowResourcesIdleTimeout();
+ assertThat(lowResourcesIdleTimeout, Matchers.lessThanOrEqualTo(monitorPeriod));
+
+ int maxLowResourcesTime = 5 * monitorPeriod;
+ _lowResourceMonitor.setMaxLowResourcesTime(maxLowResourcesTime);
+ assertFalse(_fileOnDirectoryMonitor.isLowOnResources());
+
+ try (Socket socket0 = new Socket("localhost", _connector.getLocalPort()))
+ {
+ Path tmpFile = Files.createTempFile(_monitoredPath, "yup", ".tmp");
+ // Write a file
+ Files.write(tmpFile, "foobar".getBytes());
+
+ // Wait a couple of monitor periods so that
+ // fileOnDirectoryMonitor detects it is in low mode.
+ Thread.sleep(2 * monitorPeriod);
+ assertTrue(_fileOnDirectoryMonitor.isLowOnResources());
+
+ // We already waited enough for fileOnDirectoryMonitor to close socket0.
+ assertEquals(-1, socket0.getInputStream().read());
+
+ // New connections are not affected by the
+ // low mode until maxLowResourcesTime elapses.
+ try (Socket socket1 = new Socket("localhost", _connector.getLocalPort()))
+ {
+ // Set a very short read timeout so we can test if the server closed.
+ socket1.setSoTimeout(1);
+ InputStream input1 = socket1.getInputStream();
+
+ assertTrue(_fileOnDirectoryMonitor.isLowOnResources());
+ assertThrows(SocketTimeoutException.class, () -> input1.read());
+
+ // Wait a couple of lowResources idleTimeouts.
+ Thread.sleep(2 * lowResourcesIdleTimeout);
+
+ // Verify the new socket is still open.
+ assertTrue(_fileOnDirectoryMonitor.isLowOnResources());
+ assertThrows(SocketTimeoutException.class, () -> input1.read());
+
+ Files.delete(tmpFile);
+
+ // Let the maxLowResourcesTime elapse.
+ Thread.sleep(maxLowResourcesTime);
+ assertFalse(_fileOnDirectoryMonitor.isLowOnResources());
+ }
+ }
+ }
+
+ static class FileOnDirectoryMonitor implements LowResourceMonitor.LowResourceCheck
+ {
+ private static final Logger LOG = Log.getLogger(FileOnDirectoryMonitor.class);
+
+ private final Path _pathToMonitor;
+
+ private String reason;
+
+ public FileOnDirectoryMonitor(Path pathToMonitor)
+ {
+ _pathToMonitor = pathToMonitor;
+ }
+
+ @Override
+ public boolean isLowOnResources()
+ {
+ try (Stream<Path> paths = Files.list(_pathToMonitor))
+ {
+ List<Path> content = paths.collect(Collectors.toList());
+ if (!content.isEmpty())
+ {
+ reason = "directory not empty so enable low resources";
+ return true;
+ }
+ }
+ catch (IOException e)
+ {
+ LOG.info("ignore issue looking at directory content", e);
+ }
+ return false;
+ }
+
+ @Override
+ public String getReason()
+ {
+ return reason;
+ }
+
+ @Override
+ public String toString()
+ {
+ return getClass().getName();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/DelayedServerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/DelayedServerTest.java
new file mode 100644
index 0000000..476eb44
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/DelayedServerTest.java
@@ -0,0 +1,114 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.http.HttpCompliance;
+import org.eclipse.jetty.http.MetaData;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.thread.ThreadPool;
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Extended Server Tester.
+ */
+public class DelayedServerTest extends HttpServerTestBase
+{
+ @BeforeEach
+ public void init() throws Exception
+ {
+ startServer(new ServerConnector(_server, new HttpConnectionFactory()
+ {
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endPoint)
+ {
+ return configure(new DelayedHttpConnection(getHttpConfiguration(), connector, endPoint), connector, endPoint);
+ }
+ }));
+ }
+
+ private static class DelayedHttpConnection extends HttpConnection
+ {
+ public DelayedHttpConnection(HttpConfiguration config, Connector connector, EndPoint endPoint)
+ {
+ super(config, connector, endPoint, HttpCompliance.RFC7230_LEGACY, false);
+ }
+
+ @Override
+ public void send(MetaData.Response info, boolean head, ByteBuffer content, boolean lastContent, Callback callback)
+ {
+ DelayedCallback delay = new DelayedCallback(callback, getServer().getThreadPool());
+ super.send(info, head, content, lastContent, delay);
+ }
+ }
+
+ private static class DelayedCallback extends Callback.Nested
+ {
+ final ThreadPool pool;
+
+ public DelayedCallback(Callback callback, ThreadPool threadPool)
+ {
+ super(callback);
+ pool = threadPool;
+ }
+
+ @Override
+ public void succeeded()
+ {
+ pool.execute(() ->
+ {
+ try
+ {
+ Thread.sleep(10);
+ }
+ catch (InterruptedException ignored)
+ {
+ // ignored
+ }
+ finally
+ {
+ super.succeeded();
+ }
+ });
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ pool.execute(() ->
+ {
+ try
+ {
+ Thread.sleep(20);
+ }
+ catch (InterruptedException ignored)
+ {
+ // ignored
+ }
+ finally
+ {
+ super.failed(x);
+ }
+ });
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/DetectorConnectionTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/DetectorConnectionTest.java
new file mode 100644
index 0000000..fdfa5cc
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/DetectorConnectionTest.java
@@ -0,0 +1,723 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.Socket;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.net.ssl.SSLSocketFactory;
+
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.io.AbstractConnection;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class DetectorConnectionTest
+{
+ private Server _server;
+
+ private static String inputStreamToString(InputStream is) throws IOException
+ {
+ StringBuilder sb = new StringBuilder();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.US_ASCII));
+
+ while (true)
+ {
+ String line = reader.readLine();
+ if (line == null)
+ {
+ // remove the last '\n'
+ if (sb.length() != 0)
+ sb.deleteCharAt(sb.length() - 1);
+ break;
+ }
+ sb.append(line).append('\n');
+ }
+
+ return sb.length() == 0 ? null : sb.toString();
+ }
+
+ private String getResponse(String request) throws Exception
+ {
+ return getResponse(request.getBytes(StandardCharsets.US_ASCII));
+ }
+
+ private String getResponse(byte[]... requests) throws Exception
+ {
+ try (Socket socket = new Socket(_server.getURI().getHost(), _server.getURI().getPort()))
+ {
+ for (byte[] request : requests)
+ {
+ socket.getOutputStream().write(request);
+ }
+ return inputStreamToString(socket.getInputStream());
+ }
+ }
+
+ private String getResponseOverSsl(String request) throws Exception
+ {
+ String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+ sslContextFactory.start();
+
+ SSLSocketFactory socketFactory = sslContextFactory.getSslContext().getSocketFactory();
+ try (Socket socket = socketFactory.createSocket(_server.getURI().getHost(), _server.getURI().getPort()))
+ {
+ socket.getOutputStream().write(request.getBytes(StandardCharsets.US_ASCII));
+ return inputStreamToString(socket.getInputStream());
+ }
+ finally
+ {
+ sslContextFactory.stop();
+ }
+ }
+
+ private void start(ConnectionFactory... connectionFactories) throws Exception
+ {
+ _server = new Server();
+ _server.addConnector(new ServerConnector(_server, 1, 1, connectionFactories));
+ _server.setHandler(new DumpHandler());
+ _server.start();
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ if (_server != null)
+ _server.stop();
+ }
+
+ @Test
+ public void testConnectionClosedDuringDetection() throws Exception
+ {
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol());
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(proxy);
+
+ start(detector, http);
+
+ try (Socket socket = new Socket(_server.getURI().getHost(), _server.getURI().getPort()))
+ {
+ socket.getOutputStream().write("PR".getBytes(StandardCharsets.US_ASCII));
+ Thread.sleep(100); // make sure the onFillable callback gets called
+ socket.getOutputStream().write("OX".getBytes(StandardCharsets.US_ASCII));
+ socket.getOutputStream().close();
+
+ assertThrows(SocketException.class, () -> socket.getInputStream().read());
+ }
+ }
+
+ @Test
+ public void testConnectionClosedDuringProxyV1Handling() throws Exception
+ {
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol());
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(proxy);
+
+ start(detector, http);
+
+ try (Socket socket = new Socket(_server.getURI().getHost(), _server.getURI().getPort()))
+ {
+ socket.getOutputStream().write("PROXY".getBytes(StandardCharsets.US_ASCII));
+ Thread.sleep(100); // make sure the onFillable callback gets called
+ socket.getOutputStream().write(" ".getBytes(StandardCharsets.US_ASCII));
+ socket.getOutputStream().close();
+
+ assertThrows(SocketException.class, () -> socket.getInputStream().read());
+ }
+ }
+
+ @Test
+ public void testConnectionClosedDuringProxyV2HandlingFixedLengthPart() throws Exception
+ {
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol());
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(proxy);
+
+ start(detector, http);
+
+ try (Socket socket = new Socket(_server.getURI().getHost(), _server.getURI().getPort()))
+ {
+ socket.getOutputStream().write(TypeUtil.fromHexString("0D0A0D0A000D0A515549540A")); // proxy V2 Preamble
+ Thread.sleep(100); // make sure the onFillable callback gets called
+ socket.getOutputStream().write(TypeUtil.fromHexString("21")); // V2, PROXY
+ socket.getOutputStream().close();
+
+ assertThrows(SocketException.class, () -> socket.getInputStream().read());
+ }
+ }
+
+ @Test
+ public void testConnectionClosedDuringProxyV2HandlingDynamicLengthPart() throws Exception
+ {
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol());
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(proxy);
+
+ start(detector, http);
+
+ try (Socket socket = new Socket(_server.getURI().getHost(), _server.getURI().getPort()))
+ {
+ socket.getOutputStream().write(TypeUtil.fromHexString(
+ // proxy V2 Preamble
+ "0D0A0D0A000D0A515549540A" +
+ // V2, PROXY
+ "21" +
+ // 0x1 : AF_INET 0x1 : STREAM.
+ "11" +
+ // Address length is 2*4 + 2*2 = 12 bytes.
+ // length of remaining header (4+4+2+2 = 12)
+ "000C"
+ ));
+ Thread.sleep(100); // make sure the onFillable callback gets called
+ socket.getOutputStream().write(TypeUtil.fromHexString(
+ // uint32_t src_addr; uint32_t dst_addr; uint16_t src_port; uint16_t dst_port;
+ "C0A80001" // 8080
+ ));
+ socket.getOutputStream().close();
+
+ assertThrows(SocketException.class, () -> socket.getInputStream().read());
+ }
+ }
+
+ @Test
+ public void testDetectingSslProxyToHttpNoSslWithProxy() throws Exception
+ {
+ String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol());
+ SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, http.getProtocol());
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(ssl, proxy);
+
+ start(detector, http);
+
+ String request = "PROXY TCP 1.2.3.4 5.6.7.8 111 222\r\n" +
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = getResponse(request);
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 200"));
+ assertThat(response, Matchers.containsString("pathInfo=/path"));
+ assertThat(response, Matchers.containsString("local=5.6.7.8:222"));
+ assertThat(response, Matchers.containsString("remote=1.2.3.4:111"));
+ }
+
+ @Test
+ public void testDetectingSslProxyToHttpWithSslNoProxy() throws Exception
+ {
+ String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol());
+ SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, http.getProtocol());
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(ssl, proxy);
+
+ start(detector, http);
+
+ String request = "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = getResponseOverSsl(request);
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 200"));
+ }
+
+ @Test
+ public void testDetectingSslProxyToHttpWithSslWithProxy() throws Exception
+ {
+ String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol());
+ SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, http.getProtocol());
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(ssl, proxy);
+
+ start(detector, http);
+
+ String request = "PROXY TCP 1.2.3.4 5.6.7.8 111 222\r\n" +
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = getResponseOverSsl(request);
+
+ // SSL matched, so the upgrade was made to HTTP which does not understand the proxy request
+ assertThat(response, Matchers.containsString("HTTP/1.1 400"));
+ }
+
+ @Test
+ public void testDetectionUnsuccessfulUpgradesToNextProtocol() throws Exception
+ {
+ String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol());
+ SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, http.getProtocol());
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(ssl, proxy);
+
+ start(detector, http);
+
+ String request = "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = getResponse(request);
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 200"));
+ }
+
+ @Test
+ public void testDetectorToNextDetector() throws Exception
+ {
+ String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol());
+ DetectorConnectionFactory proxyDetector = new DetectorConnectionFactory(proxy);
+ SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, proxyDetector.getProtocol());
+ DetectorConnectionFactory sslDetector = new DetectorConnectionFactory(ssl);
+
+ start(sslDetector, proxyDetector, http);
+
+ String request = "PROXY TCP 1.2.3.4 5.6.7.8 111 222\r\n" +
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = getResponseOverSsl(request);
+
+ // SSL matched, so the upgrade was made to proxy which itself upgraded to HTTP
+ assertThat(response, Matchers.containsString("HTTP/1.1 200"));
+ assertThat(response, Matchers.containsString("pathInfo=/path"));
+ assertThat(response, Matchers.containsString("local=5.6.7.8:222"));
+ assertThat(response, Matchers.containsString("remote=1.2.3.4:111"));
+ }
+
+ @Test
+ public void testDetectorWithDetectionUnsuccessful() throws Exception
+ {
+ AtomicBoolean detectionSuccessful = new AtomicBoolean(true);
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory(HttpVersion.HTTP_1_1.asString());
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(proxy)
+ {
+ @Override
+ protected void nextProtocol(Connector connector, EndPoint endPoint, ByteBuffer buffer)
+ {
+ if (!detectionSuccessful.compareAndSet(true, false))
+ throw new AssertionError("DetectionUnsuccessful callback should only have been called once");
+
+ // omitting this will leak the buffer
+ connector.getByteBufferPool().release(buffer);
+
+ Callback.Completable completable = new Callback.Completable();
+ endPoint.write(completable, ByteBuffer.wrap("No upgrade for you".getBytes(StandardCharsets.US_ASCII)));
+ completable.whenComplete((r, x) -> endPoint.close());
+ }
+ };
+ HttpConnectionFactory http = new HttpConnectionFactory();
+
+ start(detector, http);
+
+ String request = "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = getResponse(request);
+
+ assertEquals("No upgrade for you", response);
+ assertFalse(detectionSuccessful.get());
+ }
+
+ @Test
+ public void testDetectorWithProxyThatHasNoNextProto() throws Exception
+ {
+ String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory();
+ SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, http.getProtocol());
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(ssl, proxy);
+
+ start(detector, http);
+
+ String request = "PROXY TCP 1.2.3.4 5.6.7.8 111 222\r\n" +
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = getResponse(request);
+
+ // ProxyConnectionFactory has no next protocol -> it cannot upgrade
+ assertThat(response, Matchers.nullValue());
+ }
+
+ @Test
+ public void testOptionalSsl() throws Exception
+ {
+ String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, http.getProtocol());
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(ssl);
+
+ start(detector, http);
+
+ String request =
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String clearTextResponse = getResponse(request);
+ String sslResponse = getResponseOverSsl(request);
+
+ // both clear text and SSL can be responded to just fine
+ assertThat(clearTextResponse, Matchers.containsString("HTTP/1.1 200"));
+ assertThat(sslResponse, Matchers.containsString("HTTP/1.1 200"));
+ }
+
+ @Test
+ public void testDetectorThatHasNoConfiguredNextProto() throws Exception
+ {
+ String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(ssl);
+
+ start(detector);
+
+ String request =
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = getResponse(request);
+
+ assertThat(response, Matchers.nullValue());
+ }
+
+ @Test
+ public void testDetectorWithNextProtocolThatDoesNotExist() throws Exception
+ {
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory("does-not-exist");
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(proxy);
+
+ start(detector, http);
+
+ String proxyReq =
+ // proxy V2 Preamble
+ "0D0A0D0A000D0A515549540A" +
+ // V2, PROXY
+ "21" +
+ // 0x1 : AF_INET 0x1 : STREAM.
+ "11" +
+ // Address length is 2*4 + 2*2 = 12 bytes.
+ // length of remaining header (4+4+2+2 = 12)
+ "000C" +
+ // uint32_t src_addr; uint32_t dst_addr; uint16_t src_port; uint16_t dst_port;
+ "C0A80001" + // 192.168.0.1
+ "7f000001" + // 127.0.0.1
+ "3039" + // 12345
+ "1F90"; // 8080
+
+ String httpReq =
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = getResponse(TypeUtil.fromHexString(proxyReq), httpReq.getBytes(StandardCharsets.US_ASCII));
+
+ assertThat(response, Matchers.nullValue());
+ }
+
+ @Test
+ public void testDetectingWithNextProtocolThatDoesNotImplementUpgradeTo() throws Exception
+ {
+ ConnectionFactory.Detecting noUpgradeTo = new ConnectionFactory.Detecting()
+ {
+ @Override
+ public Detection detect(ByteBuffer buffer)
+ {
+ return Detection.RECOGNIZED;
+ }
+
+ @Override
+ public String getProtocol()
+ {
+ return "noUpgradeTo";
+ }
+
+ @Override
+ public List<String> getProtocols()
+ {
+ return Collections.singletonList(getProtocol());
+ }
+
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endPoint)
+ {
+ return new AbstractConnection(null, connector.getExecutor())
+ {
+ @Override
+ public void onFillable()
+ {
+ }
+ };
+ }
+ };
+
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(noUpgradeTo);
+
+ start(detector, http);
+
+ String proxyReq =
+ // proxy V2 Preamble
+ "0D0A0D0A000D0A515549540A" +
+ // V2, PROXY
+ "21" +
+ // 0x1 : AF_INET 0x1 : STREAM.
+ "11" +
+ // Address length is 2*4 + 2*2 = 12 bytes.
+ // length of remaining header (4+4+2+2 = 12)
+ "000C" +
+ // uint32_t src_addr; uint32_t dst_addr; uint16_t src_port; uint16_t dst_port;
+ "C0A80001" + // 192.168.0.1
+ "7f000001" + // 127.0.0.1
+ "3039" + // 12345
+ "1F90"; // 8080
+
+ String httpReq =
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = getResponse(TypeUtil.fromHexString(proxyReq), httpReq.getBytes(StandardCharsets.US_ASCII));
+
+ assertThat(response, Matchers.nullValue());
+ }
+
+ @Test
+ public void testDetectorWithNextProtocolThatDoesNotImplementUpgradeTo() throws Exception
+ {
+ ConnectionFactory noUpgradeTo = new ConnectionFactory()
+ {
+ @Override
+ public String getProtocol()
+ {
+ return "noUpgradeTo";
+ }
+
+ @Override
+ public List<String> getProtocols()
+ {
+ return Collections.singletonList(getProtocol());
+ }
+
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endPoint)
+ {
+ return new AbstractConnection(null, connector.getExecutor())
+ {
+ @Override
+ public void onFillable()
+ {
+ }
+ };
+ }
+ };
+
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol());
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(proxy);
+
+ start(detector, noUpgradeTo);
+
+ String request =
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = getResponse(request);
+
+ assertThat(response, Matchers.nullValue());
+ }
+
+ @Test
+ public void testGeneratedProtocolNames()
+ {
+ String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory(HttpVersion.HTTP_1_1.asString());
+ SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
+
+ assertEquals("[SSL|[proxy]]", new DetectorConnectionFactory(ssl, proxy).getProtocol());
+ assertEquals("[[proxy]|SSL]", new DetectorConnectionFactory(proxy, ssl).getProtocol());
+ }
+
+ @Test
+ public void testDetectorWithNoDetectingFails()
+ {
+ assertThrows(IllegalArgumentException.class, DetectorConnectionFactory::new);
+ }
+
+ @Test
+ public void testExerciseDetectorNotEnoughBytes() throws Exception
+ {
+ ConnectionFactory.Detecting detectingNeverRecognizes = new ConnectionFactory.Detecting()
+ {
+ @Override
+ public Detection detect(ByteBuffer buffer)
+ {
+ return Detection.NOT_RECOGNIZED;
+ }
+
+ @Override
+ public String getProtocol()
+ {
+ return "nevergood";
+ }
+
+ @Override
+ public List<String> getProtocols()
+ {
+ throw new AssertionError();
+ }
+
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endPoint)
+ {
+ throw new AssertionError();
+ }
+ };
+
+ ConnectionFactory.Detecting detectingAlwaysNeedMoreBytes = new ConnectionFactory.Detecting()
+ {
+ @Override
+ public Detection detect(ByteBuffer buffer)
+ {
+ return Detection.NEED_MORE_BYTES;
+ }
+
+ @Override
+ public String getProtocol()
+ {
+ return "neverenough";
+ }
+
+ @Override
+ public List<String> getProtocols()
+ {
+ throw new AssertionError();
+ }
+
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endPoint)
+ {
+ throw new AssertionError();
+ }
+ };
+
+ DetectorConnectionFactory detector = new DetectorConnectionFactory(detectingNeverRecognizes, detectingAlwaysNeedMoreBytes);
+ HttpConnectionFactory http = new HttpConnectionFactory();
+
+ start(detector, http);
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 32768; i++)
+ {
+ sb.append("AAAA");
+ }
+ String request = sb.toString();
+
+ try
+ {
+ String response = getResponse(request);
+ assertThat(response, Matchers.nullValue());
+ }
+ catch (SocketException expected)
+ {
+ // The test may fail writing the "request"
+ // bytes as the server sends back a TCP RST.
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java
new file mode 100644
index 0000000..d7da257
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java
@@ -0,0 +1,265 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.Enumeration;
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Dump request handler.
+ * Dumps GET and POST requests.
+ * Useful for testing and debugging.
+ */
+public class DumpHandler extends AbstractHandler
+{
+ private static final Logger LOG = Log.getLogger(DumpHandler.class);
+
+ String label = "Dump HttpHandler";
+
+ public DumpHandler()
+ {
+ }
+
+ public DumpHandler(String label)
+ {
+ this.label = label;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (!isStarted())
+ return;
+
+ if (Boolean.parseBoolean(request.getParameter("flush")))
+ response.flushBuffer();
+
+ if (Boolean.parseBoolean(request.getParameter("empty")))
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ return;
+ }
+
+ StringBuilder read = null;
+ if (request.getParameter("read") != null)
+ {
+ read = new StringBuilder();
+ int len = Integer.parseInt(request.getParameter("read"));
+ Reader in = request.getReader();
+ for (int i = len; i-- > 0; )
+ {
+ read.append((char)in.read());
+ }
+ }
+
+ if (request.getParameter("date") != null)
+ response.setHeader("Date", request.getParameter("date"));
+
+ if (request.getParameter("ISE") != null)
+ {
+ throw new IllegalStateException("Testing ISE");
+ }
+
+ if (request.getParameter("error") != null)
+ {
+ response.sendError(Integer.parseInt(request.getParameter("error")));
+ return;
+ }
+
+ baseRequest.setHandled(true);
+ response.setHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.TEXT_HTML.asString());
+
+ OutputStream out = response.getOutputStream();
+ ByteArrayOutputStream buf = new ByteArrayOutputStream(2048);
+ Writer writer = new OutputStreamWriter(buf, StandardCharsets.ISO_8859_1);
+ writer.write("<html><h1>" + label + "</h1>");
+ writer.write("<pre>\npathInfo=" + request.getPathInfo() + "\n</pre>\n");
+ writer.write("<pre>\ncontentType=" + request.getContentType() + "\n</pre>\n");
+ writer.write("<pre>\nencoding=" + request.getCharacterEncoding() + "\n</pre>\n");
+ writer.write("<pre>\nservername=" + request.getServerName() + "\n</pre>\n");
+ writer.write("<pre>\nlocal=" + request.getLocalAddr() + ":" + request.getLocalPort() + "\n</pre>\n");
+ writer.write("<pre>\nremote=" + request.getRemoteAddr() + ":" + request.getRemotePort() + "\n</pre>\n");
+ writer.write("<h3>Header:</h3><pre>");
+ writer.write(String.format("%4s %s %s\n", request.getMethod(), request.getRequestURI(), request.getProtocol()));
+ Enumeration<String> headers = request.getHeaderNames();
+ while (headers.hasMoreElements())
+ {
+ String name = headers.nextElement();
+ writer.write(name);
+ writer.write(": ");
+ String value = request.getHeader(name);
+ writer.write(value == null ? "" : value);
+ writer.write("\n");
+ }
+ writer.write("</pre>\n<h3>Parameters:</h3>\n<pre>");
+ Enumeration<String> names = request.getParameterNames();
+ while (names.hasMoreElements())
+ {
+ String name = names.nextElement();
+ String[] values = request.getParameterValues(name);
+ if (values == null || values.length == 0)
+ {
+ writer.write(name);
+ writer.write("=\n");
+ }
+ else if (values.length == 1)
+ {
+ writer.write(name);
+ writer.write("=");
+ writer.write(values[0]);
+ writer.write("\n");
+ }
+ else
+ {
+ for (int i = 0; i < values.length; i++)
+ {
+ writer.write(name);
+ writer.write("[" + i + "]=");
+ writer.write(values[i]);
+ writer.write("\n");
+ }
+ }
+ }
+
+ String cookieName = request.getParameter("CookieName");
+ if (cookieName != null && cookieName.trim().length() > 0)
+ {
+ String cookieAction = request.getParameter("Button");
+ try
+ {
+ String val = request.getParameter("CookieVal");
+ val = val.replaceAll("[ \n\r=<>]", "?");
+ Cookie cookie =
+ new Cookie(cookieName.trim(), val);
+ if ("Clear Cookie".equals(cookieAction))
+ cookie.setMaxAge(0);
+ response.addCookie(cookie);
+ }
+ catch (IllegalArgumentException e)
+ {
+ writer.write("</pre>\n<h3>BAD Set-Cookie:</h3>\n<pre>");
+ writer.write(e.toString());
+ }
+ }
+
+ writer.write("</pre>\n<h3>Cookies:</h3>\n<pre>");
+ Cookie[] cookies = request.getCookies();
+ if (cookies != null && cookies.length > 0)
+ {
+ for (int c = 0; c < cookies.length; c++)
+ {
+ Cookie cookie = cookies[c];
+ writer.write(cookie.getName());
+ writer.write("=");
+ writer.write(cookie.getValue());
+ writer.write("\n");
+ }
+ }
+
+ writer.write("</pre>\n<h3>Attributes:</h3>\n<pre>");
+ Enumeration<String> attributes = request.getAttributeNames();
+ if (attributes != null && attributes.hasMoreElements())
+ {
+ while (attributes.hasMoreElements())
+ {
+ String attr = attributes.nextElement().toString();
+ writer.write(attr);
+ writer.write("=");
+ writer.write(request.getAttribute(attr).toString());
+ writer.write("\n");
+ }
+ }
+
+ writer.write("</pre>\n<h3>Content:</h3>\n<pre>");
+
+ if (read != null)
+ {
+ writer.write(read.toString());
+ }
+ else
+ {
+ char[] content = new char[4096];
+ int len;
+ try
+ {
+ Reader in = request.getReader();
+ while ((len = in.read(content)) >= 0)
+ {
+ writer.write(new String(content, 0, len));
+ }
+ }
+ catch (IOException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.warn(e);
+ else
+ LOG.warn(e.toString());
+ writer.write(e.toString());
+ }
+ }
+
+ writer.write("</pre>\n");
+ writer.write("</html>\n");
+ writer.flush();
+
+ // commit now
+ if (!Boolean.parseBoolean(request.getParameter("no-content-length")))
+ response.setContentLength(buf.size() + 1000);
+ response.addHeader("Before-Flush", response.isCommitted() ? "Committed???" : "Not Committed");
+ buf.writeTo(out);
+ out.flush();
+ response.addHeader("After-Flush", "These headers should not be seen in the response!!!");
+ response.addHeader("After-Flush", response.isCommitted() ? "Committed" : "Not Committed?");
+
+ // write remaining content after commit
+ try
+ {
+ buf.reset();
+ writer.flush();
+ for (int pad = 998; pad-- > 0; )
+ {
+ writer.write(" ");
+ }
+ writer.write("\r\n");
+ writer.flush();
+ buf.writeTo(out);
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java
new file mode 100644
index 0000000..b31eb60
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java
@@ -0,0 +1,759 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.DispatcherType;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.ajax.JSON;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.w3c.dom.Document;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ErrorHandlerTest
+{
+ StacklessLogging stacklessLogging;
+ Server server;
+ LocalConnector connector;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ stacklessLogging = new StacklessLogging(HttpChannel.class);
+ server = new Server();
+ connector = new LocalConnector(server);
+ server.addConnector(connector);
+
+ server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+
+ if (baseRequest.getDispatcherType() == DispatcherType.ERROR)
+ {
+ baseRequest.setHandled(true);
+ response.sendError((Integer)request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE));
+ return;
+ }
+
+ if (target.startsWith("/charencoding/"))
+ {
+ baseRequest.setHandled(true);
+ response.setCharacterEncoding("utf-8");
+ response.sendError(404);
+ return;
+ }
+
+ if (target.startsWith("/badmessage/"))
+ {
+ int code = Integer.parseInt(target.substring(target.lastIndexOf('/') + 1));
+ throw new ServletException(new BadMessageException(code));
+ }
+
+ // produce an exception with an JSON formatted cause message
+ if (target.startsWith("/jsonmessage/"))
+ {
+ String message = "\"}, \"glossary\": {\n \"title\": \"example\"\n }\n {\"";
+ throw new ServletException(new RuntimeException(message));
+ }
+
+ // produce an exception with an XML cause message
+ if (target.startsWith("/xmlmessage/"))
+ {
+ String message =
+ "<!DOCTYPE glossary PUBLIC \"-//OASIS//DTD DocBook V3.1//EN\">\n" +
+ " <glossary>\n" +
+ " <title>example glossary</title>\n" +
+ " </glossary>";
+ throw new ServletException(new RuntimeException(message));
+ }
+
+ // produce an exception with an HTML cause message
+ if (target.startsWith("/htmlmessage/"))
+ {
+ String message = "<hr/><script>alert(42)</script>%3Cscript%3E";
+ throw new ServletException(new RuntimeException(message));
+ }
+
+ // produce an exception with a UTF-8 cause message
+ if (target.startsWith("/utf8message/"))
+ {
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharacters
+ String message = "Euro is € and \u20AC and %E2%82%AC";
+ // @checkstyle-enable-check : AvoidEscapedUnicodeCharacters
+ throw new ServletException(new RuntimeException(message));
+ }
+ }
+ });
+ server.start();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ server.stop();
+ stacklessLogging.close();
+ }
+
+ @Test
+ public void test404NoAccept() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\""));
+
+ assertContent(response);
+ }
+
+ @Test
+ public void test404EmptyAccept() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Accept: \r\n" +
+ "Host: Localhost\r\n" +
+ "\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), is(0));
+ assertThat("Response Content-Type", response.getField(HttpHeader.CONTENT_TYPE), is(nullValue()));
+ }
+
+ @Test
+ public void test404UnAccept() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Accept: text/*;q=0\r\n" +
+ "Host: Localhost\r\n" +
+ "\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ dump(response);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), is(0));
+ assertThat("Response Content-Type", response.getField(HttpHeader.CONTENT_TYPE), is(nullValue()));
+ }
+
+ private void dump(HttpTester.Response response)
+ {
+ System.out.println("-------------");
+ System.out.println(response);
+ System.out.println(response.getContent());
+ System.out.println();
+ }
+
+ @Test
+ public void test404AllAccept() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: */*\r\n" +
+ "\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\""));
+ assertContent(response);
+ }
+
+ @Test
+ public void test404HtmlAccept() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\""));
+ assertContent(response);
+ }
+
+ @Test
+ public void test404PostHttp10() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "POST / HTTP/1.0\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html\r\n" +
+ "Content-Length: 10\r\n" +
+ "Connection: keep-alive\r\n" +
+ "\r\n" +
+ "0123456789");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat(response.getStatus(), is(404));
+ assertThat(response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat(response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\""));
+ assertThat(response.get(HttpHeader.CONNECTION), is("keep-alive"));
+ assertContent(response);
+ }
+
+ @Test
+ public void test404PostHttp11() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "POST / HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html\r\n" +
+ "Content-Length: 10\r\n" +
+ "Connection: keep-alive\r\n" + // This is not need by HTTP/1.1 but sometimes sent anyway
+ "\r\n" +
+ "0123456789");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat(response.getStatus(), is(404));
+ assertThat(response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat(response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\""));
+ assertThat(response.getField(HttpHeader.CONNECTION), nullValue());
+ assertContent(response);
+ }
+
+ @Test
+ public void test404PostCantConsumeHttp10() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "POST / HTTP/1.0\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html\r\n" +
+ "Content-Length: 100\r\n" +
+ "Connection: keep-alive\r\n" +
+ "\r\n" +
+ "0123456789");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat(response.getStatus(), is(404));
+ assertThat(response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat(response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\""));
+ assertThat(response.getField(HttpHeader.CONNECTION), nullValue());
+ assertContent(response);
+ }
+
+ @Test
+ public void test404PostCantConsumeHttp11() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "POST / HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html\r\n" +
+ "Content-Length: 100\r\n" +
+ "Connection: keep-alive\r\n" + // This is not need by HTTP/1.1 but sometimes sent anyway
+ "\r\n" +
+ "0123456789");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat(response.getStatus(), is(404));
+ assertThat(response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat(response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\""));
+ assertThat(response.getField(HttpHeader.CONNECTION).getValue(), is("close"));
+ assertContent(response);
+ }
+
+ @Test
+ public void testMoreSpecificAccept() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html, some/other;specific=true\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\""));
+
+ assertContent(response);
+ }
+
+ @Test
+ public void test404HtmlAcceptAnyCharset() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html\r\n" +
+ "Accept-Charset: *\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=UTF-8\""));
+
+ assertContent(response);
+ }
+
+ @Test
+ public void test404HtmlAcceptUtf8Charset() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html\r\n" +
+ "Accept-Charset: utf-8\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=UTF-8\""));
+
+ assertContent(response);
+ }
+
+ @Test
+ public void test404HtmlAcceptNotUtf8Charset() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html\r\n" +
+ "Accept-Charset: utf-8;q=0\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+// System.out.println("response: " + response);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\""));
+
+ assertContent(response);
+ }
+
+ @Test
+ public void test404HtmlAcceptNotUtf8UnknownCharset() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html\r\n" +
+ "Accept-Charset: utf-8;q=0,unknown\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), is(0));
+ assertThat("Response Content-Type", response.getField(HttpHeader.CONTENT_TYPE), is(nullValue()));
+ }
+
+ @Test
+ public void test404HtmlAcceptUnknownUtf8Charset() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html\r\n" +
+ "Accept-Charset: utf-8;q=0.1,unknown\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=UTF-8\""));
+
+ assertContent(response);
+ }
+
+ @Test
+ public void test404PreferHtml() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html;q=1.0,text/json;q=0.5,*/*\r\n" +
+ "Accept-Charset: *\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=UTF-8\""));
+
+ assertContent(response);
+ }
+
+ @Test
+ public void test404PreferJson() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html;q=0.5,text/json;q=1.0,*/*\r\n" +
+ "Accept-Charset: *\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/json"));
+
+ assertContent(response);
+ }
+
+ @Test
+ public void testCharEncoding() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET /charencoding/foo HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/plain\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(404));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/plain"));
+
+ assertContent(response);
+ }
+
+ @Test
+ public void testBadMessage() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET /badmessage/444 HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(444));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\""));
+
+ assertContent(response);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "/jsonmessage/",
+ "/xmlmessage/",
+ "/htmlmessage/",
+ "/utf8message/",
+ })
+ public void testComplexCauseMessageNoAcceptHeader(String path) throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET " + path + " HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(500));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\""));
+
+ String content = assertContent(response);
+
+ if (path.startsWith("/utf8"))
+ {
+ // we are Not expecting UTF-8 output, look for mangled ISO-8859-1 version
+ assertThat("content", content, containsString("Euro is &euro; and ? and %E2%82%AC"));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "/jsonmessage/",
+ "/xmlmessage/",
+ "/htmlmessage/",
+ "/utf8message/",
+ })
+ public void testComplexCauseMessageAcceptUtf8Header(String path) throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET " + path + " HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/html\r\n" +
+ "Accept-Charset: utf-8\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ System.out.println("response: " + response);
+
+ assertThat("Response status code", response.getStatus(), is(500));
+ assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
+ assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8"));
+ assertThat(response.getContent(), containsString("content=\"text/html;charset=UTF-8\""));
+
+ String content = assertContent(response);
+
+ if (path.startsWith("/utf8"))
+ {
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharacters
+ // we are Not expecting UTF-8 output, look for mangled ISO-8859-1 version
+ assertThat("content", content, containsString("Euro is &euro; and \u20AC and %E2%82%AC"));
+ // @checkstyle-enabled-check : AvoidEscapedUnicodeCharacters
+ }
+ }
+
+ private String assertContent(HttpTester.Response response) throws Exception
+ {
+ String contentType = response.get(HttpHeader.CONTENT_TYPE);
+ String content = response.getContent();
+
+ if (contentType.contains("text/html"))
+ {
+ assertThat(content, not(containsString("<script>")));
+ assertThat(content, not(containsString("<glossary>")));
+ assertThat(content, not(containsString("<!DOCTYPE>")));
+ assertThat(content, not(containsString("€")));
+
+ // we expect that our generated output conforms to text/xhtml is well formed
+ DocumentBuilderFactory xmlDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
+ xmlDocumentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ DocumentBuilder db = xmlDocumentBuilderFactory.newDocumentBuilder();
+ try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)))
+ {
+ // We consider this content to be XML well formed if these 2 lines do not throw an Exception
+ Document doc = db.parse(inputStream);
+ doc.getDocumentElement().normalize();
+ }
+ }
+ else if (contentType.contains("text/json"))
+ {
+ Map jo = (Map)JSON.parse(response.getContent());
+
+ Set<String> acceptableKeyNames = new HashSet<>();
+ acceptableKeyNames.add("url");
+ acceptableKeyNames.add("status");
+ acceptableKeyNames.add("message");
+ acceptableKeyNames.add("servlet");
+ acceptableKeyNames.add("cause0");
+ acceptableKeyNames.add("cause1");
+ acceptableKeyNames.add("cause2");
+
+ for (Object key : jo.keySet())
+ {
+ String keyStr = (String)key;
+ assertTrue(acceptableKeyNames.contains(keyStr), "Unexpected Key [" + keyStr + "]");
+
+ Object value = jo.get(key);
+ assertThat("Unexpected value type (" + value.getClass().getName() + ")",
+ value, instanceOf(String.class));
+ }
+
+ assertThat("url field", jo.get("url"), is(notNullValue()));
+ String expectedStatus = String.valueOf(response.getStatus());
+ assertThat("status field", jo.get("status"), is(expectedStatus));
+ String message = (String)jo.get("message");
+ assertThat("message field", message, is(notNullValue()));
+ assertThat("message field", message, anyOf(
+ not(containsString("<")),
+ not(containsString(">"))));
+ }
+ else if (contentType.contains("text/plain"))
+ {
+ assertThat(content, containsString("STATUS: " + response.getStatus()));
+ }
+ else
+ {
+ System.out.println("Not checked Content-Type: " + contentType);
+ System.out.println(content);
+ }
+
+ return content;
+ }
+
+ @Test
+ public void testJsonResponse() throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET /badmessage/444 HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/json\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ assertThat("Response status code", response.getStatus(), is(444));
+
+ assertContent(response);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "/jsonmessage/",
+ "/xmlmessage/",
+ "/htmlmessage/",
+ "/utf8message/",
+ })
+ public void testJsonResponseWorse(String path) throws Exception
+ {
+ String rawResponse = connector.getResponse(
+ "GET " + path + " HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "Accept: text/json\r\n" +
+ "\r\n");
+
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat("Response status code", response.getStatus(), is(500));
+
+ String content = assertContent(response);
+
+ if (path.startsWith("/utf8"))
+ {
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharacters
+ // we are expecting UTF-8 output, look for it.
+ assertThat("content", content, containsString("Euro is &euro; and \u20AC and %E2%82%AC"));
+ // @checkstyle-enable-check : AvoidEscapedUnicodeCharacters
+ }
+ }
+
+ @Test
+ public void testErrorContextRecycle() throws Exception
+ {
+ server.stop();
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+ server.setHandler(contexts);
+ ContextHandler context = new ContextHandler("/foo");
+ contexts.addHandler(context);
+ context.setErrorHandler(new ErrorHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.getOutputStream().println("Context Error");
+ }
+ });
+ context.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.sendError(444);
+ }
+ });
+
+ server.setErrorHandler(new ErrorHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.getOutputStream().println("Server Error");
+ }
+ });
+
+ server.start();
+
+ LocalConnector.LocalEndPoint connection = connector.connect();
+ connection.addInputAndExecute(BufferUtil.toBuffer(
+ "GET /foo/test HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "\r\n"));
+ String response = connection.getResponse();
+
+ assertThat(response, containsString("HTTP/1.1 444 444"));
+ assertThat(response, containsString("Context Error"));
+
+ connection.addInputAndExecute(BufferUtil.toBuffer(
+ "GET /test HTTP/1.1\r\n" +
+ "Host: Localhost\r\n" +
+ "\r\n"));
+ response = connection.getResponse();
+ assertThat(response, containsString("HTTP/1.1 404 Not Found"));
+ assertThat(response, containsString("Server Error"));
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ExtendedServerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ExtendedServerTest.java
new file mode 100644
index 0000000..ae2471d
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ExtendedServerTest.java
@@ -0,0 +1,157 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpCompliance;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.io.ChannelEndPoint;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.ManagedSelector;
+import org.eclipse.jetty.io.SocketChannelEndPoint;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Extended Server Tester.
+ */
+public class ExtendedServerTest extends HttpServerTestBase
+{
+ @BeforeEach
+ public void init() throws Exception
+ {
+ startServer(new ServerConnector(_server, new HttpConnectionFactory()
+ {
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endPoint)
+ {
+ return configure(new ExtendedHttpConnection(getHttpConfiguration(), connector, endPoint), connector, endPoint);
+ }
+ })
+ {
+ @Override
+ protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException
+ {
+ return new ExtendedEndPoint(channel, selectSet, key, getScheduler());
+ }
+ });
+ }
+
+ private static class ExtendedEndPoint extends SocketChannelEndPoint
+ {
+ private volatile long _lastSelected;
+
+ public ExtendedEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler)
+ {
+ super(channel, selector, key, scheduler);
+ }
+
+ @Override
+ public Runnable onSelected()
+ {
+ _lastSelected = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ return super.onSelected();
+ }
+
+ long getLastSelected()
+ {
+ return _lastSelected;
+ }
+ }
+
+ private static class ExtendedHttpConnection extends HttpConnection
+ {
+ public ExtendedHttpConnection(HttpConfiguration config, Connector connector, EndPoint endPoint)
+ {
+ super(config, connector, endPoint, HttpCompliance.RFC7230_LEGACY, false);
+ }
+
+ @Override
+ protected HttpChannelOverHttp newHttpChannel()
+ {
+ return new HttpChannelOverHttp(this, getConnector(), getHttpConfiguration(), getEndPoint(), this)
+ {
+ @Override
+ public boolean startRequest(String method, String uri, HttpVersion version)
+ {
+ getRequest().setAttribute("DispatchedAt", ((ExtendedEndPoint)getEndPoint()).getLastSelected());
+ return super.startRequest(method, uri, version);
+ }
+ };
+ }
+ }
+
+ @Test
+ public void testExtended() throws Exception
+ {
+ configureServer(new DispatchedAtHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ os.write("GET / HTTP/1.0\r\n".getBytes(StandardCharsets.ISO_8859_1));
+ os.flush();
+ Thread.sleep(200);
+ long end = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ os.write("\r\n".getBytes(StandardCharsets.ISO_8859_1));
+
+ // Read the response.
+ String response = readResponse(client);
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.containsString("DispatchedAt="));
+
+ String s = response.substring(response.indexOf("DispatchedAt=") + 13);
+ s = s.substring(0, s.indexOf('\n'));
+ long dispatched = Long.parseLong(s);
+
+ assertThat(dispatched, Matchers.greaterThanOrEqualTo(start));
+ assertThat(dispatched, Matchers.lessThan(end));
+ }
+ }
+
+ protected static class DispatchedAtHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.getOutputStream().print("DispatchedAt=" + request.getAttribute("DispatchedAt") + "\r\n");
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java
new file mode 100644
index 0000000..e879546
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java
@@ -0,0 +1,1283 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ForwardedRequestCustomizerTest
+{
+ private Server server;
+ private RequestHandler handler;
+ private LocalConnector connector;
+ private LocalConnector connectorAlt;
+ private LocalConnector connectorConfigured;
+ private ForwardedRequestCustomizer customizer;
+ private ForwardedRequestCustomizer customizerAlt;
+ private ForwardedRequestCustomizer customizerConfigured;
+
+ private static class Actual
+ {
+ final AtomicReference<String> scheme = new AtomicReference<>();
+ final AtomicBoolean wasSecure = new AtomicBoolean(false);
+ final AtomicReference<String> serverName = new AtomicReference<>();
+ final AtomicReference<Integer> serverPort = new AtomicReference<>();
+ final AtomicReference<String> requestURL = new AtomicReference<>();
+ final AtomicReference<String> remoteAddr = new AtomicReference<>();
+ final AtomicReference<Integer> remotePort = new AtomicReference<>();
+ final AtomicReference<String> sslSession = new AtomicReference<>();
+ final AtomicReference<String> sslCertificate = new AtomicReference<>();
+ }
+
+ private Actual actual;
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ server = new Server();
+
+ // Default behavior Connector
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ http.getHttpConfiguration().setSecurePort(443);
+ customizer = new ForwardedRequestCustomizer();
+ http.getHttpConfiguration().addCustomizer(customizer);
+ connector = new LocalConnector(server, http);
+ server.addConnector(connector);
+
+ // Alternate behavior Connector
+ HttpConnectionFactory httpAlt = new HttpConnectionFactory();
+ httpAlt.getHttpConfiguration().setSecurePort(8443);
+ customizerAlt = new ForwardedRequestCustomizer();
+ httpAlt.getHttpConfiguration().addCustomizer(customizerAlt);
+ connectorAlt = new LocalConnector(server, httpAlt);
+ server.addConnector(connectorAlt);
+
+ // Configured behavior Connector
+ http = new HttpConnectionFactory();
+ customizerConfigured = new ForwardedRequestCustomizer();
+ customizerConfigured.setForwardedHeader("Jetty-Forwarded");
+ customizerConfigured.setForwardedHostHeader("Jetty-Forwarded-Host");
+ customizerConfigured.setForwardedServerHeader("Jetty-Forwarded-Server");
+ customizerConfigured.setForwardedProtoHeader("Jetty-Forwarded-Proto");
+ customizerConfigured.setForwardedForHeader("Jetty-Forwarded-For");
+ customizerConfigured.setForwardedPortHeader("Jetty-Forwarded-Port");
+ customizerConfigured.setForwardedHttpsHeader("Jetty-Proxied-Https");
+ customizerConfigured.setForwardedCipherSuiteHeader("Jetty-Proxy-Auth-Cert");
+ customizerConfigured.setForwardedSslSessionIdHeader("Jetty-Proxy-Ssl-Id");
+
+ http.getHttpConfiguration().addCustomizer(customizerConfigured);
+ connectorConfigured = new LocalConnector(server, http);
+ server.addConnector(connectorConfigured);
+
+ handler = new RequestHandler();
+ server.setHandler(handler);
+
+ handler.requestTester = (request, response) ->
+ {
+ actual = new Actual();
+ actual.wasSecure.set(request.isSecure());
+ actual.sslSession.set(String.valueOf(request.getAttribute("javax.servlet.request.ssl_session_id")));
+ actual.sslCertificate.set(String.valueOf(request.getAttribute("javax.servlet.request.cipher_suite")));
+ actual.scheme.set(request.getScheme());
+ actual.serverName.set(request.getServerName());
+ actual.serverPort.set(request.getServerPort());
+ actual.remoteAddr.set(request.getRemoteAddr());
+ actual.remotePort.set(request.getRemotePort());
+ actual.requestURL.set(request.getRequestURL().toString());
+ return true;
+ };
+
+ server.start();
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ server.stop();
+ }
+
+ public static Stream<Arguments> cases()
+ {
+ return Stream.of(
+ // HTTP 1.0
+ Arguments.of(
+ new Request("HTTP/1.0 - no Host header")
+ .headers(
+ "GET /example HTTP/1.0"
+ ),
+ new Expectations()
+ .scheme("http").serverName("0.0.0.0").serverPort(80)
+ .secure(false)
+ .requestURL("http://0.0.0.0/example")
+ ),
+ Arguments.of(
+ new Request("HTTP/1.0 - Empty Host header")
+ .headers(
+ "GET /example HTTP/1.0",
+ "Host:"
+ ),
+ new Expectations()
+ .scheme("http").serverName("0.0.0.0").serverPort(80)
+ .secure(false)
+ .requestURL("http://0.0.0.0/example")
+ ),
+ Arguments.of(
+ new Request("HTTP/1.0 - No Host header, with X-Forwarded-Host")
+ .headers(
+ "GET /example HTTP/1.0",
+ "X-Forwarded-Host: alt.example.net:7070"
+ ),
+ new Expectations()
+ .scheme("http").serverName("alt.example.net").serverPort(7070)
+ .secure(false)
+ .requestURL("http://alt.example.net:7070/example")
+ ),
+ Arguments.of(
+ new Request("HTTP/1.0 - Empty Host header, with X-Forwarded-Host")
+ .headers(
+ "GET /example HTTP/1.0",
+ "Host:",
+ "X-Forwarded-Host: alt.example.net:7070"
+ ),
+ new Expectations()
+ .scheme("http").serverName("alt.example.net").serverPort(7070)
+ .secure(false)
+ .requestURL("http://alt.example.net:7070/example")
+ ),
+ // Host IPv4
+ Arguments.of(
+ new Request("IPv4 Host Only")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: 1.2.3.4:2222"
+ ),
+ new Expectations()
+ .scheme("http").serverName("1.2.3.4").serverPort(2222)
+ .secure(false)
+ .requestURL("http://1.2.3.4:2222/")
+ ),
+ Arguments.of(new Request("IPv6 Host Only")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: [::1]:2222"
+ ),
+ new Expectations()
+ .scheme("http").serverName("[::1]").serverPort(2222)
+ .secure(false)
+ .requestURL("http://[::1]:2222/")
+ ),
+ Arguments.of(new Request("IPv4 in Request Line")
+ .headers(
+ "GET https://1.2.3.4:2222/ HTTP/1.1",
+ "Host: wrong"
+ ),
+ new Expectations()
+ .scheme("https").serverName("1.2.3.4").serverPort(2222)
+ .secure(true)
+ .requestURL("https://1.2.3.4:2222/")
+ ),
+ Arguments.of(new Request("IPv6 in Request Line")
+ .headers(
+ "GET http://[::1]:2222/ HTTP/1.1",
+ "Host: wrong"
+ ),
+ new Expectations()
+ .scheme("http").serverName("[::1]").serverPort(2222)
+ .secure(false)
+ .requestURL("http://[::1]:2222/")
+ ),
+
+ // =================================================================
+ // https://tools.ietf.org/html/rfc7239#section-4 - examples of syntax
+ Arguments.of(new Request("RFC7239 Examples: Section 4")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "Forwarded: for=\"_gazonk\"",
+ "Forwarded: For=\"[2001:db8:cafe::17]:4711\"",
+ "Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43",
+ "Forwarded: for=192.0.2.43, for=198.51.100.17"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/")
+ .remoteAddr("[2001:db8:cafe::17]").remotePort(4711)
+ ),
+
+ // https://tools.ietf.org/html/rfc7239#section-7 - Examples of syntax with regards to HTTP header fields
+ Arguments.of(new Request("RFC7239 Examples: Section 7")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "Forwarded: for=192.0.2.43,for=\"[2001:db8:cafe::17]\",for=unknown"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/")
+ .remoteAddr("192.0.2.43").remotePort(0)
+ ),
+
+ // (same as above, but with spaces, as shown in RFC section 7.1)
+ Arguments.of(new Request("RFC7239 Examples: Section 7 (spaced)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "Forwarded: for=192.0.2.43, for=\"[2001:db8:cafe::17]\", for=unknown"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/")
+ .remoteAddr("192.0.2.43").remotePort(0)
+ ),
+
+ // (same as above, but as multiple headers, as shown in RFC section 7.1)
+ Arguments.of(new Request("RFC7239 Examples: Section 7 (multi header)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "Forwarded: for=192.0.2.43",
+ "Forwarded: for=\"[2001:db8:cafe::17]\", for=unknown"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/")
+ .remoteAddr("192.0.2.43").remotePort(0)
+ ),
+
+ // https://tools.ietf.org/html/rfc7239#section-7.4 - Transition
+ Arguments.of(new Request("RFC7239 Examples: Section 7.4 (old syntax)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-For: 192.0.2.43, 2001:db8:cafe::17"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/")
+ .remoteAddr("192.0.2.43").remotePort(0)
+ ),
+ Arguments.of(new Request("RFC7239 Examples: Section 7.4 (new syntax)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "Forwarded: for=192.0.2.43, for=\"[2001:db8:cafe::17]\""
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/")
+ .remoteAddr("192.0.2.43").remotePort(0)
+ ),
+
+ // https://tools.ietf.org/html/rfc7239#section-7.5 - Example Usage
+ Arguments.of(new Request("RFC7239 Examples: Section 7.5")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "Forwarded: for=192.0.2.43,for=198.51.100.17;by=203.0.113.60;proto=http;host=example.com"
+ ),
+ new Expectations()
+ .scheme("http").serverName("example.com").serverPort(80)
+ .secure(false)
+ .requestURL("http://example.com/")
+ .remoteAddr("192.0.2.43").remotePort(0)
+ ),
+
+ // Forwarded, proto only
+ Arguments.of(new Request("RFC7239: Forwarded proto only")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "Forwarded: proto=https"
+ ),
+ new Expectations()
+ .scheme("https").serverName("myhost").serverPort(443)
+ .secure(true)
+ .requestURL("https://myhost/")
+ ),
+
+ // =================================================================
+ // ProxyPass usages
+ Arguments.of(new Request("ProxyPass (example.com:80 to localhost:8080)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: localhost:8080",
+ "X-Forwarded-For: 10.20.30.40",
+ "X-Forwarded-Host: example.com"
+ ),
+ new Expectations()
+ .scheme("http").serverName("example.com").serverPort(80)
+ .secure(false)
+ .remoteAddr("10.20.30.40")
+ .requestURL("http://example.com/")
+ ),
+ Arguments.of(new Request("ProxyPass (example.com:81 to localhost:8080)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: localhost:8080",
+ "X-Forwarded-For: 10.20.30.40",
+ "X-Forwarded-Host: example.com:81",
+ "X-Forwarded-Server: example.com",
+ "X-Forwarded-Proto: https"
+ ),
+ new Expectations()
+ .scheme("https").serverName("example.com").serverPort(81)
+ .secure(true)
+ .remoteAddr("10.20.30.40")
+ .requestURL("https://example.com:81/")
+ ),
+ Arguments.of(new Request("ProxyPass (example.com:443 to localhost:8443)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: localhost:8443",
+ "X-Forwarded-Host: example.com",
+ "X-Forwarded-Proto: https"
+ ),
+ new Expectations()
+ .scheme("https").serverName("example.com").serverPort(443)
+ .secure(true)
+ .requestURL("https://example.com/")
+ ),
+ Arguments.of(new Request("ProxyPass (IPv6 from [::1]:80 to localhost:8080)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: localhost:8080",
+ "X-Forwarded-For: 10.20.30.40",
+ "X-Forwarded-Host: [::1]"
+ ),
+ new Expectations()
+ .scheme("http").serverName("[::1]").serverPort(80)
+ .secure(false)
+ .remoteAddr("10.20.30.40")
+ .requestURL("http://[::1]/")
+ ),
+ Arguments.of(new Request("ProxyPass (IPv6 from [::1]:8888 to localhost:8080)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: localhost:8080",
+ "X-Forwarded-For: 10.20.30.40",
+ "X-Forwarded-Host: [::1]:8888"
+ ),
+ new Expectations()
+ .scheme("http").serverName("[::1]").serverPort(8888)
+ .secure(false)
+ .remoteAddr("10.20.30.40")
+ .requestURL("http://[::1]:8888/")
+ ),
+ Arguments.of(new Request("Multiple ProxyPass (example.com:80 to rp.example.com:82 to localhost:8080)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: localhost:8080",
+ "X-Forwarded-For: 10.20.30.40, 10.0.0.1",
+ "X-Forwarded-Host: example.com, rp.example.com:82",
+ "X-Forwarded-Server: example.com, rp.example.com",
+ "X-Forwarded-Proto: https, http"
+ ),
+ new Expectations()
+ .scheme("https").serverName("example.com").serverPort(443)
+ .secure(true)
+ .remoteAddr("10.20.30.40")
+ .requestURL("https://example.com/")
+ ),
+ // =================================================================
+ // X-Forwarded-* usages
+ Arguments.of(new Request("X-Forwarded-Proto (old syntax)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Proto: https"
+ ),
+ new Expectations()
+ .scheme("https").serverName("myhost").serverPort(443)
+ .secure(true)
+ .requestURL("https://myhost/")
+ ),
+ Arguments.of(new Request("X-Forwarded-For (multiple headers)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-For: 10.9.8.7,6.5.4.3",
+ "X-Forwarded-For: 8.9.8.7,7.5.4.3"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/")
+ .remoteAddr("10.9.8.7").remotePort(0)
+ ),
+ Arguments.of(new Request("X-Forwarded-For (IPv4 with port)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-For: 10.9.8.7:1111,6.5.4.3:2222"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/")
+ .remoteAddr("10.9.8.7").remotePort(1111)
+ ),
+ Arguments.of(new Request("X-Forwarded-For (IPv6 without port)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-For: 2001:db8:cafe::17"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/")
+ .remoteAddr("[2001:db8:cafe::17]").remotePort(0)
+ ),
+ Arguments.of(new Request("X-Forwarded-For (IPv6 with port)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-For: [2001:db8:cafe::17]:1111,6.5.4.3:2222"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/")
+ .remoteAddr("[2001:db8:cafe::17]").remotePort(1111)
+ ),
+ Arguments.of(new Request("X-Forwarded-For and X-Forwarded-Port (once)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-For: 1:2:3:4:5:6:7:8",
+ "X-Forwarded-Port: 2222"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(2222)
+ .secure(false)
+ .requestURL("http://myhost:2222/")
+ .remoteAddr("[1:2:3:4:5:6:7:8]").remotePort(0)
+ ),
+ Arguments.of(new Request("X-Forwarded-For and X-Forwarded-Port (multiple times)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Port: 2222", // this wins
+ "X-Forwarded-For: 1:2:3:4:5:6:7:8", // this wins
+ "X-Forwarded-For: 7:7:7:7:7:7:7:7", // ignored
+ "X-Forwarded-Port: 3333" // ignored
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(2222)
+ .secure(false)
+ .requestURL("http://myhost:2222/")
+ .remoteAddr("[1:2:3:4:5:6:7:8]").remotePort(0)
+ ),
+ Arguments.of(new Request("X-Forwarded-For and X-Forwarded-Port (multiple times combined)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Port: 2222, 3333",
+ "X-Forwarded-For: 1:2:3:4:5:6:7:8, 7:7:7:7:7:7:7:7"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(2222)
+ .secure(false)
+ .requestURL("http://myhost:2222/")
+ .remoteAddr("[1:2:3:4:5:6:7:8]").remotePort(0)
+ ),
+ Arguments.of(new Request("X-Forwarded-Port")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Port: 4444", // resets server port
+ "X-Forwarded-For: 192.168.1.200"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(4444)
+ .secure(false)
+ .requestURL("http://myhost:4444/")
+ .remoteAddr("192.168.1.200").remotePort(0)
+ ),
+ Arguments.of(new Request("X-Forwarded-Port (ForwardedPortAsAuthority==false)")
+ .configureCustomizer((customizer) -> customizer.setForwardedPortAsAuthority(false))
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Port: 4444",
+ "X-Forwarded-For: 192.168.1.200"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/")
+ .remoteAddr("192.168.1.200").remotePort(4444)
+ ),
+ Arguments.of(new Request("X-Forwarded-Port for Late Host header")
+ .headers(
+ "GET / HTTP/1.1",
+ "X-Forwarded-Port: 4444", // this order is intentional
+ "X-Forwarded-For: 192.168.1.200",
+ "Host: myhost" // leave this as last header
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(4444)
+ .secure(false)
+ .requestURL("http://myhost:4444/")
+ .remoteAddr("192.168.1.200").remotePort(0)
+ ),
+ Arguments.of(new Request("X-Forwarded-* (all headers except server)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Proto: https",
+ "X-Forwarded-Host: www.example.com",
+ "X-Forwarded-Port: 4333",
+ "X-Forwarded-For: 8.5.4.3:2222"
+ ),
+ new Expectations()
+ .scheme("https").serverName("www.example.com").serverPort(4333)
+ .secure(true)
+ .requestURL("https://www.example.com:4333/")
+ .remoteAddr("8.5.4.3").remotePort(2222)
+ ),
+ Arguments.of(new Request("X-Forwarded-* (all headers except server, port first)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Proto: https",
+ "X-Forwarded-Port: 4333",
+ "X-Forwarded-Host: www.example.com",
+ "X-Forwarded-For: 8.5.4.3:2222"
+ ),
+ new Expectations()
+ .scheme("https").serverName("www.example.com").serverPort(4333)
+ .secure(true)
+ .requestURL("https://www.example.com:4333/")
+ .remoteAddr("8.5.4.3").remotePort(2222)
+ ),
+ Arguments.of(new Request("X-Forwarded-* (all headers)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Proto: https",
+ "X-Forwarded-Host: www.example.com",
+ "X-Forwarded-Port: 4333",
+ "X-Forwarded-For: 8.5.4.3:2222",
+ "X-Forwarded-Server: fw.example.com"
+ ),
+ new Expectations()
+ .scheme("https").serverName("www.example.com").serverPort(4333)
+ .secure(true)
+ .requestURL("https://www.example.com:4333/")
+ .remoteAddr("8.5.4.3").remotePort(2222)
+ ),
+ Arguments.of(new Request("X-Forwarded-* (Server before Host)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Proto: https",
+ "X-Forwarded-Server: fw.example.com",
+ "X-Forwarded-Host: www.example.com",
+ "X-Forwarded-Port: 4333",
+ "X-Forwarded-For: 8.5.4.3:2222"
+ ),
+ new Expectations()
+ .scheme("https").serverName("www.example.com").serverPort(4333)
+ .secure(true)
+ .requestURL("https://www.example.com:4333/")
+ .remoteAddr("8.5.4.3").remotePort(2222)
+ ),
+ Arguments.of(new Request("X-Forwarded-* (all headers reversed)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Server: fw.example.com",
+ "X-Forwarded-For: 8.5.4.3:2222",
+ "X-Forwarded-Port: 4333",
+ "X-Forwarded-Host: www.example.com",
+ "X-Forwarded-Proto: https"
+ ),
+ new Expectations()
+ .scheme("https").serverName("www.example.com").serverPort(4333)
+ .secure(true)
+ .requestURL("https://www.example.com:4333/")
+ .remoteAddr("8.5.4.3").remotePort(2222)
+ ),
+ Arguments.of(new Request("X-Forwarded-* (Server and Port)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Server: fw.example.com",
+ "X-Forwarded-Port: 4333",
+ "X-Forwarded-For: 8.5.4.3:2222"
+ ),
+ new Expectations()
+ .scheme("http").serverName("fw.example.com").serverPort(4333)
+ .secure(false)
+ .requestURL("http://fw.example.com:4333/")
+ .remoteAddr("8.5.4.3").remotePort(2222)
+ ),
+ Arguments.of(new Request("X-Forwarded-* (Port and Server)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Port: 4333",
+ "X-Forwarded-For: 8.5.4.3:2222",
+ "X-Forwarded-Server: fw.example.com"
+ ),
+ new Expectations()
+ .scheme("http").serverName("fw.example.com").serverPort(4333)
+ .secure(false)
+ .requestURL("http://fw.example.com:4333/")
+ .remoteAddr("8.5.4.3").remotePort(2222)
+ ),
+ Arguments.of(new Request("X-Forwarded-* (Multiple Ports)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost:10001",
+ "X-Forwarded-For: 127.0.0.1:8888,127.0.0.2:9999",
+ "X-Forwarded-Port: 10002",
+ "X-Forwarded-Proto: https",
+ "X-Forwarded-Host: sub1.example.com:10003",
+ "X-Forwarded-Server: sub2.example.com"
+ ),
+ new Expectations()
+ .scheme("https").serverName("sub1.example.com").serverPort(10003)
+ .secure(true)
+ .requestURL("https://sub1.example.com:10003/")
+ .remoteAddr("127.0.0.1").remotePort(8888)
+ ),
+ Arguments.of(new Request("X-Forwarded-* (Multiple Ports - Server First)")
+ .headers(
+ "GET / HTTP/1.1",
+ "X-Forwarded-Server: sub2.example.com:10007",
+ "Host: myhost:10001",
+ "X-Forwarded-For: 127.0.0.1:8888,127.0.0.2:9999",
+ "X-Forwarded-Proto: https",
+ "X-Forwarded-Port: 10002",
+ "X-Forwarded-Host: sub1.example.com:10003"
+ ),
+ new Expectations()
+ .scheme("https").serverName("sub1.example.com").serverPort(10003)
+ .secure(true)
+ .requestURL("https://sub1.example.com:10003/")
+ .remoteAddr("127.0.0.1").remotePort(8888)
+ ),
+ Arguments.of(new Request("X-Forwarded-* (Multiple Ports - setForwardedPortAsAuthority = false)")
+ .configureCustomizer((customizer) -> customizer.setForwardedPortAsAuthority(false))
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost:10001",
+ "X-Forwarded-For: 127.0.0.1:8888,127.0.0.2:9999",
+ "X-Forwarded-Port: 10002",
+ "X-Forwarded-Proto: https",
+ "X-Forwarded-Host: sub1.example.com:10003",
+ "X-Forwarded-Server: sub2.example.com"
+ ),
+ new Expectations()
+ .scheme("https").serverName("sub1.example.com").serverPort(10003)
+ .secure(true)
+ .requestURL("https://sub1.example.com:10003/")
+ .remoteAddr("127.0.0.1").remotePort(8888)
+ ),
+ Arguments.of(new Request("X-Forwarded-* (Multiple Ports Alt Order)")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost:10001",
+ "X-Forwarded-For: 127.0.0.1:8888,127.0.0.2:9999",
+ "X-Forwarded-Proto: https",
+ "X-Forwarded-Host: sub1.example.com:10003",
+ "X-Forwarded-Port: 10002",
+ "X-Forwarded-Server: sub2.example.com"
+ ),
+ new Expectations()
+ .scheme("https").serverName("sub1.example.com").serverPort(10003)
+ .secure(true)
+ .requestURL("https://sub1.example.com:10003/")
+ .remoteAddr("127.0.0.1").remotePort(8888)
+ ),
+ // =================================================================
+ // Mixed Behavior
+ Arguments.of(new Request("RFC7239 mixed with X-Forwarded-* headers")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-For: 11.9.8.7:1111,8.5.4.3:2222",
+ "X-Forwarded-Port: 3333",
+ "Forwarded: for=192.0.2.43,for=198.51.100.17;by=203.0.113.60;proto=http;host=example.com",
+ "X-Forwarded-For: 11.9.8.7:1111,8.5.4.3:2222"
+ ),
+ new Expectations()
+ .scheme("http").serverName("example.com").serverPort(80)
+ .secure(false)
+ .requestURL("http://example.com/")
+ .remoteAddr("192.0.2.43").remotePort(0)
+ ),
+ Arguments.of(
+ new Request("RFC7239 - mixed with HTTP/1.0 - No Host header")
+ .headers(
+ "GET /example HTTP/1.0",
+ "Forwarded: for=1.1.1.1:6060,proto=http;host=alt.example.net:7070"
+ ),
+ new Expectations()
+ .scheme("http").serverName("alt.example.net").serverPort(7070)
+ .secure(false)
+ .requestURL("http://alt.example.net:7070/example")
+ .remoteAddr("1.1.1.1").remotePort(6060)
+ ),
+ Arguments.of(
+ new Request("RFC7239 - mixed with HTTP/1.0 - Empty Host header")
+ .headers(
+ "GET /example HTTP/1.0",
+ "Host:",
+ "Forwarded: for=1.1.1.1:6060,proto=http;host=alt.example.net:7070"
+ ),
+ new Expectations()
+ .scheme("http").serverName("alt.example.net").serverPort(7070)
+ .secure(false)
+ .requestURL("http://alt.example.net:7070/example")
+ .remoteAddr("1.1.1.1").remotePort(6060)
+ ),
+ // =================================================================
+ // Forced Behavior
+ Arguments.of(new Request("Forced Host (no port)")
+ .configureCustomizer((customizer) -> customizer.setForcedHost("always.example.com"))
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-For: 11.9.8.7:1111",
+ "X-Forwarded-Host: example.com:2222"
+ ),
+ new Expectations()
+ .scheme("http").serverName("always.example.com").serverPort(80)
+ .secure(false)
+ .requestURL("http://always.example.com/")
+ .remoteAddr("11.9.8.7").remotePort(1111)
+ ),
+ Arguments.of(new Request("Forced Host with port")
+ .configureCustomizer((customizer) -> customizer.setForcedHost("always.example.com:9090"))
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-For: 11.9.8.7:1111",
+ "X-Forwarded-Host: example.com:2222"
+ ),
+ new Expectations()
+ .scheme("http").serverName("always.example.com").serverPort(9090)
+ .secure(false)
+ .requestURL("http://always.example.com:9090/")
+ .remoteAddr("11.9.8.7").remotePort(1111)
+ ),
+ // =================================================================
+ // Legacy Headers
+ Arguments.of(new Request("X-Proxied-Https")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Proxied-Https: on"
+ ),
+ new Expectations()
+ .scheme("https").serverName("myhost").serverPort(443)
+ .secure(true)
+ .requestURL("https://myhost/")
+ .remoteAddr("0.0.0.0").remotePort(0)
+ ),
+ Arguments.of(new Request("Proxy-Ssl-Id (setSslIsSecure==false)")
+ .configureCustomizer((customizer) -> customizer.setSslIsSecure(false))
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "Proxy-Ssl-Id: Wibble"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/")
+ .remoteAddr("0.0.0.0").remotePort(0)
+ .sslSession("Wibble")
+ ),
+ Arguments.of(new Request("Proxy-Ssl-Id (setSslIsSecure==true)")
+ .configureCustomizer((customizer) -> customizer.setSslIsSecure(true))
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "Proxy-Ssl-Id: 0123456789abcdef"
+ ),
+ new Expectations()
+ .scheme("https").serverName("myhost").serverPort(443)
+ .secure(true)
+ .requestURL("https://myhost/")
+ .remoteAddr("0.0.0.0").remotePort(0)
+ .sslSession("0123456789abcdef")
+ ),
+ Arguments.of(new Request("Proxy-Auth-Cert (setSslIsSecure==false)")
+ .configureCustomizer((customizer) -> customizer.setSslIsSecure(false))
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "Proxy-auth-cert: Wibble"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/")
+ .remoteAddr("0.0.0.0").remotePort(0)
+ .sslCertificate("Wibble")
+ ),
+ Arguments.of(new Request("Proxy-Auth-Cert (setSslIsSecure==true)")
+ .configureCustomizer((customizer) -> customizer.setSslIsSecure(true))
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "Proxy-auth-cert: 0123456789abcdef"
+ ),
+ new Expectations()
+ .scheme("https").serverName("myhost").serverPort(443)
+ .secure(true)
+ .requestURL("https://myhost/")
+ .remoteAddr("0.0.0.0").remotePort(0)
+ .sslCertificate("0123456789abcdef")
+ ),
+ // =================================================================
+ // Complicated scenarios
+ Arguments.of(new Request("No initial authority, X-Forwarded-Proto on http, Proxy-Ssl-Id exists (setSslIsSecure==true)")
+ .configureCustomizer((customizer) -> customizer.setSslIsSecure(true))
+ .headers(
+ "GET /foo HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Proto: http",
+ "Proxy-Ssl-Id: Wibble"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(true)
+ .requestURL("http://myhost/foo")
+ .remoteAddr("0.0.0.0").remotePort(0)
+ .sslSession("Wibble")
+ ),
+ Arguments.of(new Request("https initial authority, X-Forwarded-Proto on http, Proxy-Ssl-Id exists (setSslIsSecure==false)")
+ .configureCustomizer((customizer) -> customizer.setSslIsSecure(false))
+ .headers(
+ "GET https://alt.example.net/foo HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Proto: http",
+ "Proxy-Ssl-Id: Wibble"
+ ),
+ new Expectations()
+ .scheme("http").serverName("alt.example.net").serverPort(80)
+ .secure(false)
+ .requestURL("http://alt.example.net/foo")
+ .remoteAddr("0.0.0.0").remotePort(0)
+ .sslSession("Wibble")
+ ),
+ Arguments.of(new Request("No initial authority, X-Proxied-Https off, Proxy-Ssl-Id exists (setSslIsSecure==true)")
+ .configureCustomizer((customizer) -> customizer.setSslIsSecure(true))
+ .headers(
+ "GET /foo HTTP/1.1",
+ "Host: myhost",
+ "X-Proxied-Https: off", // this wins for scheme and secure
+ "Proxy-Ssl-Id: Wibble"
+ ),
+ new Expectations()
+ .scheme("http").serverName("myhost").serverPort(80)
+ .secure(false)
+ .requestURL("http://myhost/foo")
+ .remoteAddr("0.0.0.0").remotePort(0)
+ .sslSession("Wibble")
+ ),
+ Arguments.of(new Request("Https initial authority, X-Proxied-Https off, Proxy-Ssl-Id exists (setSslIsSecure==true)")
+ .configureCustomizer((customizer) -> customizer.setSslIsSecure(true))
+ .headers(
+ "GET https://alt.example.net/foo HTTP/1.1",
+ "Host: myhost",
+ "X-Proxied-Https: off", // this wins for scheme and secure
+ "Proxy-Ssl-Id: Wibble"
+ ),
+ new Expectations()
+ .scheme("http").serverName("alt.example.net").serverPort(80)
+ .secure(false)
+ .requestURL("http://alt.example.net/foo")
+ .remoteAddr("0.0.0.0").remotePort(0)
+ .sslSession("Wibble")
+ ),
+ Arguments.of(new Request("Https initial authority, X-Proxied-Https off, Proxy-Ssl-Id exists (setSslIsSecure==true) (alt order)")
+ .configureCustomizer((customizer) -> customizer.setSslIsSecure(true))
+ .headers(
+ "GET https://alt.example.net/foo HTTP/1.1",
+ "Host: myhost",
+ "Proxy-Ssl-Id: Wibble",
+ "X-Proxied-Https: off" // this wins for scheme and secure
+ ),
+ new Expectations()
+ .scheme("http").serverName("alt.example.net").serverPort(80)
+ .secure(false)
+ .requestURL("http://alt.example.net/foo")
+ .remoteAddr("0.0.0.0").remotePort(0)
+ .sslSession("Wibble")
+ ),
+ Arguments.of(new Request("Http initial authority, X-Proxied-Https off, Proxy-Ssl-Id exists (setSslIsSecure==false)")
+ .configureCustomizer((customizer) -> customizer.setSslIsSecure(false))
+ .headers(
+ "GET https://alt.example.net/foo HTTP/1.1",
+ "Host: myhost",
+ "X-Proxied-Https: off",
+ "Proxy-Ssl-Id: Wibble",
+ "Proxy-auth-cert: 0123456789abcdef"
+ ),
+ new Expectations()
+ .scheme("http").serverName("alt.example.net").serverPort(80)
+ .secure(false)
+ .requestURL("http://alt.example.net/foo")
+ .remoteAddr("0.0.0.0").remotePort(0)
+ .sslSession("Wibble")
+ .sslCertificate("0123456789abcdef")
+ ),
+ Arguments.of(new Request("Http initial authority, X-Proxied-Https off, Proxy-Ssl-Id exists (setSslIsSecure==false) (alt)")
+ .configureCustomizer((customizer) -> customizer.setSslIsSecure(false))
+ .headers(
+ "GET https://alt.example.net/foo HTTP/1.1",
+ "Host: myhost",
+ "Proxy-Ssl-Id: Wibble",
+ "Proxy-auth-cert: 0123456789abcdef",
+ "X-Proxied-Https: off"
+ ),
+ new Expectations()
+ .scheme("http").serverName("alt.example.net").serverPort(80)
+ .secure(false)
+ .requestURL("http://alt.example.net/foo")
+ .remoteAddr("0.0.0.0").remotePort(0)
+ .sslSession("Wibble")
+ .sslCertificate("0123456789abcdef")
+ )
+ );
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("cases")
+ public void testDefaultBehavior(Request request, Expectations expectations) throws Exception
+ {
+ request.configure(customizer);
+
+ String rawRequest = request.getRawRequest((header) -> header);
+ // System.out.println(rawRequest);
+
+ HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(rawRequest));
+ assertThat("status", response.getStatus(), is(200));
+
+ expectations.accept(actual);
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("cases")
+ public void testConfiguredBehavior(Request request, Expectations expectations) throws Exception
+ {
+ request.configure(customizerConfigured);
+
+ String rawRequest = request.getRawRequest((header) -> header
+ .replaceFirst("X-Forwarded-", "Jetty-Forwarded-")
+ .replaceFirst("Forwarded:", "Jetty-Forwarded:")
+ .replaceFirst("X-Proxied-Https:", "Jetty-Proxied-Https:")
+ .replaceFirst("Proxy-Ssl-Id:", "Jetty-Proxy-Ssl-Id:")
+ .replaceFirst("Proxy-auth-cert:", "Jetty-Proxy-Auth-Cert:"));
+ // System.out.println(rawRequest);
+
+ HttpTester.Response response = HttpTester.parseResponse(connectorConfigured.getResponse(rawRequest));
+ assertThat("status", response.getStatus(), is(200));
+
+ expectations.accept(actual);
+ }
+
+ public static Stream<Arguments> nonStandardPortCases()
+ {
+ return Stream.of(
+ // RFC7239 Tests with https.
+ Arguments.of(new Request("RFC7239 with https and h2")
+ .headers(
+ "GET /test/forwarded.jsp HTTP/1.1",
+ "Host: web.example.net",
+ "Forwarded: for=192.168.2.6;host=web.example.net;proto=https;proto-version=h2"
+ ),
+ new Expectations()
+ .scheme("https").serverName("web.example.net").serverPort(443)
+ .requestURL("https://web.example.net/test/forwarded.jsp")
+ .remoteAddr("192.168.2.6").remotePort(0)
+ ),
+ // RFC7239 Tests with https and proxy provided port
+ Arguments.of(new Request("RFC7239 with proxy provided port on https and h2")
+ .headers(
+ "GET /test/forwarded.jsp HTTP/1.1",
+ "Host: web.example.net:9443",
+ "Forwarded: for=192.168.2.6;host=web.example.net:9443;proto=https;proto-version=h2"
+ ),
+ new Expectations()
+ .scheme("https").serverName("web.example.net").serverPort(9443)
+ .requestURL("https://web.example.net:9443/test/forwarded.jsp")
+ .remoteAddr("192.168.2.6").remotePort(0)
+ ),
+ // RFC7239 Tests with https, no port in Host, but proxy provided port
+ Arguments.of(new Request("RFC7239 with client provided host and different proxy provided port on https and h2")
+ .headers(
+ "GET /test/forwarded.jsp HTTP/1.1",
+ "Host: web.example.net",
+ "Forwarded: for=192.168.2.6;host=new.example.net:7443;proto=https;proto-version=h2"
+ // Client: https://web.example.net/test/forwarded.jsp
+ // Proxy Requests: https://new.example.net/test/forwarded.jsp
+ ),
+ new Expectations()
+ .scheme("https").serverName("new.example.net").serverPort(7443)
+ .requestURL("https://new.example.net:7443/test/forwarded.jsp")
+ .remoteAddr("192.168.2.6").remotePort(0)
+ )
+ );
+ }
+
+ /**
+ * Tests against a Connector with a HttpConfiguration on non-standard ports.
+ * HttpConfiguration is set to securePort of 8443
+ */
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("nonStandardPortCases")
+ public void testNonStandardPortBehavior(Request request, Expectations expectations) throws Exception
+ {
+ request.configure(customizerAlt);
+
+ String rawRequest = request.getRawRequest((header) -> header);
+ // System.out.println(rawRequest);
+
+ HttpTester.Response response = HttpTester.parseResponse(connectorAlt.getResponse(rawRequest));
+ assertThat("status", response.getStatus(), is(200));
+
+ expectations.accept(actual);
+ }
+
+ public static Stream<Request> badRequestCases()
+ {
+ return Stream.of(
+ new Request("Bad port value")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Forwarded-Port: "
+ ),
+ new Request("Invalid X-Proxied-Https value")
+ .headers(
+ "GET / HTTP/1.1",
+ "Host: myhost",
+ "X-Proxied-Https: foo"
+ )
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("badRequestCases")
+ public void testBadInput(Request request) throws Exception
+ {
+ request.configure(customizer);
+
+ String rawRequest = request.getRawRequest((header) -> header);
+
+ HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(rawRequest));
+ assertThat("status", response.getStatus(), is(400));
+ }
+
+ private static class Request
+ {
+ String description;
+ String[] requestHeaders;
+ Consumer<ForwardedRequestCustomizer> forwardedRequestCustomizerConsumer;
+
+ public Request(String description)
+ {
+ this.description = description;
+ }
+
+ public Request headers(String... headers)
+ {
+ this.requestHeaders = headers;
+ return this;
+ }
+
+ public Request configureCustomizer(Consumer<ForwardedRequestCustomizer> forwardedRequestCustomizerConsumer)
+ {
+ this.forwardedRequestCustomizerConsumer = forwardedRequestCustomizerConsumer;
+ return this;
+ }
+
+ public void configure(ForwardedRequestCustomizer customizer)
+ {
+ if (forwardedRequestCustomizerConsumer != null)
+ {
+ forwardedRequestCustomizerConsumer.accept(customizer);
+ }
+ }
+
+ public String getRawRequest(Function<String, String> headerManip)
+ {
+ StringBuilder request = new StringBuilder();
+ for (String header : requestHeaders)
+ {
+ request.append(headerManip.apply(header)).append('\n');
+ }
+ request.append('\n');
+ return request.toString();
+ }
+
+ @Override
+ public String toString()
+ {
+ return this.description;
+ }
+ }
+
+ private static class Expectations implements Consumer<Actual>
+ {
+ String expectedScheme;
+ String expectedServerName;
+ int expectedServerPort;
+ String expectedRequestURL;
+ String expectedRemoteAddr = "0.0.0.0";
+ int expectedRemotePort = 0;
+ String expectedSslSession;
+ String expectedSslCertificate;
+ Boolean secure;
+
+ @Override
+ public void accept(Actual actual)
+ {
+ assertThat("scheme", actual.scheme.get(), is(expectedScheme));
+ if (secure != null && secure)
+ {
+ assertTrue(actual.wasSecure.get(), "wasSecure");
+ }
+ assertThat("serverName", actual.serverName.get(), is(expectedServerName));
+ assertThat("serverPort", actual.serverPort.get(), is(expectedServerPort));
+ assertThat("requestURL", actual.requestURL.get(), is(expectedRequestURL));
+ if (expectedRemoteAddr != null)
+ {
+ assertThat("remoteAddr", actual.remoteAddr.get(), is(expectedRemoteAddr));
+ assertThat("remotePort", actual.remotePort.get(), is(expectedRemotePort));
+ }
+ if (expectedSslSession != null)
+ {
+ assertThat("sslSession", actual.sslSession.get(), is(expectedSslSession));
+ }
+ if (expectedSslCertificate != null)
+ {
+ assertThat("sslCertificate", actual.sslCertificate.get(), is(expectedSslCertificate));
+ }
+ }
+
+ public Expectations secure(boolean flag)
+ {
+ this.secure = flag;
+ return this;
+ }
+
+ public Expectations scheme(String scheme)
+ {
+ this.expectedScheme = scheme;
+ return this;
+ }
+
+ public Expectations serverName(String name)
+ {
+ this.expectedServerName = name;
+ return this;
+ }
+
+ public Expectations serverPort(int port)
+ {
+ this.expectedServerPort = port;
+ return this;
+ }
+
+ public Expectations requestURL(String requestURL)
+ {
+ this.expectedRequestURL = requestURL;
+ return this;
+ }
+
+ public Expectations remoteAddr(String remoteAddr)
+ {
+ this.expectedRemoteAddr = remoteAddr;
+ return this;
+ }
+
+ public Expectations remotePort(int remotePort)
+ {
+ this.expectedRemotePort = remotePort;
+ return this;
+ }
+
+ public Expectations sslSession(String sslSession)
+ {
+ this.expectedSslSession = sslSession;
+ return this;
+ }
+
+ public Expectations sslCertificate(String sslCertificate)
+ {
+ this.expectedSslCertificate = sslCertificate;
+ return this;
+ }
+ }
+
+ interface RequestTester
+ {
+ boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException;
+ }
+
+ private class RequestHandler extends AbstractHandler
+ {
+ private RequestTester requestTester;
+
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest,
+ HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+
+ if (requestTester != null && requestTester.check(request, response))
+ response.setStatus(200);
+ else
+ response.sendError(500);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/GracefulStopTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/GracefulStopTest.java
new file mode 100644
index 0000000..a5a9580
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/GracefulStopTest.java
@@ -0,0 +1,865 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.ConnectException;
+import java.net.Socket;
+import java.nio.channels.ClosedChannelException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Exchanger;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.server.LocalConnector.LocalEndPoint;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.server.handler.StatisticsHandler;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThan;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class GracefulStopTest
+{
+ /**
+ * Test of standard graceful timeout mechanism when a block request does
+ * not complete
+ *
+ * @throws Exception on test failure
+ */
+ @Test
+ public void testGracefulNoWaiter() throws Exception
+ {
+ Server server = new Server();
+ server.setStopTimeout(1000);
+
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(0);
+ server.addConnector(connector);
+
+ TestHandler handler = new TestHandler();
+ server.setHandler(handler);
+
+ server.start();
+ final int port = connector.getLocalPort();
+ Socket client = new Socket("127.0.0.1", port);
+ client.getOutputStream().write((
+ "POST / HTTP/1.0\r\n" +
+ "Host: localhost:" + port + "\r\n" +
+ "Content-Type: plain/text\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "12345"
+ ).getBytes());
+ client.getOutputStream().flush();
+ handler.latch.await();
+
+ long start = System.nanoTime();
+ server.stop();
+ long stop = System.nanoTime();
+
+ // No Graceful waiters
+ assertThat(TimeUnit.NANOSECONDS.toMillis(stop - start), lessThan(900L));
+
+ assertThat(client.getInputStream().read(), is(-1));
+ assertThat(handler.handling.get(), is(false));
+ assertThat(handler.thrown.get(), Matchers.notNullValue());
+ client.close();
+ }
+
+ /**
+ * Test of standard graceful timeout mechanism when a block request does
+ * not complete
+ *
+ * @throws Exception on test failure
+ */
+ @Test
+ public void testGracefulTimeout() throws Exception
+ {
+ Server server = new Server();
+ server.setStopTimeout(1000);
+
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(0);
+ server.addConnector(connector);
+
+ TestHandler handler = new TestHandler();
+ StatisticsHandler stats = new StatisticsHandler();
+ server.setHandler(stats);
+ stats.setHandler(handler);
+
+ server.start();
+ final int port = connector.getLocalPort();
+ Socket client = new Socket("127.0.0.1", port);
+ client.getOutputStream().write((
+ "POST / HTTP/1.0\r\n" +
+ "Host: localhost:" + port + "\r\n" +
+ "Content-Type: plain/text\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "12345"
+ ).getBytes());
+ client.getOutputStream().flush();
+ handler.latch.await();
+
+ long start = System.nanoTime();
+
+ assertThrows(TimeoutException.class, () -> server.stop());
+
+ long stop = System.nanoTime();
+ // No Graceful waiters
+ assertThat(TimeUnit.NANOSECONDS.toMillis(stop - start), greaterThan(900L));
+
+ assertThat(client.getInputStream().read(), is(-1));
+
+ assertThat(handler.handling.get(), is(false));
+ assertThat(handler.thrown.get(), instanceOf(ClosedChannelException.class));
+
+ client.close();
+ }
+
+ /**
+ * Test completed writes during shutdown do not close output
+ *
+ * @throws Exception on test failure
+ */
+ @Test
+ public void testWriteDuringShutdown() throws Exception
+ {
+ Server server = new Server();
+ server.setStopTimeout(1000);
+
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(0);
+ server.addConnector(connector);
+
+ ABHandler handler = new ABHandler();
+ StatisticsHandler stats = new StatisticsHandler();
+ server.setHandler(stats);
+ stats.setHandler(handler);
+
+ server.start();
+
+ Thread stopper = new Thread(() ->
+ {
+ try
+ {
+ handler.latchA.await();
+ server.stop();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ });
+ stopper.start();
+
+ final int port = connector.getLocalPort();
+ try (Socket client = new Socket("127.0.0.1", port))
+ {
+ client.getOutputStream().write((
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost:" + port + "\r\n" +
+ "\r\n"
+ ).getBytes());
+ client.getOutputStream().flush();
+
+ while (!connector.isShutdown())
+ {
+ Thread.sleep(10);
+ }
+
+ handler.latchB.countDown();
+
+ String response = IO.toString(client.getInputStream());
+ assertThat(response, startsWith("HTTP/1.1 200 "));
+ assertThat(response, containsString("Content-Length: 2"));
+ assertThat(response, containsString("Connection: close"));
+ assertThat(response, endsWith("ab"));
+ }
+ stopper.join();
+ }
+
+ /**
+ * Test of standard graceful timeout mechanism when a block request does
+ * complete. Note that even though the request completes after 100ms, the
+ * stop always takes 1000ms
+ *
+ * @throws Exception on test failure
+ */
+ @Test
+ public void testGracefulComplete() throws Exception
+ {
+ Server server = new Server();
+ server.setStopTimeout(10000);
+
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(0);
+ server.addConnector(connector);
+
+ TestHandler handler = new TestHandler();
+ StatisticsHandler stats = new StatisticsHandler();
+ server.setHandler(stats);
+ stats.setHandler(handler);
+
+ server.start();
+ final int port = connector.getLocalPort();
+
+ try (final Socket client1 = new Socket("127.0.0.1", port);
+ final Socket client2 = new Socket("127.0.0.1", port))
+ {
+ client1.getOutputStream().write((
+ "POST / HTTP/1.0\r\n" +
+ "Host: localhost:" + port + "\r\n" +
+ "Content-Type: plain/text\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "12345"
+ ).getBytes());
+ client1.getOutputStream().flush();
+ handler.latch.await();
+
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ long end = now + 500;
+
+ try
+ {
+ Thread.sleep(100);
+
+ // Try creating a new connection
+ try
+ {
+ try (Socket s = new Socket("127.0.0.1", port))
+ {
+ // no op
+ }
+ throw new IllegalStateException();
+ }
+ catch (ConnectException e)
+ {
+ // no op
+ }
+
+ // Try another request on existing connection
+
+ client2.getOutputStream().write((
+ "GET / HTTP/1.0\r\n" +
+ "Host: localhost:" + port + "\r\n" +
+ "\r\n"
+ ).getBytes());
+ client2.getOutputStream().flush();
+ String response2 = IO.toString(client2.getInputStream());
+ assertThat(response2, containsString(" 503 "));
+
+ now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ Thread.sleep(Math.max(1, end - now));
+ client1.getOutputStream().write("567890".getBytes());
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+
+ long start = System.nanoTime();
+ server.stop();
+ long stop = System.nanoTime();
+ assertThat(TimeUnit.NANOSECONDS.toMillis(stop - start), greaterThan(490L));
+ assertThat(TimeUnit.NANOSECONDS.toMillis(stop - start), lessThan(10000L));
+
+ String response = IO.toString(client1.getInputStream());
+
+ assertThat(handler.handling.get(), is(false));
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("read 10/10"));
+
+ // The StatisticsHandler was shutdown when it received the second request so does not contribute to the stats.
+ assertThat(stats.getRequests(), is(1));
+ assertThat(stats.getResponses4xx(), is(0));
+ }
+ }
+
+ public void testSlowClose(long stopTimeout, long closeWait, Matcher<Long> stopTimeMatcher) throws Exception
+ {
+ Server server = new Server();
+ server.setStopTimeout(stopTimeout);
+
+ CountDownLatch closed = new CountDownLatch(1);
+ ServerConnector connector = new ServerConnector(server, 2, 2, new HttpConnectionFactory()
+ {
+
+ @Override
+ public Connection newConnection(Connector con, EndPoint endPoint)
+ {
+ // Slow closing connection
+ HttpConnection conn = new HttpConnection(getHttpConfiguration(), con, endPoint, getHttpCompliance(), isRecordHttpComplianceViolations())
+ {
+ @Override
+ public void close()
+ {
+ try
+ {
+ new Thread(() ->
+ {
+ try
+ {
+ Thread.sleep(closeWait);
+ }
+ catch (InterruptedException e)
+ {
+ // no op
+ }
+ finally
+ {
+ super.close();
+ }
+ }).start();
+ }
+ catch (Exception e)
+ {
+ // e.printStackTrace();
+ }
+ finally
+ {
+ closed.countDown();
+ }
+ }
+ };
+ return configure(conn, con, endPoint);
+ }
+ });
+ connector.setPort(0);
+ server.addConnector(connector);
+
+ NoopHandler handler = new NoopHandler();
+ server.setHandler(handler);
+
+ server.start();
+ final int port = connector.getLocalPort();
+ Socket client = new Socket("127.0.0.1", port);
+ client.setSoTimeout(10000);
+ client.getOutputStream().write((
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost:" + port + "\r\n" +
+ "Content-Type: plain/text\r\n" +
+ "\r\n"
+ ).getBytes());
+ client.getOutputStream().flush();
+ handler.latch.await();
+
+ // look for a response
+ BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.ISO_8859_1));
+ while (true)
+ {
+ String line = in.readLine();
+ assertThat("Line should not be null", line, is(notNullValue()));
+ if (line.length() == 0)
+ break;
+ }
+
+ long start = System.nanoTime();
+ try
+ {
+ server.stop();
+ assertTrue(stopTimeout == 0 || stopTimeout > closeWait);
+ }
+ catch (Exception e)
+ {
+ assertTrue(stopTimeout > 0 && stopTimeout < closeWait);
+ }
+ long stop = System.nanoTime();
+
+ // Check stop time was correct
+ assertThat(TimeUnit.NANOSECONDS.toMillis(stop - start), stopTimeMatcher);
+
+ // Connection closed
+ while (true)
+ {
+ int r = client.getInputStream().read();
+ if (r == -1)
+ break;
+ }
+
+ // onClose Thread interrupted or completed
+ if (stopTimeout > 0)
+ assertTrue(closed.await(1000, TimeUnit.MILLISECONDS));
+
+ if (!client.isClosed())
+ client.close();
+ }
+
+ /**
+ * Test of non graceful stop when a connection close is slow
+ *
+ * @throws Exception on test failure
+ */
+ @Test
+ public void testSlowCloseNotGraceful() throws Exception
+ {
+ Log.getLogger(QueuedThreadPool.class).info("Expect some threads can't be stopped");
+ testSlowClose(0, 5000, lessThan(750L));
+ }
+
+ /**
+ * Test of graceful stop when close is slower than timeout
+ *
+ * @throws Exception on test failure
+ */
+ @Test
+ @Disabled // TODO disable while #2046 is fixed
+ public void testSlowCloseTinyGraceful() throws Exception
+ {
+ Log.getLogger(QueuedThreadPool.class).info("Expect some threads can't be stopped");
+ testSlowClose(1, 5000, lessThan(1500L));
+ }
+
+ /**
+ * Test of graceful stop when close is faster than timeout;
+ *
+ * @throws Exception on test failure
+ */
+ @Test
+ @Disabled // TODO disable while #2046 is fixed
+ public void testSlowCloseGraceful() throws Exception
+ {
+ testSlowClose(5000, 1000, Matchers.allOf(greaterThan(750L), lessThan(4999L)));
+ }
+
+ @Test
+ public void testResponsesAreClosed() throws Exception
+ {
+ Server server = new Server();
+
+ LocalConnector connector = new LocalConnector(server);
+ server.addConnector(connector);
+
+ StatisticsHandler stats = new StatisticsHandler();
+ server.setHandler(stats);
+
+ ContextHandler context = new ContextHandler(stats, "/");
+
+ Exchanger<Void> exchanger0 = new Exchanger<>();
+ Exchanger<Void> exchanger1 = new Exchanger<>();
+ context.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.setContentLength(13);
+ response.flushBuffer();
+
+ try
+ {
+ exchanger0.exchange(null);
+ exchanger1.exchange(null);
+ }
+ catch (Throwable x)
+ {
+ throw new ServletException(x);
+ }
+
+ response.getOutputStream().print("The Response\n");
+ }
+ });
+
+ server.setStopTimeout(1000);
+ server.start();
+
+ LocalEndPoint endp = connector.executeRequest("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
+
+ exchanger0.exchange(null);
+ exchanger1.exchange(null);
+
+ String response = endp.getResponse();
+ assertThat(response, containsString("200 OK"));
+
+ endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"));
+
+ exchanger0.exchange(null);
+
+ server.getConnectors()[0].shutdown().get();
+
+ // Check completed 200 does not have close
+ exchanger1.exchange(null);
+ response = endp.getResponse();
+ assertThat(response, containsString("200 OK"));
+ assertThat(response, Matchers.not(containsString("Connection: close")));
+
+ // But endpoint is still closes soon after
+ long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(1);
+ while (endp.isOpen() && System.nanoTime() < end)
+ {
+ Thread.sleep(10);
+ }
+ assertFalse(endp.isOpen());
+ }
+
+ @Test
+ public void testCommittedResponsesAreClosed() throws Exception
+ {
+ Server server = new Server();
+
+ LocalConnector connector = new LocalConnector(server);
+ server.addConnector(connector);
+
+ StatisticsHandler stats = new StatisticsHandler();
+ server.setHandler(stats);
+
+ ContextHandler context = new ContextHandler(stats, "/");
+
+ Exchanger<Void> exchanger0 = new Exchanger<>();
+ Exchanger<Void> exchanger1 = new Exchanger<>();
+ context.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ try
+ {
+ exchanger0.exchange(null);
+ exchanger1.exchange(null);
+ }
+ catch (Throwable x)
+ {
+ throw new ServletException(x);
+ }
+
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.getWriter().println("The Response");
+ response.getWriter().close();
+ }
+ });
+
+ server.setStopTimeout(1000);
+ server.start();
+
+ LocalEndPoint endp = connector.executeRequest(
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n"
+ );
+
+ exchanger0.exchange(null);
+ exchanger1.exchange(null);
+
+ String response = endp.getResponse();
+ assertThat(response, containsString("200 OK"));
+ assertThat(response, Matchers.not(containsString("Connection: close")));
+
+ endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
+
+ exchanger0.exchange(null);
+
+ CountDownLatch latch = new CountDownLatch(1);
+ new Thread(() ->
+ {
+ try
+ {
+ server.stop();
+ latch.countDown();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }).start();
+ while (server.isStarted())
+ {
+ Thread.sleep(10);
+ }
+
+ // Check new connections rejected!
+ String unavailable = connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n");
+ assertThat(unavailable, containsString(" 503 Service Unavailable"));
+ assertThat(unavailable, Matchers.containsString("Connection: close"));
+
+ // Check completed 200 has close
+ exchanger1.exchange(null);
+ response = endp.getResponse();
+ assertThat(response, containsString("200 OK"));
+ assertThat(response, Matchers.containsString("Connection: close"));
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void testContextStop() throws Exception
+ {
+ Server server = new Server();
+
+ LocalConnector connector = new LocalConnector(server);
+ server.addConnector(connector);
+
+ ContextHandler context = new ContextHandler(server, "/");
+
+ StatisticsHandler stats = new StatisticsHandler();
+ context.setHandler(stats);
+
+ Exchanger<Void> exchanger0 = new Exchanger<>();
+ Exchanger<Void> exchanger1 = new Exchanger<>();
+ stats.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ try
+ {
+ exchanger0.exchange(null);
+ exchanger1.exchange(null);
+ }
+ catch (Throwable x)
+ {
+ throw new ServletException(x);
+ }
+
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.getWriter().println("The Response");
+ response.getWriter().close();
+ }
+ });
+
+ context.setStopTimeout(1000);
+ server.start();
+
+ LocalEndPoint endp = connector.executeRequest(
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n"
+ );
+
+ exchanger0.exchange(null);
+ exchanger1.exchange(null);
+
+ String response = endp.getResponse();
+ assertThat(response, containsString("200 OK"));
+ assertThat(response, Matchers.not(containsString("Connection: close")));
+
+ endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
+ exchanger0.exchange(null);
+
+ CountDownLatch latch = new CountDownLatch(1);
+ new Thread(() ->
+ {
+ try
+ {
+ context.stop();
+ latch.countDown();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }).start();
+ while (context.isStarted())
+ {
+ Thread.sleep(10);
+ }
+
+ // Check new connections accepted, but don't find context!
+ String unavailable = connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n");
+ assertThat(unavailable, containsString(" 404 Not Found"));
+
+ // Check completed 200 does not have close
+ exchanger1.exchange(null);
+ response = endp.getResponse();
+ assertThat(response, containsString("200 OK"));
+ assertThat(response, Matchers.not(Matchers.containsString("Connection: close")));
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void testFailedStart()
+ {
+ Server server = new Server();
+
+ LocalConnector connector = new LocalConnector(server);
+ server.addConnector(connector);
+
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+ server.setHandler(contexts);
+ AtomicBoolean context0Started = new AtomicBoolean(false);
+ ContextHandler context0 = new ContextHandler("/zero")
+ {
+ @Override
+ protected void doStart() throws Exception
+ {
+ context0Started.set(true);
+ }
+ };
+ ContextHandler context1 = new ContextHandler("/one")
+ {
+ @Override
+ protected void doStart() throws Exception
+ {
+ throw new Exception("Test start failure");
+ }
+ };
+ AtomicBoolean context2Started = new AtomicBoolean(false);
+ ContextHandler context2 = new ContextHandler("/two")
+ {
+ @Override
+ protected void doStart() throws Exception
+ {
+ context2Started.set(true);
+ }
+ };
+ contexts.setHandlers(new Handler[]{context0, context1, context2});
+
+ try
+ {
+ server.start();
+ fail();
+ }
+ catch (Exception e)
+ {
+ assertThat(e.getMessage(), is("Test start failure"));
+ }
+
+ assertTrue(server.getContainedBeans(LifeCycle.class).stream().noneMatch(LifeCycle::isRunning));
+ assertTrue(server.getContainedBeans(LifeCycle.class).stream().anyMatch(LifeCycle::isFailed));
+ assertTrue(context0Started.get());
+ assertFalse(context2Started.get());
+ }
+
+ static class NoopHandler extends AbstractHandler
+ {
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ NoopHandler()
+ {
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ latch.countDown();
+ }
+ }
+
+ static class ABHandler extends AbstractHandler
+ {
+ final CountDownLatch latchA = new CountDownLatch(1);
+ final CountDownLatch latchB = new CountDownLatch(1);
+
+ @Override
+ public void handle(String s, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ response.setContentLength(2);
+ response.getOutputStream().write("a".getBytes());
+ try
+ {
+ latchA.countDown();
+ latchB.await();
+ }
+ catch (InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ response.flushBuffer();
+ response.getOutputStream().write("b".getBytes());
+ }
+ }
+
+ static class TestHandler extends AbstractHandler
+ {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference<Throwable> thrown = new AtomicReference<Throwable>();
+ final AtomicBoolean handling = new AtomicBoolean(false);
+
+ @Override
+ public void handle(String target, Request baseRequest,
+ HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ handling.set(true);
+ latch.countDown();
+ int c = 0;
+ try
+ {
+ int contentLength = request.getContentLength();
+ InputStream in = request.getInputStream();
+
+ while (true)
+ {
+ if (in.read() < 0)
+ break;
+ c++;
+ }
+
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.getWriter().printf("read %d/%d%n", c, contentLength);
+ }
+ catch (Throwable th)
+ {
+ thrown.set(th);
+ }
+ finally
+ {
+ handling.set(false);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HalfCloseTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HalfCloseTest.java
new file mode 100644
index 0000000..99bdcf6
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HalfCloseTest.java
@@ -0,0 +1,210 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.IO;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class HalfCloseTest
+{
+ @Test
+ public void testHalfCloseRace() throws Exception
+ {
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server, 1, 1);
+ connector.setPort(0);
+ connector.setIdleTimeout(500);
+ server.addConnector(connector);
+ TestHandler handler = new TestHandler();
+ server.setHandler(handler);
+
+ server.start();
+
+ try (Socket client = new Socket("localhost", connector.getLocalPort());)
+ {
+ int in = client.getInputStream().read();
+ assertEquals(-1, in);
+
+ client.getOutputStream().write("GET / HTTP/1.0\r\n\r\n".getBytes());
+
+ Thread.sleep(200);
+ assertEquals(0, handler.getHandled());
+ }
+ }
+
+ @Test
+ public void testCompleteClose() throws Exception
+ {
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server, 1, 1);
+ connector.setPort(0);
+ connector.setIdleTimeout(5000);
+ final AtomicInteger opened = new AtomicInteger(0);
+ final CountDownLatch closed = new CountDownLatch(1);
+ connector.addBean(new Connection.Listener()
+ {
+ @Override
+ public void onOpened(Connection connection)
+ {
+ opened.incrementAndGet();
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ closed.countDown();
+ }
+ });
+ server.addConnector(connector);
+ TestHandler handler = new TestHandler();
+ server.setHandler(handler);
+
+ server.start();
+
+ try (Socket client = new Socket("localhost", connector.getLocalPort());)
+ {
+ client.getOutputStream().write("GET / HTTP/1.0\r\n\r\n".getBytes());
+ IO.toString(client.getInputStream());
+ assertEquals(1, handler.getHandled());
+ assertEquals(1, opened.get());
+ }
+ assertEquals(true, closed.await(1, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void testAsyncClose() throws Exception
+ {
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server, 1, 1);
+ connector.setPort(0);
+ connector.setIdleTimeout(5000);
+ final AtomicInteger opened = new AtomicInteger(0);
+ final CountDownLatch closed = new CountDownLatch(1);
+ connector.addBean(new Connection.Listener()
+ {
+ @Override
+ public void onOpened(Connection connection)
+ {
+ opened.incrementAndGet();
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ closed.countDown();
+ }
+ });
+ server.addConnector(connector);
+ AsyncHandler handler = new AsyncHandler();
+ server.setHandler(handler);
+
+ server.start();
+
+ try (Socket client = new Socket("localhost", connector.getLocalPort());)
+ {
+ client.getOutputStream().write("GET / HTTP/1.0\r\n\r\n".getBytes());
+ IO.toString(client.getInputStream());
+ assertEquals(1, handler.getHandled());
+ assertEquals(1, opened.get());
+ }
+ assertEquals(true, closed.await(1, TimeUnit.SECONDS));
+ }
+
+ public static class TestHandler extends AbstractHandler
+ {
+ transient int handled;
+
+ public TestHandler()
+ {
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ handled++;
+ response.setContentType("text/html;charset=utf-8");
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.getWriter().println("<h1>Test</h1>");
+ }
+
+ public int getHandled()
+ {
+ return handled;
+ }
+ }
+
+ public static class AsyncHandler extends AbstractHandler
+ {
+ transient int handled;
+
+ public AsyncHandler()
+ {
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ handled++;
+
+ final AsyncContext async = request.startAsync();
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ response.setContentType("text/html;charset=utf-8");
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.getWriter().println("<h1>Test</h1>");
+ }
+ catch (Exception ex)
+ {
+ System.err.println(ex);
+ }
+ finally
+ {
+ async.complete();
+ }
+ }
+ }.start();
+ }
+
+ public int getHandled()
+ {
+ return handled;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HostHeaderCustomizerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HostHeaderCustomizerTest.java
new file mode 100644
index 0000000..aed47d1
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HostHeaderCustomizerTest.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HostHeaderCustomizerTest
+{
+ @Test
+ public void testHostHeaderCustomizer() throws Exception
+ {
+ Server server = new Server();
+ HttpConfiguration httpConfig = new HttpConfiguration();
+ final String serverName = "test_server_name";
+ final int serverPort = 13;
+ final String redirectPath = "/redirect";
+ httpConfig.addCustomizer(new HostHeaderCustomizer(serverName, serverPort));
+ ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
+ server.addConnector(connector);
+ server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ assertEquals(serverName, request.getServerName());
+ assertEquals(serverPort, request.getServerPort());
+ response.sendRedirect(redirectPath);
+ }
+ });
+ server.start();
+ try
+ {
+ try (Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ try (OutputStream output = socket.getOutputStream())
+ {
+ String request =
+ "GET / HTTP/1.0\r\n" +
+ "\r\n";
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ HttpTester.Input input = HttpTester.from(socket.getInputStream());
+ HttpTester.Response response = HttpTester.parseResponse(input);
+
+ String location = response.get("location");
+ assertNotNull(location);
+ String schemePrefix = "http://";
+ assertTrue(location.startsWith(schemePrefix));
+ assertTrue(location.endsWith(redirectPath));
+ String hostPort = location.substring(schemePrefix.length(), location.length() - redirectPath.length());
+ assertEquals(serverName + ":" + serverPort, hostPort);
+ }
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpChannelEventTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpChannelEventTest.java
new file mode 100644
index 0000000..d8a370d
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpChannelEventTest.java
@@ -0,0 +1,287 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HttpChannelEventTest
+{
+ private Server server;
+ private LocalConnector connector;
+
+ public void start(Handler handler) throws Exception
+ {
+ server = new Server();
+ connector = new LocalConnector(server);
+ server.addConnector(connector);
+ server.setHandler(handler);
+ server.start();
+ }
+
+ @AfterEach
+ public void dispose() throws Exception
+ {
+ if (server != null)
+ server.stop();
+ }
+
+ @Test
+ public void testRequestContentSlice() throws Exception
+ {
+ int data = 'x';
+ CountDownLatch applicationLatch = new CountDownLatch(1);
+ start(new TestHandler()
+ {
+ @Override
+ protected void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ ServletInputStream input = request.getInputStream();
+ int content = input.read();
+ assertEquals(data, content);
+ applicationLatch.countDown();
+ }
+ });
+
+ CountDownLatch listenerLatch = new CountDownLatch(1);
+ connector.addBean(new HttpChannel.Listener()
+ {
+ @Override
+ public void onRequestContent(Request request, ByteBuffer content)
+ {
+ // Consume the buffer to verify it's a slice.
+ content.position(content.limit());
+ listenerLatch.countDown();
+ }
+ });
+
+ HttpTester.Request request = HttpTester.newRequest();
+ request.setHeader("Host", "localhost");
+ request.setContent(new byte[]{(byte)data});
+
+ ByteBuffer buffer = connector.getResponse(request.generate(), 5, TimeUnit.SECONDS);
+
+ // Listener event happens before the application.
+ assertTrue(listenerLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(applicationLatch.await(5, TimeUnit.SECONDS));
+
+ HttpTester.Response response = HttpTester.parseResponse(buffer);
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ }
+
+ @Test
+ public void testResponseContentSlice() throws Exception
+ {
+ byte[] data = new byte[]{'y'};
+ start(new TestHandler()
+ {
+ @Override
+ protected void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.getOutputStream().write(data);
+ }
+ });
+
+ CountDownLatch latch = new CountDownLatch(1);
+ connector.addBean(new HttpChannel.Listener()
+ {
+ @Override
+ public void onResponseContent(Request request, ByteBuffer content)
+ {
+ assertTrue(content.hasRemaining());
+ latch.countDown();
+ }
+ });
+
+ HttpTester.Request request = HttpTester.newRequest();
+ request.setHeader("Host", "localhost");
+ HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request.toString(), 5, TimeUnit.SECONDS));
+
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertArrayEquals(data, response.getContentBytes());
+ }
+
+ @Test
+ public void testRequestFailure() throws Exception
+ {
+ start(new TestHandler());
+
+ CountDownLatch latch = new CountDownLatch(2);
+ connector.addBean(new HttpChannel.Listener()
+ {
+ @Override
+ public void onRequestFailure(Request request, Throwable failure)
+ {
+ latch.countDown();
+ }
+
+ @Override
+ public void onComplete(Request request)
+ {
+ latch.countDown();
+ }
+ });
+
+ // No Host header, request will fail.
+ String request = HttpTester.newRequest().toString();
+ HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request, 5, TimeUnit.SECONDS));
+
+ assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus());
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void testResponseFailure() throws Exception
+ {
+ start(new TestHandler()
+ {
+ @Override
+ protected void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ // Closes all connections, response will fail.
+ connector.getConnectedEndPoints().forEach(EndPoint::close);
+ }
+ });
+
+ CountDownLatch latch = new CountDownLatch(2);
+ connector.addBean(new HttpChannel.Listener()
+ {
+ @Override
+ public void onResponseFailure(Request request, Throwable failure)
+ {
+ latch.countDown();
+ }
+
+ @Override
+ public void onComplete(Request request)
+ {
+ latch.countDown();
+ }
+ });
+
+ HttpTester.Request request = HttpTester.newRequest();
+ request.setHeader("Host", "localhost");
+ HttpTester.parseResponse(connector.getResponse(request.toString(), 5, TimeUnit.SECONDS));
+
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void testExchangeTimeRecording() throws Exception
+ {
+ start(new TestHandler());
+
+ CountDownLatch latch = new CountDownLatch(1);
+ AtomicLong elapsed = new AtomicLong();
+ connector.addBean(new HttpChannel.Listener()
+ {
+ private final String attribute = getClass().getName() + ".begin";
+
+ @Override
+ public void onRequestBegin(Request request)
+ {
+ request.setAttribute(attribute, System.nanoTime());
+ }
+
+ @Override
+ public void onComplete(Request request)
+ {
+ long endTime = System.nanoTime();
+ long beginTime = (Long)request.getAttribute(attribute);
+ elapsed.set(endTime - beginTime);
+ latch.countDown();
+ }
+ });
+
+ HttpTester.Request request = HttpTester.newRequest();
+ request.setHeader("Host", "localhost");
+ HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request.toString(), 5, TimeUnit.SECONDS));
+
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ assertThat(elapsed.get(), Matchers.greaterThan(0L));
+ }
+
+ @Test
+ public void testTransientListener() throws Exception
+ {
+ start(new TestHandler());
+
+ CountDownLatch latch = new CountDownLatch(1);
+ connector.addBean(new HttpChannel.TransientListeners());
+ connector.addBean(new HttpChannel.Listener()
+ {
+ @Override
+ public void onRequestBegin(Request request)
+ {
+ request.getHttpChannel().addListener(new HttpChannel.Listener()
+ {
+ @Override
+ public void onComplete(Request request)
+ {
+ latch.countDown();
+ }
+ });
+ }
+ });
+
+ HttpTester.Request request = HttpTester.newRequest();
+ request.setHeader("Host", "localhost");
+ HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request.toString(), 5, TimeUnit.SECONDS));
+
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ }
+
+ private static class TestHandler extends AbstractHandler
+ {
+ @Override
+ public final void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ jettyRequest.setHandled(true);
+ handle(request, response);
+ }
+
+ protected void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
new file mode 100644
index 0000000..29ab153
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
@@ -0,0 +1,1442 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/*
+ * Created on 9/01/2004
+ *
+ * To change the template for this generated file go to
+ * Window>Preferences>Java>Code Generation>Code and Comments
+ */
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpCompliance;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpParser;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.server.LocalConnector.LocalEndPoint;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HttpConnectionTest
+{
+ private Server server;
+ private LocalConnector connector;
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ server = new Server();
+
+ HttpConfiguration config = new HttpConfiguration();
+ config.setRequestHeaderSize(1024);
+ config.setResponseHeaderSize(1024);
+ config.setSendDateHeader(true);
+ HttpConnectionFactory http = new HttpConnectionFactory(config);
+
+ connector = new LocalConnector(server, http, null);
+ connector.setIdleTimeout(5000);
+ server.addConnector(connector);
+ server.setHandler(new DumpHandler());
+ ErrorHandler eh = new ErrorHandler();
+ eh.setServer(server);
+ server.addBean(eh);
+ server.start();
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ server.stop();
+ server.join();
+ }
+
+ @Test
+ public void testFragmentedChunk() throws Exception
+ {
+ String response = null;
+ try
+ {
+ int offset = 0;
+
+ // Chunk last
+ response = connector.getResponse("GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n" +
+ "0;\r\n" +
+ "\r\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "/R1");
+ checkContains(response, offset, "12345");
+
+ offset = 0;
+ response = connector.getResponse("GET /R2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "ABCDE\r\n" +
+ "0;\r\n" +
+ "\r\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "/R2");
+ checkContains(response, offset, "ABCDE");
+ }
+ catch (Exception e)
+ {
+ if (response != null)
+ System.err.println(response);
+ throw e;
+ }
+ }
+
+ /**
+ * HTTP/0.9 does not support HttpVersion (this is a bad request)
+ */
+ @Test
+ public void testHttp09NoVersion() throws Exception
+ {
+ connector.getConnectionFactory(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC2616);
+ String request = "GET / HTTP/0.9\r\n\r\n";
+ String response = connector.getResponse(request);
+ assertThat(response, containsString("505 Unsupported Version"));
+
+ connector.getConnectionFactory(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC7230);
+ request = "GET / HTTP/0.9\r\n\r\n";
+ response = connector.getResponse(request);
+ assertThat(response, containsString("505 Unsupported Version"));
+ }
+
+ /**
+ * HTTP/0.9 does not support headers
+ */
+ @Test
+ public void testHttp09NoHeaders() throws Exception
+ {
+ connector.getConnectionFactory(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC2616);
+ // header looking like another request is ignored
+ String request = "GET /one\r\nGET :/two\r\n\r\n";
+ String response = BufferUtil.toString(connector.executeRequest(request).waitForOutput(10, TimeUnit.SECONDS));
+ assertThat(response, containsString("pathInfo=/"));
+ assertThat(response, not(containsString("two")));
+ }
+
+ /**
+ * Http/0.9 does not support pipelining.
+ */
+ @Test
+ public void testHttp09MultipleRequests() throws Exception
+ {
+ connector.getConnectionFactory(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC2616);
+
+ // Verify that pipelining does not work with HTTP/0.9.
+ String requests = "GET /?id=123\r\n\r\nGET /?id=456\r\n\r\n";
+ LocalEndPoint endp = connector.executeRequest(requests);
+ String response = BufferUtil.toString(endp.waitForOutput(10, TimeUnit.SECONDS));
+
+ assertThat(response, containsString("id=123"));
+ assertThat(response, not(containsString("id=456")));
+ }
+
+ /**
+ * Ensure that excessively large hexadecimal chunk body length is parsed properly.
+ */
+ @Test
+ public void testHttp11ChunkedBodyTruncation() throws Exception
+ {
+ String request = "POST /?id=123 HTTP/1.1\r\n" +
+ "Host: local\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "1ff00000008\r\n" +
+ "abcdefgh\r\n" +
+ "\r\n" +
+ "0\r\n" +
+ "\r\n" +
+ "POST /?id=bogus HTTP/1.1\r\n" +
+ "Content-Length: 5\r\n" +
+ "Host: dummy-host.example.com\r\n" +
+ "\r\n" +
+ "12345";
+
+ String response = connector.getResponse(request);
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("Connection: close"));
+ assertThat(response, containsString("Early EOF"));
+ }
+
+ /**
+ * More then 1 Content-Length is a bad requests per HTTP rfcs.
+ */
+ @Test
+ public void testHttp11MultipleContentLength() throws Exception
+ {
+ HttpParser.LOG.info("badMessage: 400 Bad messages EXPECTED...");
+ int[][] contentLengths = {
+ {0, 8},
+ {8, 0},
+ {8, 8},
+ {0, 8, 0},
+ {1, 2, 3, 4, 5, 6, 7, 8},
+ {8, 2, 1},
+ {0, 0},
+ {8, 0, 8},
+ {-1, 8},
+ {8, -1},
+ {-1, 8, -1},
+ {-1, -1},
+ {8, -1, 8},
+ };
+
+ for (int x = 0; x < contentLengths.length; x++)
+ {
+ StringBuilder request = new StringBuilder();
+ request.append("POST /?id=").append(Integer.toString(x)).append(" HTTP/1.1\r\n");
+ request.append("Host: local\r\n");
+ int[] clen = contentLengths[x];
+ for (int n = 0; n < clen.length; n++)
+ {
+ request.append("Content-Length: ").append(Integer.toString(clen[n])).append("\r\n");
+ }
+ request.append("Content-Type: text/plain\r\n");
+ request.append("Connection: close\r\n");
+ request.append("\r\n");
+ request.append("abcdefgh"); // actual content of 8 bytes
+
+ String rawResponse = connector.getResponse(request.toString());
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat("Response.status", response.getStatus(), is(HttpServletResponse.SC_BAD_REQUEST));
+ }
+ }
+
+ static final int CHUNKED = -1;
+ static final int DQUOTED_CHUNKED = -2;
+ static final int BAD_CHUNKED = -3;
+ static final int UNKNOWN_TE = -4;
+
+ public static Stream<Arguments> http11ContentLengthAndChunkedData()
+ {
+ return Stream.of(
+ Arguments.of(new int[]{CHUNKED, 8}),
+ Arguments.of(new int[]{8, CHUNKED}),
+ Arguments.of(new int[]{8, CHUNKED, 8}),
+ Arguments.of(new int[]{DQUOTED_CHUNKED, 8}),
+ Arguments.of(new int[]{8, DQUOTED_CHUNKED}),
+ Arguments.of(new int[]{8, DQUOTED_CHUNKED, 8}),
+ Arguments.of(new int[]{BAD_CHUNKED, 8}),
+ Arguments.of(new int[]{8, BAD_CHUNKED}),
+ Arguments.of(new int[]{8, BAD_CHUNKED, 8}),
+ Arguments.of(new int[]{UNKNOWN_TE, 8}),
+ Arguments.of(new int[]{8, UNKNOWN_TE}),
+ Arguments.of(new int[]{8, UNKNOWN_TE, 8}),
+ Arguments.of(new int[]{8, UNKNOWN_TE, CHUNKED, DQUOTED_CHUNKED, BAD_CHUNKED, 8})
+ );
+ }
+
+ /**
+ * More then 1 Content-Length is a bad requests per HTTP rfcs.
+ */
+ @ParameterizedTest
+ @MethodSource("http11ContentLengthAndChunkedData")
+ public void testHttp11ContentLengthAndChunk(int[] contentLengths) throws Exception
+ {
+ HttpParser.LOG.info("badMessage: 400 Bad messages EXPECTED...");
+
+ StringBuilder request = new StringBuilder();
+ request.append("POST / HTTP/1.1\r\n");
+ request.append("Host: local\r\n");
+ for (int n = 0; n < contentLengths.length; n++)
+ {
+ switch (contentLengths[n])
+ {
+ case CHUNKED:
+ request.append("Transfer-Encoding: chunked\r\n");
+ break;
+ case DQUOTED_CHUNKED:
+ request.append("Transfer-Encoding: \"chunked\"\r\n");
+ break;
+ case BAD_CHUNKED:
+ request.append("Transfer-Encoding: 'chunked'\r\n");
+ break;
+ case UNKNOWN_TE:
+ request.append("Transfer-Encoding: bogus\r\n");
+ break;
+ default:
+ request.append("Content-Length: ").append(contentLengths[n]).append("\r\n");
+ break;
+ }
+ }
+ request.append("Content-Type: text/plain\r\n");
+ request.append("\r\n");
+ request.append("8;\r\n"); // chunk header
+ request.append("abcdefgh"); // actual content of 8 bytes
+ request.append("\r\n0;\r\n\r\n"); // last chunk
+
+ String rawResponse = connector.getResponse(request.toString());
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat("Response.status", response.getStatus(), is(HttpServletResponse.SC_BAD_REQUEST));
+ }
+
+ /**
+ * Examples of valid Chunked behaviors.
+ */
+ public static Stream<Arguments> http11TransferEncodingChunked()
+ {
+ return Stream.of(
+ Arguments.of(Arrays.asList("chunked, ")), // results in 1 entry
+ Arguments.of(Arrays.asList(", chunked")),
+
+ // invalid tokens with chunked as last
+ // no conflicts, chunked token is specified and is last, will result in chunked
+ Arguments.of(Arrays.asList("bogus, chunked")),
+ Arguments.of(Arrays.asList("'chunked', chunked")), // apostrophe characters with and without
+ Arguments.of(Arrays.asList("identity, chunked")), // identity was removed in RFC2616 errata and has been dropped in RFC7230
+
+ // multiple headers
+ Arguments.of(Arrays.asList("identity", "chunked")), // 2 separate headers
+ Arguments.of(Arrays.asList("", "chunked")) // 2 separate headers
+ );
+ }
+
+ /**
+ * Test Chunked Transfer-Encoding behavior indicated by
+ * https://tools.ietf.org/html/rfc7230#section-3.3.1
+ */
+ @ParameterizedTest
+ @MethodSource("http11TransferEncodingChunked")
+ public void testHttp11TransferEncodingChunked(List<String> tokens) throws Exception
+ {
+ StringBuilder request = new StringBuilder();
+ request.append("POST / HTTP/1.1\r\n");
+ request.append("Host: local\r\n");
+ tokens.forEach((token) -> request.append("Transfer-Encoding: ").append(token).append("\r\n"));
+ request.append("Content-Type: text/plain\r\n");
+ request.append("\r\n");
+ request.append("8;\r\n"); // chunk header
+ request.append("abcdefgh"); // actual content of 8 bytes
+ request.append("\r\n0;\r\n\r\n"); // last chunk
+
+ System.out.println(request.toString());
+
+ String rawResponse = connector.getResponse(request.toString());
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat("Response.status (" + response.getReason() + ")", response.getStatus(), is(HttpServletResponse.SC_OK));
+ }
+
+ public static Stream<Arguments> http11TransferEncodingInvalidChunked()
+ {
+ return Stream.of(
+ // == Results in 400 Bad Request
+ Arguments.of(Arrays.asList("bogus", "identity")), // 2 separate headers
+
+ Arguments.of(Arrays.asList("bad")),
+ Arguments.of(Arrays.asList("identity")), // identity was removed in RFC2616 errata and has been dropped in RFC7230
+ Arguments.of(Arrays.asList("'chunked'")), // apostrophe characters
+ Arguments.of(Arrays.asList("`chunked`")), // backtick "quote" characters
+ Arguments.of(Arrays.asList("[chunked]")), // bracketed (seen as mistake in several REST libraries)
+ Arguments.of(Arrays.asList("{chunked}")), // json'd (seen as mistake in several REST libraries)
+ Arguments.of(Arrays.asList("\u201Cchunked\u201D")), // opening and closing (fancy) double quotes characters
+
+ // invalid tokens with chunked not as last
+ Arguments.of(Arrays.asList("chunked, bogus")),
+ Arguments.of(Arrays.asList("chunked, 'chunked'")),
+ Arguments.of(Arrays.asList("chunked, identity")),
+ Arguments.of(Arrays.asList("chunked, identity, chunked")), // duplicate chunked
+ Arguments.of(Arrays.asList("chunked", "identity")), // 2 separate header lines
+
+ // multiple chunked tokens present
+ Arguments.of(Arrays.asList("chunked", "identity", "chunked")), // 3 separate header lines
+ Arguments.of(Arrays.asList("chunked", "chunked")), // 2 separate header lines
+ Arguments.of(Arrays.asList("chunked, chunked")) // on same line
+ );
+ }
+
+ /**
+ * Test bad Transfer-Encoding behavior as indicated by
+ * https://tools.ietf.org/html/rfc7230#section-3.3.1
+ */
+ @ParameterizedTest
+ @MethodSource("http11TransferEncodingInvalidChunked")
+ public void testHttp11TransferEncodingInvalidChunked(List<String> tokens) throws Exception
+ {
+ HttpParser.LOG.info("badMessage: 400 Bad messages EXPECTED...");
+ StringBuilder request = new StringBuilder();
+ request.append("POST / HTTP/1.1\r\n");
+ request.append("Host: local\r\n");
+ tokens.forEach((token) -> request.append("Transfer-Encoding: ").append(token).append("\r\n"));
+ request.append("Content-Type: text/plain\r\n");
+ request.append("\r\n");
+ request.append("8;\r\n"); // chunk header
+ request.append("abcdefgh"); // actual content of 8 bytes
+ request.append("\r\n0;\r\n\r\n"); // last chunk
+
+ System.out.println(request.toString());
+
+ String rawResponse = connector.getResponse(request.toString());
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat("Response.status", response.getStatus(), is(HttpServletResponse.SC_BAD_REQUEST));
+ }
+
+ @Test
+ public void testNoPath() throws Exception
+ {
+ String response = connector.getResponse("GET http://localhost:80 HTTP/1.1\r\n" +
+ "Host: localhost:80\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ int offset = 0;
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ checkContains(response, offset, "pathInfo=/");
+ }
+
+ @Test
+ public void testDate() throws Exception
+ {
+ String response = connector.getResponse("GET / HTTP/1.1\r\n" +
+ "Host: localhost:80\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ int offset = 0;
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "Date: ");
+ checkContains(response, offset, "pathInfo=/");
+ }
+
+ @Test
+ public void testSetDate() throws Exception
+ {
+ String response = connector.getResponse("GET /?date=1+Jan+1970 HTTP/1.1\r\n" +
+ "Host: localhost:80\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ int offset = 0;
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "Date: 1 Jan 1970");
+ checkContains(response, offset, "pathInfo=/");
+ }
+
+ @Test
+ public void testBadNoPath() throws Exception
+ {
+ String response = connector.getResponse("GET http://localhost:80/../cheat HTTP/1.1\r\n" +
+ "Host: localhost:80\r\n" +
+ "\r\n");
+ checkContains(response, 0, "HTTP/1.1 400");
+ }
+
+ @Test
+ public void testOKPathDotDotPath() throws Exception
+ {
+ String response = connector.getResponse("GET /ooops/../path HTTP/1.0\r\nHost: localhost:80\r\n\n");
+ checkContains(response, 0, "HTTP/1.1 200 OK");
+ checkContains(response, 0, "pathInfo=/path");
+ }
+
+ @Test
+ public void testBadPathDotDotPath() throws Exception
+ {
+ String response = connector.getResponse("GET /ooops/../../path HTTP/1.0\r\nHost: localhost:80\r\n\n");
+ checkContains(response, 0, "HTTP/1.1 400 ");
+ }
+
+ @Test
+ public void testBadDotDotPath() throws Exception
+ {
+ String response = connector.getResponse("GET ../path HTTP/1.0\r\nHost: localhost:80\r\n\n");
+ checkContains(response, 0, "HTTP/1.1 400 ");
+ }
+
+ @Test
+ public void testBadSlashDotDotPath() throws Exception
+ {
+ String response = connector.getResponse("GET /../path HTTP/1.0\r\nHost: localhost:80\r\n\n");
+ checkContains(response, 0, "HTTP/1.1 400 ");
+ }
+
+ @Test
+ public void testEncodedBadDotDotPath() throws Exception
+ {
+ String response = connector.getResponse("GET %2e%2e/path HTTP/1.0\r\nHost: localhost:80\r\n\n");
+ checkContains(response, 0, "HTTP/1.1 400 ");
+ }
+
+ @Test
+ public void test09() throws Exception
+ {
+ connector.getConnectionFactory(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC2616_LEGACY);
+ LocalEndPoint endp = connector.executeRequest("GET /R1\n");
+ endp.waitUntilClosed();
+ String response = BufferUtil.toString(endp.takeOutput());
+
+ int offset = 0;
+ checkNotContained(response, offset, "HTTP/1.1");
+ checkNotContained(response, offset, "200");
+ checkContains(response, offset, "pathInfo=/R1");
+ }
+
+ @Test
+ public void testSimple() throws Exception
+ {
+ String response = connector.getResponse("GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ int offset = 0;
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ checkContains(response, offset, "/R1");
+ }
+
+ @Test
+ public void testEmptyNotPersistent() throws Exception
+ {
+ String response = connector.getResponse("GET /R1?empty=true HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n");
+
+ int offset = 0;
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ checkNotContained(response, offset, "Content-Length");
+
+ response = connector.getResponse("GET /R1?empty=true HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ offset = 0;
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ checkContains(response, offset, "Connection: close");
+ checkNotContained(response, offset, "Content-Length");
+ }
+
+ @Test
+ public void testEmptyPersistent() throws Exception
+ {
+ String response = connector.getResponse("GET /R1?empty=true HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: keep-alive\r\n" +
+ "\r\n");
+
+ int offset = 0;
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ checkContains(response, offset, "Content-Length: 0");
+ checkNotContained(response, offset, "Connection: close");
+
+ response = connector.getResponse("GET /R1?empty=true HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n");
+
+ offset = 0;
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ checkContains(response, offset, "Content-Length: 0");
+ checkNotContained(response, offset, "Connection: close");
+ }
+
+ @Test
+ public void testEmptyChunk() throws Exception
+ {
+ String response = connector.getResponse("GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "0\r\n" +
+ "\r\n");
+
+ int offset = 0;
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ checkContains(response, offset, "/R1");
+ }
+
+ @Test
+ public void testChunk() throws Exception
+ {
+ String response = connector.getResponse("GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "A\r\n" +
+ "0123456789\r\n" +
+ "0\r\n" +
+ "\r\n");
+
+ int offset = 0;
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "/R1");
+ checkContains(response, offset, "0123456789");
+ }
+
+ @Test
+ public void testChunkTrailer() throws Exception
+ {
+ String response = connector.getResponse("GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "A\r\n" +
+ "0123456789\r\n" +
+ "0\r\n" +
+ "Trailer: ignored\r\n" +
+ "\r\n");
+
+ int offset = 0;
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "/R1");
+ checkContains(response, offset, "0123456789");
+ }
+
+ @Test
+ public void testChunkNoTrailer() throws Exception
+ {
+ // Expect TimeoutException logged
+ String response = connector.getResponse("GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "A\r\n" +
+ "0123456789\r\n" +
+ "0\r\n\r\n");
+
+ int offset = 0;
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "/R1");
+ checkContains(response, offset, "0123456789");
+ }
+
+ @Test
+ public void testHead() throws Exception
+ {
+ String responsePOST = connector.getResponse("POST /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ String responseHEAD = connector.getResponse("HEAD /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ String postLine;
+ boolean postDate = false;
+ Set<String> postHeaders = new HashSet<>();
+ try (BufferedReader in = new BufferedReader(new StringReader(responsePOST)))
+ {
+ postLine = in.readLine();
+ String line = in.readLine();
+ while (line != null && line.length() > 0)
+ {
+ if (line.startsWith("Date:"))
+ postDate = true;
+ else
+ postHeaders.add(line);
+ line = in.readLine();
+ }
+ }
+ String headLine;
+ boolean headDate = false;
+ Set<String> headHeaders = new HashSet<>();
+ try (BufferedReader in = new BufferedReader(new StringReader(responseHEAD)))
+ {
+ headLine = in.readLine();
+ String line = in.readLine();
+ while (line != null && line.length() > 0)
+ {
+ if (line.startsWith("Date:"))
+ headDate = true;
+ else
+ headHeaders.add(line);
+ line = in.readLine();
+ }
+ }
+
+ assertThat(postLine, equalTo(headLine));
+ assertThat(postDate, equalTo(headDate));
+ assertTrue(postHeaders.equals(headHeaders));
+ }
+
+ @Test
+ public void testHeadChunked() throws Exception
+ {
+ String responsePOST = connector.getResponse("POST /R1?no-content-length=true HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n", false, 1, TimeUnit.SECONDS);
+
+ String responseHEAD = connector.getResponse("HEAD /R1?no-content-length=true HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n", true, 1, TimeUnit.SECONDS);
+
+ String postLine;
+ boolean postDate = false;
+ Set<String> postHeaders = new HashSet<>();
+ try (BufferedReader in = new BufferedReader(new StringReader(responsePOST)))
+ {
+ postLine = in.readLine();
+ String line = in.readLine();
+ while (line != null && line.length() > 0)
+ {
+ if (line.startsWith("Date:"))
+ postDate = true;
+ else
+ postHeaders.add(line);
+ line = in.readLine();
+ }
+ }
+ String headLine;
+ boolean headDate = false;
+ Set<String> headHeaders = new HashSet<>();
+ try (BufferedReader in = new BufferedReader(new StringReader(responseHEAD)))
+ {
+ headLine = in.readLine();
+ String line = in.readLine();
+ while (line != null && line.length() > 0)
+ {
+ if (line.startsWith("Date:"))
+ headDate = true;
+ else
+ headHeaders.add(line);
+ line = in.readLine();
+ }
+ }
+
+ assertThat(postLine, equalTo(headLine));
+ assertThat(postDate, equalTo(headDate));
+ assertTrue(postHeaders.equals(headHeaders));
+ }
+
+ @Test
+ public void testBadHostPort() throws Exception
+ {
+ Log.getLogger(HttpParser.class).info("badMessage: Number formate exception expected ...");
+ String response;
+
+ response = connector.getResponse("GET http://localhost:EXPECTED_NUMBER_FORMAT_EXCEPTION/ HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+ checkContains(response, 0, "HTTP/1.1 400");
+ }
+
+ @Test
+ public void testNoHost() throws Exception
+ {
+ String response;
+
+ response = connector.getResponse("GET / HTTP/1.1\r\n" +
+ "\r\n");
+ checkContains(response, 0, "HTTP/1.1 400");
+ }
+
+ @Test
+ public void testEmptyHost() throws Exception
+ {
+ String response;
+
+ response = connector.getResponse("GET / HTTP/1.1\r\n" +
+ "Host:\r\n" +
+ "\r\n");
+ checkContains(response, 0, "HTTP/1.1 200");
+ }
+
+ @Test
+ public void testBadURIencoding() throws Exception
+ {
+ String response = connector.getResponse("GET /bad/encoding%x HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+ checkContains(response, 0, "HTTP/1.1 400");
+ }
+
+ @Test
+ public void testBadUTF8FallsbackTo8859() throws Exception
+ {
+ Log.getLogger(HttpParser.class).info("badMessage: bad encoding expected ...");
+ String response;
+
+ response = connector.getResponse("GET /foo/bar%c0%00 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+ checkContains(response, 0, "HTTP/1.1 400");
+
+ response = connector.getResponse("GET /bad/utf8%c1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+ checkContains(response, 0, "HTTP/1.1 200"); //now fallback to iso-8859-1
+ }
+
+ @Test
+ public void testAutoFlush() throws Exception
+ {
+ int offset = 0;
+
+ String response = connector.getResponse("GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n" +
+ "0;\r\n" +
+ "\r\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ checkNotContained(response, offset, "IgnoreMe");
+ offset = checkContains(response, offset, "/R1");
+ checkContains(response, offset, "12345");
+ }
+
+ @Test
+ public void testEmptyFlush() throws Exception
+ {
+ server.stop();
+ server.setHandler(new AbstractHandler()
+ {
+ @SuppressWarnings("unused")
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.setStatus(200);
+ OutputStream out = response.getOutputStream();
+ out.flush();
+ out.flush();
+ }
+ });
+ server.start();
+
+ String response = connector.getResponse("GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ assertThat(response, Matchers.containsString("200 OK"));
+ }
+
+ @Test
+ public void testCharset() throws Exception
+ {
+ String response = null;
+ try
+ {
+ int offset = 0;
+ response = connector.getResponse("GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n" +
+ "0;\r\n" +
+ "\r\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "/R1");
+ offset = checkContains(response, offset, "encoding=UTF-8");
+ checkContains(response, offset, "12345");
+
+ offset = 0;
+ response = connector.getResponse("GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain; charset = iso-8859-1 ; other=value\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n" +
+ "0;\r\n" +
+ "\r\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "encoding=iso-8859-1");
+ offset = checkContains(response, offset, "/R1");
+ checkContains(response, offset, "12345");
+
+ offset = 0;
+ Log.getLogger(DumpHandler.class).info("Expecting java.io.UnsupportedEncodingException");
+ response = connector.getResponse("GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain; charset=unknown\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n" +
+ "0;\r\n" +
+ "\r\n");
+
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "encoding=unknown");
+ offset = checkContains(response, offset, "/R1");
+ checkContains(response, offset, "UnsupportedEncodingException");
+ }
+ catch (Exception e)
+ {
+ if (response != null)
+ System.err.println(response);
+ throw e;
+ }
+ }
+
+ @Test
+ public void testUnconsumed() throws Exception
+ {
+ int offset = 0;
+ String requests =
+ "GET /R1?read=4 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n" +
+ "5;\r\n" +
+ "67890\r\n" +
+ "0;\r\n" +
+ "\r\n" +
+ "GET /R2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "Content-Length: 10\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "abcdefghij\r\n";
+
+ LocalEndPoint endp = connector.executeRequest(requests);
+ String response = endp.getResponse() + endp.getResponse();
+
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "pathInfo=/R1");
+ offset = checkContains(response, offset, "1234");
+ checkNotContained(response, offset, "56789");
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "pathInfo=/R2");
+ offset = checkContains(response, offset, "encoding=UTF-8");
+ checkContains(response, offset, "abcdefghij");
+ }
+
+ @Test
+ public void testUnconsumedTimeout() throws Exception
+ {
+ connector.setIdleTimeout(500);
+ int offset = 0;
+ String requests =
+ "GET /R1?read=4 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n";
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ String response = connector.getResponse(requests, 2000, TimeUnit.MILLISECONDS);
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, lessThanOrEqualTo(2000L));
+
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "pathInfo=/R1");
+ offset = checkContains(response, offset, "1234");
+ checkNotContained(response, offset, "56789");
+ }
+
+ @Test
+ public void testUnconsumedErrorRead() throws Exception
+ {
+ int offset = 0;
+ String requests =
+ "GET /R1?read=1&error=499 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n" +
+ "5;\r\n" +
+ "67890\r\n" +
+ "0;\r\n" +
+ "\r\n" +
+ "GET /R2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "Content-Length: 10\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "abcdefghij\r\n";
+
+ LocalEndPoint endp = connector.executeRequest(requests);
+ String response = endp.getResponse() + endp.getResponse();
+
+ offset = checkContains(response, offset, "HTTP/1.1 499");
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "/R2");
+ offset = checkContains(response, offset, "encoding=UTF-8");
+ checkContains(response, offset, "abcdefghij");
+ }
+
+ @Test
+ public void testUnconsumedErrorStream() throws Exception
+ {
+ int offset = 0;
+ String requests =
+ "GET /R1?error=599 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: application/data; charset=utf-8\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n" +
+ "5;\r\n" +
+ "67890\r\n" +
+ "0;\r\n" +
+ "\r\n" +
+ "GET /R2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "Content-Length: 10\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "abcdefghij\r\n";
+
+ LocalEndPoint endp = connector.executeRequest(requests);
+ String response = endp.getResponse() + endp.getResponse();
+
+ offset = checkContains(response, offset, "HTTP/1.1 599");
+ offset = checkContains(response, offset, "HTTP/1.1 200");
+ offset = checkContains(response, offset, "/R2");
+ offset = checkContains(response, offset, "encoding=UTF-8");
+ checkContains(response, offset, "abcdefghij");
+ }
+
+ @Test
+ public void testUnconsumedException() throws Exception
+ {
+ int offset = 0;
+ String requests = "GET /R1?read=1&ISE=true HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n" +
+ "5;\r\n" +
+ "67890\r\n" +
+ "0;\r\n" +
+ "\r\n" +
+ "GET /R2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "abcdefghij\r\n";
+
+ Logger logger = Log.getLogger(HttpChannel.class);
+ try (StacklessLogging stackless = new StacklessLogging(logger))
+ {
+ logger.info("EXPECTING: java.lang.IllegalStateException...");
+ String response = connector.getResponse(requests);
+ offset = checkContains(response, offset, "HTTP/1.1 500");
+ offset = checkContains(response, offset, "Connection: close");
+ checkNotContained(response, offset, "HTTP/1.1 200");
+ }
+ }
+
+ @Test
+ public void testConnection() throws Exception
+ {
+ String response = null;
+ try
+ {
+ int offset = 0;
+ response = connector.getResponse("GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: TE, close\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n" +
+ "0;\r\n" +
+ "\r\n");
+ checkContains(response, offset, "Connection: close");
+ }
+ catch (Exception e)
+ {
+ if (response != null)
+ System.err.println(response);
+ throw e;
+ }
+ }
+
+ /**
+ * Creates a request header over 1k in size, by creating a single header entry with an huge value.
+ *
+ * @throws Exception if test failure
+ */
+ @Test
+ public void testOversizedBuffer() throws Exception
+ {
+ String response = null;
+ try
+ {
+ int offset = 0;
+ String cookie = "thisisastringthatshouldreachover1kbytes";
+ for (int i = 0; i < 100; i++)
+ {
+ cookie += "xxxxxxxxxxxx";
+ }
+ response = connector.getResponse("GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Cookie: " + cookie + "\r\n" +
+ "\r\n"
+ );
+ checkContains(response, offset, "HTTP/1.1 431");
+ }
+ catch (Exception e)
+ {
+ if (response != null)
+ System.err.println(response);
+ throw e;
+ }
+ }
+
+ /**
+ * Creates a request header with over 1000 entries.
+ *
+ * @throws Exception if test failure
+ */
+ @Test
+ public void testExcessiveHeader() throws Exception
+ {
+ int offset = 0;
+
+ StringBuilder request = new StringBuilder();
+ request.append("GET / HTTP/1.1\r\n");
+ request.append("Host: localhost\r\n");
+ request.append("Cookie: thisisastring\r\n");
+ for (int i = 0; i < 1000; i++)
+ {
+ request.append(String.format("X-Header-%04d: %08x\r\n", i, i));
+ }
+ request.append("\r\n");
+
+ String response = connector.getResponse(request.toString());
+ offset = checkContains(response, offset, "HTTP/1.1 431");
+ checkContains(response, offset, "<h1>Bad Message 431</h1>");
+ }
+
+ @Test
+ public void testOversizedResponse() throws Exception
+ {
+ String str = "thisisastringthatshouldreachover1kbytes-";
+ for (int i = 0; i < 500; i++)
+ {
+ str += "xxxxxxxxxxxx";
+ }
+ final String longstr = str;
+ final CountDownLatch checkError = new CountDownLatch(1);
+ server.stop();
+ server.setHandler(new AbstractHandler()
+ {
+ @SuppressWarnings("unused")
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setHeader(HttpHeader.CONTENT_TYPE.toString(), MimeTypes.Type.TEXT_HTML.toString());
+ response.setHeader("LongStr", longstr);
+ PrintWriter writer = response.getWriter();
+ writer.write("<html><h1>FOO</h1></html>");
+ writer.flush();
+ if (writer.checkError())
+ checkError.countDown();
+ response.flushBuffer();
+ }
+ });
+ server.start();
+
+ Logger logger = Log.getLogger(HttpChannel.class);
+ String response = null;
+ try (StacklessLogging stackless = new StacklessLogging(logger))
+ {
+ logger.info("Expect IOException: Response header too large...");
+ response = connector.getResponse("GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n"
+ );
+
+ checkContains(response, 0, "HTTP/1.1 500");
+ assertTrue(checkError.await(1, TimeUnit.SECONDS));
+ }
+ catch (Exception e)
+ {
+ if (response != null)
+ System.err.println(response);
+ throw e;
+ }
+ }
+
+ @Test
+ public void testAllowedLargeResponse() throws Exception
+ {
+ connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setResponseHeaderSize(16 * 1024);
+ connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setOutputBufferSize(8 * 1024);
+
+ byte[] bytes = new byte[12 * 1024];
+ Arrays.fill(bytes, (byte)'X');
+ final String longstr = "thisisastringthatshouldreachover12kbytes-" + new String(bytes, StandardCharsets.ISO_8859_1) + "_Z_";
+ final CountDownLatch checkError = new CountDownLatch(1);
+ server.stop();
+ server.setHandler(new AbstractHandler()
+ {
+ @SuppressWarnings("unused")
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setHeader(HttpHeader.CONTENT_TYPE.toString(), MimeTypes.Type.TEXT_HTML.toString());
+ response.setHeader("LongStr", longstr);
+ PrintWriter writer = response.getWriter();
+ writer.write("<html><h1>FOO</h1></html>");
+ writer.flush();
+ if (writer.checkError())
+ checkError.countDown();
+ response.flushBuffer();
+ }
+ });
+ server.start();
+
+ String response = null;
+ response = connector.getResponse("GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n"
+ );
+
+ checkContains(response, 0, "HTTP/1.1 200");
+ checkContains(response, 0, "LongStr: thisisastringthatshouldreachover12kbytes");
+ checkContains(response, 0, "XXX_Z_");
+ assertThat(checkError.getCount(), is(1L));
+ }
+
+ @Test
+ public void testAsterisk() throws Exception
+ {
+ String response = null;
+ try (StacklessLogging stackless = new StacklessLogging(HttpParser.LOG))
+ {
+ int offset = 0;
+
+ response = connector.getResponse("OPTIONS * HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n" +
+ "0;\r\n" +
+ "\r\n");
+ checkContains(response, offset, "HTTP/1.1 200");
+
+ offset = 0;
+ response = connector.getResponse("GET * HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n" +
+ "0;\r\n" +
+ "\r\n");
+ checkContains(response, offset, "HTTP/1.1 400");
+
+ offset = 0;
+ response = connector.getResponse("GET ** HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "5;\r\n" +
+ "12345\r\n" +
+ "0;\r\n" +
+ "\r\n");
+ checkContains(response, offset, "HTTP/1.1 400 Bad Request");
+ }
+ catch (Exception e)
+ {
+ if (response != null)
+ System.err.println(response);
+ throw e;
+ }
+ }
+
+ @Test
+ public void testCONNECT() throws Exception
+ {
+ String response = null;
+ try
+ {
+ int offset = 0;
+
+ response = connector.getResponse("CONNECT www.webtide.com:8080 HTTP/1.1\r\n" +
+ "Host: myproxy:8888\r\n" +
+ "\r\n", 200, TimeUnit.MILLISECONDS);
+ checkContains(response, offset, "HTTP/1.1 200");
+ }
+ catch (Exception e)
+ {
+ if (response != null)
+ System.err.println(response);
+ throw e;
+ }
+ }
+
+ @Test
+ public void testBytesIn() throws Exception
+ {
+ String chunk1 = "0123456789ABCDEF";
+ String chunk2 = IntStream.range(0, 64).mapToObj(i -> chunk1).collect(Collectors.joining());
+ long dataLength = chunk1.length() + chunk2.length();
+ server.stop();
+ server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ jettyRequest.setHandled(true);
+ IO.copy(request.getInputStream(), IO.getNullStream());
+
+ HttpConnection connection = HttpConnection.getCurrentConnection();
+ long bytesIn = connection.getBytesIn();
+ assertThat(bytesIn, greaterThan(dataLength));
+ }
+ });
+ server.start();
+
+ LocalEndPoint localEndPoint = connector.executeRequest("" +
+ "POST / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: " + dataLength + "\r\n" +
+ "\r\n" +
+ chunk1);
+
+ // Wait for the server to block on the read().
+ Thread.sleep(500);
+
+ // Send more content.
+ localEndPoint.addInput(chunk2);
+
+ HttpTester.Response response = HttpTester.parseResponse(localEndPoint.getResponse());
+ assertEquals(response.getStatus(), HttpStatus.OK_200);
+ localEndPoint.close();
+
+ localEndPoint = connector.executeRequest("" +
+ "POST / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ Integer.toHexString(chunk1.length()) + "\r\n" +
+ chunk1 + "\r\n");
+
+ // Wait for the server to block on the read().
+ Thread.sleep(500);
+
+ // Send more content.
+ localEndPoint.addInput("" +
+ Integer.toHexString(chunk2.length()) + "\r\n" +
+ chunk2 + "\r\n" +
+ "0\r\n" +
+ "\r\n");
+
+ response = HttpTester.parseResponse(localEndPoint.getResponse());
+ assertEquals(response.getStatus(), HttpStatus.OK_200);
+ localEndPoint.close();
+ }
+
+ private int checkContains(String s, int offset, String c)
+ {
+ assertThat(s.substring(offset), Matchers.containsString(c));
+ return s.indexOf(c, offset);
+ }
+
+ private void checkNotContained(String s, int offset, String c)
+ {
+ assertThat(s.substring(offset), Matchers.not(Matchers.containsString(c)));
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java
new file mode 100644
index 0000000..c0b7ab3
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java
@@ -0,0 +1,735 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+import javax.servlet.ReadListener;
+
+import org.eclipse.jetty.server.HttpChannelState.Action;
+import org.eclipse.jetty.server.HttpInput.Content;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.eclipse.jetty.server.HttpInput.EARLY_EOF_CONTENT;
+import static org.eclipse.jetty.server.HttpInput.EOF_CONTENT;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * this tests HttpInput and its interaction with HttpChannelState
+ */
+
+public class HttpInputAsyncStateTest
+{
+
+ private static final Queue<String> __history = new LinkedBlockingQueue<>();
+ private ByteBuffer _expected = BufferUtil.allocate(16 * 1024);
+ private boolean _eof;
+ private boolean _noReadInDataAvailable;
+ private boolean _completeInOnDataAvailable;
+
+ private final ReadListener _listener = new ReadListener()
+ {
+ @Override
+ public void onError(Throwable t)
+ {
+ __history.add("onError:" + t);
+ }
+
+ @Override
+ public void onDataAvailable() throws IOException
+ {
+ __history.add("onDataAvailable");
+ if (!_noReadInDataAvailable && readAvailable() && _completeInOnDataAvailable)
+ {
+ __history.add("complete");
+ _state.complete();
+ }
+ }
+
+ @Override
+ public void onAllDataRead() throws IOException
+ {
+ __history.add("onAllDataRead");
+ }
+ };
+ private HttpInput _in;
+ HttpChannelState _state;
+
+ public static class TContent extends HttpInput.Content
+ {
+ public TContent(String content)
+ {
+ super(BufferUtil.toBuffer(content));
+ }
+ }
+
+ @BeforeEach
+ public void before()
+ {
+ _noReadInDataAvailable = false;
+ _in = new HttpInput(new HttpChannelState(new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null)
+ {
+ @Override
+ public void onAsyncWaitForContent()
+ {
+ __history.add("onAsyncWaitForContent");
+ }
+
+ @Override
+ public Scheduler getScheduler()
+ {
+ return null;
+ }
+ })
+ {
+ @Override
+ public void onReadUnready()
+ {
+ super.onReadUnready();
+ __history.add("onReadUnready");
+ }
+
+ @Override
+ public boolean onContentAdded()
+ {
+ boolean wake = super.onContentAdded();
+ __history.add("onReadPossible " + wake);
+ return wake;
+ }
+
+ @Override
+ public boolean onReadReady()
+ {
+ boolean wake = super.onReadReady();
+ __history.add("onReadReady " + wake);
+ return wake;
+ }
+ })
+ {
+ @Override
+ public void wake()
+ {
+ __history.add("wake");
+ }
+ };
+
+ _state = _in.getHttpChannelState();
+ __history.clear();
+ }
+
+ private void check(String... history)
+ {
+ if (history == null || history.length == 0)
+ assertThat(__history, empty());
+ else
+ assertThat(__history.toArray(new String[__history.size()]), Matchers.arrayContaining(history));
+ __history.clear();
+ }
+
+ private void wake()
+ {
+ handle(null);
+ }
+
+ private void handle()
+ {
+ handle(null);
+ }
+
+ private void handle(Runnable run)
+ {
+ Action action = _state.handling();
+ loop:
+ while (true)
+ {
+ switch (action)
+ {
+ case DISPATCH:
+ if (run == null)
+ fail("Run is null during DISPATCH");
+ run.run();
+ break;
+
+ case READ_CALLBACK:
+ _in.run();
+ break;
+
+ case TERMINATED:
+ case WAIT:
+ break loop;
+
+ case COMPLETE:
+ __history.add("COMPLETE");
+ break;
+
+ case READ_REGISTER:
+ _state.getHttpChannel().onAsyncWaitForContent();
+ break;
+
+ default:
+ fail("Bad Action: " + action);
+ }
+ action = _state.unhandle();
+ }
+ }
+
+ private void deliver(Content... content)
+ {
+ if (content != null)
+ {
+ for (Content c : content)
+ {
+ if (c == EOF_CONTENT)
+ {
+ _in.eof();
+ _eof = true;
+ }
+ else if (c == HttpInput.EARLY_EOF_CONTENT)
+ {
+ _in.earlyEOF();
+ _eof = true;
+ }
+ else
+ {
+ _in.addContent(c);
+ BufferUtil.append(_expected, c.getByteBuffer().slice());
+ }
+ }
+ }
+ }
+
+ boolean readAvailable() throws IOException
+ {
+ int len = 0;
+ try
+ {
+ while (_in.isReady())
+ {
+ int b = _in.read();
+
+ if (b < 0)
+ {
+ if (len > 0)
+ __history.add("read " + len);
+ __history.add("read -1");
+ assertTrue(BufferUtil.isEmpty(_expected));
+ assertTrue(_eof);
+ return true;
+ }
+ else
+ {
+ len++;
+ assertFalse(BufferUtil.isEmpty(_expected));
+ int a = 0xff & _expected.get();
+ assertThat(b, equalTo(a));
+ }
+ }
+ __history.add("read " + len);
+ assertTrue(BufferUtil.isEmpty(_expected));
+ }
+ catch (IOException e)
+ {
+ if (len > 0)
+ __history.add("read " + len);
+ __history.add("read " + e);
+ throw e;
+ }
+ return false;
+ }
+
+ @AfterEach
+ public void after()
+ {
+ assertThat(__history.poll(), Matchers.nullValue());
+ }
+
+ @Test
+ public void testInitialEmptyListenInHandle() throws Exception
+ {
+ deliver(EOF_CONTENT);
+ check();
+
+ handle(() ->
+ {
+ _state.startAsync(null);
+ _in.setReadListener(_listener);
+ check("onReadReady false");
+ });
+
+ check("onAllDataRead");
+ }
+
+ @Test
+ public void testInitialEmptyListenAfterHandle() throws Exception
+ {
+ deliver(EOF_CONTENT);
+
+ handle(() ->
+ {
+ _state.startAsync(null);
+ check();
+ });
+
+ _in.setReadListener(_listener);
+ check("onReadReady true", "wake");
+ wake();
+ check("onAllDataRead");
+ }
+
+ @Test
+ public void testListenInHandleEmpty() throws Exception
+ {
+ handle(() ->
+ {
+ _state.startAsync(null);
+ _in.setReadListener(_listener);
+ check("onReadUnready");
+ });
+
+ check("onAsyncWaitForContent");
+
+ deliver(EOF_CONTENT);
+ check("onReadPossible true");
+ handle();
+ check("onAllDataRead");
+ }
+
+ @Test
+ public void testEmptyListenAfterHandle() throws Exception
+ {
+ handle(() ->
+ {
+ _state.startAsync(null);
+ check();
+ });
+
+ deliver(EOF_CONTENT);
+ check();
+
+ _in.setReadListener(_listener);
+ check("onReadReady true", "wake");
+ wake();
+ check("onAllDataRead");
+ }
+
+ @Test
+ public void testListenAfterHandleEmpty() throws Exception
+ {
+ handle(() ->
+ {
+ _state.startAsync(null);
+ check();
+ });
+
+ _in.setReadListener(_listener);
+ check("onAsyncWaitForContent", "onReadUnready");
+
+ deliver(EOF_CONTENT);
+ check("onReadPossible true");
+
+ handle();
+ check("onAllDataRead");
+ }
+
+ @Test
+ public void testInitialEarlyEOFListenInHandle() throws Exception
+ {
+ deliver(EARLY_EOF_CONTENT);
+ check();
+
+ handle(() ->
+ {
+ _state.startAsync(null);
+ _in.setReadListener(_listener);
+ check("onReadReady false");
+ });
+
+ check("onError:org.eclipse.jetty.io.EofException: Early EOF");
+ }
+
+ @Test
+ public void testInitialEarlyEOFListenAfterHandle() throws Exception
+ {
+ deliver(EARLY_EOF_CONTENT);
+
+ handle(() ->
+ {
+ _state.startAsync(null);
+ check();
+ });
+
+ _in.setReadListener(_listener);
+ check("onReadReady true", "wake");
+ wake();
+ check("onError:org.eclipse.jetty.io.EofException: Early EOF");
+ }
+
+ @Test
+ public void testListenInHandleEarlyEOF() throws Exception
+ {
+ handle(() ->
+ {
+ _state.startAsync(null);
+ _in.setReadListener(_listener);
+ check("onReadUnready");
+ });
+
+ check("onAsyncWaitForContent");
+
+ deliver(EARLY_EOF_CONTENT);
+ check("onReadPossible true");
+ handle();
+ check("onError:org.eclipse.jetty.io.EofException: Early EOF");
+ }
+
+ @Test
+ public void testEarlyEOFListenAfterHandle() throws Exception
+ {
+ handle(() ->
+ {
+ _state.startAsync(null);
+ check();
+ });
+
+ deliver(EARLY_EOF_CONTENT);
+ check();
+
+ _in.setReadListener(_listener);
+ check("onReadReady true", "wake");
+ wake();
+ check("onError:org.eclipse.jetty.io.EofException: Early EOF");
+ }
+
+ @Test
+ public void testListenAfterHandleEarlyEOF() throws Exception
+ {
+ handle(() ->
+ {
+ _state.startAsync(null);
+ check();
+ });
+
+ _in.setReadListener(_listener);
+ check("onAsyncWaitForContent", "onReadUnready");
+
+ deliver(EARLY_EOF_CONTENT);
+ check("onReadPossible true");
+
+ handle();
+ check("onError:org.eclipse.jetty.io.EofException: Early EOF");
+ }
+
+ @Test
+ public void testInitialAllContentListenInHandle() throws Exception
+ {
+ deliver(new TContent("Hello"), EOF_CONTENT);
+ check();
+
+ handle(() ->
+ {
+ _state.startAsync(null);
+ _in.setReadListener(_listener);
+ check("onReadReady false");
+ });
+
+ check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
+ }
+
+ @Test
+ public void testInitialAllContentListenAfterHandle() throws Exception
+ {
+ deliver(new TContent("Hello"), EOF_CONTENT);
+
+ handle(() ->
+ {
+ _state.startAsync(null);
+ check();
+ });
+
+ _in.setReadListener(_listener);
+ check("onReadReady true", "wake");
+ wake();
+ check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
+ }
+
+ @Test
+ public void testListenInHandleAllContent() throws Exception
+ {
+ handle(() ->
+ {
+ _state.startAsync(null);
+ _in.setReadListener(_listener);
+ check("onReadUnready");
+ });
+
+ check("onAsyncWaitForContent");
+
+ deliver(new TContent("Hello"), EOF_CONTENT);
+ check("onReadPossible true", "onReadPossible false");
+ handle();
+ check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
+ }
+
+ @Test
+ public void testAllContentListenAfterHandle() throws Exception
+ {
+ handle(() ->
+ {
+ _state.startAsync(null);
+ check();
+ });
+
+ deliver(new TContent("Hello"), EOF_CONTENT);
+ check();
+
+ _in.setReadListener(_listener);
+ check("onReadReady true", "wake");
+ wake();
+ check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
+ }
+
+ @Test
+ public void testListenAfterHandleAllContent() throws Exception
+ {
+ handle(() ->
+ {
+ _state.startAsync(null);
+ check();
+ });
+
+ _in.setReadListener(_listener);
+ check("onAsyncWaitForContent", "onReadUnready");
+
+ deliver(new TContent("Hello"), EOF_CONTENT);
+ check("onReadPossible true", "onReadPossible false");
+
+ handle();
+ check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
+ }
+
+ @Test
+ public void testInitialIncompleteContentListenInHandle() throws Exception
+ {
+ deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
+ check();
+
+ handle(() ->
+ {
+ _state.startAsync(null);
+ _in.setReadListener(_listener);
+ check("onReadReady false");
+ });
+
+ check(
+ "onDataAvailable",
+ "read 5",
+ "read org.eclipse.jetty.io.EofException: Early EOF",
+ "onError:org.eclipse.jetty.io.EofException: Early EOF");
+ }
+
+ @Test
+ public void testInitialPartialContentListenAfterHandle() throws Exception
+ {
+ deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
+
+ handle(() ->
+ {
+ _state.startAsync(null);
+ check();
+ });
+
+ _in.setReadListener(_listener);
+ check("onReadReady true", "wake");
+ wake();
+ check(
+ "onDataAvailable",
+ "read 5",
+ "read org.eclipse.jetty.io.EofException: Early EOF",
+ "onError:org.eclipse.jetty.io.EofException: Early EOF");
+ }
+
+ @Test
+ public void testListenInHandlePartialContent() throws Exception
+ {
+ handle(() ->
+ {
+ _state.startAsync(null);
+ _in.setReadListener(_listener);
+ check("onReadUnready");
+ });
+
+ check("onAsyncWaitForContent");
+
+ deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
+ check("onReadPossible true", "onReadPossible false");
+ handle();
+ check(
+ "onDataAvailable",
+ "read 5",
+ "read org.eclipse.jetty.io.EofException: Early EOF",
+ "onError:org.eclipse.jetty.io.EofException: Early EOF");
+ }
+
+ @Test
+ public void testPartialContentListenAfterHandle() throws Exception
+ {
+ handle(() ->
+ {
+ _state.startAsync(null);
+ check();
+ });
+
+ deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
+ check();
+
+ _in.setReadListener(_listener);
+ check("onReadReady true", "wake");
+ wake();
+ check(
+ "onDataAvailable",
+ "read 5",
+ "read org.eclipse.jetty.io.EofException: Early EOF",
+ "onError:org.eclipse.jetty.io.EofException: Early EOF");
+ }
+
+ @Test
+ public void testListenAfterHandlePartialContent() throws Exception
+ {
+ handle(() ->
+ {
+ _state.startAsync(null);
+ check();
+ });
+
+ _in.setReadListener(_listener);
+ check("onAsyncWaitForContent", "onReadUnready");
+
+ deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
+ check("onReadPossible true", "onReadPossible false");
+
+ handle();
+ check(
+ "onDataAvailable",
+ "read 5",
+ "read org.eclipse.jetty.io.EofException: Early EOF",
+ "onError:org.eclipse.jetty.io.EofException: Early EOF");
+ }
+
+ @Test
+ public void testReadAfterOnDataAvailable() throws Exception
+ {
+ _noReadInDataAvailable = true;
+ handle(() ->
+ {
+ _state.startAsync(null);
+ _in.setReadListener(_listener);
+ check("onReadUnready");
+ });
+
+ check("onAsyncWaitForContent");
+
+ deliver(new TContent("Hello"), EOF_CONTENT);
+ check("onReadPossible true", "onReadPossible false");
+
+ handle();
+ check("onDataAvailable");
+
+ readAvailable();
+ check("wake", "read 5", "read -1");
+ wake();
+ check("onAllDataRead");
+ }
+
+ @Test
+ public void testReadOnlyExpectedAfterOnDataAvailable() throws Exception
+ {
+ _noReadInDataAvailable = true;
+ handle(() ->
+ {
+ _state.startAsync(null);
+ _in.setReadListener(_listener);
+ check("onReadUnready");
+ });
+
+ check("onAsyncWaitForContent");
+
+ deliver(new TContent("Hello"), EOF_CONTENT);
+ check("onReadPossible true", "onReadPossible false");
+
+ handle();
+ check("onDataAvailable");
+
+ byte[] buffer = new byte[_expected.remaining()];
+ assertThat(_in.read(buffer), equalTo(buffer.length));
+ assertThat(new String(buffer), equalTo(BufferUtil.toString(_expected)));
+ BufferUtil.clear(_expected);
+ check();
+
+ assertTrue(_in.isReady());
+ check();
+
+ assertThat(_in.read(), equalTo(-1));
+ check("wake");
+
+ wake();
+ check("onAllDataRead");
+ }
+
+ @Test
+ public void testReadAndCompleteInOnDataAvailable() throws Exception
+ {
+ _completeInOnDataAvailable = true;
+ handle(() ->
+ {
+ _state.startAsync(null);
+ _in.setReadListener(_listener);
+ check("onReadUnready");
+ });
+
+ check("onAsyncWaitForContent");
+
+ deliver(new TContent("Hello"), EOF_CONTENT);
+ check("onReadPossible true", "onReadPossible false");
+
+ handle(() ->
+ {
+ __history.add(_state.getState().toString());
+ });
+ System.err.println(__history);
+ check(
+ "onDataAvailable",
+ "read 5",
+ "read -1",
+ "complete",
+ "COMPLETE"
+ );
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java
new file mode 100644
index 0000000..c284d90
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java
@@ -0,0 +1,534 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeoutException;
+import javax.servlet.ReadListener;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HttpInputTest
+{
+ private final Queue<String> _history = new LinkedBlockingQueue<>();
+ private final Queue<String> _fillAndParseSimulate = new LinkedBlockingQueue<>();
+ private final ReadListener _listener = new ReadListener()
+ {
+ @Override
+ public void onError(Throwable t)
+ {
+ _history.add("l.onError:" + t);
+ }
+
+ @Override
+ public void onDataAvailable() throws IOException
+ {
+ _history.add("l.onDataAvailable");
+ }
+
+ @Override
+ public void onAllDataRead() throws IOException
+ {
+ _history.add("l.onAllDataRead");
+ }
+ };
+ private HttpInput _in;
+
+ public class TContent extends HttpInput.Content
+ {
+ private final String _content;
+
+ public TContent(String content)
+ {
+ super(BufferUtil.toBuffer(content));
+ _content = content;
+ }
+
+ @Override
+ public void succeeded()
+ {
+ _history.add("Content succeeded " + _content);
+ super.succeeded();
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ _history.add("Content failed " + _content);
+ super.failed(x);
+ }
+ }
+
+ @BeforeEach
+ public void before()
+ {
+ _in = new HttpInput(new HttpChannelState(new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null)
+ {
+ @Override
+ public void onAsyncWaitForContent()
+ {
+ _history.add("asyncReadInterested");
+ }
+ })
+ {
+ @Override
+ public void onReadUnready()
+ {
+ _history.add("s.onReadUnready");
+ super.onReadUnready();
+ }
+
+ @Override
+ public boolean onReadPossible()
+ {
+ _history.add("s.onReadPossible");
+ return super.onReadPossible();
+ }
+
+ @Override
+ public boolean onContentAdded()
+ {
+ _history.add("s.onDataAvailable");
+ return super.onContentAdded();
+ }
+
+ @Override
+ public boolean onReadReady()
+ {
+ _history.add("s.onReadReady");
+ return super.onReadReady();
+ }
+ })
+ {
+ @Override
+ protected void produceContent() throws IOException
+ {
+ _history.add("produceContent " + _fillAndParseSimulate.size());
+
+ for (String s = _fillAndParseSimulate.poll(); s != null; s = _fillAndParseSimulate.poll())
+ {
+ if ("_EOF_".equals(s))
+ _in.eof();
+ else
+ _in.addContent(new TContent(s));
+ }
+ }
+
+ @Override
+ protected void blockForContent() throws IOException
+ {
+ _history.add("blockForContent");
+ super.blockForContent();
+ }
+ };
+ }
+
+ @AfterEach
+ public void after()
+ {
+ assertThat(_history.poll(), nullValue());
+ }
+
+ @Test
+ public void testEmpty() throws Exception
+ {
+ assertThat(_in.available(), equalTo(0));
+ assertThat(_history.poll(), equalTo("produceContent 0"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isFinished(), equalTo(false));
+ assertThat(_in.isReady(), equalTo(true));
+ assertThat(_history.poll(), nullValue());
+ }
+
+ @Test
+ public void testRead() throws Exception
+ {
+ _in.addContent(new TContent("AB"));
+ _in.addContent(new TContent("CD"));
+ _fillAndParseSimulate.offer("EF");
+ _fillAndParseSimulate.offer("GH");
+ assertThat(_in.available(), equalTo(2));
+ assertThat(_in.isFinished(), equalTo(false));
+ assertThat(_in.isReady(), equalTo(true));
+
+ assertThat(_in.getContentConsumed(), equalTo(0L));
+ assertThat(_in.read(), equalTo((int)'A'));
+ assertThat(_in.getContentConsumed(), equalTo(1L));
+ assertThat(_in.read(), equalTo((int)'B'));
+ assertThat(_in.getContentConsumed(), equalTo(2L));
+
+ assertThat(_history.poll(), equalTo("Content succeeded AB"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.read(), equalTo((int)'C'));
+ assertThat(_in.read(), equalTo((int)'D'));
+
+ assertThat(_history.poll(), equalTo("Content succeeded CD"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.read(), equalTo((int)'E'));
+ assertThat(_in.read(), equalTo((int)'F'));
+
+ assertThat(_history.poll(), equalTo("produceContent 2"));
+ assertThat(_history.poll(), equalTo("Content succeeded EF"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.read(), equalTo((int)'G'));
+ assertThat(_in.read(), equalTo((int)'H'));
+
+ assertThat(_history.poll(), equalTo("Content succeeded GH"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.getContentConsumed(), equalTo(8L));
+
+ assertThat(_history.poll(), nullValue());
+ }
+
+ @Test
+ public void testBlockingRead() throws Exception
+ {
+ new Thread(() ->
+ {
+ try
+ {
+ Thread.sleep(500);
+ _in.addContent(new TContent("AB"));
+ }
+ catch (Throwable th)
+ {
+ th.printStackTrace();
+ }
+ }).start();
+
+ assertThat(_in.read(), equalTo((int)'A'));
+
+ assertThat(_history.poll(), equalTo("produceContent 0"));
+ assertThat(_history.poll(), equalTo("blockForContent"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.read(), equalTo((int)'B'));
+
+ assertThat(_history.poll(), equalTo("Content succeeded AB"));
+ assertThat(_history.poll(), nullValue());
+ }
+
+ @Test
+ public void testReadEOF() throws Exception
+ {
+ _in.addContent(new TContent("AB"));
+ _in.addContent(new TContent("CD"));
+ _in.eof();
+
+ assertThat(_in.isFinished(), equalTo(false));
+ assertThat(_in.available(), equalTo(2));
+ assertThat(_in.isFinished(), equalTo(false));
+
+ assertThat(_in.read(), equalTo((int)'A'));
+ assertThat(_in.read(), equalTo((int)'B'));
+ assertThat(_history.poll(), equalTo("Content succeeded AB"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.read(), equalTo((int)'C'));
+ assertThat(_in.isFinished(), equalTo(false));
+ assertThat(_in.read(), equalTo((int)'D'));
+ assertThat(_history.poll(), equalTo("Content succeeded CD"));
+ assertThat(_history.poll(), nullValue());
+ assertThat(_in.isFinished(), equalTo(false));
+
+ assertThat(_in.read(), equalTo(-1));
+ assertThat(_in.isFinished(), equalTo(true));
+
+ assertThat(_history.poll(), nullValue());
+ }
+
+ @Test
+ public void testReadEarlyEOF() throws Exception
+ {
+ _in.addContent(new TContent("AB"));
+ _in.addContent(new TContent("CD"));
+ _in.earlyEOF();
+
+ assertThat(_in.isFinished(), equalTo(false));
+ assertThat(_in.available(), equalTo(2));
+ assertThat(_in.isFinished(), equalTo(false));
+
+ assertThat(_in.read(), equalTo((int)'A'));
+ assertThat(_in.read(), equalTo((int)'B'));
+
+ assertThat(_in.read(), equalTo((int)'C'));
+ assertThat(_in.isFinished(), equalTo(false));
+ assertThat(_in.read(), equalTo((int)'D'));
+
+ assertThrows(EOFException.class, () -> _in.read());
+ assertTrue(_in.isFinished());
+
+ assertThat(_history.poll(), equalTo("Content succeeded AB"));
+ assertThat(_history.poll(), equalTo("Content succeeded CD"));
+ assertThat(_history.poll(), nullValue());
+ }
+
+ @Test
+ public void testBlockingEOF() throws Exception
+ {
+ new Thread(() ->
+ {
+ try
+ {
+ Thread.sleep(500);
+ _in.eof();
+ }
+ catch (Throwable th)
+ {
+ th.printStackTrace();
+ }
+ }).start();
+
+ assertThat(_in.isFinished(), equalTo(false));
+ assertThat(_in.read(), equalTo(-1));
+ assertThat(_in.isFinished(), equalTo(true));
+
+ assertThat(_history.poll(), equalTo("produceContent 0"));
+ assertThat(_history.poll(), equalTo("blockForContent"));
+ assertThat(_history.poll(), nullValue());
+ }
+
+ @Test
+ public void testAsyncEmpty() throws Exception
+ {
+ _in.setReadListener(_listener);
+ assertThat(_history.poll(), equalTo("produceContent 0"));
+ assertThat(_history.poll(), equalTo("s.onReadUnready"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isReady(), equalTo(false));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isReady(), equalTo(false));
+ assertThat(_history.poll(), nullValue());
+ }
+
+ @Test
+ public void testAsyncRead() throws Exception
+ {
+ _in.setReadListener(_listener);
+ assertThat(_history.poll(), equalTo("produceContent 0"));
+ assertThat(_history.poll(), equalTo("s.onReadUnready"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isReady(), equalTo(false));
+ assertThat(_history.poll(), nullValue());
+
+ _in.addContent(new TContent("AB"));
+ _fillAndParseSimulate.add("CD");
+
+ assertThat(_history.poll(), equalTo("s.onDataAvailable"));
+ assertThat(_history.poll(), nullValue());
+ _in.run();
+ assertThat(_history.poll(), equalTo("l.onDataAvailable"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isReady(), equalTo(true));
+ assertThat(_in.read(), equalTo((int)'A'));
+
+ assertThat(_in.isReady(), equalTo(true));
+ assertThat(_in.read(), equalTo((int)'B'));
+
+ assertThat(_history.poll(), equalTo("Content succeeded AB"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isReady(), equalTo(true));
+ assertThat(_history.poll(), equalTo("produceContent 1"));
+ assertThat(_history.poll(), equalTo("s.onDataAvailable"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.read(), equalTo((int)'C'));
+
+ assertThat(_in.isReady(), equalTo(true));
+ assertThat(_in.read(), equalTo((int)'D'));
+ assertThat(_history.poll(), equalTo("Content succeeded CD"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isReady(), equalTo(false));
+ assertThat(_history.poll(), equalTo("produceContent 0"));
+ assertThat(_history.poll(), equalTo("s.onReadUnready"));
+ assertThat(_history.poll(), nullValue());
+ }
+
+ @Test
+ public void testAsyncEOF() throws Exception
+ {
+ _in.setReadListener(_listener);
+ assertThat(_history.poll(), equalTo("produceContent 0"));
+ assertThat(_history.poll(), equalTo("s.onReadUnready"));
+ assertThat(_history.poll(), nullValue());
+
+ _in.eof();
+ assertThat(_in.isReady(), equalTo(true));
+ assertThat(_in.isFinished(), equalTo(false));
+ assertThat(_history.poll(), equalTo("s.onDataAvailable"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.read(), equalTo(-1));
+ assertThat(_in.isFinished(), equalTo(true));
+ assertThat(_history.poll(), nullValue());
+ }
+
+ @Test
+ public void testAsyncReadEOF() throws Exception
+ {
+ _in.setReadListener(_listener);
+ assertThat(_history.poll(), equalTo("produceContent 0"));
+ assertThat(_history.poll(), equalTo("s.onReadUnready"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isReady(), equalTo(false));
+ assertThat(_history.poll(), nullValue());
+
+ _in.addContent(new TContent("AB"));
+ _fillAndParseSimulate.add("_EOF_");
+
+ assertThat(_history.poll(), equalTo("s.onDataAvailable"));
+ assertThat(_history.poll(), nullValue());
+
+ _in.run();
+ assertThat(_history.poll(), equalTo("l.onDataAvailable"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isReady(), equalTo(true));
+ assertThat(_in.read(), equalTo((int)'A'));
+
+ assertThat(_in.isReady(), equalTo(true));
+ assertThat(_in.read(), equalTo((int)'B'));
+
+ assertThat(_history.poll(), equalTo("Content succeeded AB"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isFinished(), equalTo(false));
+ assertThat(_in.isReady(), equalTo(true));
+ assertThat(_history.poll(), equalTo("produceContent 1"));
+ assertThat(_history.poll(), equalTo("s.onDataAvailable"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isFinished(), equalTo(false));
+ assertThat(_in.read(), equalTo(-1));
+ assertThat(_in.isFinished(), equalTo(true));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isReady(), equalTo(true));
+ assertThat(_history.poll(), nullValue());
+ }
+
+ @Test
+ public void testAsyncError() throws Exception
+ {
+ _in.setReadListener(_listener);
+ assertThat(_history.poll(), equalTo("produceContent 0"));
+ assertThat(_history.poll(), equalTo("s.onReadUnready"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isReady(), equalTo(false));
+ assertThat(_history.poll(), nullValue());
+
+ _in.failed(new TimeoutException());
+ assertThat(_history.poll(), equalTo("s.onDataAvailable"));
+ assertThat(_history.poll(), nullValue());
+
+ _in.run();
+ assertThat(_in.isFinished(), equalTo(true));
+ assertThat(_history.poll(), equalTo("l.onError:java.util.concurrent.TimeoutException"));
+ assertThat(_history.poll(), nullValue());
+
+ assertThat(_in.isReady(), equalTo(true));
+
+ IOException e = assertThrows(IOException.class, () -> _in.read());
+ assertThat(e.getCause(), instanceOf(TimeoutException.class));
+ assertThat(_in.isFinished(), equalTo(true));
+
+ assertThat(_history.poll(), nullValue());
+ }
+
+ @Test
+ public void testRecycle() throws Exception
+ {
+ testAsyncRead();
+ _in.recycle();
+ testAsyncRead();
+ _in.recycle();
+ testReadEOF();
+ }
+
+ @Test
+ public void testConsumeAll() throws Exception
+ {
+ _in.addContent(new TContent("AB"));
+ _in.addContent(new TContent("CD"));
+ _fillAndParseSimulate.offer("EF");
+ _fillAndParseSimulate.offer("GH");
+ assertThat(_in.read(), equalTo((int)'A'));
+
+ assertFalse(_in.consumeAll());
+ assertThat(_in.getContentConsumed(), equalTo(1L));
+ assertThat(_in.getContentReceived(), equalTo(8L));
+
+ assertThat(_history.poll(), equalTo("Content succeeded AB"));
+ assertThat(_history.poll(), equalTo("Content succeeded CD"));
+ assertThat(_history.poll(), equalTo("produceContent 2"));
+ assertThat(_history.poll(), equalTo("Content succeeded EF"));
+ assertThat(_history.poll(), equalTo("Content succeeded GH"));
+ assertThat(_history.poll(), equalTo("produceContent 0"));
+ assertThat(_history.poll(), nullValue());
+ }
+
+ @Test
+ public void testConsumeAllEOF() throws Exception
+ {
+ _in.addContent(new TContent("AB"));
+ _in.addContent(new TContent("CD"));
+ _fillAndParseSimulate.offer("EF");
+ _fillAndParseSimulate.offer("GH");
+ _fillAndParseSimulate.offer("_EOF_");
+ assertThat(_in.read(), equalTo((int)'A'));
+
+ assertTrue(_in.consumeAll());
+ assertThat(_in.getContentConsumed(), equalTo(1L));
+ assertThat(_in.getContentReceived(), equalTo(8L));
+
+ assertThat(_history.poll(), equalTo("Content succeeded AB"));
+ assertThat(_history.poll(), equalTo("Content succeeded CD"));
+ assertThat(_history.poll(), equalTo("produceContent 3"));
+ assertThat(_history.poll(), equalTo("Content succeeded EF"));
+ assertThat(_history.poll(), equalTo("Content succeeded GH"));
+ assertThat(_history.poll(), nullValue());
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java
new file mode 100644
index 0000000..a3ea247
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java
@@ -0,0 +1,1028 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Stream;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeaderValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
+
+//TODO: reset buffer tests
+//TODO: add protocol specific tests for connection: close and/or chunking
+public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest
+{
+ private final String contextAttribute = getClass().getName() + ".asyncContext";
+
+ public static Stream<Arguments> httpVersion()
+ {
+ // boolean dispatch - if true we dispatch, otherwise we complete
+ final boolean DISPATCH = true;
+ final boolean COMPLETE = false;
+ final boolean IN_WAIT = true;
+ final boolean WHILE_DISPATCHED = false;
+
+ List<Arguments> ret = new ArrayList<>();
+ ret.add(Arguments.of(HttpVersion.HTTP_1_0, DISPATCH, IN_WAIT));
+ ret.add(Arguments.of(HttpVersion.HTTP_1_1, DISPATCH, IN_WAIT));
+ ret.add(Arguments.of(HttpVersion.HTTP_1_0, COMPLETE, IN_WAIT));
+ ret.add(Arguments.of(HttpVersion.HTTP_1_1, COMPLETE, IN_WAIT));
+ ret.add(Arguments.of(HttpVersion.HTTP_1_0, DISPATCH, WHILE_DISPATCHED));
+ ret.add(Arguments.of(HttpVersion.HTTP_1_1, DISPATCH, WHILE_DISPATCHED));
+ ret.add(Arguments.of(HttpVersion.HTTP_1_0, COMPLETE, WHILE_DISPATCHED));
+ ret.add(Arguments.of(HttpVersion.HTTP_1_1, COMPLETE, WHILE_DISPATCHED));
+ return ret.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testHandlerDoesNotSetHandled(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ DoesNotSetHandledHandler handler = new DoesNotSetHandledHandler(false, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat(response.getStatus(), is(404));
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testHandlerDoesNotSetHandledAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ DoesNotSetHandledHandler handler = new DoesNotSetHandledHandler(true, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response;
+ if (inWait)
+ {
+ // exception thrown and handled before any async processing
+ response = executeRequest(httpVersion);
+ }
+ else
+ {
+ // exception thrown after async processing, so cannot be handled
+ try (StacklessLogging log = new StacklessLogging(HttpChannelState.class))
+ {
+ response = executeRequest(httpVersion);
+ }
+ }
+
+ assertThat(response.getStatus(), is(500));
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ private class DoesNotSetHandledHandler extends ThrowExceptionOnDemandHandler
+ {
+ private final boolean dispatch;
+ private final boolean inWait;
+
+ private DoesNotSetHandledHandler(boolean throwException, boolean dispatch, boolean inWait)
+ {
+ super(throwException);
+ this.dispatch = dispatch;
+ this.inWait = inWait;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (request.getAttribute(contextAttribute) == null)
+ {
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ request.setAttribute(contextAttribute, asyncContext);
+ runAsync(baseRequest, inWait, () ->
+ {
+ if (dispatch)
+ asyncContext.dispatch();
+ else
+ asyncContext.complete();
+ });
+ }
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testHandlerSetsHandledTrueOnly(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ OnlySetHandledHandler handler = new OnlySetHandledHandler(false, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat(response.getStatus(), is(200));
+ if (httpVersion.is("HTTP/1.1"))
+ assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "0"));
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testHandlerSetsHandledTrueOnlyAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ OnlySetHandledHandler handler = new OnlySetHandledHandler(true, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response;
+ if (inWait)
+ {
+ // exception thrown and handled before any async processing
+ response = executeRequest(httpVersion);
+ }
+ else
+ {
+ // exception thrown after async processing, so cannot be handled
+ try (StacklessLogging log = new StacklessLogging(HttpChannelState.class))
+ {
+ response = executeRequest(httpVersion);
+ }
+ }
+
+ assertThat(response.getStatus(), is(500));
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ private class OnlySetHandledHandler extends ThrowExceptionOnDemandHandler
+ {
+ private final boolean dispatch;
+ private final boolean inWait;
+
+ private OnlySetHandledHandler(boolean throwException, boolean dispatch, boolean inWait)
+ {
+ super(throwException);
+ this.dispatch = dispatch;
+ this.inWait = inWait;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (request.getAttribute(contextAttribute) == null)
+ {
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ request.setAttribute(contextAttribute, asyncContext);
+ runAsync(baseRequest, inWait, () ->
+ {
+ if (dispatch)
+ asyncContext.dispatch();
+ else
+ asyncContext.complete();
+ });
+ }
+ baseRequest.setHandled(true);
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testHandlerSetsHandledAndWritesSomeContent(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ SetHandledWriteSomeDataHandler handler = new SetHandledWriteSomeDataHandler(false, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat(response.getStatus(), is(200));
+ assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "6"));
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testHandlerSetsHandledAndWritesSomeContentAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ SetHandledWriteSomeDataHandler handler = new SetHandledWriteSomeDataHandler(true, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+ HttpTester.Response response;
+ if (inWait)
+ {
+ // exception thrown and handled before any async processing
+ response = executeRequest(httpVersion);
+ }
+ else
+ {
+ // exception thrown after async processing, so cannot be handled
+ try (StacklessLogging log = new StacklessLogging(HttpChannelState.class))
+ {
+ response = executeRequest(httpVersion);
+ }
+ }
+
+ assertThat(response.getStatus(), is(500));
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ private class SetHandledWriteSomeDataHandler extends ThrowExceptionOnDemandHandler
+ {
+ private final boolean dispatch;
+ private final boolean inWait;
+
+ private SetHandledWriteSomeDataHandler(boolean throwException, boolean dispatch, boolean inWait)
+ {
+ super(throwException);
+ this.dispatch = dispatch;
+ this.inWait = inWait;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (request.getAttribute(contextAttribute) == null)
+ {
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ request.setAttribute(contextAttribute, asyncContext);
+ runAsync(baseRequest, inWait, () ->
+ {
+ try
+ {
+ asyncContext.getResponse().getWriter().write("foobar");
+ if (dispatch)
+ asyncContext.dispatch();
+ else
+ asyncContext.complete();
+ }
+ catch (IOException e)
+ {
+ markFailed(e);
+ }
+ });
+ }
+ baseRequest.setHandled(true);
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testHandlerExplicitFlush(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ ExplicitFlushHandler handler = new ExplicitFlushHandler(false, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat(response.getStatus(), is(200));
+ assertThat(handler.failure(), is(nullValue()));
+ if (httpVersion.is("HTTP/1.1"))
+ assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testHandlerExplicitFlushAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ ExplicitFlushHandler handler = new ExplicitFlushHandler(true, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ if (inWait)
+ {
+ // throw happens before flush
+ assertThat(response.getStatus(), is(500));
+ }
+ else
+ {
+ // flush happens before throw
+ assertThat(response.getStatus(), is(200));
+ if (httpVersion.is("HTTP/1.1"))
+ assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked"));
+ }
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ private class ExplicitFlushHandler extends ThrowExceptionOnDemandHandler
+ {
+ private final boolean dispatch;
+ private final boolean inWait;
+
+ private ExplicitFlushHandler(boolean throwException, boolean dispatch, boolean inWait)
+ {
+ super(throwException);
+ this.dispatch = dispatch;
+ this.inWait = inWait;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (request.getAttribute(contextAttribute) == null)
+ {
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ request.setAttribute(contextAttribute, asyncContext);
+ runAsync(baseRequest, inWait, () ->
+ {
+ try
+ {
+ ServletResponse asyncContextResponse = asyncContext.getResponse();
+ asyncContextResponse.getWriter().write("foobar");
+ asyncContextResponse.flushBuffer();
+ if (dispatch)
+ asyncContext.dispatch();
+ else
+ asyncContext.complete();
+ }
+ catch (IOException e)
+ {
+ markFailed(e);
+ }
+ });
+ }
+ baseRequest.setHandled(true);
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testHandledAndFlushWithoutContent(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ SetHandledAndFlushWithoutContentHandler handler = new SetHandledAndFlushWithoutContentHandler(false, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat(response.getStatus(), is(200));
+ assertThat(handler.failure(), is(nullValue()));
+ if (httpVersion.is("HTTP/1.1"))
+ assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testHandledAndFlushWithoutContentAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ SetHandledAndFlushWithoutContentHandler handler = new SetHandledAndFlushWithoutContentHandler(true, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ if (inWait)
+ {
+ // throw happens before async behaviour, so is handled
+ assertThat(response.getStatus(), is(500));
+ }
+ else
+ {
+ assertThat(response.getStatus(), is(200));
+ if (httpVersion.is("HTTP/1.1"))
+ assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked"));
+ }
+
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ private class SetHandledAndFlushWithoutContentHandler extends ThrowExceptionOnDemandHandler
+ {
+ private final boolean dispatch;
+ private final boolean inWait;
+
+ private SetHandledAndFlushWithoutContentHandler(boolean throwException, boolean dispatch, boolean inWait)
+ {
+ super(throwException);
+ this.dispatch = dispatch;
+ this.inWait = inWait;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (request.getAttribute(contextAttribute) == null)
+ {
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ request.setAttribute(contextAttribute, asyncContext);
+ runAsync(baseRequest, inWait, () ->
+ {
+ try
+ {
+ asyncContext.getResponse().flushBuffer();
+ if (dispatch)
+ asyncContext.dispatch();
+ else
+ asyncContext.complete();
+ }
+ catch (IOException e)
+ {
+ markFailed(e);
+ }
+ });
+ }
+ baseRequest.setHandled(true);
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testWriteFlushWriteMore(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ WriteFlushWriteMoreHandler handler = new WriteFlushWriteMoreHandler(false, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat(response.getStatus(), is(200));
+ assertThat(handler.failure(), is(nullValue()));
+
+ // HTTP/1.0 does not do chunked. it will just send content and close
+ if (httpVersion.is("HTTP/1.1"))
+ assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testWriteFlushWriteMoreAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ WriteFlushWriteMoreHandler handler = new WriteFlushWriteMoreHandler(true, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ if (inWait)
+ {
+ // The exception is thrown before we do any writing or async operations, so it delivered as onError and then
+ // dispatched.
+ assertThat(response.getStatus(), is(500));
+ }
+ else
+ {
+ assertThat(response.getStatus(), is(200));
+ if (httpVersion.is("HTTP/1.1"))
+ assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked"));
+ }
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ private class WriteFlushWriteMoreHandler extends ThrowExceptionOnDemandHandler
+ {
+ private final boolean dispatch;
+ private final boolean inWait;
+
+ private WriteFlushWriteMoreHandler(boolean throwException, boolean dispatch, boolean inWait)
+ {
+ super(throwException);
+ this.dispatch = dispatch;
+ this.inWait = inWait;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (request.getAttribute(contextAttribute) == null)
+ {
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ request.setAttribute(contextAttribute, asyncContext);
+ runAsync(baseRequest, inWait, () ->
+ {
+ try
+ {
+ ServletResponse asyncContextResponse = asyncContext.getResponse();
+ asyncContextResponse.getWriter().write("foo");
+ asyncContextResponse.flushBuffer();
+ asyncContextResponse.getWriter().write("bar");
+ if (dispatch)
+ asyncContext.dispatch();
+ else
+ asyncContext.complete();
+ }
+ catch (IOException e)
+ {
+ markFailed(e);
+ }
+ });
+ }
+ baseRequest.setHandled(true);
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testBufferOverflow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ OverflowHandler handler = new OverflowHandler(false, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("foobar"));
+ if (httpVersion.is("HTTP/1.1"))
+ assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked"));
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testBufferOverflowAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ OverflowHandler handler = new OverflowHandler(true, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ // Buffer size smaller than content, so writing will commit response.
+ // If this happens before the exception is thrown we get a 200, else a 500 is produced
+ if (inWait)
+ {
+ assertThat(response.getStatus(), is(500));
+ assertThat(response.getContent(), containsString("TestCommitException: Thrown by test"));
+ }
+ else
+ {
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("foobar"));
+ if (httpVersion.is("HTTP/1.1"))
+ assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked"));
+ assertThat(handler.failure(), is(nullValue()));
+ }
+ }
+
+ private class OverflowHandler extends ThrowExceptionOnDemandHandler
+ {
+ private final boolean dispatch;
+ private final boolean inWait;
+
+ private OverflowHandler(boolean throwException, boolean dispatch, boolean inWait)
+ {
+ super(throwException);
+ this.dispatch = dispatch;
+ this.inWait = inWait;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (request.getAttribute(contextAttribute) == null)
+ {
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ request.setAttribute(contextAttribute, asyncContext);
+ runAsync(baseRequest, inWait, () ->
+ {
+ try
+ {
+ ServletResponse asyncContextResponse = asyncContext.getResponse();
+ asyncContextResponse.setBufferSize(3);
+ asyncContextResponse.getWriter().write("foobar");
+ if (dispatch)
+ asyncContext.dispatch();
+ else
+ asyncContext.complete();
+ }
+ catch (IOException e)
+ {
+ markFailed(e);
+ }
+ });
+ }
+ baseRequest.setHandled(true);
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testSetContentLengthAndWriteExactlyThatAmountOfBytes(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ SetContentLengthAndWriteThatAmountOfBytesHandler handler = new SetContentLengthAndWriteThatAmountOfBytesHandler(false, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("foo"));
+ assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3"));
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testSetContentLengthAndWriteExactlyThatAmountOfBytesAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ SetContentLengthAndWriteThatAmountOfBytesHandler handler = new SetContentLengthAndWriteThatAmountOfBytesHandler(true, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ if (inWait)
+ {
+ // too late!
+ assertThat(response.getStatus(), is(500));
+ }
+ else
+ {
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("foo"));
+ assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3"));
+ }
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ private class SetContentLengthAndWriteThatAmountOfBytesHandler extends ThrowExceptionOnDemandHandler
+ {
+ private final boolean dispatch;
+ private final boolean inWait;
+
+ private SetContentLengthAndWriteThatAmountOfBytesHandler(boolean throwException, boolean dispatch, boolean inWait)
+ {
+ super(throwException);
+ this.dispatch = dispatch;
+ this.inWait = inWait;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (request.getAttribute(contextAttribute) == null)
+ {
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ request.setAttribute(contextAttribute, asyncContext);
+ runAsync(baseRequest, inWait, () ->
+ {
+ try
+ {
+ ServletResponse asyncContextResponse = asyncContext.getResponse();
+ asyncContextResponse.setContentLength(3);
+ asyncContextResponse.getWriter().write("foo");
+ if (dispatch)
+ asyncContext.dispatch();
+ else
+ asyncContext.complete();
+ }
+ catch (IOException e)
+ {
+ markFailed(e);
+ }
+ });
+ }
+ baseRequest.setHandled(true);
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testSetContentLengthAndWriteMoreBytes(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ SetContentLengthAndWriteMoreBytesHandler handler = new SetContentLengthAndWriteMoreBytesHandler(false, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat(response.getStatus(), is(200));
+ // jetty truncates the body when content-length is reached.! This is correct and desired behaviour?
+ assertThat(response.getContent(), is("foo"));
+ assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3"));
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testSetContentLengthAndWriteMoreAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ SetContentLengthAndWriteMoreBytesHandler handler = new SetContentLengthAndWriteMoreBytesHandler(true, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ if (inWait)
+ {
+ // too late!
+ assertThat(response.getStatus(), is(500));
+ }
+ else
+ {
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("foo"));
+ assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3"));
+ }
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ private class SetContentLengthAndWriteMoreBytesHandler extends ThrowExceptionOnDemandHandler
+ {
+ private final boolean dispatch;
+ private final boolean inWait;
+
+ private SetContentLengthAndWriteMoreBytesHandler(boolean throwException, boolean dispatch, boolean inWait)
+ {
+ super(throwException);
+ this.dispatch = dispatch;
+ this.inWait = inWait;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (request.getAttribute(contextAttribute) == null)
+ {
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ request.setAttribute(contextAttribute, asyncContext);
+ runAsync(baseRequest, inWait, () ->
+ {
+ try
+ {
+ ServletResponse asyncContextResponse = asyncContext.getResponse();
+ asyncContextResponse.setContentLength(3);
+ asyncContextResponse.getWriter().write("foobar");
+ if (dispatch)
+ asyncContext.dispatch();
+ else
+ asyncContext.complete();
+ }
+ catch (IOException e)
+ {
+ markFailed(e);
+ }
+ });
+ }
+ baseRequest.setHandled(true);
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testWriteAndSetContentLength(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ WriteAndSetContentLengthHandler handler = new WriteAndSetContentLengthHandler(false, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat(response.getStatus(), is(200));
+ assertThat(handler.failure(), is(nullValue()));
+ //TODO: jetty ignores setContentLength and sends transfer-encoding header. Correct?
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testWriteAndSetContentLengthAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ WriteAndSetContentLengthHandler handler = new WriteAndSetContentLengthHandler(true, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+ if (inWait)
+ {
+ // too late
+ assertThat(response.getStatus(), is(500));
+ }
+ else
+ {
+ assertThat(response.getStatus(), is(200));
+ }
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ private class WriteAndSetContentLengthHandler extends ThrowExceptionOnDemandHandler
+ {
+ private final boolean dispatch;
+ private final boolean inWait;
+
+ private WriteAndSetContentLengthHandler(boolean throwException, boolean dispatch, boolean inWait)
+ {
+ super(throwException);
+ this.dispatch = dispatch;
+ this.inWait = inWait;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (request.getAttribute(contextAttribute) == null)
+ {
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ request.setAttribute(contextAttribute, asyncContext);
+ runAsync(baseRequest, inWait, () ->
+ {
+ try
+ {
+ ServletResponse asyncContextResponse = asyncContext.getResponse();
+ asyncContextResponse.getWriter().write("foo");
+ asyncContextResponse.setContentLength(3); // This should commit the response
+ if (dispatch)
+ asyncContext.dispatch();
+ else
+ asyncContext.complete();
+ }
+ catch (IOException e)
+ {
+ markFailed(e);
+ }
+ });
+ }
+ baseRequest.setHandled(true);
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testWriteAndSetContentLengthTooSmall(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ WriteAndSetContentLengthTooSmallHandler handler = new WriteAndSetContentLengthTooSmallHandler(false, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ // Setting a content-length too small throws an IllegalStateException,
+ // but only in the async handler, which completes or dispatches anyway
+ assertThat(response.getStatus(), is(200));
+ assertThat(handler.failure(), not(is(nullValue())));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersion")
+ public void testWriteAndSetContentLengthTooSmallAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception
+ {
+ WriteAndSetContentLengthTooSmallHandler handler = new WriteAndSetContentLengthTooSmallHandler(true, dispatch, inWait);
+ server.setHandler(handler);
+ server.start();
+
+ HttpTester.Response response;
+ try (StacklessLogging stackless = new StacklessLogging(HttpChannelState.class))
+ {
+ response = executeRequest(httpVersion);
+ }
+
+ assertThat(response.getStatus(), is(500));
+
+ if (!inWait)
+ assertThat(handler.failure(), not(is(nullValue())));
+ else
+ assertThat(handler.failure(), is(nullValue()));
+ }
+
+ private class WriteAndSetContentLengthTooSmallHandler extends ThrowExceptionOnDemandHandler
+ {
+ private final boolean dispatch;
+ private final boolean inWait;
+
+ private WriteAndSetContentLengthTooSmallHandler(boolean throwException, boolean dispatch, boolean inWait)
+ {
+ super(throwException);
+ this.dispatch = dispatch;
+ this.inWait = inWait;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (request.getAttribute(contextAttribute) == null)
+ {
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ request.setAttribute(contextAttribute, asyncContext);
+ runAsync(baseRequest, inWait, () ->
+ {
+ try
+ {
+ ServletResponse asyncContextResponse = asyncContext.getResponse();
+ asyncContextResponse.getWriter().write("foobar");
+ asyncContextResponse.setContentLength(3);
+ }
+ catch (Throwable e)
+ {
+ markFailed(e);
+ if (dispatch)
+ asyncContext.dispatch();
+ else
+ asyncContext.complete();
+ }
+ });
+ }
+ baseRequest.setHandled(true);
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ private void runAsyncInAsyncWait(Request request, Runnable task)
+ {
+ server.getThreadPool().execute(() ->
+ {
+ long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+ try
+ {
+ while (System.nanoTime() < end)
+ {
+ switch (request.getHttpChannelState().getState())
+ {
+ case WAITING:
+ task.run();
+ return;
+
+ case HANDLING:
+ Thread.sleep(100);
+ continue;
+
+ default:
+ request.getHttpChannel().abort(new IllegalStateException());
+ return;
+ }
+ }
+ request.getHttpChannel().abort(new TimeoutException());
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ private void runAsyncWhileDispatched(Runnable task)
+ {
+ CountDownLatch ran = new CountDownLatch(1);
+
+ server.getThreadPool().execute(() ->
+ {
+ try
+ {
+ task.run();
+ }
+ finally
+ {
+ ran.countDown();
+ }
+ });
+
+ try
+ {
+ ran.await(10, TimeUnit.SECONDS);
+ }
+ catch (InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void runAsync(Request request, boolean inWait, Runnable task)
+ {
+ if (inWait)
+ runAsyncInAsyncWait(request, task);
+ else
+ runAsyncWhileDispatched(task);
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToCommitTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToCommitTest.java
new file mode 100644
index 0000000..8985317
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToCommitTest.java
@@ -0,0 +1,705 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.util.stream.Stream;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeaderValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumingThat;
+
+//TODO: reset buffer tests
+//TODO: add protocol specific tests for connection: close and/or chunking
+public class HttpManyWaysToCommitTest extends AbstractHttpTest
+{
+ public static Stream<Arguments> httpVersions()
+ {
+ return Stream.of(
+ HttpVersion.HTTP_1_0,
+ HttpVersion.HTTP_1_1
+ ).map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandlerDoesNotSetHandled(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new DoesNotSetHandledHandler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(404));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandlerDoesNotSetHandledAndThrow(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new DoesNotSetHandledHandler(true));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(500));
+ }
+
+ private class DoesNotSetHandledHandler extends ThrowExceptionOnDemandHandler
+ {
+ private DoesNotSetHandledHandler(boolean throwException)
+ {
+ super(throwException);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(false); // not needed, but lets be explicit about what the test does
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandlerSetsHandledTrueOnly(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new OnlySetHandledHandler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(200));
+ if (HttpVersion.HTTP_1_1.asString().equals(httpVersion))
+ assertThat(response, containsHeaderValue("content-length", "0"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandlerSetsHandledTrueOnlyAndThrow(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new OnlySetHandledHandler(true));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(500));
+ }
+
+ private class OnlySetHandledHandler extends ThrowExceptionOnDemandHandler
+ {
+ private OnlySetHandledHandler(boolean throwException)
+ {
+ super(throwException);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandlerSetsHandledAndWritesSomeContent(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new SetHandledWriteSomeDataHandler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat(response.getContent(), is("foobar"));
+ assertThat(response, containsHeaderValue("content-length", "6"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandlerSetsHandledAndWritesSomeContentAndThrow(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new SetHandledWriteSomeDataHandler(true));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(500));
+ assertThat("response body", response.getContent(), not(is("foobar")));
+ }
+
+ private class SetHandledWriteSomeDataHandler extends ThrowExceptionOnDemandHandler
+ {
+ private SetHandledWriteSomeDataHandler(boolean throwException)
+ {
+ super(throwException);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.getWriter().write("foobar");
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandlerExplicitFlush(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new ExplicitFlushHandler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat(response.getContent(), is("foobar"));
+ assumingThat(httpVersion == HttpVersion.HTTP_1_1,
+ () -> assertThat(response, containsHeaderValue("transfer-encoding", "chunked")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandlerExplicitFlushAndThrow(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new ExplicitFlushHandler(true));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ // Since the 200 was committed, the 500 did not get the chance to be written
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat("response body", response.getContent(), is("foobar"));
+ assumingThat(httpVersion == HttpVersion.HTTP_1_1,
+ () -> assertThat(response, containsHeaderValue("transfer-encoding", "chunked")));
+ }
+
+ private class ExplicitFlushHandler extends ThrowExceptionOnDemandHandler
+ {
+ private ExplicitFlushHandler(boolean throwException)
+ {
+ super(throwException);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.getWriter().write("foobar");
+ response.flushBuffer();
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandledAndFlushWithoutContent(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new SetHandledAndFlushWithoutContentHandler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(200));
+ assumingThat(httpVersion == HttpVersion.HTTP_1_1,
+ () -> assertThat(response, containsHeaderValue("transfer-encoding", "chunked")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandledAndFlushWithoutContentAndThrow(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new SetHandledAndFlushWithoutContentHandler(true));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(200));
+ assumingThat(httpVersion == HttpVersion.HTTP_1_1,
+ () -> assertThat(response, containsHeaderValue("transfer-encoding", "chunked")));
+ }
+
+ private class SetHandledAndFlushWithoutContentHandler extends ThrowExceptionOnDemandHandler
+ {
+ private SetHandledAndFlushWithoutContentHandler(boolean throwException)
+ {
+ super(throwException);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.flushBuffer();
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandledWriteFlushWriteMore(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new WriteFlushWriteMoreHandler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat(response.getContent(), is("foobar"));
+ assumingThat(httpVersion == HttpVersion.HTTP_1_1,
+ () -> assertThat(response, containsHeaderValue("transfer-encoding", "chunked")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandledWriteFlushWriteMoreAndThrow(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new WriteFlushWriteMoreHandler(true));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ // Since the 200 was committed, the 500 did not get the chance to be written
+ assertThat("response code", response.getStatus(), is(200));
+ assumingThat(httpVersion == HttpVersion.HTTP_1_1,
+ () -> assertThat(response, containsHeaderValue("transfer-encoding", "chunked")));
+ }
+
+ private class WriteFlushWriteMoreHandler extends ThrowExceptionOnDemandHandler
+ {
+ private WriteFlushWriteMoreHandler(boolean throwException)
+ {
+ super(throwException);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.getWriter().write("foo");
+ response.flushBuffer();
+ response.getWriter().write("bar");
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandledOverflow(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new OverflowHandler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat(response.getContent(), is("foobar"));
+ assumingThat(httpVersion == HttpVersion.HTTP_1_1,
+ () -> assertThat(response, containsHeaderValue("transfer-encoding", "chunked")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandledOverflow2(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new Overflow2Handler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat(response.getContent(), is("foobarfoobar"));
+ assumingThat(httpVersion == HttpVersion.HTTP_1_1,
+ () -> assertThat(response, containsHeaderValue("transfer-encoding", "chunked")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandledOverflow3(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new Overflow3Handler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat(response.getContent(), is("foobarfoobar"));
+ assumingThat(httpVersion == HttpVersion.HTTP_1_1,
+ () -> assertThat(response, containsHeaderValue("transfer-encoding", "chunked")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testHandledBufferOverflowAndThrow(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new OverflowHandler(true));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ // Response was committed when we throw, so 200 expected
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat(response.getContent(), is("foobar"));
+ assumingThat(httpVersion == HttpVersion.HTTP_1_1,
+ () -> assertThat(response, containsHeaderValue("transfer-encoding", "chunked")));
+ }
+
+ private class OverflowHandler extends ThrowExceptionOnDemandHandler
+ {
+ private OverflowHandler(boolean throwException)
+ {
+ super(throwException);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(4);
+ response.getWriter().write("foobar");
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ private class Overflow2Handler extends ThrowExceptionOnDemandHandler
+ {
+ private Overflow2Handler(boolean throwException)
+ {
+ super(throwException);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(8);
+ response.getWriter().write("fo");
+ response.getWriter().write("obarfoobar");
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ private class Overflow3Handler extends ThrowExceptionOnDemandHandler
+ {
+ private Overflow3Handler(boolean throwException)
+ {
+ super(throwException);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(8);
+ response.getWriter().write("fo");
+ response.getWriter().write("ob");
+ response.getWriter().write("ar");
+ response.getWriter().write("fo");
+ response.getWriter().write("ob");
+ response.getWriter().write("ar");
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testSetContentLengthAnd304Status(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new SetContentLength304Handler());
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+ assertThat("response code", response.getStatus(), is(304));
+ assertThat(response, containsHeaderValue("content-length", "32768"));
+ byte[] content = response.getContentBytes();
+ assertThat(content.length, is(0));
+ assertFalse(response.isEarlyEOF());
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testSetContentLengthFlushAndWriteInsufficientBytes(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new SetContentLengthAndWriteInsufficientBytesHandler(true));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat(response, containsHeaderValue("content-length", "6"));
+ byte[] content = response.getContentBytes();
+ assertThat("content bytes", content.length, is(0));
+ assertTrue(response.isEarlyEOF(), "response eof");
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testSetContentLengthAndWriteInsufficientBytes(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new SetContentLengthAndWriteInsufficientBytesHandler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+ assertThat("response is error", response.getStatus(), is(500));
+ assertFalse(response.isEarlyEOF(), "response not eof");
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testSetContentLengthAndFlushWriteInsufficientBytes(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new SetContentLengthAndWriteInsufficientBytesHandler(true));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+ assertThat("response has no status", response.getStatus(), is(200));
+ assertTrue(response.isEarlyEOF(), "response eof");
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testSetContentLengthAndWriteExactlyThatAmountOfBytes(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new SetContentLengthAndWriteThatAmountOfBytesHandler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat("response body", response.getContent(), is("foo"));
+ assertThat(response, containsHeaderValue("content-length", "3"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testSetContentLengthAndWriteExactlyThatAmountOfBytesAndThrow(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new SetContentLengthAndWriteThatAmountOfBytesHandler(true));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ // Setting the content-length and then writing the bytes commits the response
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat("response body", response.getContent(), is("foo"));
+ }
+
+ private class SetContentLengthAndWriteInsufficientBytesHandler extends AbstractHandler
+ {
+ boolean flush;
+
+ private SetContentLengthAndWriteInsufficientBytesHandler(boolean flush)
+ {
+ this.flush = flush;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setContentLength(6);
+ if (flush)
+ response.flushBuffer();
+ response.getWriter().write("foo");
+ }
+ }
+
+ private class SetContentLength304Handler extends AbstractHandler
+ {
+ private SetContentLength304Handler()
+ {
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setContentLength(32768);
+ response.setStatus(HttpStatus.NOT_MODIFIED_304);
+ }
+ }
+
+ private class SetContentLengthAndWriteThatAmountOfBytesHandler extends ThrowExceptionOnDemandHandler
+ {
+ private SetContentLengthAndWriteThatAmountOfBytesHandler(boolean throwException)
+ {
+ super(throwException);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setContentLength(3);
+ response.getWriter().write("foo");
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testSetContentLengthAndWriteMoreBytes(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new SetContentLengthAndWriteMoreBytesHandler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat("response body", response.getContent(), is("foo"));
+ assertThat(response, containsHeaderValue("content-length", "3"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testSetContentLengthAndWriteMoreAndThrow(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new SetContentLengthAndWriteMoreBytesHandler(true));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ // Setting the content-length and then writing the bytes commits the response
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat("response body", response.getContent(), is("foo"));
+ }
+
+ private class SetContentLengthAndWriteMoreBytesHandler extends ThrowExceptionOnDemandHandler
+ {
+ private SetContentLengthAndWriteMoreBytesHandler(boolean throwException)
+ {
+ super(throwException);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setContentLength(3);
+ // Only "foo" will get written and "bar" will be discarded
+ response.getWriter().write("foobar");
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testWriteAndSetContentLength(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new WriteAndSetContentLengthHandler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat("response body", response.getContent(), is("foo"));
+ assertThat(response, containsHeaderValue("content-length", "3"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testWriteAndSetContentLengthAndThrow(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new WriteAndSetContentLengthHandler(true));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ // Writing the bytes and then setting the content-length commits the response
+ assertThat("response code", response.getStatus(), is(200));
+ assertThat("response body", response.getContent(), is("foo"));
+ }
+
+ private class WriteAndSetContentLengthHandler extends ThrowExceptionOnDemandHandler
+ {
+ private WriteAndSetContentLengthHandler(boolean throwException)
+ {
+ super(throwException);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.getWriter().write("foo");
+ response.setContentLength(3);
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testWriteAndSetContentLengthTooSmall(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new WriteAndSetContentLengthTooSmallHandler(false));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ // Setting a content-length too small throws an IllegalStateException
+ assertThat("response code", response.getStatus(), is(500));
+ assertThat("response body", response.getContent(), not(is("foo")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("httpVersions")
+ public void testWriteAndSetContentLengthTooSmallAndThrow(HttpVersion httpVersion) throws Exception
+ {
+ server.setHandler(new WriteAndSetContentLengthTooSmallHandler(true));
+ server.start();
+
+ HttpTester.Response response = executeRequest(httpVersion);
+
+ // Setting a content-length too small throws an IllegalStateException
+ assertThat("response code", response.getStatus(), is(500));
+ assertThat("response body", response.getContent(), not(is("foo")));
+ }
+
+ private class WriteAndSetContentLengthTooSmallHandler extends ThrowExceptionOnDemandHandler
+ {
+ private WriteAndSetContentLengthTooSmallHandler(boolean throwException)
+ {
+ super(throwException);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.getWriter().write("foobar");
+ response.setContentLength(3);
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpOutputTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpOutputTest.java
new file mode 100644
index 0000000..5a02990
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpOutputTest.java
@@ -0,0 +1,1391 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.WriteListener;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.server.HttpOutput.Interceptor;
+import org.eclipse.jetty.server.LocalConnector.LocalEndPoint;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.HotSwapHandler;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.FuturePromise;
+import org.eclipse.jetty.util.resource.Resource;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ *
+ */
+public class HttpOutputTest
+{
+ public static final int OUTPUT_AGGREGATION_SIZE = 1024;
+ public static final int OUTPUT_BUFFER_SIZE = 4096;
+ private Server _server;
+ private LocalConnector _connector;
+ private ContentHandler _handler;
+ private HotSwapHandler _swap;
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ _server = new Server();
+
+ _server.addBean(new ByteBufferPool()
+ {
+ @Override
+ public ByteBuffer acquire(int size, boolean direct)
+ {
+ return direct ? BufferUtil.allocateDirect(size) : BufferUtil.allocate(size);
+ }
+
+ @Override
+ public void release(ByteBuffer buffer)
+ {
+ }
+ });
+
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ http.getHttpConfiguration().setRequestHeaderSize(1024);
+ http.getHttpConfiguration().setResponseHeaderSize(1024);
+ http.getHttpConfiguration().setOutputBufferSize(OUTPUT_BUFFER_SIZE);
+ http.getHttpConfiguration().setOutputAggregationSize(OUTPUT_AGGREGATION_SIZE);
+
+ _connector = new LocalConnector(_server, http, null);
+ _server.addConnector(_connector);
+ _swap = new HotSwapHandler();
+ _handler = new ContentHandler();
+ _swap.setHandler(_handler);
+ _server.setHandler(_swap);
+ _server.start();
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ _server.stop();
+ _server.join();
+ }
+
+ @Test
+ public void testSimple() throws Exception
+ {
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ }
+
+ @Test
+ public void testByteUnknown() throws Exception
+ {
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ }
+
+ @Test
+ public void testSendArray() throws Exception
+ {
+ byte[] buffer = new byte[16 * 1024];
+ Arrays.fill(buffer, 0, 4 * 1024, (byte)0x99);
+ Arrays.fill(buffer, 4 * 1024, 12 * 1024, (byte)0x58);
+ Arrays.fill(buffer, 12 * 1024, 16 * 1024, (byte)0x66);
+ _handler._content = ByteBuffer.wrap(buffer);
+ _handler._content.limit(12 * 1024);
+ _handler._content.position(4 * 1024);
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("\r\nXXXXXXXXXXXXXXXXXXXXXXXXXXX"));
+
+ for (int i = 0; i < 4 * 1024; i++)
+ {
+ assertEquals((byte)0x99, buffer[i], "i=" + i);
+ }
+ for (int i = 12 * 1024; i < 16 * 1024; i++)
+ {
+ assertEquals((byte)0x66, buffer[i], "i=" + i);
+ }
+ }
+
+ @Test
+ public void testSendInputStreamSimple() throws Exception
+ {
+ Resource simple = Resource.newClassPathResource("simple/simple.txt");
+ _handler._contentInputStream = simple.getInputStream();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length: 11"));
+ }
+
+ @Test
+ public void testSendInputStreamBig() throws Exception
+ {
+ Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._contentInputStream = big.getInputStream();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ }
+
+ @Test
+ public void testSendInputStreamBigChunked() throws Exception
+ {
+ Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._contentInputStream = new FilterInputStream(big.getInputStream())
+ {
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException
+ {
+ int filled = super.read(b, off, len > 2000 ? 2000 : len);
+ return filled;
+ }
+ };
+ LocalEndPoint endp = _connector.executeRequest(
+ "GET / HTTP/1.1\nHost: localhost:80\n\n" +
+ "GET / HTTP/1.1\nHost: localhost:80\nConnection: close\n\n"
+ );
+
+ String response = endp.getResponse();
+
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Transfer-Encoding: chunked"));
+ assertThat(response, containsString("1\tThis is a big file"));
+ assertThat(response, containsString("400\tThis is a big file"));
+ assertThat(response, containsString("\r\n0\r\n"));
+
+ response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Connection: close"));
+ }
+
+ @Test
+ public void testSendChannelSimple() throws Exception
+ {
+ Resource simple = Resource.newClassPathResource("simple/simple.txt");
+ _handler._contentChannel = simple.getReadableByteChannel();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length: 11"));
+ }
+
+ @Test
+ public void testSendChannelBig() throws Exception
+ {
+ Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._contentChannel = big.getReadableByteChannel();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ }
+
+ @Test
+ public void testSendBigDirect() throws Exception
+ {
+ Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content = BufferUtil.toBuffer(big, true);
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length"));
+ assertThat(response, endsWith(toUTF8String(big)));
+ }
+
+ @Test
+ public void testSendBigInDirect() throws Exception
+ {
+ Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content = BufferUtil.toBuffer(big, false);
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length"));
+ assertThat(response, endsWith(toUTF8String(big)));
+ }
+
+ @Test
+ public void testSendChannelBigChunked() throws Exception
+ {
+ Resource big = Resource.newClassPathResource("simple/big.txt");
+ final ReadableByteChannel channel = big.getReadableByteChannel();
+ _handler._contentChannel = new ReadableByteChannel()
+ {
+ @Override
+ public boolean isOpen()
+ {
+ return channel.isOpen();
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ channel.close();
+ }
+
+ @Override
+ public int read(ByteBuffer dst) throws IOException
+ {
+ int filled = 0;
+ if (dst.position() == 0 && dst.limit() > 2000)
+ {
+ int limit = dst.limit();
+ dst.limit(2000);
+ filled = channel.read(dst);
+ dst.limit(limit);
+ }
+ else
+ filled = channel.read(dst);
+ return filled;
+ }
+ };
+
+ LocalEndPoint endp = _connector.executeRequest(
+ "GET / HTTP/1.1\nHost: localhost:80\n\n" +
+ "GET / HTTP/1.1\nHost: localhost:80\nConnection: close\n\n"
+ );
+
+ String response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Transfer-Encoding: chunked"));
+ assertThat(response, containsString("1\tThis is a big file"));
+ assertThat(response, containsString("400\tThis is a big file"));
+ assertThat(response, containsString("\r\n0\r\n"));
+
+ response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Connection: close"));
+ }
+
+ @Test
+ public void testWriteByte() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[1];
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ }
+
+ @Test
+ public void testWriteSmall() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[8];
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ }
+
+ @Test
+ public void testWriteMed() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[4000];
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ }
+
+ @Test
+ public void testWriteLarge() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[8192];
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ }
+
+ @Test
+ public void testWriteByteKnown() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = true;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[1];
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length"));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(true));
+ }
+
+ @Test
+ public void testWriteSmallKnown() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = true;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[8];
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length"));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(true));
+ }
+
+ @Test
+ public void testWriteMedKnown() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = true;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[4000];
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length"));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(true));
+ }
+
+ @Test
+ public void testWriteLargeKnown() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = true;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[8192];
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length"));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(true));
+ }
+
+ @Test
+ public void testWriteHugeKnown() throws Exception
+ {
+ _handler._writeLengthIfKnown = true;
+ _handler._content = BufferUtil.allocate(4 * 1024 * 1024);
+ _handler._content.limit(_handler._content.capacity());
+ for (int i = _handler._content.capacity(); i-- > 0; )
+ {
+ _handler._content.put(i, (byte)'x');
+ }
+ _handler._arrayBuffer = new byte[8192];
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length"));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(true));
+ }
+
+ @Test
+ public void testWriteBufferSmall() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._byteBuffer = BufferUtil.allocate(8);
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(false));
+ }
+
+ @Test
+ public void testWriteBufferMed() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._byteBuffer = BufferUtil.allocate(4000);
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(false));
+ }
+
+ @Test
+ public void testWriteBufferLarge() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._byteBuffer = BufferUtil.allocate(8192);
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(false));
+ }
+
+ @Test
+ public void testWriteBufferSmallKnown() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = true;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._byteBuffer = BufferUtil.allocate(8);
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length"));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(true));
+ }
+
+ @Test
+ public void testWriteBufferMedKnown() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = true;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._byteBuffer = BufferUtil.allocate(4000);
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length"));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(true));
+ }
+
+ @Test
+ public void testWriteBufferLargeKnown() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = true;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._byteBuffer = BufferUtil.allocate(8192);
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length"));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(true));
+ }
+
+ @Test
+ public void testAsyncWriteByte() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[1];
+ _handler._async = true;
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(false));
+ }
+
+ @Test
+ public void testAsyncWriteSmall() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[8];
+ _handler._async = true;
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(false));
+ }
+
+ @Test
+ public void testAsyncWriteMed() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[4000];
+ _handler._async = true;
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(false));
+ }
+
+ @Test
+ public void testAsyncWriteLarge() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[8192];
+ _handler._async = true;
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(false));
+ }
+
+ @Test
+ public void testAsyncWriteHuge() throws Exception
+ {
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.allocate(4 * 1024 * 1024);
+ _handler._content.limit(_handler._content.capacity());
+ for (int i = _handler._content.capacity(); i-- > 0; )
+ {
+ _handler._content.put(i, (byte)'x');
+ }
+ _handler._arrayBuffer = new byte[8192];
+ _handler._async = true;
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(false));
+ }
+
+ @Test
+ public void testAsyncWriteBufferSmall() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._byteBuffer = BufferUtil.allocate(8);
+ _handler._async = true;
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(false));
+ }
+
+ @Test
+ public void testAsyncWriteBufferMed() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._byteBuffer = BufferUtil.allocate(4000);
+ _handler._async = true;
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(false));
+ }
+
+ @Test
+ public void testAsyncWriteBufferLarge() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._byteBuffer = BufferUtil.allocate(8192);
+ _handler._async = true;
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(false));
+ }
+
+ @Test
+ public void testAsyncWriteBufferLargeDirect()
+ throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, true);
+ _handler._byteBuffer = BufferUtil.allocateDirect(8192);
+ _handler._async = true;
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, endsWith(toUTF8String(big)));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(false));
+ }
+
+ @Test
+ public void testAsyncWriteBufferLargeHEAD() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._byteBuffer = BufferUtil.allocate(8192);
+ _handler._async = true;
+
+ int start = _handler._owp.get();
+ String response = _connector.getResponse("HEAD / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(_handler._owp.get() - start, Matchers.greaterThan(0));
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, Matchers.not(containsString("1\tThis is a big file")));
+ assertThat(response, Matchers.not(containsString("400\tThis is a big file")));
+ }
+
+ @Test
+ public void testAsyncWriteSimpleKnown() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/simple.txt");
+
+ _handler._async = true;
+ _handler._writeLengthIfKnown = true;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[4000];
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length: 11"));
+ assertThat(response, containsString("simple text"));
+ assertThat(_handler._closedAfterWrite.get(10, TimeUnit.SECONDS), is(true));
+ }
+
+ @Test
+ public void testAsyncWriteSimpleKnownHEAD() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/simple.txt");
+
+ _handler._async = true;
+ _handler._writeLengthIfKnown = true;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[4000];
+
+ int start = _handler._owp.get();
+ String response = _connector.getResponse("HEAD / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(_handler._owp.get() - start, Matchers.equalTo(1));
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length: 11"));
+ assertThat(response, Matchers.not(containsString("simple text")));
+ }
+
+ @Test
+ public void testWriteInterception() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._writeLengthIfKnown = false;
+ _handler._content = BufferUtil.toBuffer(big, false);
+ _handler._arrayBuffer = new byte[1024];
+ _handler._interceptor = new ChainedInterceptor()
+ {
+ Interceptor _next;
+
+ @Override
+ public void write(ByteBuffer content, boolean complete, Callback callback)
+ {
+ String s = BufferUtil.toString(content).toUpperCase().replaceAll("BIG", "BIGGER");
+ _next.write(BufferUtil.toBuffer(s), complete, callback);
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return _next.isOptimizedForDirectBuffers();
+ }
+
+ @Override
+ public Interceptor getNextInterceptor()
+ {
+ return _next;
+ }
+
+ @Override
+ public void setNext(Interceptor interceptor)
+ {
+ _next = interceptor;
+ }
+ };
+
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.not(containsString("Content-Length")));
+ assertThat(response, containsString("400\tTHIS IS A BIGGER FILE"));
+ }
+
+ @Test
+ public void testEmptyArray() throws Exception
+ {
+ FuturePromise<Boolean> committed = new FuturePromise<>();
+ AbstractHandler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ try
+ {
+ response.getOutputStream().write(new byte[0]);
+ committed.succeeded(response.isCommitted());
+ }
+ catch (Throwable t)
+ {
+ committed.failed(t);
+ }
+ }
+ };
+
+ _swap.setHandler(handler);
+ handler.start();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(committed.get(), is(false));
+ }
+
+ @Test
+ public void testEmptyArrayKnown() throws Exception
+ {
+ FuturePromise<Boolean> committed = new FuturePromise<>();
+ AbstractHandler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.setContentLength(0);
+ try
+ {
+ response.getOutputStream().write(new byte[0]);
+ committed.succeeded(response.isCommitted());
+ }
+ catch (Throwable t)
+ {
+ committed.failed(t);
+ }
+ }
+ };
+
+ _swap.setHandler(handler);
+ handler.start();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length: 0"));
+ assertThat(committed.get(), is(true));
+ }
+
+ @Test
+ public void testEmptyBuffer() throws Exception
+ {
+ FuturePromise<Boolean> committed = new FuturePromise<>();
+ AbstractHandler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ ((HttpOutput)response.getOutputStream()).write(ByteBuffer.wrap(new byte[0]));
+ committed.succeeded(response.isCommitted());
+ }
+ };
+
+ _swap.setHandler(handler);
+ handler.start();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(committed.get(10, TimeUnit.SECONDS), is(false));
+ }
+
+ @Test
+ public void testEmptyBufferWithZeroContentLength() throws Exception
+ {
+ FuturePromise<Boolean> committed = new FuturePromise<>();
+ AbstractHandler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.setContentLength(0);
+ ((HttpOutput)response.getOutputStream()).write(ByteBuffer.wrap(new byte[0]));
+ committed.succeeded(response.isCommitted());
+ }
+ };
+
+ _swap.setHandler(handler);
+ handler.start();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Content-Length: 0"));
+ assertThat(committed.get(10, TimeUnit.SECONDS), is(true));
+ }
+
+ @Test
+ public void testAggregation() throws Exception
+ {
+ AggregateHandler handler = new AggregateHandler();
+ _swap.setHandler(handler);
+ handler.start();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString(handler.expected.toString()));
+ }
+
+ static class AggregateHandler extends AbstractHandler
+ {
+ ByteArrayOutputStream expected = new ByteArrayOutputStream();
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ HttpOutput out = (HttpOutput)response.getOutputStream();
+
+ // Add interceptor to check aggregation is done
+ HttpOutput.Interceptor interceptor = out.getInterceptor();
+ out.setInterceptor(new AggregationChecker(interceptor));
+
+ int bufferSize = baseRequest.getHttpChannel().getHttpConfiguration().getOutputBufferSize();
+ int len = bufferSize * 3 / 2;
+
+ byte[] data = new byte[AggregationChecker.MAX_SIZE];
+ int fill = 0;
+ while (expected.size() < len)
+ {
+ Arrays.fill(data, (byte)('A' + (fill++ % 26)));
+ expected.write(data);
+ out.write(data);
+ }
+ }
+ }
+
+ @Test
+ public void testAsyncAggregation() throws Exception
+ {
+ AsyncAggregateHandler handler = new AsyncAggregateHandler();
+ _swap.setHandler(handler);
+ handler.start();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString(handler.expected.toString()));
+ }
+
+ static class AsyncAggregateHandler extends AbstractHandler
+ {
+ ByteArrayOutputStream expected = new ByteArrayOutputStream();
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ HttpOutput out = (HttpOutput)response.getOutputStream();
+
+ // Add interceptor to check aggregation is done
+ HttpOutput.Interceptor interceptor = out.getInterceptor();
+ out.setInterceptor(new AggregationChecker(interceptor));
+
+ int bufferSize = baseRequest.getHttpChannel().getHttpConfiguration().getOutputBufferSize();
+ int len = bufferSize * 3 / 2;
+
+ AsyncContext async = request.startAsync();
+ out.setWriteListener(new WriteListener()
+ {
+ int fill = 0;
+
+ @Override
+ public void onWritePossible() throws IOException
+ {
+ byte[] data = new byte[AggregationChecker.MAX_SIZE];
+ while (out.isReady())
+ {
+ if (expected.size() >= len)
+ {
+ async.complete();
+ return;
+ }
+
+ Arrays.fill(data, (byte)('A' + (fill++ % 26)));
+ expected.write(data);
+ out.write(data);
+ }
+ }
+
+ @Override
+ public void onError(Throwable t)
+ {
+ }
+ });
+ }
+ }
+
+ private static class AggregationChecker implements Interceptor
+ {
+ static final int MAX_SIZE = OUTPUT_AGGREGATION_SIZE / 2 - 1;
+ private final Interceptor interceptor;
+
+ public AggregationChecker(Interceptor interceptor)
+ {
+ this.interceptor = interceptor;
+ }
+
+ @Override
+ public void write(ByteBuffer content, boolean last, Callback callback)
+ {
+ if (content.remaining() <= MAX_SIZE)
+ throw new IllegalStateException("Not Aggregated!");
+ interceptor.write(content, last, callback);
+ }
+
+ @Override
+ public Interceptor getNextInterceptor()
+ {
+ return interceptor;
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return interceptor.isOptimizedForDirectBuffers();
+ }
+ }
+
+ @Test
+ public void testAggregateResidue() throws Exception
+ {
+ AggregateResidueHandler handler = new AggregateResidueHandler();
+ _swap.setHandler(handler);
+ handler.start();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString(handler.expected.toString()));
+ }
+
+ static class AggregateResidueHandler extends AbstractHandler
+ {
+ ByteArrayOutputStream expected = new ByteArrayOutputStream();
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ HttpOutput out = (HttpOutput)response.getOutputStream();
+
+ int bufferSize = baseRequest.getHttpChannel().getHttpConfiguration().getOutputBufferSize();
+ int commitSize = baseRequest.getHttpChannel().getHttpConfiguration().getOutputAggregationSize();
+ char fill = 'A';
+
+ // write data that will be aggregated
+ byte[] data = new byte[commitSize - 1];
+ Arrays.fill(data, (byte)(fill++));
+ expected.write(data);
+ out.write(data);
+ int aggregated = data.length;
+
+ // write data that will almost fill the aggregate buffer
+ while (aggregated < (bufferSize - 1))
+ {
+ data = new byte[Math.min(commitSize - 1, bufferSize - aggregated - 1)];
+ Arrays.fill(data, (byte)(fill++));
+ expected.write(data);
+ out.write(data);
+ aggregated += data.length;
+ }
+
+ // write data that will not be aggregated because it is too large
+ data = new byte[bufferSize + 1];
+ Arrays.fill(data, (byte)(fill++));
+ expected.write(data);
+ out.write(data);
+ }
+ }
+
+ @Test
+ public void testPrint() throws Exception
+ {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ PrintWriter exp = new PrintWriter(bout);
+ _swap.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setCharacterEncoding("UTF8");
+ HttpOutput out = (HttpOutput)response.getOutputStream();
+
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+ exp.print("\u20AC\u0939\uD55C");
+ out.print("\u20AC\u0939\uD55C");
+ exp.print("zero");
+ out.print("zero");
+ exp.print(1);
+ out.print(1);
+ exp.print(2L);
+ out.print(2L);
+ exp.print(3.0F);
+ out.print(3.0F);
+ exp.print('4');
+ out.print('4');
+ exp.print(5.0D);
+ out.print(5.0D);
+ exp.print(true);
+ out.print(true);
+ exp.println("zero");
+ out.println("zero");
+ exp.println(-1);
+ out.println(-1);
+ exp.println(-2L);
+ out.println(-2L);
+ exp.println(-3.0F);
+ out.println(-3.0F);
+ exp.println('4');
+ out.println('4');
+ exp.println(-5.0D);
+ out.println(-5.0D);
+ exp.println(false);
+ out.println(false);
+ }
+ });
+ _swap.getHandler().start();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString(bout.toString()));
+ }
+
+ @Test
+ public void testReset() throws Exception
+ {
+ ByteArrayOutputStream exp = new ByteArrayOutputStream();
+ _swap.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ HttpOutput out = (HttpOutput)response.getOutputStream();
+ Interceptor interceptor = out.getInterceptor();
+ out.setInterceptor(new Interceptor()
+ {
+ @Override
+ public void write(ByteBuffer content, boolean last, Callback callback)
+ {
+ interceptor.write(content, last, callback);
+ }
+
+ @Override
+ public Interceptor getNextInterceptor()
+ {
+ return interceptor;
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return interceptor.isOptimizedForDirectBuffers();
+ }
+ });
+
+ out.setBufferSize(128);
+ out.println("NOT TO BE SEEN!");
+ out.resetBuffer();
+
+ byte[] data = "TO BE SEEN\n".getBytes(StandardCharsets.ISO_8859_1);
+ exp.write(data);
+ out.write(data);
+
+ out.flush();
+
+ data = "Not reset after flush\n".getBytes(StandardCharsets.ISO_8859_1);
+ exp.write(data);
+ try
+ {
+ out.resetBuffer();
+ }
+ catch (IllegalStateException e)
+ {
+ out.write(data);
+ }
+ }
+ });
+ _swap.getHandler().start();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString(exp.toString()));
+ }
+
+ @Test
+ public void testZeroLengthWrite() throws Exception
+ {
+ _swap.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setContentLength(0);
+ AsyncContext async = request.startAsync();
+ response.getOutputStream().setWriteListener(new WriteListener()
+ {
+ @Override
+ public void onWritePossible() throws IOException
+ {
+ response.getOutputStream().write(new byte[0]);
+ async.complete();
+ }
+
+ @Override
+ public void onError(Throwable t)
+ {
+ }
+ });
+ }
+ });
+ _swap.getHandler().start();
+ String response = _connector.getResponse("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ }
+
+ private static String toUTF8String(Resource resource)
+ throws IOException
+ {
+ return BufferUtil.toUTF8String(BufferUtil.toBuffer(resource, false));
+ }
+
+ interface ChainedInterceptor extends HttpOutput.Interceptor
+ {
+ default void init(Request baseRequest)
+ {
+ }
+
+ void setNext(Interceptor interceptor);
+ }
+
+ static class ContentHandler extends AbstractHandler
+ {
+ AtomicInteger _owp = new AtomicInteger();
+ boolean _writeLengthIfKnown = true;
+ boolean _async;
+ ByteBuffer _byteBuffer;
+ byte[] _arrayBuffer;
+ InputStream _contentInputStream;
+ ReadableByteChannel _contentChannel;
+ ByteBuffer _content;
+ ChainedInterceptor _interceptor;
+ final FuturePromise<Boolean> _closedAfterWrite = new FuturePromise<>();
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ if (_interceptor != null)
+ {
+ _interceptor.init(baseRequest);
+ _interceptor.setNext(baseRequest.getResponse().getHttpOutput().getInterceptor());
+ baseRequest.getResponse().getHttpOutput().setInterceptor(_interceptor);
+ }
+
+ response.setContentType("text/plain");
+
+ final HttpOutput out = (HttpOutput)response.getOutputStream();
+
+ if (_contentInputStream != null)
+ {
+ out.sendContent(_contentInputStream);
+ _contentInputStream = null;
+ _closedAfterWrite.succeeded(out.isClosed());
+ return;
+ }
+
+ if (_contentChannel != null)
+ {
+ out.sendContent(_contentChannel);
+ _contentChannel = null;
+ _closedAfterWrite.succeeded(out.isClosed());
+ return;
+ }
+
+ if (_content != null && _writeLengthIfKnown)
+ response.setContentLength(_content.remaining());
+
+ if (_arrayBuffer != null)
+ {
+ if (_async)
+ {
+ final AsyncContext async = request.startAsync();
+ out.setWriteListener(new WriteListener()
+ {
+ @Override
+ public void onWritePossible() throws IOException
+ {
+ _owp.incrementAndGet();
+
+ while (out.isReady())
+ {
+ assertTrue(out.isReady());
+ int len = _content.remaining();
+ if (len > _arrayBuffer.length)
+ len = _arrayBuffer.length;
+ if (len == 0)
+ {
+ _closedAfterWrite.succeeded(out.isClosed());
+ async.complete();
+ break;
+ }
+
+ _content.get(_arrayBuffer, 0, len);
+ if (len == 1)
+ out.write(_arrayBuffer[0]);
+ else
+ out.write(_arrayBuffer, 0, len);
+ }
+ }
+
+ @Override
+ public void onError(Throwable t)
+ {
+ t.printStackTrace();
+ async.complete();
+ }
+ });
+
+ return;
+ }
+
+ while (BufferUtil.hasContent(_content))
+ {
+ int len = _content.remaining();
+ if (len > _arrayBuffer.length)
+ len = _arrayBuffer.length;
+ _content.get(_arrayBuffer, 0, len);
+ if (len == 1)
+ out.write(_arrayBuffer[0]);
+ else
+ out.write(_arrayBuffer, 0, len);
+ }
+ _closedAfterWrite.succeeded(out.isClosed());
+ return;
+ }
+
+ if (_byteBuffer != null)
+ {
+ if (_async)
+ {
+ final AsyncContext async = request.startAsync();
+ out.setWriteListener(new WriteListener()
+ {
+ private boolean isFirstWrite = true;
+
+ @Override
+ public void onWritePossible() throws IOException
+ {
+ _owp.incrementAndGet();
+
+ while (out.isReady())
+ {
+ assertTrue(isFirstWrite || !_byteBuffer.hasRemaining());
+ assertTrue(out.isReady());
+ if (BufferUtil.isEmpty(_content))
+ {
+ _closedAfterWrite.succeeded(out.isClosed());
+ async.complete();
+ break;
+ }
+
+ BufferUtil.clearToFill(_byteBuffer);
+ BufferUtil.put(_content, _byteBuffer);
+ BufferUtil.flipToFlush(_byteBuffer, 0);
+ out.write(_byteBuffer);
+ isFirstWrite = false;
+ }
+ }
+
+ @Override
+ public void onError(Throwable t)
+ {
+ t.printStackTrace();
+ async.complete();
+ }
+ });
+
+ return;
+ }
+
+ while (BufferUtil.hasContent(_content))
+ {
+ BufferUtil.clearToFill(_byteBuffer);
+ BufferUtil.put(_content, _byteBuffer);
+ BufferUtil.flipToFlush(_byteBuffer, 0);
+ out.write(_byteBuffer);
+ }
+ _closedAfterWrite.succeeded(out.isClosed());
+ return;
+ }
+
+ if (_content != null)
+ {
+ if (_content.hasArray())
+ out.write(_content.array(), _content.arrayOffset() + _content.position(), _content.remaining());
+ else
+ out.sendContent(_content);
+ _content = null;
+ _closedAfterWrite.succeeded(out.isClosed());
+ return;
+ }
+ }
+ }
+}
+
+
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java
new file mode 100644
index 0000000..eb8bd3b
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java
@@ -0,0 +1,1884 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.Exchanger;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.EofException;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.AbstractLogger;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+import org.junit.jupiter.api.condition.DisabledOnJre;
+import org.junit.jupiter.api.condition.JRE;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public abstract class HttpServerTestBase extends HttpServerTestFixture
+{
+ private static final String REQUEST1_HEADER = "POST / HTTP/1.0\n" +
+ "Host: localhost\n" +
+ "Content-Type: text/xml; charset=utf-8\n" +
+ "Connection: close\n" +
+ "Content-Length: ";
+ private static final String REQUEST1_CONTENT = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<nimbus xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
+ " xsi:noNamespaceSchemaLocation=\"nimbus.xsd\" version=\"1.0\">\n" +
+ "</nimbus>";
+ private static final String REQUEST1 = REQUEST1_HEADER + REQUEST1_CONTENT.getBytes().length + "\n\n" + REQUEST1_CONTENT;
+
+ private static final String RESPONSE1 = "HTTP/1.1 200 OK\n" +
+ "Content-Length: 13\n" +
+ "Server: Jetty(" + Server.getVersion() + ")\n" +
+ "\n" +
+ "Hello world\n";
+
+ // Break the request up into three pieces, splitting the header.
+ private static final String FRAGMENT1 = REQUEST1.substring(0, 16);
+ private static final String FRAGMENT2 = REQUEST1.substring(16, 34);
+ private static final String FRAGMENT3 = REQUEST1.substring(34);
+
+ protected static final String REQUEST2_HEADER =
+ "POST / HTTP/1.0\n" +
+ "Host: localhost\n" +
+ "Content-Type: text/xml; charset=ISO-8859-1\n" +
+ "Content-Length: ";
+ protected static final String REQUEST2_CONTENT =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<nimbus xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
+ " xsi:noNamespaceSchemaLocation=\"nimbus.xsd\" version=\"1.0\">\n" +
+ " <request requestId=\"1\">\n" +
+ " <getJobDetails>\n" +
+ " <jobId>73</jobId>\n" +
+ " </getJobDetails>\n" +
+ " </request>\n" +
+ "</nimbus>";
+ protected static final String REQUEST2 = REQUEST2_HEADER + REQUEST2_CONTENT.getBytes().length + "\n\n" + REQUEST2_CONTENT;
+
+ protected static final String RESPONSE2_CONTENT =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<nimbus xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
+ " xsi:noNamespaceSchemaLocation=\"nimbus.xsd\" version=\"1.0\">\n" +
+ " <request requestId=\"1\">\n" +
+ " <getJobDetails>\n" +
+ " <jobId>73</jobId>\n" +
+ " </getJobDetails>\n" +
+ " </request>\n" +
+ "</nimbus>\n";
+ protected static final String RESPONSE2 =
+ "HTTP/1.1 200 OK\n" +
+ "Content-Type: text/xml;charset=iso-8859-1\n" +
+ "Content-Length: " + RESPONSE2_CONTENT.getBytes().length + "\n" +
+ "Server: Jetty(" + Server.getVersion() + ")\n" +
+ "\n" +
+ RESPONSE2_CONTENT;
+
+ @Test
+ public void testSimple() throws Exception
+ {
+ configureServer(new HelloWorldHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ os.write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
+ os.flush();
+
+ // Read the response.
+ String response = readResponse(client);
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.containsString("Hello world"));
+ }
+ }
+
+ @Test
+ public void testOPTIONS() throws Exception
+ {
+ configureServer(new OptionsHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ os.write(("OPTIONS * HTTP/1.1\r\n" +
+ "Host: " + _serverURI.getHost() + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n").getBytes(StandardCharsets.ISO_8859_1));
+ os.flush();
+
+ // Read the response.
+ String response = readResponse(client);
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.containsString("Allow: GET"));
+ }
+ }
+
+ @Test
+ public void testGETStar() throws Exception
+ {
+ configureServer(new OptionsHandler());
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ os.write(("GET * HTTP/1.1\r\n" +
+ "Host: " + _serverURI.getHost() + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n").getBytes(StandardCharsets.ISO_8859_1));
+ os.flush();
+
+ // Read the response.
+ String response = readResponse(client);
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 400 "));
+ assertThat(response, Matchers.not(Matchers.containsString("Allow: ")));
+ }
+ }
+
+ /*
+ * Feed a full header method
+ */
+ @Test
+ public void testFullMethod() throws Exception
+ {
+ configureServer(new HelloWorldHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ StacklessLogging stackless = new StacklessLogging(HttpConnection.class))
+ {
+ client.setSoTimeout(10000);
+ Log.getLogger(HttpConnection.class).info("expect request is too large, then ISE extra data ...");
+ OutputStream os = client.getOutputStream();
+
+ byte[] buffer = new byte[64 * 1024];
+ Arrays.fill(buffer, (byte)'A');
+
+ os.write(buffer);
+ os.flush();
+
+ // Read the response.
+ String response = readResponse(client);
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 431 "));
+ }
+ }
+
+ /*
+ * Feed a full header method
+ */
+ @Test
+ public void testFullURI() throws Exception
+ {
+ configureServer(new HelloWorldHandler());
+
+ int maxHeaderSize = 1000;
+ _httpConfiguration.setRequestHeaderSize(maxHeaderSize);
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ StacklessLogging stackless = new StacklessLogging(HttpConnection.class))
+ {
+ Log.getLogger(HttpConnection.class).info("expect URI is too large");
+ OutputStream os = client.getOutputStream();
+
+ // Take into account the initial bytes for the HTTP method.
+ byte[] buffer = new byte[5 + maxHeaderSize];
+ buffer[0] = 'G';
+ buffer[1] = 'E';
+ buffer[2] = 'T';
+ buffer[3] = ' ';
+ buffer[4] = '/';
+ Arrays.fill(buffer, 5, buffer.length, (byte)'A');
+
+ os.write(buffer);
+ os.flush();
+
+ // Read the response.
+ String response = readResponse(client);
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 414 "));
+ }
+ }
+
+ @Test
+ public void testBadURI() throws Exception
+ {
+ configureServer(new HelloWorldHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ os.write("GET /%xx HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
+ os.flush();
+
+ // Read the response.
+ String response = readResponse(client);
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 400 "));
+ }
+ }
+
+ @Test
+ public void testExceptionThrownInHandlerLoop() throws Exception
+ {
+ configureServer(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ throw new QuietServletException("TEST handler exception");
+ }
+ });
+
+ StringBuffer request = new StringBuffer("GET / HTTP/1.0\r\n");
+ request.append("Host: localhost\r\n\r\n");
+
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ OutputStream os = client.getOutputStream();
+
+ try (StacklessLogging stackless = new StacklessLogging(HttpChannel.class))
+ {
+ Log.getLogger(HttpChannel.class).info("Expecting ServletException: TEST handler exception...");
+ os.write(request.toString().getBytes());
+ os.flush();
+
+ String response = readResponse(client);
+ assertThat(response, Matchers.containsString(" 500 "));
+ }
+ }
+
+ @Test
+ public void testExceptionThrownInHandler() throws Exception
+ {
+ configureServer(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ throw new QuietServletException("TEST handler exception");
+ }
+ });
+
+ StringBuffer request = new StringBuffer("GET / HTTP/1.0\r\n");
+ request.append("Host: localhost\r\n\r\n");
+
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ OutputStream os = client.getOutputStream();
+
+ try (StacklessLogging stackless = new StacklessLogging(HttpChannel.class))
+ {
+ Log.getLogger(HttpChannel.class).info("Expecting ServletException: TEST handler exception...");
+ os.write(request.toString().getBytes());
+ os.flush();
+
+ String response = readResponse(client);
+ assertThat(response, Matchers.containsString(" 500 "));
+ }
+ }
+
+ @Test
+ public void testInterruptedRequest() throws Exception
+ {
+ final AtomicBoolean fourBytesRead = new AtomicBoolean(false);
+ final AtomicBoolean earlyEOFException = new AtomicBoolean(false);
+ configureServer(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ int contentLength = request.getContentLength();
+ ServletInputStream inputStream = request.getInputStream();
+ for (int i = 0; i < contentLength; i++)
+ {
+ try
+ {
+ inputStream.read();
+ }
+ catch (EofException e)
+ {
+ earlyEOFException.set(true);
+ throw new QuietServletException(e);
+ }
+ if (i == 3)
+ fourBytesRead.set(true);
+ }
+ }
+ });
+
+ StringBuffer request = new StringBuffer("GET / HTTP/1.0\n");
+ request.append("Host: localhost\n");
+ request.append("Content-length: 6\n\n");
+ request.append("foo");
+
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ OutputStream os = client.getOutputStream();
+
+ os.write(request.toString().getBytes());
+ os.flush();
+ client.shutdownOutput();
+ String response = readResponse(client);
+ client.close();
+
+ assertThat("response contains 500", response, Matchers.containsString(" 500 "));
+ assertThat("The 4th byte (-1) has not been passed to the handler", fourBytesRead.get(), is(false));
+ assertThat("EofException has been caught", earlyEOFException.get(), is(true));
+ }
+
+ /*
+ * Feed a full header method
+ */
+ @Test
+ public void testFullHeader() throws Exception
+ {
+
+ configureServer(new HelloWorldHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ StacklessLogging stackless = new StacklessLogging(HttpConnection.class))
+ {
+ Log.getLogger(HttpConnection.class).info("expect header is too large ...");
+ OutputStream os = client.getOutputStream();
+
+ byte[] buffer = new byte[64 * 1024];
+ buffer[0] = 'G';
+ buffer[1] = 'E';
+ buffer[2] = 'T';
+ buffer[3] = ' ';
+ buffer[4] = '/';
+ buffer[5] = ' ';
+ buffer[6] = 'H';
+ buffer[7] = 'T';
+ buffer[8] = 'T';
+ buffer[9] = 'P';
+ buffer[10] = '/';
+ buffer[11] = '1';
+ buffer[12] = '.';
+ buffer[13] = '0';
+ buffer[14] = '\n';
+ buffer[15] = 'H';
+ buffer[16] = ':';
+ Arrays.fill(buffer, 17, buffer.length - 1, (byte)'A');
+ // write the request.
+ try
+ {
+ os.write(buffer);
+ os.flush();
+ }
+ catch (Exception e)
+ {
+ // Ignore exceptions during writing, so long as we can read response below
+ }
+
+ // Read the response.
+ try
+ {
+ String response = readResponse(client);
+ assertThat(response, Matchers.containsString("HTTP/1.1 431 "));
+ }
+ catch (Exception e)
+ {
+ Log.getLogger(HttpServerTestBase.class).warn("TODO Early close???");
+ // TODO #1832 evaluate why we sometimes get an early close on this test
+ }
+ }
+ }
+
+ /*
+ * Feed the server the entire request at once.
+ */
+ @Test
+ public void testRequest1() throws Exception
+ {
+ configureServer(new HelloWorldHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ os.write(REQUEST1.getBytes());
+ os.flush();
+
+ // Read the response.
+ String response = readResponse(client);
+
+ // Check the response
+ assertEquals(RESPONSE1, response, "response");
+ }
+ }
+
+ @Test
+ public void testFragmentedChunk() throws Exception
+ {
+ configureServer(new EchoHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ os.write(("GET /R2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n").getBytes());
+ os.flush();
+ Thread.sleep(1000);
+ os.write(("5").getBytes());
+ Thread.sleep(1000);
+ os.write(("\r\n").getBytes());
+ os.flush();
+ Thread.sleep(1000);
+ os.write(("ABCDE\r\n" +
+ "0;\r\n\r\n").getBytes());
+ os.flush();
+
+ // Read the response.
+ String response = readResponse(client);
+ assertThat(response, containsString("200"));
+ }
+ }
+
+ @Test
+ public void testTrailingContent() throws Exception
+ {
+ configureServer(new EchoHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+ //@checkstyle-disable-check : IllegalTokenText
+ os.write(("GET /R2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: 5\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "ABCDE\r\n" +
+ "\r\n"
+ //@checkstyle-enable-check : IllegalTokenText
+ ).getBytes());
+ os.flush();
+
+ // Read the response.
+ String response = readResponse(client);
+ assertTrue(response.indexOf("200") > 0);
+ }
+ }
+
+ /*
+ * Feed the server fragmentary headers and see how it copes with it.
+ */
+ @Test
+ public void testRequest1Fragments() throws Exception
+ {
+ configureServer(new HelloWorldHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ // Write a fragment, flush, sleep, write the next fragment, etc.
+ os.write(FRAGMENT1.getBytes());
+ os.flush();
+ Thread.sleep(PAUSE);
+ os.write(FRAGMENT2.getBytes());
+ os.flush();
+ Thread.sleep(PAUSE);
+ os.write(FRAGMENT3.getBytes());
+ os.flush();
+
+ // Read the response
+ String response = readResponse(client);
+
+ // Check the response
+ assertEquals(RESPONSE1, response, "response");
+ }
+ }
+
+ @Test
+ public void testRequest2() throws Exception
+ {
+ configureServer(new EchoHandler());
+
+ byte[] bytes = REQUEST2.getBytes();
+ for (int i = 0; i < LOOPS; i++)
+ {
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ os.write(bytes);
+ os.flush();
+
+ // Read the response
+ String response = readResponse(client);
+
+ // Check the response
+ assertEquals(RESPONSE2, response, "response " + i);
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ _server.dumpStdErr();
+ throw e;
+ }
+ }
+ }
+
+ @Test
+ @DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review
+ public void testRequest2Sliced2() throws Exception
+ {
+ configureServer(new EchoHandler());
+
+ byte[] bytes = REQUEST2.getBytes();
+ int splits = bytes.length - REQUEST2_CONTENT.length() + 5;
+ for (int i = 0; i < splits; i += 1)
+ {
+ int[] points = new int[]{i};
+ StringBuilder message = new StringBuilder();
+
+ message.append("iteration #").append(i + 1);
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ writeFragments(bytes, points, message, os);
+
+ // Read the response
+ String response = readResponse(client);
+
+ // Check the response
+ assertEquals(RESPONSE2, response, "response for " + i + " " + message.toString());
+
+ Thread.sleep(10);
+ }
+ }
+ }
+
+ @Test
+ @DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review
+ public void testRequest2Sliced3() throws Exception
+ {
+ configureServer(new EchoHandler());
+
+ byte[] bytes = REQUEST2.getBytes();
+ int splits = bytes.length - REQUEST2_CONTENT.length() + 5;
+ for (int i = 0; i < splits; i += 1)
+ {
+ int[] points = new int[]{i, i + 1};
+ StringBuilder message = new StringBuilder();
+
+ message.append("iteration #").append(i + 1);
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ writeFragments(bytes, points, message, os);
+
+ // Read the response
+ String response = readResponse(client);
+
+ // Check the response
+ assertEquals(RESPONSE2, response, "response for " + i + " " + message.toString());
+
+ Thread.sleep(10);
+ }
+ }
+ }
+
+ @Test // TODO: Parameterize
+ public void testFlush() throws Exception
+ {
+ configureServer(new DataHandler());
+
+ String[] encoding = {"NONE", "UTF-8", "ISO-8859-1", "ISO-8859-2"};
+ for (int e = 0; e < encoding.length; e++)
+ {
+ for (int b = 1; b <= 128; b = b == 1 ? 2 : b == 2 ? 32 : b == 32 ? 128 : 129)
+ {
+ for (int w = 41; w < 42; w += 4096)
+ {
+ for (int c = 0; c < 1; c++)
+ {
+ String test = encoding[e] + "x" + b + "x" + w + "x" + c;
+ try
+ {
+ URL url = new URL(_scheme + "://" + _serverURI.getHost() + ":" + _serverURI.getPort() + "/?writes=" + w + "&block=" + b + (e == 0 ? "" : ("&encoding=" + encoding[e])) + (c == 0 ? "&chars=true" : ""));
+
+ InputStream in = (InputStream)url.getContent();
+ String response = IO.toString(in, e == 0 ? null : encoding[e]);
+
+ assertEquals(b * w, response.length(), test);
+ }
+ catch (Exception x)
+ {
+ System.err.println(test);
+ x.printStackTrace();
+ throw x;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testBlockingWhileReadingRequestContent() throws Exception
+ {
+ configureServer(new DataHandler());
+
+ long start = System.currentTimeMillis();
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ os.write((
+ "GET /data?writes=1024&block=256 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: close\r\n" +
+ "content-type: unknown\r\n" +
+ "content-length: 30\r\n" +
+ "\r\n"
+ ).getBytes());
+ os.flush();
+ Thread.sleep(200);
+ os.write((
+ "\r\n23456890"
+ ).getBytes());
+ os.flush();
+ Thread.sleep(1000);
+ os.write((
+ "abcdefghij"
+ ).getBytes());
+ os.flush();
+ Thread.sleep(1000);
+ os.write((
+ "0987654321\r\n"
+ ).getBytes());
+ os.flush();
+
+ int total = 0;
+ int len = 0;
+
+ byte[] buf = new byte[1024 * 64];
+ int sleeps = 0;
+ while (len >= 0)
+ {
+ len = is.read(buf);
+ if (len > 0)
+ {
+ total += len;
+ if ((total / 10240) > sleeps)
+ {
+ sleeps++;
+ Thread.sleep(100);
+ }
+ }
+ }
+
+ assertTrue(total > (1024 * 256));
+ assertTrue(30000L > (System.currentTimeMillis() - start));
+ }
+ }
+
+ @Test
+ public void testBlockingReadBadChunk() throws Exception
+ {
+ configureServer(new ReadHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ client.setSoTimeout(600000);
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ os.write((
+ "GET /data HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "content-type: unknown\r\n" +
+ "transfer-encoding: chunked\r\n" +
+ "\r\n"
+ ).getBytes());
+ os.flush();
+ Thread.sleep(50);
+ os.write((
+ "a\r\n" +
+ "123456890\r\n"
+ ).getBytes());
+ os.flush();
+
+ Thread.sleep(50);
+ os.write((
+ "4\r\n" +
+ "abcd\r\n"
+ ).getBytes());
+ os.flush();
+
+ Thread.sleep(50);
+ os.write((
+ "X\r\n" +
+ "abcd\r\n"
+ ).getBytes());
+ os.flush();
+
+ HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(is));
+
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), containsString("EofException"));
+ assertThat(response.getContent(), containsString("Early EOF"));
+ }
+ }
+
+ @Test
+ public void testBlockingWhileWritingResponseContent() throws Exception
+ {
+ configureServer(new DataHandler());
+
+ long start = System.currentTimeMillis();
+ int total = 0;
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ os.write((
+ "GET /data?writes=256&block=1024 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: close\r\n" +
+ "content-type: unknown\r\n" +
+ "\r\n"
+ ).getBytes());
+ os.flush();
+
+ int len = 0;
+ byte[] buf = new byte[1024 * 32];
+ int sleeps = 0;
+ while (len >= 0)
+ {
+ len = is.read(buf);
+ if (len > 0)
+ {
+ total += len;
+ if ((total / 10240) > sleeps)
+ {
+ Thread.sleep(200);
+ sleeps++;
+ }
+ }
+ }
+
+ assertTrue(total > (256 * 1024));
+ assertTrue(30000L > (System.currentTimeMillis() - start));
+ }
+ }
+
+ @Test
+ public void testCloseWhileWriteBlocked() throws Exception
+ {
+ configureServer(new DataHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ os.write((
+ "GET /data?encoding=iso-8859-1&writes=100&block=100000 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: close\r\n" +
+ "content-type: unknown\r\n" +
+ "\r\n"
+ ).getBytes());
+ os.flush();
+
+ // Read the first part of the response
+ byte[] buf = new byte[1024 * 8];
+ is.read(buf);
+
+ // sleep to ensure server is blocking
+ Thread.sleep(500);
+
+ // Close the client
+ client.close();
+ }
+
+ Thread.sleep(200);
+ // check server is still handling requests quickly
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ client.setSoTimeout(500);
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ os.write(("GET /data?writes=1&block=1024 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: close\r\n" +
+ "content-type: unknown\r\n" +
+ "\r\n"
+ ).getBytes());
+ os.flush();
+
+ String response = IO.toString(is);
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ }
+ }
+
+ @Test
+ public void testBigBlocks() throws Exception
+ {
+ configureServer(new BigBlockHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ client.setSoTimeout(20000);
+
+ OutputStream os = client.getOutputStream();
+ BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
+
+ os.write((
+ "GET /r1 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "\r\n" +
+ "GET /r2 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: close\r\n" +
+ "\r\n"
+ ).getBytes());
+ os.flush();
+
+ // read the chunked response header
+ boolean chunked = false;
+ boolean closed = false;
+ while (true)
+ {
+ String line = in.readLine();
+ if (line == null || line.length() == 0)
+ break;
+
+ chunked |= "Transfer-Encoding: chunked".equals(line);
+ closed |= "Connection: close".equals(line);
+ }
+ assertTrue(chunked);
+ assertFalse(closed);
+
+ // Read the chunks
+ int max = Integer.MIN_VALUE;
+ while (true)
+ {
+ String chunk = in.readLine();
+ String line = in.readLine();
+ if (line.length() == 0)
+ break;
+ int len = line.length();
+ assertEquals(Integer.valueOf(chunk, 16).intValue(), len);
+ if (max < len)
+ max = len;
+ }
+
+ // Check that biggest chunk was <= buffer size
+ assertEquals(_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().getOutputBufferSize(), max);
+
+ // read and check the times are < 999ms
+ String[] times = in.readLine().split(",");
+ for (String t : times)
+ {
+ assertTrue(Integer.parseInt(t) < 999);
+ }
+
+ // read the EOF chunk
+ String end = in.readLine();
+ assertEquals("0", end);
+ end = in.readLine();
+ assertEquals(0, end.length());
+
+ // read the non-chunked response header
+ chunked = false;
+ closed = false;
+ while (true)
+ {
+ String line = in.readLine();
+ if (line == null || line.length() == 0)
+ break;
+
+ chunked |= "Transfer-Encoding: chunked".equals(line);
+ closed |= "Connection: close".equals(line);
+ }
+ assertFalse(chunked);
+ assertTrue(closed);
+
+ String bigline = in.readLine();
+ assertEquals(10 * 128 * 1024, bigline.length());
+
+ // read and check the times are < 999ms
+ times = in.readLine().split(",");
+ for (String t : times)
+ {
+ assertTrue(Integer.parseInt(t) < 999, t);
+ }
+
+ // check close
+ assertNull(in.readLine());
+ }
+ }
+
+ // Handler that sends big blocks of data in each of 10 writes, and then sends the time it took for each big block.
+ protected static class BigBlockHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ byte[] buf = new byte[128 * 1024];
+ for (int i = 0; i < buf.length; i++)
+ {
+ buf[i] = (byte)("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_".charAt(i % 63));
+ }
+
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.setContentType("text/plain");
+ ServletOutputStream out = response.getOutputStream();
+ long[] times = new long[10];
+ for (int i = 0; i < times.length; i++)
+ {
+ // System.err.println("\nBLOCK "+request.getRequestURI()+" "+i);
+ long start = System.currentTimeMillis();
+ out.write(buf);
+ long end = System.currentTimeMillis();
+ times[i] = end - start;
+ // System.err.println("Block "+request.getRequestURI()+" "+i+" "+times[i]);
+ }
+ out.println();
+ for (long t : times)
+ {
+ out.print(t);
+ out.print(",");
+ }
+ out.close();
+ }
+ }
+
+ @Test
+ public void testPipeline() throws Exception
+ {
+ AtomicInteger served = new AtomicInteger();
+ configureServer(new HelloWorldHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ served.incrementAndGet();
+ super.handle(target, baseRequest, request, response);
+ }
+ });
+
+ int pipeline = 64;
+ {
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ served.set(0);
+ client.setSoTimeout(5000);
+ OutputStream os = client.getOutputStream();
+
+ String request = "";
+
+ for (int i = 1; i < pipeline; i++)
+ {
+ request +=
+ "GET /data?writes=1&block=16&id=" + i + " HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "user-agent: testharness/1.0 (blah foo/bar)\r\n" +
+ "accept-encoding: nothing\r\n" +
+ "cookie: aaa=1234567890\r\n" +
+ "\r\n";
+ }
+
+ request +=
+ "GET /data?writes=1&block=16 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "user-agent: testharness/1.0 (blah foo/bar)\r\n" +
+ "accept-encoding: nothing\r\n" +
+ "cookie: aaa=bbbbbb\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+
+ os.write(request.getBytes());
+ os.flush();
+
+ LineNumberReader in = new LineNumberReader(new InputStreamReader(client.getInputStream()));
+
+ String line = in.readLine();
+ int count = 0;
+ while (line != null)
+ {
+ if ("HTTP/1.1 200 OK".equals(line))
+ count++;
+ line = in.readLine();
+ }
+ assertEquals(pipeline, served.get());
+ assertEquals(pipeline, count);
+ }
+ }
+ }
+
+ @Test
+ public void testRecycledWriters() throws Exception
+ {
+ configureServer(new EchoHandler());
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ os.write((
+ "POST /echo?charset=utf-8 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "content-type: text/plain; charset=utf-8\r\n" +
+ "content-length: 10\r\n" +
+ "\r\n").getBytes(StandardCharsets.ISO_8859_1));
+
+ os.write((
+ "123456789\n"
+ ).getBytes("utf-8"));
+
+ os.write((
+ "POST /echo?charset=utf-8 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "content-type: text/plain; charset=utf-8\r\n" +
+ "content-length: 10\r\n" +
+ "\r\n"
+ ).getBytes(StandardCharsets.ISO_8859_1));
+
+ os.write((
+ "abcdefghZ\n"
+ ).getBytes("utf-8"));
+
+ String content = "Wibble";
+ byte[] contentB = content.getBytes("utf-8");
+ os.write((
+ "POST /echo?charset=utf-16 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "content-type: text/plain; charset=utf-8\r\n" +
+ "content-length: " + contentB.length + "\r\n" +
+ "connection: close\r\n" +
+ "\r\n"
+ ).getBytes(StandardCharsets.ISO_8859_1));
+ os.write(contentB);
+
+ os.flush();
+
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ IO.copy(is, bout);
+ byte[] b = bout.toByteArray();
+
+ //System.err.println("OUTPUT: "+new String(b));
+ int i = 0;
+ while (b[i] != 'Z')
+ {
+ i++;
+ }
+ int state = 0;
+ while (state != 4)
+ {
+ switch (b[i++])
+ {
+ case '\r':
+ if (state == 0 || state == 2)
+ state++;
+ continue;
+ case '\n':
+ if (state == 1 || state == 3)
+ state++;
+ continue;
+
+ default:
+ state = 0;
+ }
+ }
+
+ String in = new String(b, 0, i, StandardCharsets.UTF_8);
+ assertThat(in, containsString("123456789"));
+ assertThat(in, containsString("abcdefghZ"));
+ assertFalse(in.contains("Wibble"));
+
+ in = new String(b, i, b.length - i, StandardCharsets.UTF_16);
+ assertEquals("Wibble\n", in);
+ }
+ }
+
+ @Test
+ public void testHead() throws Exception
+ {
+ configureServer(new EchoHandler(false));
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ //@checkstyle-disable-check : IllegalTokenText
+ os.write((
+ "POST /R1 HTTP/1.1\r\n" +
+ "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "content-type: text/plain; charset=utf-8\r\n" +
+ "content-length: 10\r\n" +
+ "\r\n" +
+ "123456789\n" +
+
+ "HEAD /R2 HTTP/1.1\r\n" +
+ "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "content-type: text/plain; charset=utf-8\r\n" +
+ "content-length: 10\r\n" +
+ "\r\n" +
+ "ABCDEFGHI\n" +
+
+ "POST /R3 HTTP/1.1\r\n" +
+ "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "content-type: text/plain; charset=utf-8\r\n" +
+ "content-length: 10\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "abcdefghi\n"
+ //@checkstyle-enable-check : IllegalTokenText
+ ).getBytes(StandardCharsets.ISO_8859_1));
+
+ String in = IO.toString(is);
+ assertThat(in, containsString("123456789"));
+ assertThat(in, not(containsString("ABCDEFGHI")));
+ assertThat(in, containsString("abcdefghi"));
+ }
+ }
+
+ @Test
+ public void testRecycledReaders() throws Exception
+ {
+ configureServer(new EchoHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ os.write((
+ "POST /echo/0?charset=utf-8 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "content-type: text/plain; charset=utf-8\r\n" +
+ "content-length: 10\r\n" +
+ "\r\n").getBytes(StandardCharsets.ISO_8859_1));
+
+ os.write((
+ "123456789\n"
+ ).getBytes("utf-8"));
+
+ os.write((
+ "POST /echo/1?charset=utf-8 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "content-type: text/plain; charset=utf-8\r\n" +
+ "content-length: 10\r\n" +
+ "\r\n"
+ ).getBytes(StandardCharsets.ISO_8859_1));
+
+ os.write((
+ "abcdefghi\n"
+ ).getBytes(StandardCharsets.UTF_8));
+
+ String content = "Wibble";
+ byte[] contentB = content.getBytes(StandardCharsets.UTF_16);
+ os.write((
+ "POST /echo/2?charset=utf-8 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "content-type: text/plain; charset=utf-16\r\n" +
+ "content-length: " + contentB.length + "\r\n" +
+ "connection: close\r\n" +
+ "\r\n"
+ ).getBytes(StandardCharsets.ISO_8859_1));
+ os.write(contentB);
+
+ os.flush();
+
+ String in = IO.toString(is);
+ assertThat(in, containsString("123456789"));
+ assertThat(in, containsString("abcdefghi"));
+ assertThat(in, containsString("Wibble"));
+ }
+ }
+
+ @Test
+ public void testBlockedClient() throws Exception
+ {
+ configureServer(new HelloWorldHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ // Send a request with chunked input and expect 100
+ os.write((
+ "GET / HTTP/1.1\r\n" +
+ "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Expect: 100-continue\r\n" +
+ "Connection: Keep-Alive\r\n" +
+ "\r\n"
+ ).getBytes());
+
+ // Never send a body.
+ // HelloWorldHandler does not read content, so 100 is not sent.
+ // So close will have to happen anyway, without reset!
+
+ os.flush();
+
+ client.setSoTimeout(2000);
+ long start = System.currentTimeMillis();
+ String in = IO.toString(is);
+ assertTrue(System.currentTimeMillis() - start < 1000);
+ assertTrue(in.indexOf("Connection: close") > 0);
+ assertTrue(in.indexOf("Hello world") > 0);
+ }
+ }
+
+ @Test
+ public void testCommittedError() throws Exception
+ {
+ CommittedErrorHandler handler = new CommittedErrorHandler();
+ configureServer(handler);
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ StacklessLogging stackless = new StacklessLogging(HttpChannel.class))
+ {
+ ((AbstractLogger)Log.getLogger(HttpChannel.class)).info("Expecting exception after commit then could not send 500....");
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ // Send a request
+ os.write(("GET / HTTP/1.1\r\n" +
+ "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "\r\n"
+ ).getBytes());
+ os.flush();
+
+ client.setSoTimeout(2000);
+ String in = IO.toString(is);
+
+ assertEquals(-1, is.read()); // Closed by error!
+
+ assertThat(in, containsString("HTTP/1.1 200 OK"));
+ assertTrue(in.indexOf("Transfer-Encoding: chunked") > 0);
+ assertTrue(in.indexOf("Now is the time for all good men to come to the aid of the party") > 0);
+ assertThat(in, Matchers.not(Matchers.containsString("\r\n0\r\n")));
+
+ client.close();
+ Thread.sleep(200);
+
+ assertFalse(handler._endp.isOpen());
+ }
+ }
+
+ public static class CommittedErrorHandler extends AbstractHandler
+ {
+ public EndPoint _endp;
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ _endp = baseRequest.getHttpChannel().getEndPoint();
+ response.setHeader("test", "value");
+ response.setStatus(200);
+ response.setContentType("text/plain");
+ response.getWriter().println("Now is the time for all good men to come to the aid of the party");
+ response.getWriter().flush();
+ response.flushBuffer();
+
+ throw new ServletException(new Exception("exception after commit"));
+ }
+ }
+
+ protected static class AvailableHandler extends AbstractHandler
+ {
+ public Exchanger<Object> _ex = new Exchanger<>();
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.setContentType("text/plain");
+ InputStream in = request.getInputStream();
+ ServletOutputStream out = response.getOutputStream();
+
+ // this should initially be 0 bytes available.
+ int avail = in.available();
+ out.println(avail);
+
+ // block for the first character
+ String buf = "";
+ buf += (char)in.read();
+
+ // read remaining available bytes
+ avail = in.available();
+ out.println(avail);
+ for (int i = 0; i < avail; i++)
+ {
+ buf += (char)in.read();
+ }
+
+ avail = in.available();
+ out.println(avail);
+
+ try
+ {
+ _ex.exchange(null);
+ _ex.exchange(null);
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+
+ avail = in.available();
+
+ if (avail == 0)
+ {
+ // handle blocking channel connectors
+ buf += (char)in.read();
+ avail = in.available();
+ out.println(avail + 1);
+ }
+ else if (avail == 1)
+ {
+ // handle blocking socket connectors
+ buf += (char)in.read();
+ avail = in.available();
+ out.println(avail + 1);
+ }
+ else
+ out.println(avail);
+
+ while (avail > 0)
+ {
+ buf += (char)in.read();
+ avail = in.available();
+ }
+
+ out.println(avail);
+
+ // read remaining no matter what
+ int b = in.read();
+ while (b >= 0)
+ {
+ buf += (char)b;
+ b = in.read();
+ }
+ out.println(buf);
+ out.close();
+ }
+ }
+
+ @Test
+ public void testAvailable() throws Exception
+ {
+ AvailableHandler ah = new AvailableHandler();
+ configureServer(ah);
+ _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(false);
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ os.write((
+ "GET /data?writes=1024&block=256 HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: close\r\n" +
+ "content-type: unknown\r\n" +
+ "content-length: 30\r\n" +
+ "\r\n"
+ ).getBytes());
+ os.flush();
+ Thread.sleep(500);
+ os.write((
+ "1234567890"
+ ).getBytes());
+ os.flush();
+
+ ah._ex.exchange(null);
+
+ os.write((
+ "abcdefghijklmnopqrst"
+ ).getBytes());
+ os.flush();
+ Thread.sleep(500);
+ ah._ex.exchange(null);
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+ // skip header
+ while (reader.readLine().length() > 0)
+ {
+ ;
+ }
+ assertThat(Integer.parseInt(reader.readLine()), Matchers.equalTo(0));
+ assertThat(Integer.parseInt(reader.readLine()), Matchers.equalTo(9));
+ assertThat(Integer.parseInt(reader.readLine()), Matchers.equalTo(0));
+ assertThat(Integer.parseInt(reader.readLine()), Matchers.greaterThan(0));
+ assertThat(Integer.parseInt(reader.readLine()), Matchers.equalTo(0));
+ assertEquals("1234567890abcdefghijklmnopqrst", reader.readLine());
+ }
+ }
+
+ @Test
+ public void testDualRequest1() throws Exception
+ {
+ configureServer(new HelloWorldHandler());
+
+ try (Socket client1 = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ Socket client2 = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os1 = client1.getOutputStream();
+ OutputStream os2 = client2.getOutputStream();
+
+ os1.write(REQUEST1.getBytes());
+ os2.write(REQUEST1.getBytes());
+ os1.flush();
+ os2.flush();
+
+ // Read the response.
+ String response1 = readResponse(client1);
+ String response2 = readResponse(client2);
+
+ // Check the response
+ assertEquals(RESPONSE1, response1, "client1");
+ assertEquals(RESPONSE1, response2, "client2");
+ }
+ }
+
+ /**
+ * Read entire response from the client. Close the output.
+ *
+ * @param client Open client socket.
+ * @return The response string.
+ * @throws IOException in case of I/O problems
+ */
+ protected static String readResponse(Socket client) throws IOException
+ {
+
+ StringBuilder sb = new StringBuilder();
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())))
+ {
+ String line;
+
+ while ((line = br.readLine()) != null)
+ {
+ sb.append(line);
+ sb.append('\n');
+ }
+
+ return sb.toString();
+ }
+ catch (IOException e)
+ {
+ System.err.println(e + " while reading '" + sb + "'");
+ throw e;
+ }
+ }
+
+ protected void writeFragments(byte[] bytes, int[] points, StringBuilder message, OutputStream os) throws IOException, InterruptedException
+ {
+ int last = 0;
+
+ // Write out the fragments
+ for (int j = 0; j < points.length; ++j)
+ {
+ int point = points[j];
+
+ // System.err.println("write: "+new String(bytes, last, point - last));
+ os.write(bytes, last, point - last);
+ last = point;
+ os.flush();
+ Thread.sleep(PAUSE);
+
+ // Update the log message
+ message.append(" point #").append(j + 1).append(": ").append(point);
+ }
+
+ // Write the last fragment
+ // System.err.println("Write: "+new String(bytes, last, bytes.length - last));
+ os.write(bytes, last, bytes.length - last);
+ os.flush();
+ Thread.sleep(PAUSE);
+ }
+
+ @Test
+ public void testUnreadInput() throws Exception
+ {
+ configureServer(new NoopHandler());
+ final int REQS = 2;
+ final String content = "This is a coooooooooooooooooooooooooooooooooo" +
+ "ooooooooooooooooooooooooooooooooooooooooooooo" +
+ "ooooooooooooooooooooooooooooooooooooooooooooo" +
+ "ooooooooooooooooooooooooooooooooooooooooooooo" +
+ "ooooooooooooooooooooooooooooooooooooooooooooo" +
+ "ooooooooooooooooooooooooooooooooooooooooooooo" +
+ "ooooooooooooooooooooooooooooooooooooooooooooo" +
+ "ooooooooooooooooooooooooooooooooooooooooooooo" +
+ "ooooooooooooooooooooooooooooooooooooooooooooo" +
+ "oooooooooooonnnnnnnnnnnnnnnntent";
+ final int cl = content.getBytes().length;
+
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ final OutputStream out = client.getOutputStream();
+
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ byte[] bytes = ("GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: " + cl + "\r\n" +
+ "\r\n" +
+ content).getBytes(StandardCharsets.ISO_8859_1);
+
+ for (int i = 0; i < REQS; i++)
+ {
+ out.write(bytes, 0, bytes.length);
+ }
+ out.write("GET / HTTP/1.1\r\nHost: last\r\nConnection: close\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
+ out.flush();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+
+ String resps = readResponse(client);
+
+ int offset = 0;
+ for (int i = 0; i < (REQS + 1); i++)
+ {
+ int ok = resps.indexOf("HTTP/1.1 200 OK", offset);
+ assertThat("resp" + i, ok, greaterThanOrEqualTo(offset));
+ offset = ok + 15;
+ }
+ }
+
+ @Test
+ public void testWriteBodyAfterNoBodyResponse() throws Exception
+ {
+ configureServer(new WriteBodyAfterNoBodyResponseHandler());
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ final OutputStream out = client.getOutputStream();
+
+ out.write("GET / HTTP/1.1\r\nHost: test\r\n\r\n".getBytes());
+ out.write("GET / HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n".getBytes());
+ out.flush();
+
+ BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
+
+ String line = in.readLine();
+ assertThat(line, containsString(" 304 "));
+ while (true)
+ {
+ line = in.readLine();
+ if (line == null)
+ throw new EOFException();
+ if (line.length() == 0)
+ break;
+
+ assertThat(line, not(containsString("Content-Length")));
+ assertThat(line, not(containsString("Content-Type")));
+ assertThat(line, not(containsString("Transfer-Encoding")));
+ }
+
+ line = in.readLine();
+ assertThat(line, containsString(" 304 "));
+ while (true)
+ {
+ line = in.readLine();
+ if (line == null)
+ throw new EOFException();
+ if (line.length() == 0)
+ break;
+
+ assertThat(line, not(containsString("Content-Length")));
+ assertThat(line, not(containsString("Content-Type")));
+ assertThat(line, not(containsString("Transfer-Encoding")));
+ }
+
+ do
+ {
+ line = in.readLine();
+ }
+ while (line != null);
+ }
+
+ private class WriteBodyAfterNoBodyResponseHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(304);
+ response.getOutputStream().print("yuck");
+ response.flushBuffer();
+ }
+ }
+
+ public class NoopHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest,
+ HttpServletRequest request, HttpServletResponse response) throws IOException,
+ ServletException
+ {
+ //don't read the input, just send something back
+ ((Request)request).setHandled(true);
+ response.setStatus(200);
+ }
+ }
+
+ @Test
+ public void testSuspendedPipeline() throws Exception
+ {
+ SuspendHandler suspend = new SuspendHandler();
+ suspend.setSuspendFor(30000);
+ suspend.setResumeAfter(1000);
+ configureServer(suspend);
+
+ long start = System.currentTimeMillis();
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(5000);
+ try
+ {
+ OutputStream os = client.getOutputStream();
+
+ // write an initial request
+ os.write((
+ "GET / HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "\r\n"
+ ).getBytes());
+ os.flush();
+
+ Thread.sleep(200);
+
+ // write an pipelined request
+ os.write((
+ "GET / HTTP/1.1\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "connection: close\r\n" +
+ "\r\n"
+ ).getBytes());
+ os.flush();
+
+ String response = readResponse(client);
+ assertThat(response, containsString("RESUMEDHTTP/1.1 200 OK"));
+ assertThat((System.currentTimeMillis() - start), greaterThanOrEqualTo(1999L));
+
+ // TODO This test should also check that that the CPU did not spin during the suspend.
+ }
+ finally
+ {
+ client.close();
+ }
+ }
+
+ @Test
+ @DisabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10})
+ public void testShutdown() throws Exception
+ {
+ configureServer(new ReadExactHandler());
+ byte[] content = new byte[4096];
+ Arrays.fill(content, (byte)'X');
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ // Send two persistent pipelined requests and then shutdown output
+ os.write(("GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: " + content.length + "\r\n" +
+ "\r\n").getBytes(StandardCharsets.ISO_8859_1));
+ os.write(content);
+ os.write(("GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: " + content.length + "\r\n" +
+ "\r\n").getBytes(StandardCharsets.ISO_8859_1));
+ os.write(content);
+ os.flush();
+ // Thread.sleep(50);
+ client.shutdownOutput();
+
+ // Read the two pipelined responses
+ HttpTester.Response response = HttpTester.parseResponse(client.getInputStream());
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), containsString("Read " + content.length));
+
+ response = HttpTester.parseResponse(client.getInputStream());
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), containsString("Read " + content.length));
+
+ // Read the close
+ assertThat(client.getInputStream().read(), is(-1));
+ }
+ }
+
+ @Test
+ @DisabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10})
+ public void testChunkedShutdown() throws Exception
+ {
+ configureServer(new ReadExactHandler(4096));
+ byte[] content = new byte[4096];
+ Arrays.fill(content, (byte)'X');
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ // Send two persistent pipelined requests and then shutdown output
+ os.write(("GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "1000\r\n").getBytes(StandardCharsets.ISO_8859_1));
+ os.write(content);
+ os.write("\r\n0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
+ os.write(("GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ "1000\r\n").getBytes(StandardCharsets.ISO_8859_1));
+ os.write(content);
+ os.write("\r\n0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
+ os.flush();
+ client.shutdownOutput();
+
+ // Read the two pipelined responses
+ HttpTester.Response response = HttpTester.parseResponse(client.getInputStream());
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), containsString("Read " + content.length));
+
+ response = HttpTester.parseResponse(client.getInputStream());
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), containsString("Read " + content.length));
+
+ // Read the close
+ assertThat(client.getInputStream().read(), is(-1));
+ }
+ }
+
+ @Test
+ public void testSendAsyncContent() throws Exception
+ {
+ int size = 64 * 1024;
+ configureServer(new SendAsyncContentHandler(size));
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+ os.write(("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").getBytes(StandardCharsets.ISO_8859_1));
+ os.flush();
+
+ HttpTester.Response response = HttpTester.parseResponse(client.getInputStream());
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContentBytes().length, is(size));
+
+ // Try again to check previous request completed OK
+ os.write(("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").getBytes(StandardCharsets.ISO_8859_1));
+ os.flush();
+ response = HttpTester.parseResponse(client.getInputStream());
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContentBytes().length, is(size));
+ }
+ }
+
+ private class SendAsyncContentHandler extends AbstractHandler
+ {
+ final ByteBuffer content;
+
+ public SendAsyncContentHandler(int size)
+ {
+ content = BufferUtil.allocate(size);
+ Arrays.fill(content.array(), 0, size, (byte)'X');
+ content.position(0);
+ content.limit(size);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.setContentType("application/unknown");
+ response.setContentLength(content.remaining());
+ AsyncContext async = request.startAsync();
+ ((HttpOutput)response.getOutputStream()).sendContent(content.slice(), Callback.from(async::complete));
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java
new file mode 100644
index 0000000..54612a1
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java
@@ -0,0 +1,333 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.HotSwapHandler;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+public class HttpServerTestFixture
+{
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+
+ // Useful constants
+ protected static final long PAUSE = 10L;
+ protected static final int LOOPS = 50;
+
+ protected QueuedThreadPool _threadPool;
+ protected Server _server;
+ protected URI _serverURI;
+ protected HttpConfiguration _httpConfiguration;
+ protected ServerConnector _connector;
+ protected String _scheme = "http";
+
+ protected Socket newSocket(String host, int port) throws Exception
+ {
+ Socket socket = new Socket(host, port);
+ socket.setSoTimeout(10000);
+ socket.setTcpNoDelay(true);
+ return socket;
+ }
+
+ @BeforeEach
+ public void before()
+ {
+ _threadPool = new QueuedThreadPool();
+ _server = new Server(_threadPool);
+ }
+
+ protected void startServer(ServerConnector connector) throws Exception
+ {
+ startServer(connector, new HotSwapHandler());
+ }
+
+ protected void startServer(ServerConnector connector, Handler handler) throws Exception
+ {
+ _connector = connector;
+ _httpConfiguration = _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration();
+ _httpConfiguration.setBlockingTimeout(-1);
+ _httpConfiguration.setSendDateHeader(false);
+ _server.addConnector(_connector);
+ _server.setHandler(handler);
+ _server.start();
+ _serverURI = _server.getURI();
+ }
+
+ @AfterEach
+ public void stopServer() throws Exception
+ {
+ _server.stop();
+ _server.join();
+ _server.setConnectors(new Connector[]{});
+ }
+
+ protected void configureServer(Handler handler) throws Exception
+ {
+ HotSwapHandler swapper = (HotSwapHandler)_server.getHandler();
+ swapper.setHandler(handler);
+ handler.start();
+ }
+
+ protected static class EchoHandler extends AbstractHandler
+ {
+ boolean _musthavecontent = true;
+
+ public EchoHandler()
+ {
+ }
+
+ public EchoHandler(boolean content)
+ {
+ _musthavecontent = false;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ Log.getRootLogger().debug("handle " + target);
+ baseRequest.setHandled(true);
+
+ if (request.getContentType() != null)
+ response.setContentType(request.getContentType());
+ if (request.getParameter("charset") != null)
+ response.setCharacterEncoding(request.getParameter("charset"));
+ else if (request.getCharacterEncoding() != null)
+ response.setCharacterEncoding(request.getCharacterEncoding());
+
+ PrintWriter writer = response.getWriter();
+
+ int count = 0;
+ BufferedReader reader = request.getReader();
+
+ if (request.getContentLength() != 0)
+ {
+ String line = reader.readLine();
+ while (line != null)
+ {
+ writer.print(line);
+ writer.print("\n");
+ count += line.length();
+ line = reader.readLine();
+ }
+ }
+
+ if (count == 0)
+ {
+ if (_musthavecontent)
+ throw new IllegalStateException("no input received");
+
+ writer.println("No content");
+ }
+
+ // just to be difficult
+ reader.close();
+ writer.close();
+
+ if (reader.read() >= 0)
+ throw new IllegalStateException("Not closed");
+
+ Log.getRootLogger().debug("handled " + target);
+ }
+ }
+
+ protected static class OptionsHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ if (request.getMethod().equals("OPTIONS"))
+ response.setStatus(200);
+ else
+ response.setStatus(500);
+
+ response.setHeader("Allow", "GET");
+ }
+ }
+
+ protected static class HelloWorldHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.getOutputStream().print("Hello world\r\n");
+ }
+ }
+
+ protected static class SendErrorHandler extends AbstractHandler
+ {
+ private final int code;
+ private final String message;
+
+ public SendErrorHandler()
+ {
+ this(500, null);
+ }
+
+ public SendErrorHandler(int code, String message)
+ {
+ this.code = code;
+ this.message = message;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.sendError(code, message);
+ }
+ }
+
+ protected static class ReadExactHandler extends AbstractHandler
+ {
+ private int expected;
+
+ public ReadExactHandler()
+ {
+ this(-1);
+ }
+
+ public ReadExactHandler(int expected)
+ {
+ this.expected = expected;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ int len = expected < 0 ? request.getContentLength() : expected;
+ if (len < 0)
+ throw new IllegalStateException();
+ byte[] content = new byte[len];
+ int offset = 0;
+ while (offset < len)
+ {
+ int read = request.getInputStream().read(content, offset, len - offset);
+ if (read < 0)
+ break;
+ offset += read;
+ }
+ response.setStatus(200);
+ String reply = "Read " + offset + "\r\n";
+ response.setContentLength(reply.length());
+ response.getOutputStream().write(reply.getBytes(StandardCharsets.ISO_8859_1));
+ }
+ }
+
+ protected static class ReadHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+
+ try
+ {
+ InputStream in = request.getInputStream();
+ String input = IO.toString(in);
+ response.getWriter().printf("read %d%n", input.length());
+ }
+ catch (Exception e)
+ {
+ response.getWriter().printf("caught %s%n", e);
+ }
+ }
+ }
+
+ protected static class DataHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+
+ InputStream in = request.getInputStream();
+ String input = IO.toString(in);
+
+ String tmp = request.getParameter("writes");
+ int writes = Integer.parseInt(tmp == null ? "10" : tmp);
+ tmp = request.getParameter("block");
+ int block = Integer.parseInt(tmp == null ? "10" : tmp);
+ String encoding = request.getParameter("encoding");
+ String chars = request.getParameter("chars");
+
+ String data = "\u0a870123456789A\u0a87CDEFGHIJKLMNOPQRSTUVWXYZ\u0250bcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ while (data.length() < block)
+ {
+ data += data;
+ }
+
+ String chunk = (input + data).substring(0, block);
+ response.setContentType("text/plain");
+ if (encoding == null)
+ {
+ byte[] bytes = chunk.getBytes(StandardCharsets.ISO_8859_1);
+ OutputStream out = response.getOutputStream();
+ for (int i = 0; i < writes; i++)
+ {
+ out.write(bytes);
+ }
+ }
+ else if ("true".equals(chars))
+ {
+ response.setCharacterEncoding(encoding);
+ PrintWriter out = response.getWriter();
+ char[] c = chunk.toCharArray();
+ for (int i = 0; i < writes; i++)
+ {
+ out.write(c);
+ if (out.checkError())
+ break;
+ }
+ }
+ else
+ {
+ response.setCharacterEncoding(encoding);
+ PrintWriter out = response.getWriter();
+ for (int i = 0; i < writes; i++)
+ {
+ out.write(chunk);
+ if (out.checkError())
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpVersionCustomizerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpVersionCustomizerTest.java
new file mode 100644
index 0000000..b490859
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpVersionCustomizerTest.java
@@ -0,0 +1,81 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.channels.SocketChannel;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class HttpVersionCustomizerTest
+{
+ @Test
+ public void testCustomizeHttpVersion() throws Exception
+ {
+ Server server = new Server();
+ HttpConfiguration httpConfig = new HttpConfiguration();
+ httpConfig.addCustomizer((connector, config, request) -> request.setHttpVersion(HttpVersion.HTTP_1_1));
+ ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
+ server.addConnector(connector);
+ server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(500);
+ assertEquals(HttpVersion.HTTP_1_1.asString(), request.getProtocol());
+ response.setStatus(200);
+ response.getWriter().println("OK");
+ }
+ });
+ server.start();
+
+ try
+ {
+ try (SocketChannel socket = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())))
+ {
+ HttpTester.Request request = HttpTester.newRequest();
+ request.setVersion(HttpVersion.HTTP_1_0);
+ socket.write(request.generate());
+
+ HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(socket));
+ assertNotNull(response);
+ assertThat(response.getStatus(), Matchers.equalTo(HttpStatus.OK_200));
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java
new file mode 100644
index 0000000..c4cc124
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java
@@ -0,0 +1,261 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jetty.io.ArrayByteBufferPool;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.util.Utf8StringBuilder;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class HttpWriterTest
+{
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+ private HttpOutput _httpOut;
+ private ByteBuffer _bytes;
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ _bytes = BufferUtil.allocate(2048);
+
+ final ByteBufferPool pool = new ArrayByteBufferPool();
+
+ HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null)
+ {
+ @Override
+ public ByteBufferPool getByteBufferPool()
+ {
+ return pool;
+ }
+ };
+
+ _httpOut = new HttpOutput(channel)
+ {
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException
+ {
+ BufferUtil.append(_bytes, b, off, len);
+ }
+ };
+ }
+
+ @Test
+ public void testSimpleUTF8() throws Exception
+ {
+ HttpWriter writer = new Utf8HttpWriter(_httpOut);
+ writer.write("Now is the time");
+ assertArrayEquals("Now is the time".getBytes(StandardCharsets.UTF_8), BufferUtil.toArray(_bytes));
+ }
+
+ @Test
+ public void testUTF8() throws Exception
+ {
+ HttpWriter writer = new Utf8HttpWriter(_httpOut);
+ writer.write("How now \uFF22rown cow");
+ assertArrayEquals("How now \uFF22rown cow".getBytes(StandardCharsets.UTF_8), BufferUtil.toArray(_bytes));
+ }
+
+ @Test
+ public void testUTF16() throws Exception
+ {
+ HttpWriter writer = new EncodingHttpWriter(_httpOut, StringUtil.__UTF16);
+ writer.write("How now \uFF22rown cow");
+ assertArrayEquals("How now \uFF22rown cow".getBytes(StandardCharsets.UTF_16), BufferUtil.toArray(_bytes));
+ }
+
+ @Test
+ public void testNotCESU8() throws Exception
+ {
+ HttpWriter writer = new Utf8HttpWriter(_httpOut);
+ String data = "xxx\uD801\uDC00xxx";
+ writer.write(data);
+ assertEquals("787878F0909080787878", TypeUtil.toHexString(BufferUtil.toArray(_bytes)));
+ assertArrayEquals(data.getBytes(StandardCharsets.UTF_8), BufferUtil.toArray(_bytes));
+ assertEquals(3 + 4 + 3, _bytes.remaining());
+
+ Utf8StringBuilder buf = new Utf8StringBuilder();
+ buf.append(BufferUtil.toArray(_bytes), 0, _bytes.remaining());
+ assertEquals(data, buf.toString());
+ }
+
+ @Test
+ public void testMultiByteOverflowUTF8() throws Exception
+ {
+ HttpWriter writer = new Utf8HttpWriter(_httpOut);
+ final String singleByteStr = "a";
+ final String multiByteDuplicateStr = "\uFF22";
+ int remainSize = 1;
+
+ int multiByteStrByteLength = multiByteDuplicateStr.getBytes(StandardCharsets.UTF_8).length;
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < HttpWriter.MAX_OUTPUT_CHARS - multiByteStrByteLength; i++)
+ {
+ sb.append(singleByteStr);
+ }
+ sb.append(multiByteDuplicateStr);
+ for (int i = 0; i < remainSize; i++)
+ {
+ sb.append(singleByteStr);
+ }
+ char[] buf = new char[HttpWriter.MAX_OUTPUT_CHARS * 3];
+
+ int length = HttpWriter.MAX_OUTPUT_CHARS - multiByteStrByteLength + remainSize + 1;
+ sb.toString().getChars(0, length, buf, 0);
+
+ writer.write(buf, 0, length);
+
+ assertEquals(sb.toString(), new String(BufferUtil.toArray(_bytes), StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void testISO8859() throws Exception
+ {
+ HttpWriter writer = new Iso88591HttpWriter(_httpOut);
+ writer.write("How now \uFF22rown cow");
+ assertEquals(new String(BufferUtil.toArray(_bytes), StandardCharsets.ISO_8859_1), "How now ?rown cow");
+ }
+
+ @Test
+ public void testUTF16x2() throws Exception
+ {
+ HttpWriter writer = new Utf8HttpWriter(_httpOut);
+
+ String source = "\uD842\uDF9F";
+
+ byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
+ writer.write(source.toCharArray(), 0, source.toCharArray().length);
+
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ java.io.OutputStreamWriter osw = new java.io.OutputStreamWriter(baos, StandardCharsets.UTF_8);
+ osw.write(source.toCharArray(), 0, source.toCharArray().length);
+ osw.flush();
+
+ myReportBytes(bytes);
+ myReportBytes(baos.toByteArray());
+ myReportBytes(BufferUtil.toArray(_bytes));
+
+ assertArrayEquals(bytes, BufferUtil.toArray(_bytes));
+ assertArrayEquals(baos.toByteArray(), BufferUtil.toArray(_bytes));
+ }
+
+ @Test
+ public void testMultiByteOverflowUTF16x2() throws Exception
+ {
+ HttpWriter writer = new Utf8HttpWriter(_httpOut);
+
+ final String singleByteStr = "a";
+ int remainSize = 1;
+ final String multiByteDuplicateStr = "\uD842\uDF9F";
+ int adjustSize = -1;
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < HttpWriter.MAX_OUTPUT_CHARS + adjustSize; i++)
+ {
+ sb.append(singleByteStr);
+ }
+ sb.append(multiByteDuplicateStr);
+ for (int i = 0; i < remainSize; i++)
+ {
+ sb.append(singleByteStr);
+ }
+ String source = sb.toString();
+
+ byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
+ writer.write(source.toCharArray(), 0, source.toCharArray().length);
+
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ java.io.OutputStreamWriter osw = new java.io.OutputStreamWriter(baos, StandardCharsets.UTF_8);
+ osw.write(source.toCharArray(), 0, source.toCharArray().length);
+ osw.flush();
+
+ myReportBytes(bytes);
+ myReportBytes(baos.toByteArray());
+ myReportBytes(BufferUtil.toArray(_bytes));
+
+ assertArrayEquals(bytes, BufferUtil.toArray(_bytes));
+ assertArrayEquals(baos.toByteArray(), BufferUtil.toArray(_bytes));
+ }
+
+ @Test
+ public void testMultiByteOverflowUTF16X22() throws Exception
+ {
+ HttpWriter writer = new Utf8HttpWriter(_httpOut);
+
+ final String singleByteStr = "a";
+ int remainSize = 1;
+ final String multiByteDuplicateStr = "\uD842\uDF9F";
+ int adjustSize = -2;
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < HttpWriter.MAX_OUTPUT_CHARS + adjustSize; i++)
+ {
+ sb.append(singleByteStr);
+ }
+ sb.append(multiByteDuplicateStr);
+ for (int i = 0; i < remainSize; i++)
+ {
+ sb.append(singleByteStr);
+ }
+ String source = sb.toString();
+
+ byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
+ writer.write(source.toCharArray(), 0, source.toCharArray().length);
+
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ java.io.OutputStreamWriter osw = new java.io.OutputStreamWriter(baos, StandardCharsets.UTF_8);
+ osw.write(source.toCharArray(), 0, source.toCharArray().length);
+ osw.flush();
+
+ myReportBytes(bytes);
+ myReportBytes(baos.toByteArray());
+ myReportBytes(BufferUtil.toArray(_bytes));
+
+ assertArrayEquals(bytes, BufferUtil.toArray(_bytes));
+ assertArrayEquals(baos.toByteArray(), BufferUtil.toArray(_bytes));
+ }
+
+ private void myReportBytes(byte[] bytes) throws Exception
+ {
+// for (int i = 0; i < bytes.length; i++)
+// {
+// System.err.format("%s%x",(i == 0)?"[":(i % (HttpWriter.MAX_OUTPUT_CHARS) == 0)?"][":",",bytes[i]);
+// }
+// System.err.format("]->%s\n",new String(bytes,StringUtil.__UTF8));
+ }
+
+ private void assertArrayEquals(byte[] b1, byte[] b2)
+ {
+ String test = new String(b1) + "==" + new String(b2);
+ assertEquals(b1.length, b2.length, test);
+ for (int i = 0; i < b1.length; i++)
+ {
+ assertEquals(b1[i], b2[i], test);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/InclusiveByteRangeTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/InclusiveByteRangeTest.java
new file mode 100644
index 0000000..697544b
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/InclusiveByteRangeTest.java
@@ -0,0 +1,356 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Vector;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class InclusiveByteRangeTest
+{
+ private void assertInvalidRange(String rangeString)
+ {
+ Vector<String> strings = new Vector<>();
+ strings.add(rangeString);
+
+ List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(strings.elements(), 200);
+ assertNull(ranges, "Invalid Range [" + rangeString + "] should result in no satisfiable ranges");
+ }
+
+ private void assertRange(String msg, int expectedFirst, int expectedLast, int size, InclusiveByteRange actualRange)
+ {
+ assertEquals(expectedFirst, actualRange.getFirst(), msg + " - first");
+ assertEquals(expectedLast, actualRange.getLast(), msg + " - last");
+ String expectedHeader = String.format("bytes %d-%d/%d", expectedFirst, expectedLast, size);
+ assertThat(msg + " - header range string", actualRange.toHeaderRangeString(size), is(expectedHeader));
+ }
+
+ private void assertSimpleRange(int expectedFirst, int expectedLast, String rangeId, int size)
+ {
+ InclusiveByteRange range = parseRange(rangeId, size);
+
+ assertEquals(expectedFirst, range.getFirst(), "Range [" + rangeId + "] - first");
+ assertEquals(expectedLast, range.getLast(), "Range [" + rangeId + "] - last");
+ String expectedHeader = String.format("bytes %d-%d/%d", expectedFirst, expectedLast, size);
+ assertEquals(expectedHeader, range.toHeaderRangeString(size), "Range [" + rangeId + "] - header range string");
+ }
+
+ private InclusiveByteRange parseRange(String rangeString, int size)
+ {
+ Vector<String> strings = new Vector<>();
+ strings.add(rangeString);
+
+ List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(strings.elements(), size);
+ assertNotNull(ranges, "Satisfiable Ranges should not be null");
+ assertEquals(1, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
+ return ranges.iterator().next();
+ }
+
+ private List<InclusiveByteRange> parseRanges(int size, String... rangeString)
+ {
+ Vector<String> strings = new Vector<>();
+ for (String range : rangeString)
+ {
+ strings.add(range);
+ }
+
+ List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(strings.elements(), size);
+ assertNotNull(ranges, "Satisfiable Ranges should not be null");
+ return ranges;
+ }
+
+ @Test
+ public void testHeader416RangeString()
+ {
+ assertEquals("bytes */100", InclusiveByteRange.to416HeaderRangeString(100), "416 Header on size 100");
+ assertEquals("bytes */123456789", InclusiveByteRange.to416HeaderRangeString(123456789), "416 Header on size 123456789");
+ }
+
+ @Test
+ public void testInvalidRanges()
+ {
+ // Invalid if parsing "Range" header
+ assertInvalidRange("bytes=a-b"); // letters invalid
+ assertInvalidRange("byte=10-3"); // key is bad
+ assertInvalidRange("onceuponatime=5-10"); // key is bad
+ assertInvalidRange("bytes=300-310"); // outside of size (200)
+ }
+
+ /**
+ * Ranges have a multiple ranges, all absolutely defined.
+ */
+ @Test
+ public void testMultipleAbsoluteRanges()
+ {
+ int size = 50;
+ String rangeString;
+
+ rangeString = "bytes=5-20,35-65";
+
+ List<InclusiveByteRange> ranges = parseRanges(size, rangeString);
+ assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
+ Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
+ assertRange("Range [" + rangeString + "]", 5, 20, size, inclusiveByteRangeIterator.next());
+ assertRange("Range [" + rangeString + "]", 35, 49, size, inclusiveByteRangeIterator.next());
+ }
+
+ /**
+ * Ranges have a multiple ranges, all absolutely defined.
+ */
+ @Test
+ public void testMultipleAbsoluteRangesSplit()
+ {
+ int size = 50;
+
+ List<InclusiveByteRange> ranges = parseRanges(size, "bytes=5-20", "bytes=35-65");
+ assertEquals(2, ranges.size());
+ Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
+ assertRange("testMultipleAbsoluteRangesSplit[0]", 5, 20, size, inclusiveByteRangeIterator.next());
+ assertRange("testMultipleAbsoluteRangesSplit[1]", 35, 49, size, inclusiveByteRangeIterator.next());
+ }
+
+ /**
+ * Range definition has a range that is clipped due to the size.
+ */
+ @Test
+ public void testMultipleRangesClipped()
+ {
+ int size = 50;
+ String rangeString;
+
+ rangeString = "bytes=5-20,35-65,-5";
+
+ List<InclusiveByteRange> ranges = parseRanges(size, rangeString);
+ assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
+ Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
+ assertRange("Range [" + rangeString + "]", 5, 20, size, inclusiveByteRangeIterator.next());
+ assertRange("Range [" + rangeString + "]", 35, 49, size, inclusiveByteRangeIterator.next());
+ }
+
+ @Test
+ public void testMultipleRangesOverlapping()
+ {
+ int size = 200;
+ String rangeString;
+
+ rangeString = "bytes=5-20,15-25";
+
+ List<InclusiveByteRange> ranges = parseRanges(size, rangeString);
+ assertEquals(1, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
+ Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
+ assertRange("Range [" + rangeString + "]", 5, 25, size, inclusiveByteRangeIterator.next());
+ }
+
+ @Test
+ public void testMultipleRangesSplit()
+ {
+ int size = 200;
+ String rangeString;
+ rangeString = "bytes=5-10,15-20";
+
+ List<InclusiveByteRange> ranges = parseRanges(size, rangeString);
+ assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
+ Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
+ assertRange("Range [" + rangeString + "]", 5, 10, size, inclusiveByteRangeIterator.next());
+ assertRange("Range [" + rangeString + "]", 15, 20, size, inclusiveByteRangeIterator.next());
+ }
+
+ @Test
+ public void testMultipleSameRangesSplit()
+ {
+ int size = 200;
+ String rangeString;
+ rangeString = "bytes=5-10,15-20,5-10,15-20,5-10,5-10,5-10,5-10,5-10,5-10";
+
+ List<InclusiveByteRange> ranges = parseRanges(size, rangeString);
+ assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
+ Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
+ assertRange("Range [" + rangeString + "]", 5, 10, size, inclusiveByteRangeIterator.next());
+ assertRange("Range [" + rangeString + "]", 15, 20, size, inclusiveByteRangeIterator.next());
+ }
+
+ @Test
+ public void testMultipleOverlappingRanges()
+ {
+ int size = 200;
+ String rangeString;
+ rangeString = "bytes=5-15,20-30,10-25";
+
+ List<InclusiveByteRange> ranges = parseRanges(size, rangeString);
+ assertEquals(1, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
+ Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
+ assertRange("Range [" + rangeString + "]", 5, 30, size, inclusiveByteRangeIterator.next());
+ }
+
+ @Test
+ public void testMultipleOverlappingRangesOrdered()
+ {
+ int size = 200;
+ String rangeString;
+ rangeString = "bytes=20-30,5-15,0-5,25-35";
+
+ List<InclusiveByteRange> ranges = parseRanges(size, rangeString);
+ assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
+ Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
+ assertRange("Range [" + rangeString + "]", 20, 35, size, inclusiveByteRangeIterator.next());
+ assertRange("Range [" + rangeString + "]", 0, 15, size, inclusiveByteRangeIterator.next());
+ }
+
+ @Test
+ public void testMultipleOverlappingRangesOrderedSplit()
+ {
+ int size = 200;
+ String rangeString;
+ rangeString = "bytes=20-30,5-15,0-5,25-35";
+ List<InclusiveByteRange> ranges = parseRanges(size, "bytes=20-30", "bytes=5-15", "bytes=0-5,25-35");
+
+ assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
+ Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
+ assertRange("Range [" + rangeString + "]", 20, 35, size, inclusiveByteRangeIterator.next());
+ assertRange("Range [" + rangeString + "]", 0, 15, size, inclusiveByteRangeIterator.next());
+ }
+
+ @Test
+ public void testNasty()
+ {
+ int size = 200;
+ String rangeString;
+
+ rangeString = "bytes=90-100, 10-20, 30-40, -161";
+ List<InclusiveByteRange> ranges = parseRanges(size, rangeString);
+
+ assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
+ Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
+ assertRange("Range [" + rangeString + "]", 30, 199, size, inclusiveByteRangeIterator.next());
+ assertRange("Range [" + rangeString + "]", 10, 20, size, inclusiveByteRangeIterator.next());
+ }
+
+ @Test
+ public void testRangeOpenEnded()
+ {
+ assertSimpleRange(50, 499, "bytes=50-", 500);
+ }
+
+ @Test
+ public void testSimpleRange()
+ {
+ assertSimpleRange(5, 10, "bytes=5-10", 200);
+ assertSimpleRange(195, 199, "bytes=-5", 200);
+ assertSimpleRange(50, 119, "bytes=50-150", 120);
+ assertSimpleRange(50, 119, "bytes=50-", 120);
+
+ assertSimpleRange(1, 50, "bytes= 1 - 50", 120);
+ }
+
+ // TODO: evaluate this vs assertInvalidRange() above, which behavior is correct? null? or empty list?
+ private void assertBadRangeList(int size, String badRange)
+ {
+ Vector<String> strings = new Vector<>();
+ strings.add(badRange);
+
+ List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(strings.elements(), size);
+ // if one part is bad, the entire set of ranges should be treated as bad, per RFC7233
+ assertThat("Should have no ranges", ranges, is(nullValue()));
+ }
+
+ @Test
+ @Disabled
+ public void testBadRangeSetPartiallyBad()
+ {
+ assertBadRangeList(500, "bytes=1-50,1-b,a-50");
+ }
+
+ @Test
+ public void testBadRangeNoNumbers()
+ {
+ assertBadRangeList(500, "bytes=a-b");
+ }
+
+ @Test
+ public void testBadRangeEmpty()
+ {
+ assertBadRangeList(500, "bytes=");
+ }
+
+ @Test
+ @Disabled
+ public void testBadRangeZeroPrefixed()
+ {
+ assertBadRangeList(500, "bytes=01-050");
+ }
+
+ @Test
+ public void testBadRangeHex()
+ {
+ assertBadRangeList(500, "bytes=0F-FF");
+ }
+
+ @Test
+ @Disabled
+ public void testBadRangeTabWhitespace()
+ {
+ assertBadRangeList(500, "bytes=\t1\t-\t50");
+ }
+
+ @Test
+ public void testBadRangeTabDelim()
+ {
+ assertBadRangeList(500, "bytes=1-50\t90-101\t200-250");
+ }
+
+ @Test
+ public void testBadRangeSemiColonDelim()
+ {
+ assertBadRangeList(500, "bytes=1-50;90-101;200-250");
+ }
+
+ @Test
+ public void testBadRangeNegativeSize()
+ {
+ assertBadRangeList(500, "bytes=50-1");
+ }
+
+ @Test
+ public void testBadRangeDoubleDash()
+ {
+ assertBadRangeList(500, "bytes=1--20");
+ }
+
+ @Test
+ public void testBadRangeTrippleDash()
+ {
+ assertBadRangeList(500, "bytes=1---");
+ }
+
+ @Test
+ public void testBadRangeZeroedNegativeSize()
+ {
+ assertBadRangeList(500, "bytes=050-001");
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/InsufficientThreadsDetectionTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/InsufficientThreadsDetectionTest.java
new file mode 100644
index 0000000..9821662
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/InsufficientThreadsDetectionTest.java
@@ -0,0 +1,98 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.ThreadPool;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class InsufficientThreadsDetectionTest
+{
+
+ private Server _server;
+
+ @AfterEach
+ public void dispose() throws Exception
+ {
+ _server.stop();
+ }
+
+ @Test
+ public void testConnectorUsesServerExecutorWithNotEnoughThreads() throws Exception
+ {
+ assertThrows(IllegalStateException.class, () ->
+ {
+ // server has 3 threads in the executor
+ _server = new Server(new QueuedThreadPool(3));
+
+ // connector will use executor from server because connectorPool is null
+ ThreadPool connectorPool = null;
+ // connector requires 7 threads(2 + 4 + 1)
+ ServerConnector connector = new ServerConnector(_server, connectorPool, null, null, 2, 4, new HttpConnectionFactory());
+ connector.setPort(0);
+ _server.addConnector(connector);
+
+ // should throw IllegalStateException because there are no required threads in server pool
+ _server.start();
+ });
+ }
+
+ @Test
+ public void testConnectorWithDedicatedExecutor() throws Exception
+ {
+ // server has 3 threads in the executor
+ _server = new Server(new QueuedThreadPool(3));
+
+ // connector pool has 100 threads
+ ThreadPool connectorPool = new QueuedThreadPool(100);
+ // connector requires 7 threads(2 + 4 + 1)
+ ServerConnector connector = new ServerConnector(_server, connectorPool, null, null, 2, 4, new HttpConnectionFactory());
+ connector.setPort(0);
+ _server.addConnector(connector);
+
+ // should not throw exception because connector uses own executor, so its threads should not be counted
+ _server.start();
+ }
+
+ @Test
+ public void testCaseForMultipleConnectors() throws Exception
+ {
+ assertThrows(IllegalStateException.class, () ->
+ {
+ // server has 4 threads in the executor
+ _server = new Server(new QueuedThreadPool(4));
+
+ // first connector consumes 3 threads from server pool
+ _server.addConnector(new ServerConnector(_server, null, null, null, 1, 1, new HttpConnectionFactory()));
+
+ // second connect also require 3 threads but uses own executor, so its threads should not be counted
+ final QueuedThreadPool connectorPool = new QueuedThreadPool(4, 4);
+ _server.addConnector(new ServerConnector(_server, connectorPool, null, null, 1, 1, new HttpConnectionFactory()));
+
+ // third connector consumes 3 threads from server pool
+ _server.addConnector(new ServerConnector(_server, null, null, null, 1, 1, new HttpConnectionFactory()));
+
+ // should throw exception because limit was overflown
+ _server.start();
+ });
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/LargeHeaderTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/LargeHeaderTest.java
new file mode 100644
index 0000000..8137fa3
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/LargeHeaderTest.java
@@ -0,0 +1,142 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.util.Arrays;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class LargeHeaderTest
+{
+ private Server server;
+
+ @BeforeEach
+ public void setup() throws Exception
+ {
+ server = new Server();
+
+ HttpConfiguration config = new HttpConfiguration();
+ HttpConnectionFactory http = new HttpConnectionFactory(config);
+
+ ServerConnector connector = new ServerConnector(server, http);
+ connector.setPort(0);
+ connector.setIdleTimeout(5000);
+ server.addConnector(connector);
+
+ server.setErrorHandler(new ErrorHandler());
+
+ server.setHandler(new AbstractHandler()
+ {
+ final String largeHeaderValue;
+
+ {
+ byte[] bytes = new byte[8 * 1024];
+ Arrays.fill(bytes, (byte)'X');
+ largeHeaderValue = "LargeHeaderOver8k-" + new String(bytes, UTF_8) + "_Z_";
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ response.setHeader(HttpHeader.CONTENT_TYPE.toString(), MimeTypes.Type.TEXT_HTML.toString());
+ response.setHeader("LongStr", largeHeaderValue);
+ PrintWriter writer = response.getWriter();
+ writer.write("<html><h1>FOO</h1></html>");
+ writer.flush();
+ response.flushBuffer();
+ baseRequest.setHandled(true);
+ }
+ });
+ server.start();
+ }
+
+ @AfterEach
+ public void teardown()
+ {
+ LifeCycle.stop(server);
+ }
+
+ @Test
+ public void testLargeHeader() throws Throwable
+ {
+ final Logger CLIENTLOG = Log.getLogger(LargeHeaderTest.class).getLogger(".client");
+ ExecutorService executorService = Executors.newFixedThreadPool(8);
+
+ int localPort = server.getURI().getPort();
+ String rawRequest = "GET / HTTP/1.1\r\n" +
+ "Host: localhost:" + localPort + "\r\n" +
+ "\r\n";
+
+ Throwable issues = new Throwable();
+
+ for (int i = 0; i < 500; ++i)
+ {
+ executorService.submit(() ->
+ {
+ try (Socket client = new Socket("localhost", localPort);
+ OutputStream output = client.getOutputStream();
+ InputStream input = client.getInputStream())
+ {
+ output.write(rawRequest.getBytes(UTF_8));
+ output.flush();
+
+ String rawResponse = IO.toString(input, UTF_8);
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.getStatus(), is(500));
+ }
+ catch (Throwable t)
+ {
+ CLIENTLOG.warn("Client Issue", t);
+ issues.addSuppressed(t);
+ }
+ });
+ }
+
+ executorService.awaitTermination(5, TimeUnit.SECONDS);
+ if (issues.getSuppressed().length > 0)
+ {
+ throw issues;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/LocalAsyncContextTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/LocalAsyncContextTest.java
new file mode 100644
index 0000000..1dbd2a8
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/LocalAsyncContextTest.java
@@ -0,0 +1,529 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import javax.servlet.AsyncContext;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.handler.HandlerWrapper;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class LocalAsyncContextTest
+{
+ public static final Logger LOG = Log.getLogger(LocalAsyncContextTest.class);
+ protected Server _server;
+ protected SuspendHandler _handler;
+ protected Connector _connector;
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ _server = new Server();
+ _connector = initConnector();
+ _server.addConnector(_connector);
+
+ SessionHandler session = new SessionHandler();
+ _handler = new SuspendHandler();
+ session.setHandler(_handler);
+
+ _server.setHandler(session);
+ _server.start();
+
+ reset();
+ }
+
+ public void reset()
+ {
+ }
+
+ protected Connector initConnector()
+ {
+ return new LocalConnector(_server);
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ _server.stop();
+ _server.join();
+ }
+
+ @Test
+ public void testSuspendTimeout() throws Exception
+ {
+ String response;
+ _handler.setRead(0);
+ _handler.setSuspendFor(1000);
+ _handler.setResumeAfter(-1);
+ _handler.setCompleteAfter(-1);
+ response = process(null);
+ check(response, "TIMEOUT");
+ }
+
+ @Test
+ public void testSuspendResume0() throws Exception
+ {
+ String response;
+ _handler.setRead(0);
+ _handler.setSuspendFor(10000);
+ _handler.setResumeAfter(0);
+ _handler.setCompleteAfter(-1);
+ response = process(null);
+ check(response, "STARTASYNC", "DISPATCHED");
+ }
+
+ @Test
+ public void testSuspendResume100() throws Exception
+ {
+ String response;
+ _handler.setRead(0);
+ _handler.setSuspendFor(10000);
+ _handler.setResumeAfter(100);
+ _handler.setCompleteAfter(-1);
+ response = process(null);
+ check(response, "STARTASYNC", "DISPATCHED");
+ }
+
+ @Test
+ public void testSuspendComplete0() throws Exception
+ {
+ String response;
+ _handler.setRead(0);
+ _handler.setSuspendFor(10000);
+ _handler.setResumeAfter(-1);
+ _handler.setCompleteAfter(0);
+ response = process(null);
+ check(response, "STARTASYNC", "COMPLETED");
+ }
+
+ @Test
+ public void testSuspendComplete200() throws Exception
+ {
+ String response;
+ _handler.setRead(0);
+ _handler.setSuspendFor(10000);
+ _handler.setResumeAfter(-1);
+ _handler.setCompleteAfter(200);
+ response = process(null);
+ check(response, "STARTASYNC", "COMPLETED");
+ }
+
+ @Test
+ public void testSuspendReadResume0() throws Exception
+ {
+ String response;
+ _handler.setSuspendFor(10000);
+ _handler.setRead(-1);
+ _handler.setResumeAfter(0);
+ _handler.setCompleteAfter(-1);
+ response = process("wibble");
+ check(response, "STARTASYNC", "DISPATCHED");
+ }
+
+ @Test
+ public void testSuspendReadResume100() throws Exception
+ {
+ String response;
+ _handler.setSuspendFor(10000);
+ _handler.setRead(-1);
+ _handler.setResumeAfter(100);
+ _handler.setCompleteAfter(-1);
+ response = process("wibble");
+ check(response, "DISPATCHED");
+ }
+
+ @Test
+ public void testSuspendOther() throws Exception
+ {
+ String response;
+ _handler.setSuspendFor(10000);
+ _handler.setRead(-1);
+ _handler.setResumeAfter(-1);
+ _handler.setCompleteAfter(0);
+ response = process("wibble");
+ check(response, "COMPLETED");
+
+ _handler.setResumeAfter(-1);
+ _handler.setCompleteAfter(100);
+ response = process("wibble");
+ check(response, "COMPLETED");
+
+ _handler.setRead(6);
+ _handler.setResumeAfter(0);
+ _handler.setCompleteAfter(-1);
+ response = process("wibble");
+ check(response, "DISPATCHED");
+
+ _handler.setResumeAfter(100);
+ _handler.setCompleteAfter(-1);
+ response = process("wibble");
+
+ check(response, "DISPATCHED");
+
+ _handler.setResumeAfter(-1);
+ _handler.setCompleteAfter(0);
+ response = process("wibble");
+ check(response, "COMPLETED");
+
+ _handler.setResumeAfter(-1);
+ _handler.setCompleteAfter(100);
+ response = process("wibble");
+ check(response, "COMPLETED");
+ }
+
+ @Test
+ public void testTwoCycles() throws Exception
+ {
+ String response;
+
+ _handler.setRead(0);
+ _handler.setSuspendFor(1000);
+ _handler.setResumeAfter(100);
+ _handler.setCompleteAfter(-1);
+ _handler.setSuspendFor2(1000);
+ _handler.setResumeAfter2(200);
+ _handler.setCompleteAfter2(-1);
+ response = process(null);
+ check(response, "STARTASYNC", "DISPATCHED", "startasync", "STARTASYNC2", "DISPATCHED");
+ }
+
+ protected void check(String response, String... content)
+ {
+ assertThat(response, Matchers.startsWith("HTTP/1.1 200 OK"));
+ int i = 0;
+ for (String m : content)
+ {
+ assertThat(response, Matchers.containsString(m));
+ i = response.indexOf(m, i);
+ i += m.length();
+ }
+ }
+
+ private synchronized String process(String content) throws Exception
+ {
+ LOG.debug("TEST process: {}", content);
+ reset();
+ String request = "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n";
+
+ if (content == null)
+ request += "\r\n";
+ else
+ request += "Content-Length: " + content.length() + "\r\n" + "\r\n" + content;
+
+ return getResponse(request);
+ }
+
+ protected String getResponse(String request) throws Exception
+ {
+ LocalConnector connector = (LocalConnector)_connector;
+ LocalConnector.LocalEndPoint endp = connector.executeRequest(request);
+ endp.waitUntilClosed();
+ return endp.takeOutputString();
+ }
+
+ private class SuspendHandler extends HandlerWrapper
+ {
+ private int _read;
+ private long _suspendFor = -1;
+ private long _resumeAfter = -1;
+ private long _completeAfter = -1;
+ private long _suspendFor2 = -1;
+ private long _resumeAfter2 = -1;
+ private long _completeAfter2 = -1;
+
+ public SuspendHandler()
+ {
+ }
+
+ public void setRead(int read)
+ {
+ _read = read;
+ }
+
+ public void setSuspendFor(long suspendFor)
+ {
+ _suspendFor = suspendFor;
+ }
+
+ public void setResumeAfter(long resumeAfter)
+ {
+ _resumeAfter = resumeAfter;
+ }
+
+ public void setCompleteAfter(long completeAfter)
+ {
+ _completeAfter = completeAfter;
+ }
+
+ public void setSuspendFor2(long suspendFor2)
+ {
+ _suspendFor2 = suspendFor2;
+ }
+
+ public void setResumeAfter2(long resumeAfter2)
+ {
+ _resumeAfter2 = resumeAfter2;
+ }
+
+ public void setCompleteAfter2(long completeAfter2)
+ {
+ _completeAfter2 = completeAfter2;
+ }
+
+ @Override
+ public void handle(String target, final Request baseRequest, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException
+ {
+ LOG.debug("handle {} {}", baseRequest.getDispatcherType(), baseRequest);
+ if (DispatcherType.REQUEST.equals(baseRequest.getDispatcherType()))
+ {
+ if (_read > 0)
+ {
+ int read = _read;
+ byte[] buf = new byte[read];
+ while (read > 0)
+ {
+ read -= request.getInputStream().read(buf);
+ }
+ }
+ else if (_read < 0)
+ {
+ InputStream in = request.getInputStream();
+ int b = in.read();
+ while (b != -1)
+ {
+ b = in.read();
+ }
+ }
+
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ response.getOutputStream().println("STARTASYNC");
+ asyncContext.addListener(_asyncListener);
+ if (_suspendFor > 0)
+ asyncContext.setTimeout(_suspendFor);
+
+ if (_completeAfter > 0)
+ {
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ Thread.sleep(_completeAfter);
+ response.getOutputStream().println("COMPLETED");
+ response.setStatus(200);
+ baseRequest.setHandled(true);
+ asyncContext.complete();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+ }
+ else if (_completeAfter == 0)
+ {
+ response.getOutputStream().println("COMPLETED");
+ response.setStatus(200);
+ baseRequest.setHandled(true);
+ asyncContext.complete();
+ }
+
+ if (_resumeAfter > 0)
+ {
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ Thread.sleep(_resumeAfter);
+ if (((HttpServletRequest)asyncContext.getRequest()).getSession(true).getId() != null)
+ asyncContext.dispatch();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+ }
+ else if (_resumeAfter == 0)
+ {
+ asyncContext.dispatch();
+ }
+ }
+ else
+ {
+ if (request.getAttribute("TIMEOUT") != null)
+ response.getOutputStream().println("TIMEOUT");
+ else
+ response.getOutputStream().println("DISPATCHED");
+
+ if (_suspendFor2 >= 0)
+ {
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ response.getOutputStream().println("STARTASYNC2");
+ if (_suspendFor2 > 0)
+ asyncContext.setTimeout(_suspendFor2);
+ _suspendFor2 = -1;
+
+ if (_completeAfter2 > 0)
+ {
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ Thread.sleep(_completeAfter2);
+ response.getOutputStream().println("COMPLETED2");
+ response.setStatus(200);
+ baseRequest.setHandled(true);
+ asyncContext.complete();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+ }
+ else if (_completeAfter2 == 0)
+ {
+ response.getOutputStream().println("COMPLETED2");
+ response.setStatus(200);
+ baseRequest.setHandled(true);
+ asyncContext.complete();
+ }
+
+ if (_resumeAfter2 > 0)
+ {
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ Thread.sleep(_resumeAfter2);
+ asyncContext.dispatch();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+ }
+ else if (_resumeAfter2 == 0)
+ {
+ asyncContext.dispatch();
+ }
+ }
+ else
+ {
+ response.setStatus(200);
+ baseRequest.setHandled(true);
+ }
+ }
+ }
+ }
+
+ private AsyncListener _asyncListener = new AsyncListener()
+ {
+ @Override
+ public void onComplete(AsyncEvent event) throws IOException
+ {
+ }
+
+ @Override
+ public void onError(AsyncEvent event) throws IOException
+ {
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event) throws IOException
+ {
+ event.getSuppliedResponse().getOutputStream().println("startasync");
+ event.getAsyncContext().addListener(this);
+ }
+
+ @Override
+ public void onTimeout(AsyncEvent event) throws IOException
+ {
+ event.getSuppliedRequest().setAttribute("TIMEOUT", Boolean.TRUE);
+ event.getAsyncContext().dispatch();
+ }
+ };
+
+ static <T> void spinAssertEquals(T expected, Supplier<T> actualSupplier)
+ {
+ spinAssertEquals(expected, actualSupplier, 10, TimeUnit.SECONDS);
+ }
+
+ static <T> void spinAssertEquals(T expected, Supplier<T> actualSupplier, long waitFor, TimeUnit units)
+ {
+ long now = System.nanoTime();
+ long end = now + units.toNanos(waitFor);
+ T actual = null;
+ while (now < end)
+ {
+ actual = actualSupplier.get();
+ if (actual == null && expected == null ||
+ actual != null && actual.equals(expected))
+ break;
+ try
+ {
+ Thread.sleep(10);
+ }
+ catch (InterruptedException e)
+ {
+ // Ignored
+ }
+ now = System.nanoTime();
+ }
+
+ assertEquals(expected, actual);
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/LocalConnectorTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/LocalConnectorTest.java
new file mode 100644
index 0000000..9f79fc2
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/LocalConnectorTest.java
@@ -0,0 +1,406 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.server.LocalConnector.LocalEndPoint;
+import org.eclipse.jetty.util.BufferUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class LocalConnectorTest
+{
+ private Server _server;
+ private LocalConnector _connector;
+
+ @BeforeEach
+ public void prepare() throws Exception
+ {
+ _server = new Server();
+ _connector = new LocalConnector(_server);
+ _connector.setIdleTimeout(60000);
+ _server.addConnector(_connector);
+ _server.setHandler(new DumpHandler());
+ _server.start();
+ }
+
+ @AfterEach
+ public void dispose() throws Exception
+ {
+ _server.stop();
+ _server = null;
+ _connector = null;
+ }
+
+ @Test
+ public void testOpenClose() throws Exception
+ {
+ final CountDownLatch openLatch = new CountDownLatch(1);
+ final CountDownLatch closeLatch = new CountDownLatch(1);
+ _connector.addBean(new Connection.Listener.Adapter()
+ {
+ @Override
+ public void onOpened(Connection connection)
+ {
+ openLatch.countDown();
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ closeLatch.countDown();
+ }
+ });
+
+ _connector.getResponse(
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ assertTrue(openLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(closeLatch.await(5, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void testOneGET() throws Exception
+ {
+ String response = _connector.getResponse("GET /R1 HTTP/1.0\r\n\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+ }
+
+ @Test
+ public void testOneResponse10() throws Exception
+ {
+ String response = _connector.getResponse("GET /R1 HTTP/1.0\r\n\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+ }
+
+ @Test
+ public void testOneResponse10KeepAlive() throws Exception
+ {
+ String response = _connector.getResponse(
+ "GET /R1 HTTP/1.0\r\n" +
+ "Connection: keep-alive\r\n" +
+ "\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+ }
+
+ @Test
+ public void testOneResponse10KeepAliveEmpty() throws Exception
+ {
+ String response = _connector.getResponse(
+ "GET /R1?empty=true HTTP/1.0\r\n" +
+ "Connection: keep-alive\r\n" +
+ "\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, not(containsString("pathInfo=/R1")));
+ }
+
+ @Test
+ public void testOneResponse11() throws Exception
+ {
+ String response = _connector.getResponse(
+ "GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+ }
+
+ @Test
+ public void testOneResponse11close() throws Exception
+ {
+ String response = _connector.getResponse(
+ "GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+ }
+
+ @Test
+ public void testOneResponse11empty() throws Exception
+ {
+ String response = _connector.getResponse(
+ "GET /R1?empty=true HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, not(containsString("pathInfo=/R1")));
+ }
+
+ @Test
+ public void testOneResponse11chunked() throws Exception
+ {
+ String response = _connector.getResponse(
+ "GET /R1?flush=true HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+ assertThat(response, containsString("\r\n0\r\n"));
+ }
+
+ @Test
+ public void testThreeResponsePipeline11() throws Exception
+ {
+ LocalEndPoint endp = _connector.connect();
+ endp.addInput(
+ "GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ "GET /R2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ "GET /R3 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n"
+ );
+ String response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+ response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R2"));
+ response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R3"));
+ }
+
+ @Test
+ public void testThreeResponse11() throws Exception
+ {
+ LocalEndPoint endp = _connector.connect();
+ endp.addInput(
+ "GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n");
+
+ String response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+
+ endp.addInput(
+ "GET /R2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n");
+
+ response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R2"));
+
+ endp.addInput(
+ "GET /R3 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n"
+ );
+
+ response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R3"));
+ }
+
+ @Test
+ public void testThreeResponseClosed11() throws Exception
+ {
+ LocalEndPoint endp = _connector.connect();
+ endp.addInput(
+ "GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ "GET /R2 HTTP/1.1\r\n" +
+ "Connection: close\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ "GET /R3 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n"
+ );
+ String response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+ response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R2"));
+ response = endp.getResponse();
+ assertThat(response, nullValue());
+ }
+
+ @Test
+ public void testExpectContinuesAvailable() throws Exception
+ {
+ LocalEndPoint endp = _connector.connect();
+ endp.addInput(
+ "GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Expect: 100-Continue\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "01234567890\r\n");
+ String response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+ assertThat(response, containsString("0123456789"));
+ }
+
+ @Test
+ public void testExpectContinues() throws Exception
+ {
+ LocalEndPoint endp = _connector.executeRequest(
+ "GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Expect: 100-Continue\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n");
+ String response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 100 Continue"));
+ endp.addInput("01234567890\r\n");
+ response = endp.getResponse();
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+ assertThat(response, containsString("0123456789"));
+ }
+
+ @Test
+ public void testStopStart() throws Exception
+ {
+ String response = _connector.getResponse("GET /R1 HTTP/1.0\r\n\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+
+ _server.stop();
+ _server.start();
+
+ response = _connector.getResponse("GET /R2 HTTP/1.0\r\n\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R2"));
+ }
+
+ @Test
+ public void testTwoGETs() throws Exception
+ {
+ LocalEndPoint endp = _connector.connect();
+ endp.addInput(
+ "GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ "GET /R2 HTTP/1.0\r\n\r\n");
+
+ String response = endp.getResponse() + endp.getResponse();
+
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+
+ response = response.substring(response.indexOf("</html>") + 8);
+
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R2"));
+ }
+
+ @Test
+ public void testTwoGETsParsed() throws Exception
+ {
+ LocalConnector.LocalEndPoint endp = _connector.executeRequest(
+ "GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ "GET /R2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n");
+
+ String response = BufferUtil.toString(endp.waitForResponse(false, 10, TimeUnit.SECONDS), StandardCharsets.ISO_8859_1);
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+
+ response = BufferUtil.toString(endp.waitForResponse(false, 10, TimeUnit.SECONDS), StandardCharsets.ISO_8859_1);
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R2"));
+ }
+
+ @Test
+ public void testManyGETs() throws Exception
+ {
+ LocalEndPoint endp = _connector.connect();
+ endp.addInput(
+ "GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ "GET /R2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ "GET /R3 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ "GET /R4 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ "GET /R5 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ "GET /R6 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+
+ String r = "";
+
+ for (String response = endp.getResponse(); response != null; response = endp.getResponse())
+ {
+ r += response;
+ }
+
+ for (int i = 1; i <= 6; i++)
+ {
+ assertThat(r, containsString("HTTP/1.1 200 OK"));
+ assertThat(r, containsString("pathInfo=/R" + i));
+ r = r.substring(r.indexOf("</html>") + 8);
+ }
+ }
+
+ @Test
+ public void testGETandGET() throws Exception
+ {
+ String response = _connector.getResponse("GET /R1 HTTP/1.0\r\n\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R1"));
+
+ response = _connector.getResponse("GET /R2 HTTP/1.0\r\n\r\n");
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("pathInfo=/R2"));
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/LowResourcesMonitorTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/LowResourcesMonitorTest.java
new file mode 100644
index 0000000..b892e18
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/LowResourcesMonitorTest.java
@@ -0,0 +1,276 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.InputStream;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.TimerScheduler;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class LowResourcesMonitorTest
+{
+ QueuedThreadPool _threadPool;
+ Server _server;
+ ServerConnector _connector;
+ LowResourceMonitor _lowResourcesMonitor;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ _threadPool = new QueuedThreadPool();
+ _threadPool.setMaxThreads(50);
+
+ _server = new Server(_threadPool);
+ _server.manage(_threadPool);
+
+ _server.addBean(new TimerScheduler());
+
+ _connector = new ServerConnector(_server);
+ _connector.setPort(0);
+ _connector.setIdleTimeout(35000);
+ _server.addConnector(_connector);
+
+ _server.setHandler(new DumpHandler());
+
+ _lowResourcesMonitor = new LowResourceMonitor(_server);
+ _lowResourcesMonitor.setLowResourcesIdleTimeout(200);
+ _lowResourcesMonitor.setMaxConnections(20);
+ _lowResourcesMonitor.setPeriod(900);
+ _lowResourcesMonitor.setMonitoredConnectors(Collections.singleton(_connector));
+ _server.addBean(_lowResourcesMonitor);
+
+ _server.start();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _server.stop();
+ }
+
+ @Test
+ public void testLowOnThreads() throws Exception
+ {
+ _lowResourcesMonitor.setMonitorThreads(true);
+ Thread.sleep(1200);
+ _threadPool.setMaxThreads(_threadPool.getThreads() - _threadPool.getIdleThreads() + 10);
+ Thread.sleep(1200);
+ assertFalse(_lowResourcesMonitor.isLowOnResources(), _lowResourcesMonitor.getReasons());
+
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ for (int i = 0; i < 100; i++)
+ {
+ _threadPool.execute(() ->
+ {
+ try
+ {
+ latch.await();
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ Thread.sleep(1200);
+ assertTrue(_lowResourcesMonitor.isLowOnResources());
+
+ latch.countDown();
+ Thread.sleep(1200);
+
+ assertFalse(_lowResourcesMonitor.isLowOnResources(), _lowResourcesMonitor.getReasons());
+ }
+
+ @Test
+ public void testNotAccepting() throws Exception
+ {
+ _lowResourcesMonitor.setAcceptingInLowResources(false);
+ _lowResourcesMonitor.setMonitorThreads(true);
+ Thread.sleep(1200);
+ int maxThreads = _threadPool.getThreads() - _threadPool.getIdleThreads() + 10;
+ System.out.println("maxThreads:" + maxThreads);
+ _threadPool.setMaxThreads(maxThreads);
+ Thread.sleep(1200);
+ assertFalse(_lowResourcesMonitor.isLowOnResources(), _lowResourcesMonitor.getReasons());
+
+ for (AbstractConnector c : _server.getBeans(AbstractConnector.class))
+ {
+ assertThat(c.isAccepting(), Matchers.is(true));
+ }
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ for (int i = 0; i < 100; i++)
+ {
+ _threadPool.execute(() ->
+ {
+ try
+ {
+ latch.await();
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ Thread.sleep(1200);
+ assertTrue(_lowResourcesMonitor.isLowOnResources());
+ for (AbstractConnector c : _server.getBeans(AbstractConnector.class))
+ {
+ assertThat(c.isAccepting(), Matchers.is(false));
+ }
+
+ latch.countDown();
+ Thread.sleep(1200);
+ assertFalse(_lowResourcesMonitor.isLowOnResources(), _lowResourcesMonitor.getReasons());
+ for (AbstractConnector c : _server.getBeans(AbstractConnector.class))
+ {
+ assertThat(c.isAccepting(), Matchers.is(true));
+ }
+ }
+
+ @Disabled("not reliable")
+ @Test
+ public void testLowOnMemory() throws Exception
+ {
+ _lowResourcesMonitor.setMaxMemory(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory() + (100 * 1024 * 1024));
+ Thread.sleep(1200);
+ assertFalse(_lowResourcesMonitor.isLowOnResources(), _lowResourcesMonitor.getReasons());
+
+ byte[] data = new byte[100 * 1024 * 1024];
+ Arrays.fill(data, (byte)1);
+ int hash = Arrays.hashCode(data);
+ assertThat(hash, not(equalTo(0)));
+
+ Thread.sleep(1200);
+ assertTrue(_lowResourcesMonitor.isLowOnResources());
+ data = null;
+ System.gc();
+ System.gc();
+
+ Thread.sleep(1200);
+ assertFalse(_lowResourcesMonitor.isLowOnResources(), _lowResourcesMonitor.getReasons());
+ }
+
+ @Test
+ public void testMaxConnectionsAndMaxIdleTime() throws Exception
+ {
+ _lowResourcesMonitor.setMaxMemory(0);
+ assertFalse(_lowResourcesMonitor.isLowOnResources(), _lowResourcesMonitor.getReasons());
+
+ assertEquals(20, _lowResourcesMonitor.getMaxConnections());
+ Socket[] socket = new Socket[_lowResourcesMonitor.getMaxConnections() + 1];
+ for (int i = 0; i < socket.length; i++)
+ {
+ socket[i] = new Socket("localhost", _connector.getLocalPort());
+ }
+
+ Thread.sleep(1200);
+ assertTrue(_lowResourcesMonitor.isLowOnResources());
+
+ try (Socket newSocket = new Socket("localhost", _connector.getLocalPort()))
+ {
+ // wait for low idle time to close sockets, but not new Socket
+ Thread.sleep(1200);
+ assertFalse(_lowResourcesMonitor.isLowOnResources(), _lowResourcesMonitor.getReasons());
+
+ for (int i = 0; i < socket.length; i++)
+ {
+ assertEquals(-1, socket[i].getInputStream().read());
+ }
+
+ newSocket.getOutputStream().write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.UTF_8));
+ assertEquals('H', newSocket.getInputStream().read());
+ }
+ }
+
+ @Test
+ public void testMaxLowResourcesTime() throws Exception
+ {
+ int monitorPeriod = _lowResourcesMonitor.getPeriod();
+ int lowResourcesIdleTimeout = _lowResourcesMonitor.getLowResourcesIdleTimeout();
+ assertThat(lowResourcesIdleTimeout, Matchers.lessThan(monitorPeriod));
+
+ int maxLowResourcesTime = 5 * monitorPeriod;
+ _lowResourcesMonitor.setMaxLowResourcesTime(maxLowResourcesTime);
+ assertFalse(_lowResourcesMonitor.isLowOnResources(), _lowResourcesMonitor.getReasons());
+
+ try (Socket socket0 = new Socket("localhost", _connector.getLocalPort()))
+ {
+ // Put the lowResourceMonitor in low mode.
+ _lowResourcesMonitor.setMaxMemory(1);
+
+ // Wait a couple of monitor periods so that
+ // lowResourceMonitor detects it is in low mode.
+ Thread.sleep(2 * monitorPeriod);
+ assertTrue(_lowResourcesMonitor.isLowOnResources());
+
+ // We already waited enough for lowResourceMonitor to close socket0.
+ assertEquals(-1, socket0.getInputStream().read());
+
+ // New connections are not affected by the
+ // low mode until maxLowResourcesTime elapses.
+ try (Socket socket1 = new Socket("localhost", _connector.getLocalPort()))
+ {
+ // Set a very short read timeout so we can test if the server closed.
+ socket1.setSoTimeout(1);
+ InputStream input1 = socket1.getInputStream();
+
+ assertTrue(_lowResourcesMonitor.isLowOnResources());
+ assertThrows(SocketTimeoutException.class, () -> input1.read());
+
+ // Wait a couple of lowResources idleTimeouts.
+ Thread.sleep(2 * lowResourcesIdleTimeout);
+
+ // Verify the new socket is still open.
+ assertTrue(_lowResourcesMonitor.isLowOnResources());
+ assertThrows(SocketTimeoutException.class, () -> input1.read());
+
+ // Let the maxLowResourcesTime elapse.
+ Thread.sleep(maxLowResourcesTime);
+
+ assertTrue(_lowResourcesMonitor.isLowOnResources());
+ // Now also the new socket should be closed.
+ assertEquals(-1, input1.read());
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/MockConnector.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/MockConnector.java
new file mode 100644
index 0000000..7821174
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/MockConnector.java
@@ -0,0 +1,46 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+
+class MockConnector extends AbstractConnector
+{
+ public MockConnector()
+ {
+ super(new Server(), null, null, null, 0);
+ }
+
+ @Override
+ protected void accept(int acceptorID) throws IOException, InterruptedException
+ {
+ }
+
+ @Override
+ public Object getTransport()
+ {
+ return null;
+ }
+
+ @Override
+ public String dumpSelf()
+ {
+ return null;
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/NotAcceptingTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/NotAcceptingTest.java
new file mode 100644
index 0000000..f635d29
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/NotAcceptingTest.java
@@ -0,0 +1,466 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.concurrent.Exchanger;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.LocalConnector.LocalEndPoint;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class NotAcceptingTest
+{
+ private final long idleTimeout = 2000;
+ Server server;
+ LocalConnector localConnector;
+ ServerConnector blockingConnector;
+ ServerConnector asyncConnector;
+
+ @BeforeEach
+ public void before()
+ {
+ server = new Server();
+
+ localConnector = new LocalConnector(server);
+ localConnector.setIdleTimeout(idleTimeout);
+ server.addConnector(localConnector);
+
+ blockingConnector = new ServerConnector(server, 1, 1);
+ blockingConnector.setPort(0);
+ blockingConnector.setIdleTimeout(idleTimeout);
+ blockingConnector.setAcceptQueueSize(10);
+ server.addConnector(blockingConnector);
+
+ asyncConnector = new ServerConnector(server, 0, 1);
+ asyncConnector.setPort(0);
+ asyncConnector.setIdleTimeout(idleTimeout);
+ asyncConnector.setAcceptQueueSize(10);
+ server.addConnector(asyncConnector);
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ server.stop();
+ server = null;
+ }
+
+ @Test
+ public void testServerConnectorBlockingAccept() throws Exception
+ {
+ TestHandler handler = new TestHandler();
+ server.setHandler(handler);
+
+ server.start();
+
+ try (Socket client0 = new Socket("localhost", blockingConnector.getLocalPort());)
+ {
+ HttpTester.Input in0 = HttpTester.from(client0.getInputStream());
+
+ client0.getOutputStream().write("GET /one HTTP/1.1\r\nHost:localhost\r\n\r\n".getBytes());
+ String uri = handler.exchange.exchange("data");
+ assertThat(uri, is("/one"));
+ HttpTester.Response response = HttpTester.parseResponse(in0);
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("data"));
+
+ blockingConnector.setAccepting(false);
+
+ // 0th connection still working
+ client0.getOutputStream().write("GET /two HTTP/1.1\r\nHost:localhost\r\n\r\n".getBytes());
+ uri = handler.exchange.exchange("more data");
+ assertThat(uri, is("/two"));
+ response = HttpTester.parseResponse(in0);
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("more data"));
+
+ try (Socket client1 = new Socket("localhost", blockingConnector.getLocalPort());)
+ {
+ // can't stop next connection being accepted
+ HttpTester.Input in1 = HttpTester.from(client1.getInputStream());
+ client1.getOutputStream().write("GET /three HTTP/1.1\r\nHost:localhost\r\n\r\n".getBytes());
+ uri = handler.exchange.exchange("new connection");
+ assertThat(uri, is("/three"));
+ response = HttpTester.parseResponse(in1);
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("new connection"));
+
+ try (Socket client2 = new Socket("localhost", blockingConnector.getLocalPort());)
+ {
+
+ HttpTester.Input in2 = HttpTester.from(client2.getInputStream());
+ client2.getOutputStream().write("GET /four HTTP/1.1\r\nHost:localhost\r\n\r\n".getBytes());
+
+ try
+ {
+ uri = handler.exchange.exchange("delayed connection", idleTimeout, TimeUnit.MILLISECONDS);
+ fail("Failed near URI: " + uri); // this displays last URI, not current (obviously)
+ }
+ catch (TimeoutException e)
+ {
+ // Can we accept the original?
+ blockingConnector.setAccepting(true);
+ uri = handler.exchange.exchange("delayed connection");
+ assertThat(uri, is("/four"));
+ response = HttpTester.parseResponse(in2);
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("delayed connection"));
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ @Disabled
+ public void testLocalConnector() throws Exception
+ {
+ server.setHandler(new HelloHandler());
+ server.start();
+
+ try (LocalEndPoint client0 = localConnector.connect())
+ {
+ client0.addInputAndExecute(BufferUtil.toBuffer("GET /one HTTP/1.1\r\nHost:localhost\r\n\r\n"));
+ HttpTester.Response response = HttpTester.parseResponse(client0.getResponse());
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("Hello\n"));
+
+ localConnector.setAccepting(false);
+
+ // 0th connection still working
+ client0.addInputAndExecute(BufferUtil.toBuffer("GET /two HTTP/1.1\r\nHost:localhost\r\n\r\n"));
+ response = HttpTester.parseResponse(client0.getResponse());
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("Hello\n"));
+
+ LocalEndPoint[] local = new LocalEndPoint[10];
+ for (int i = 0; i < 10; i++)
+ {
+ try (LocalEndPoint client = localConnector.connect())
+ {
+ try
+ {
+ local[i] = client;
+ client.addInputAndExecute(BufferUtil.toBuffer("GET /three HTTP/1.1\r\nHost:localhost\r\n\r\n"));
+ response = HttpTester.parseResponse(client.getResponse(false, idleTimeout, TimeUnit.MILLISECONDS));
+
+ // A few local connections may succeed
+ if (i == local.length - 1)
+ // but not 10 of them!
+ fail("Expected TimeoutException");
+ }
+ catch (TimeoutException e)
+ {
+ // A connection finally failed!
+ break;
+ }
+ }
+ }
+ // 0th connection still working
+ client0.addInputAndExecute(BufferUtil.toBuffer("GET /four HTTP/1.1\r\nHost:localhost\r\n\r\n"));
+ response = HttpTester.parseResponse(client0.getResponse());
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("Hello\n"));
+
+ localConnector.setAccepting(true);
+ // New connection working again
+ try (LocalEndPoint client = localConnector.connect())
+ {
+ client.addInputAndExecute(BufferUtil.toBuffer("GET /five HTTP/1.1\r\nHost:localhost\r\n\r\n"));
+ response = HttpTester.parseResponse(client.getResponse());
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("Hello\n"));
+ }
+ }
+ }
+
+ @Test
+ public void testServerConnectorAsyncAccept() throws Exception
+ {
+ TestHandler handler = new TestHandler();
+ server.setHandler(handler);
+
+ server.start();
+
+ try (Socket client0 = new Socket("localhost", asyncConnector.getLocalPort());)
+ {
+ HttpTester.Input in0 = HttpTester.from(client0.getInputStream());
+
+ client0.getOutputStream().write("GET /one HTTP/1.1\r\nHost:localhost\r\n\r\n".getBytes());
+ String uri = handler.exchange.exchange("data");
+ assertThat(uri, is("/one"));
+ HttpTester.Response response = HttpTester.parseResponse(in0);
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("data"));
+
+ asyncConnector.setAccepting(false);
+
+ // 0th connection still working
+ client0.getOutputStream().write("GET /two HTTP/1.1\r\nHost:localhost\r\n\r\n".getBytes());
+ uri = handler.exchange.exchange("more data");
+ assertThat(uri, is("/two"));
+ response = HttpTester.parseResponse(in0);
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("more data"));
+
+ try (Socket client1 = new Socket("localhost", asyncConnector.getLocalPort());)
+ {
+ HttpTester.Input in1 = HttpTester.from(client1.getInputStream());
+ client1.getOutputStream().write("GET /three HTTP/1.1\r\nHost:localhost\r\n\r\n".getBytes());
+
+ try
+ {
+ uri = handler.exchange.exchange("delayed connection", idleTimeout, TimeUnit.MILLISECONDS);
+ fail(uri);
+ }
+ catch (TimeoutException e)
+ {
+ // Can we accept the original?
+ asyncConnector.setAccepting(true);
+ uri = handler.exchange.exchange("delayed connection");
+ assertThat(uri, is("/three"));
+ response = HttpTester.parseResponse(in1);
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is("delayed connection"));
+ }
+ }
+ }
+ }
+
+ public static class TestHandler extends AbstractHandler
+ {
+ final Exchanger<String> exchange = new Exchanger<>();
+ transient int handled;
+
+ public TestHandler()
+ {
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ try
+ {
+ String content = exchange.exchange(baseRequest.getRequestURI());
+ baseRequest.setHandled(true);
+ handled++;
+ response.setContentType("text/html;charset=utf-8");
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.getWriter().print(content);
+ }
+ catch (InterruptedException e)
+ {
+ throw new ServletException(e);
+ }
+ }
+
+ public int getHandled()
+ {
+ return handled;
+ }
+ }
+
+ @Test
+ public void testAcceptRateLimit() throws Exception
+ {
+ AcceptRateLimit limit = new AcceptRateLimit(4, 1, TimeUnit.HOURS, server);
+ server.addBean(limit);
+ server.setHandler(new HelloHandler());
+
+ server.start();
+
+ try (
+ Socket async0 = new Socket("localhost", asyncConnector.getLocalPort());
+ Socket async1 = new Socket("localhost", asyncConnector.getLocalPort());
+ Socket async2 = new Socket("localhost", asyncConnector.getLocalPort());
+ )
+ {
+ String expectedContent = "Hello" + System.lineSeparator();
+
+ for (Socket client : new Socket[]{async2})
+ {
+ HttpTester.Input in = HttpTester.from(client.getInputStream());
+ client.getOutputStream().write("GET /test HTTP/1.1\r\nHost:localhost\r\n\r\n".getBytes());
+ HttpTester.Response response = HttpTester.parseResponse(in);
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is(expectedContent));
+ }
+
+ assertThat(localConnector.isAccepting(), is(true));
+ assertThat(blockingConnector.isAccepting(), is(true));
+ assertThat(asyncConnector.isAccepting(), is(true));
+ }
+
+ limit.age(45, TimeUnit.MINUTES);
+
+ try (
+ Socket async0 = new Socket("localhost", asyncConnector.getLocalPort());
+ Socket async1 = new Socket("localhost", asyncConnector.getLocalPort());
+ )
+ {
+ String expectedContent = "Hello" + System.lineSeparator();
+
+ for (Socket client : new Socket[]{async1})
+ {
+ HttpTester.Input in = HttpTester.from(client.getInputStream());
+ client.getOutputStream().write("GET /test HTTP/1.1\r\nHost:localhost\r\n\r\n".getBytes());
+ HttpTester.Response response = HttpTester.parseResponse(in);
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is(expectedContent));
+ }
+
+ assertThat(localConnector.isAccepting(), is(false));
+ assertThat(blockingConnector.isAccepting(), is(false));
+ assertThat(asyncConnector.isAccepting(), is(false));
+ }
+
+ limit.age(45, TimeUnit.MINUTES);
+ assertThat(localConnector.isAccepting(), is(false));
+ assertThat(blockingConnector.isAccepting(), is(false));
+ assertThat(asyncConnector.isAccepting(), is(false));
+ limit.run();
+ assertThat(localConnector.isAccepting(), is(true));
+ assertThat(blockingConnector.isAccepting(), is(true));
+ assertThat(asyncConnector.isAccepting(), is(true));
+ }
+
+ @Test
+ public void testConnectionLimit() throws Exception
+ {
+ server.addBean(new ConnectionLimit(9, server));
+ server.setHandler(new HelloHandler());
+
+ server.start();
+
+ Log.getLogger(ConnectionLimit.class).debug("CONNECT:");
+ try (
+ LocalEndPoint local0 = localConnector.connect();
+ LocalEndPoint local1 = localConnector.connect();
+ LocalEndPoint local2 = localConnector.connect();
+ Socket blocking0 = new Socket("localhost", blockingConnector.getLocalPort());
+ Socket blocking1 = new Socket("localhost", blockingConnector.getLocalPort());
+ Socket blocking2 = new Socket("localhost", blockingConnector.getLocalPort());
+ Socket async0 = new Socket("localhost", asyncConnector.getLocalPort());
+ Socket async1 = new Socket("localhost", asyncConnector.getLocalPort());
+ Socket async2 = new Socket("localhost", asyncConnector.getLocalPort());
+ )
+ {
+ String expectedContent = "Hello" + System.lineSeparator();
+
+ Log.getLogger(ConnectionLimit.class).debug("LOCAL:");
+ for (LocalEndPoint client : new LocalEndPoint[]{local0, local1, local2})
+ {
+ client.addInputAndExecute(BufferUtil.toBuffer("GET /test HTTP/1.1\r\nHost:localhost\r\n\r\n"));
+ HttpTester.Response response = HttpTester.parseResponse(client.getResponse());
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is(expectedContent));
+ }
+
+ Log.getLogger(ConnectionLimit.class).debug("NETWORK:");
+ for (Socket client : new Socket[]{blocking0, blocking1, blocking2, async0, async1, async2})
+ {
+ HttpTester.Input in = HttpTester.from(client.getInputStream());
+ client.getOutputStream().write("GET /test HTTP/1.1\r\nHost:localhost\r\n\r\n".getBytes());
+ HttpTester.Response response = HttpTester.parseResponse(in);
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is(expectedContent));
+ }
+
+ assertThat(localConnector.isAccepting(), is(false));
+ assertThat(blockingConnector.isAccepting(), is(false));
+ assertThat(asyncConnector.isAccepting(), is(false));
+
+ {
+ // Close a async connection
+ HttpTester.Input in = HttpTester.from(async1.getInputStream());
+ async1.getOutputStream().write("GET /test HTTP/1.1\r\nHost:localhost\r\nConnection: close\r\n\r\n".getBytes());
+ HttpTester.Response response = HttpTester.parseResponse(in);
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContent(), is(expectedContent));
+ }
+ }
+
+ waitFor(localConnector::isAccepting, is(true), 2 * idleTimeout, TimeUnit.MILLISECONDS);
+ waitFor(blockingConnector::isAccepting, is(true), 2 * idleTimeout, TimeUnit.MILLISECONDS);
+ waitFor(asyncConnector::isAccepting, is(true), 2 * idleTimeout, TimeUnit.MILLISECONDS);
+ }
+
+ public static class HelloHandler extends AbstractHandler
+ {
+ public HelloHandler()
+ {
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setContentType("text/html;charset=utf-8");
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.getWriter().println("Hello");
+ }
+ }
+
+ public static <T> void waitFor(Supplier<T> value, Matcher<T> matcher, long wait, TimeUnit units)
+ {
+ long start = System.nanoTime();
+
+ while (true)
+ {
+ try
+ {
+ matcher.matches(value.get());
+ return;
+ }
+ catch (Throwable e)
+ {
+ if ((System.nanoTime() - start) > units.toNanos(wait))
+ throw e;
+ }
+
+ try
+ {
+ TimeUnit.MILLISECONDS.sleep(50);
+ }
+ catch (InterruptedException e)
+ {
+ // no op
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/OptionalSslConnectionTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/OptionalSslConnectionTest.java
new file mode 100644
index 0000000..9e70fa6
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/OptionalSslConnectionTest.java
@@ -0,0 +1,257 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Function;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class OptionalSslConnectionTest
+{
+ private Server server;
+ private ServerConnector connector;
+
+ private void startServer(Function<SslConnectionFactory, OptionalSslConnectionFactory> configFn, Handler handler) throws Exception
+ {
+ QueuedThreadPool serverThreads = new QueuedThreadPool();
+ serverThreads.setName("server");
+ server = new Server(serverThreads);
+
+ String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ HttpConfiguration httpConfig = new HttpConfiguration();
+ HttpConnectionFactory http = new HttpConnectionFactory(httpConfig);
+ SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, http.getProtocol());
+ OptionalSslConnectionFactory sslOrOther = configFn.apply(ssl);
+ connector = new ServerConnector(server, 1, 1, sslOrOther, http);
+ server.addConnector(connector);
+
+ server.setHandler(handler);
+
+ server.start();
+ }
+
+ @AfterEach
+ public void stopServer() throws Exception
+ {
+ if (server != null)
+ server.stop();
+ }
+
+ private OptionalSslConnectionFactory optionalSsl(SslConnectionFactory ssl)
+ {
+ return new OptionalSslConnectionFactory(ssl, ssl.getNextProtocol());
+ }
+
+ private OptionalSslConnectionFactory optionalSslNoOtherProtocol(SslConnectionFactory ssl)
+ {
+ return new OptionalSslConnectionFactory(ssl, null);
+ }
+
+ @Test
+ public void testOptionalSslConnection() throws Exception
+ {
+ startServer(this::optionalSsl, new EmptyServerHandler());
+
+ String request =
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ byte[] requestBytes = request.getBytes(StandardCharsets.US_ASCII);
+
+ // Try first a plain text connection.
+ try (Socket plain = new Socket())
+ {
+ plain.connect(new InetSocketAddress("localhost", connector.getLocalPort()), 1000);
+ OutputStream plainOutput = plain.getOutputStream();
+ plainOutput.write(requestBytes);
+ plainOutput.flush();
+
+ plain.setSoTimeout(5000);
+ InputStream plainInput = plain.getInputStream();
+ HttpTester.Response response = HttpTester.parseResponse(plainInput);
+ assertNotNull(response);
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ }
+
+ // Then try an SSL connection.
+ SslContextFactory sslContextFactory = new SslContextFactory.Client(true);
+ sslContextFactory.start();
+ try (Socket ssl = sslContextFactory.newSslSocket())
+ {
+ ssl.connect(new InetSocketAddress("localhost", connector.getLocalPort()), 1000);
+ OutputStream sslOutput = ssl.getOutputStream();
+ sslOutput.write(requestBytes);
+ sslOutput.flush();
+
+ ssl.setSoTimeout(5000);
+ InputStream sslInput = ssl.getInputStream();
+ HttpTester.Response response = HttpTester.parseResponse(sslInput);
+ assertNotNull(response);
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ }
+ finally
+ {
+ sslContextFactory.stop();
+ }
+ }
+
+ @Test
+ public void testOptionalSslConnectionWithOnlyOneByteShouldIdleTimeout() throws Exception
+ {
+ startServer(this::optionalSsl, new EmptyServerHandler());
+ long idleTimeout = 1000;
+ connector.setIdleTimeout(idleTimeout);
+
+ try (Socket socket = new Socket())
+ {
+ socket.connect(new InetSocketAddress("localhost", connector.getLocalPort()), 1000);
+ OutputStream output = socket.getOutputStream();
+ output.write(0x16);
+ output.flush();
+
+ socket.setSoTimeout((int)(2 * idleTimeout));
+ InputStream input = socket.getInputStream();
+ int read = input.read();
+ assertEquals(-1, read);
+ }
+ }
+
+ @Test
+ public void testOptionalSslConnectionWithUnknownBytes() throws Exception
+ {
+ startServer(this::optionalSslNoOtherProtocol, new EmptyServerHandler());
+
+ try (Socket socket = new Socket())
+ {
+ socket.connect(new InetSocketAddress("localhost", connector.getLocalPort()), 1000);
+ OutputStream output = socket.getOutputStream();
+ output.write(0x00);
+ output.flush();
+ Thread.sleep(500);
+ output.write(0x00);
+ output.flush();
+
+ socket.setSoTimeout(5000);
+ InputStream input = socket.getInputStream();
+ int read = input.read();
+ assertEquals(-1, read);
+ }
+ }
+
+ @Test
+ public void testOptionalSslConnectionWithHTTPBytes() throws Exception
+ {
+ startServer(this::optionalSslNoOtherProtocol, new EmptyServerHandler());
+
+ String request =
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ byte[] requestBytes = request.getBytes(StandardCharsets.US_ASCII);
+
+ // Send a plain text HTTP request to SSL port,
+ // we should get back a minimal HTTP response.
+ try (Socket socket = new Socket())
+ {
+ socket.connect(new InetSocketAddress("localhost", connector.getLocalPort()), 1000);
+ OutputStream output = socket.getOutputStream();
+ output.write(requestBytes);
+ output.flush();
+
+ socket.setSoTimeout(5000);
+ InputStream input = socket.getInputStream();
+ HttpTester.Response response = HttpTester.parseResponse(input);
+ assertNotNull(response);
+ assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus());
+ }
+ }
+
+ @Test
+ public void testNextProtocolIsNotNullButNotConfiguredEither() throws Exception
+ {
+ QueuedThreadPool serverThreads = new QueuedThreadPool();
+ serverThreads.setName("server");
+ server = new Server(serverThreads);
+
+ String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ HttpConfiguration httpConfig = new HttpConfiguration();
+ HttpConnectionFactory http = new HttpConnectionFactory(httpConfig);
+ SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, http.getProtocol());
+ OptionalSslConnectionFactory optSsl = new OptionalSslConnectionFactory(ssl, "no-such-protocol");
+ connector = new ServerConnector(server, 1, 1, optSsl, http);
+ server.addConnector(connector);
+ server.setHandler(new EmptyServerHandler());
+ server.start();
+
+ try (Socket socket = new Socket(server.getURI().getHost(), server.getURI().getPort()))
+ {
+ OutputStream sslOutput = socket.getOutputStream();
+ String request =
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ byte[] requestBytes = request.getBytes(StandardCharsets.US_ASCII);
+
+ sslOutput.write(requestBytes);
+ sslOutput.flush();
+
+ socket.setSoTimeout(5000);
+ InputStream sslInput = socket.getInputStream();
+ HttpTester.Response response = HttpTester.parseResponse(sslInput);
+ assertNull(response);
+ }
+ }
+
+ private static class EmptyServerHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
+ {
+ jettyRequest.setHandled(true);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/PartialRFC2616Test.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/PartialRFC2616Test.java
new file mode 100644
index 0000000..f0787cb
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/PartialRFC2616Test.java
@@ -0,0 +1,726 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpParser;
+import org.eclipse.jetty.server.LocalConnector.LocalEndPoint;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class PartialRFC2616Test
+{
+ private Server server;
+ private LocalConnector connector;
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ server = new Server();
+ connector = new LocalConnector(server);
+ connector.setIdleTimeout(10000);
+ server.addConnector(connector);
+
+ ContextHandler vcontext = new ContextHandler();
+ vcontext.setContextPath("/");
+ vcontext.setVirtualHosts(new String[]
+ {"VirtualHost"});
+ vcontext.setHandler(new DumpHandler("Virtual Dump"));
+
+ ContextHandler context = new ContextHandler();
+ context.setContextPath("/");
+ context.setHandler(new DumpHandler());
+
+ HandlerCollection collection = new HandlerCollection();
+ collection.setHandlers(new Handler[]
+ {vcontext, context});
+
+ server.setHandler(collection);
+
+ server.start();
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ server.stop();
+ server.join();
+ }
+
+ @Test
+ public void test33()
+ {
+ try
+ {
+ HttpFields fields = new HttpFields();
+
+ fields.put("D1", "Sun, 6 Nov 1994 08:49:37 GMT");
+ fields.put("D2", "Sunday, 6-Nov-94 08:49:37 GMT");
+ fields.put("D3", "Sun Nov 6 08:49:37 1994");
+ Date d1 = new Date(fields.getDateField("D1"));
+ Date d2 = new Date(fields.getDateField("D2"));
+ Date d3 = new Date(fields.getDateField("D3"));
+
+ assertEquals(d2, d1, "3.3.1 RFC 822 RFC 850");
+ assertEquals(d3, d2, "3.3.1 RFC 850 ANSI C");
+
+ fields.putDateField("Date", d1.getTime());
+ assertEquals("Sun, 06 Nov 1994 08:49:37 GMT", fields.get("Date"), "3.3.1 RFC 822 preferred");
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ assertTrue(false);
+ }
+ }
+
+ @Test
+ public void test332()
+ {
+ try
+ {
+ String get = connector.getResponse("GET /R1 HTTP/1.0\n" + "Host: localhost\n" + "\n");
+ checkContains(get, 0, "HTTP/1.1 200", "GET");
+ checkContains(get, 0, "Content-Type: text/html", "GET _content");
+ checkContains(get, 0, "<html>", "GET body");
+ int cli = get.indexOf("Content-Length");
+ String contentLength = get.substring(cli, get.indexOf("\r", cli));
+
+ String head = connector.getResponse("HEAD /R1 HTTP/1.0\n" + "Host: localhost\n" + "\n");
+ checkContains(head, 0, "HTTP/1.1 200", "HEAD");
+ checkContains(head, 0, "Content-Type: text/html", "HEAD _content");
+ assertEquals(-1, head.indexOf("<html>"), "HEAD no body");
+ checkContains(head, 0, contentLength, "3.3.2 HEAD");
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ assertTrue(false);
+ }
+ }
+
+ @Test
+ public void test36a() throws Exception
+ {
+ int offset = 0;
+ // Chunk last
+ String response = connector.getResponse(
+ "GET /R1 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Transfer-Encoding: chunked,identity\n" +
+ "Content-Type: text/plain\n" +
+ //@checkstyle-disable-check : IllegalTokenText
+ "\015\012" +
+ "5;\015\012" +
+ "123\015\012\015\012" +
+ "0;\015\012\015\012");
+ //@checkstyle-enable-check : IllegalTokenText
+ checkContains(response, offset, "HTTP/1.1 400 Bad", "Chunked last");
+ }
+
+ @Test
+ public void test36b() throws Exception
+ {
+ String response;
+ int offset = 0;
+ // Chunked
+ LocalEndPoint endp = connector.executeRequest(
+ "GET /R1 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Transfer-Encoding: chunked\n" +
+ "Content-Type: text/plain\n" +
+ "\n" +
+ "2;\n" +
+ "12\n" +
+ "3;\n" +
+ "345\n" +
+ "0;\n\n" +
+
+ "GET /R2 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Transfer-Encoding: chunked\n" +
+ "Content-Type: text/plain\n" +
+ "\n" +
+ "4;\n" +
+ "6789\n" +
+ "5;\n" +
+ "abcde\n" +
+ "0;\n\n" +
+
+ "GET /R3 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Connection: close\n" +
+ "\n");
+ offset = 0;
+ response = endp.getResponse();
+ offset = checkContains(response, offset, "HTTP/1.1 200", "3.6.1 Chunking");
+ offset = checkContains(response, offset, "12345", "3.6.1 Chunking");
+ offset = 0;
+ response = endp.getResponse();
+ offset = checkContains(response, offset, "HTTP/1.1 200", "3.6.1 Chunking");
+ offset = checkContains(response, offset, "6789abcde", "3.6.1 Chunking");
+ offset = 0;
+ response = endp.getResponse();
+ offset = checkContains(response, offset, "/R3", "3.6.1 Chunking");
+ }
+
+ @Test
+ public void test36c() throws Exception
+ {
+ String response;
+ int offset = 0;
+ LocalEndPoint endp = connector.executeRequest(
+ "POST /R1 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Transfer-Encoding: chunked\n" +
+ "Content-Type: text/plain\n" +
+ "\n" +
+ "3;\n" +
+ "fgh\n" +
+ "3;\n" +
+ "Ijk\n" +
+ "0;\n\n" +
+
+ "POST /R2 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Transfer-Encoding: chunked\n" +
+ "Content-Type: text/plain\n" +
+ "\n" +
+ "4;\n" +
+ "lmno\n" +
+ "5;\n" +
+ "Pqrst\n" +
+ "0;\n\n" +
+
+ "GET /R3 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Connection: close\n" +
+ "\n");
+ offset = 0;
+ response = endp.getResponse();
+ checkNotContained(response, "HTTP/1.1 100", "3.6.1 Chunking");
+ offset = checkContains(response, offset, "HTTP/1.1 200", "3.6.1 Chunking");
+ offset = checkContains(response, offset, "fghIjk", "3.6.1 Chunking");
+ offset = 0;
+ response = endp.getResponse();
+ checkNotContained(response, "HTTP/1.1 100", "3.6.1 Chunking");
+ offset = checkContains(response, offset, "HTTP/1.1 200", "3.6.1 Chunking");
+ offset = checkContains(response, offset, "lmnoPqrst", "3.6.1 Chunking");
+ offset = 0;
+ response = endp.getResponse();
+ checkNotContained(response, "HTTP/1.1 100", "3.6.1 Chunking");
+ offset = checkContains(response, offset, "/R3", "3.6.1 Chunking");
+ }
+
+ @Test
+ public void test36d() throws Exception
+ {
+ String response;
+ int offset = 0;
+ // Chunked and keep alive
+ LocalEndPoint endp = connector.executeRequest(
+ "GET /R1 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Transfer-Encoding: chunked\n" +
+ "Content-Type: text/plain\n" +
+ "Connection: keep-alive\n" +
+ "\n" +
+ "3;\n" +
+ "123\n" +
+ "3;\n" +
+ "456\n" +
+ "0;\n\n" +
+
+ "GET /R2 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Connection: close\n" +
+ "\n");
+ offset = 0;
+ response = endp.getResponse();
+ offset = checkContains(response, offset, "HTTP/1.1 200", "3.6.1 Chunking") + 10;
+ offset = checkContains(response, offset, "123456", "3.6.1 Chunking");
+ offset = 0;
+ response = endp.getResponse();
+ offset = checkContains(response, offset, "/R2", "3.6.1 Chunking") + 10;
+ }
+
+ @Test
+ public void test39() throws Exception
+ {
+ HttpFields fields = new HttpFields();
+
+ fields.put("Q", "bbb;q=0.5,aaa,ccc;q=0.002,d;q=0,e;q=0.0001,ddd;q=0.001,aa2,abb;q=0.7");
+ Enumeration<String> qualities = fields.getValues("Q", ", \t");
+ List<String> list = HttpFields.qualityList(qualities);
+ assertEquals("aaa", HttpFields.valueParameters(list.get(0), null), "Quality parameters");
+ assertEquals("aa2", HttpFields.valueParameters(list.get(1), null), "Quality parameters");
+ assertEquals("abb", HttpFields.valueParameters(list.get(2), null), "Quality parameters");
+ assertEquals("bbb", HttpFields.valueParameters(list.get(3), null), "Quality parameters");
+ assertEquals("ccc", HttpFields.valueParameters(list.get(4), null), "Quality parameters");
+ assertEquals("ddd", HttpFields.valueParameters(list.get(5), null), "Quality parameters");
+ }
+
+ @Test
+ public void test41() throws Exception
+ {
+ int offset = 0;
+ // If _content length not used, second request will not be read.
+ String response = connector.getResponses(
+ "\r\n" +
+ "GET /R1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "GET /R2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n" +
+ " \r\n" +
+ "GET /R3 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n"
+ );
+ offset = checkContains(response, offset, "HTTP/1.1 200 OK", "2. identity") + 10;
+ offset = checkContains(response, offset, "/R1", "2. identity") + 3;
+ offset = checkContains(response, offset, "HTTP/1.1 200 OK", "2. identity") + 10;
+ offset = checkContains(response, offset, "/R2", "2. identity") + 3;
+ checkNotContained(response, offset, "HTTP/1.1 200 OK", "2. identity");
+ checkNotContained(response, offset, "/R3", "2. identity");
+ }
+
+ @Test
+ public void test442() throws Exception
+ {
+ String response;
+ int offset = 0;
+ // If _content length not used, second request will not be read.
+ LocalEndPoint endp = connector.executeRequest(
+ "GET /R1 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Transfer-Encoding: identity\n" +
+ "Content-Type: text/plain\n" +
+ "Content-Length: 5\n" +
+ "\n" +
+ //@checkstyle-disable-check : IllegalTokenText
+ "123\015\012" +
+ //@checkstyle-enable-check : IllegalTokenText
+ "GET /R2 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Transfer-Encoding: other\n" +
+ "Connection: close\n" +
+ "\n");
+ offset = 0;
+ response = endp.getResponse();
+ offset = checkContains(response, offset, "HTTP/1.1 400 ", "2. identity") + 10;
+ offset = 0;
+ response = endp.getResponse();
+ assertThat("There should be no next response as first one closed connection", response, is(nullValue()));
+ }
+
+ @Test
+ public void test443() throws Exception
+ {
+ // Due to smuggling concerns, handling has been changed to
+ // treat content length and chunking as a bad request.
+ int offset = 0;
+ String response;
+ LocalEndPoint endp = connector.executeRequest(
+ "GET /R1 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Transfer-Encoding: chunked\n" +
+ "Content-Type: text/plain\n" +
+ "Content-Length: 100\n" +
+ "\n" +
+ "3;\n" +
+ "123\n" +
+ "3;\n" +
+ "456\n" +
+ "0;\n" +
+ "\n" +
+
+ "GET /R2 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Connection: close\n" +
+ "Content-Type: text/plain\n" +
+ "Content-Length: 6\n" +
+ "\n" +
+ "abcdef");
+ response = endp.getResponse();
+ offset = checkContains(response, offset, "HTTP/1.1 400 ", "3. ignore c-l") + 1;
+ checkNotContained(response, offset, "/R2", "3. _content-length");
+ }
+
+ @Test
+ public void test444() throws Exception
+ {
+ // No _content length
+ assertTrue(true, "Skip 411 checks as IE breaks this rule");
+ // offset=0; connector.reopen();
+ // response=connector.getResponse("GET /R2 HTTP/1.1\n"+
+ // "Host: localhost\n"+
+ // "Content-Type: text/plain\n"+
+ // "Connection: close\n"+
+ // "\n"+
+ // "123456");
+ // offset=checkContains(response,offset,
+ // "HTTP/1.1 411 ","411 length required")+10;
+ // offset=0; connector.reopen();
+ // response=connector.getResponse("GET /R2 HTTP/1.0\n"+
+ // "Content-Type: text/plain\n"+
+ // "\n"+
+ // "123456");
+ // offset=checkContains(response,offset,
+ // "HTTP/1.0 411 ","411 length required")+10;
+
+ }
+
+ @Test
+ public void test521() throws Exception
+ {
+ // Default Host
+ int offset = 0;
+ String response = connector.getResponse("GET http://VirtualHost:8888/path/R1 HTTP/1.1\n" + "Host: wronghost\n" + "Connection: close\n" + "\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200", "Virtual host") + 1;
+ offset = checkContains(response, offset, "Virtual Dump", "Virtual host") + 1;
+ offset = checkContains(response, offset, "pathInfo=/path/R1", "Virtual host") + 1;
+ offset = checkContains(response, offset, "servername=VirtualHost", "Virtual host") + 1;
+ }
+
+ @Test
+ public void test522() throws Exception
+ {
+ // Default Host
+ int offset = 0;
+ String response = connector.getResponse("GET /path/R1 HTTP/1.1\n" + "Host: localhost\n" + "Connection: close\n" + "\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200", "Default host") + 1;
+ offset = checkContains(response, offset, "Dump HttpHandler", "Default host") + 1;
+ offset = checkContains(response, offset, "pathInfo=/path/R1", "Default host") + 1;
+
+ // Virtual Host
+ offset = 0;
+ response = connector.getResponse("GET /path/R2 HTTP/1.1\n" + "Host: VirtualHost\n" + "Connection: close\n" + "\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200", "Default host") + 1;
+ offset = checkContains(response, offset, "Virtual Dump", "virtual host") + 1;
+ offset = checkContains(response, offset, "pathInfo=/path/R2", "Default host") + 1;
+ }
+
+ @Test
+ public void test52() throws Exception
+ {
+ // Virtual Host
+ int offset = 0;
+ String response = connector.getResponse("GET /path/R1 HTTP/1.1\n" + "Host: VirtualHost\n" + "Connection: close\n" + "\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200", "2. virtual host field") + 1;
+ offset = checkContains(response, offset, "Virtual Dump", "2. virtual host field") + 1;
+ offset = checkContains(response, offset, "pathInfo=/path/R1", "2. virtual host field") + 1;
+
+ // Virtual Host case insensitive
+ offset = 0;
+ response = connector.getResponse("GET /path/R1 HTTP/1.1\n" + "Host: ViRtUalhOst\n" + "Connection: close\n" + "\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200", "2. virtual host field") + 1;
+ offset = checkContains(response, offset, "Virtual Dump", "2. virtual host field") + 1;
+ offset = checkContains(response, offset, "pathInfo=/path/R1", "2. virtual host field") + 1;
+
+ // Virtual Host
+ offset = 0;
+ response = connector.getResponse("GET /path/R1 HTTP/1.1\n" + "\n");
+ offset = checkContains(response, offset, "HTTP/1.1 400", "3. no host") + 1;
+ }
+
+ @Test
+ public void test81() throws Exception
+ {
+ int offset = 0;
+ String response = connector.getResponse("GET /R1 HTTP/1.1\n" + "Host: localhost\n" + "\n", 250, TimeUnit.MILLISECONDS);
+ //@checkstyle-disable-check : IllegalTokenText
+ offset = checkContains(response, offset, "HTTP/1.1 200 OK\015\012", "8.1.2 default") + 10;
+ //@checkstyle-enable-check : IllegalTokenText
+ checkContains(response, offset, "Content-Length: ", "8.1.2 default");
+
+ LocalEndPoint endp = connector.executeRequest("GET /R1 HTTP/1.1\n" + "Host: localhost\n" + "\n" +
+ "GET /R2 HTTP/1.1\n" + "Host: localhost\n" + "Connection: close\n" + "\n" +
+ "GET /R3 HTTP/1.1\n" + "Host: localhost\n" + "Connection: close\n" + "\n");
+
+ offset = 0;
+ response = endp.getResponse();
+ //@checkstyle-disable-check : IllegalTokenText
+ offset = checkContains(response, offset, "HTTP/1.1 200 OK\015\012", "8.1.2 default") + 1;
+ //@checkstyle-enable-check : IllegalTokenText
+ offset = checkContains(response, offset, "/R1", "8.1.2 default") + 1;
+
+ offset = 0;
+ response = endp.getResponse();
+ //@checkstyle-disable-check : IllegalTokenText
+ offset = checkContains(response, offset, "HTTP/1.1 200 OK\015\012", "8.1.2.2 pipeline") + 11;
+ //@checkstyle-enable-check : IllegalTokenText
+ offset = checkContains(response, offset, "Connection: close", "8.1.2.2 pipeline") + 1;
+ offset = checkContains(response, offset, "/R2", "8.1.2.1 close") + 3;
+
+ offset = 0;
+ response = endp.getResponse();
+ assertThat(response, nullValue());
+ }
+
+ @Test
+ public void test10418() throws Exception
+ {
+ // Expect Failure
+ int offset = 0;
+ String response = connector.getResponse(
+ "GET /R1 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Expect: unknown\n" +
+ "Content-Type: text/plain\n" +
+ "Content-Length: 8\n" +
+ "\n");
+ offset = checkContains(response, offset, "HTTP/1.1 417", "8.2.3 expect failure") + 1;
+ }
+
+ @Test
+ public void test823Dash5() throws Exception
+ {
+ // Expect with body: client sends the content right away, we should not send 100-Continue
+ int offset = 0;
+ String response = connector.getResponse(
+ "GET /R1 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Expect: 100-continue\n" +
+ "Content-Type: text/plain\n" +
+ "Content-Length: 8\n" +
+ "Connection: close\n" +
+ "\n" +
+ //@checkstyle-disable-check : IllegalTokenText
+ "123456\015\012");
+ //@checkstyle-enable-check : IllegalTokenText
+ checkNotContained(response, offset, "HTTP/1.1 100 ", "8.2.3 expect 100");
+ offset = checkContains(response, offset, "HTTP/1.1 200 OK", "8.2.3 expect with body") + 1;
+ }
+
+ @Test
+ public void test823() throws Exception
+ {
+ int offset = 0;
+ // Expect 100
+ LocalConnector.LocalEndPoint endp = connector.executeRequest("GET /R1 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Connection: close\n" +
+ "Expect: 100-continue\n" +
+ "Content-Type: text/plain\n" +
+ "Content-Length: 8\n" +
+ "\n");
+ String infomational = endp.getResponse();
+ offset = checkContains(infomational, offset, "HTTP/1.1 100 ", "8.2.3 expect 100") + 1;
+ checkNotContained(infomational, offset, "HTTP/1.1 200", "8.2.3 expect 100");
+ //@checkstyle-disable-check : IllegalTokenText
+ endp.addInput("654321\015\012");
+ //@checkstyle-enable-check : IllegalTokenText
+ String response = endp.getResponse();
+ offset = 0;
+ offset = checkContains(response, offset, "HTTP/1.1 200", "8.2.3 expect 100") + 1;
+ offset = checkContains(response, offset, "654321", "8.2.3 expect 100") + 1;
+ }
+
+ @Test
+ public void test824() throws Exception
+ {
+ // Expect 100 not sent
+ int offset = 0;
+ String response = connector.getResponse("GET /R1?error=401 HTTP/1.1\n" +
+ "Host: localhost\n" +
+ "Expect: 100-continue\n" +
+ "Content-Type: text/plain\n" +
+ "Content-Length: 8\n" +
+ "\n");
+ checkNotContained(response, offset, "HTTP/1.1 100", "8.2.3 expect 100");
+ offset = checkContains(response, offset, "HTTP/1.1 401 ", "8.2.3 expect 100") + 1;
+ offset = checkContains(response, offset, "Connection: close", "8.2.3 expect 100") + 1;
+ }
+
+ @Test
+ public void test92() throws Exception
+ {
+ int offset = 0;
+
+ String response = connector.getResponse("OPTIONS * HTTP/1.1\n" +
+ "Connection: close\n" +
+ "Host: localhost\n" +
+ "\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200", "200") + 1;
+
+ offset = 0;
+ response = connector.getResponse("GET * HTTP/1.1\n" +
+ "Connection: close\n" +
+ "Host: localhost\n" +
+ "\n");
+ offset = checkContains(response, offset, "HTTP/1.1 400", "400") + 1;
+ }
+
+ @Test
+ public void test94()
+ {
+ try
+ {
+ String get = connector.getResponse("GET /R1 HTTP/1.0\n" + "Host: localhost\n" + "\n");
+
+ checkContains(get, 0, "HTTP/1.1 200", "GET");
+ checkContains(get, 0, "Content-Type: text/html", "GET _content");
+ checkContains(get, 0, "<html>", "GET body");
+
+ String head = connector.getResponse("HEAD /R1 HTTP/1.0\n" + "Host: localhost\n" + "\n");
+ checkContains(head, 0, "HTTP/1.1 200", "HEAD");
+ checkContains(head, 0, "Content-Type: text/html", "HEAD _content");
+ assertEquals(-1, head.indexOf("<html>"), "HEAD no body");
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ assertTrue(false);
+ }
+ }
+
+ @Test
+ public void test1423() throws Exception
+ {
+ try (StacklessLogging stackless = new StacklessLogging(HttpParser.class))
+ {
+ int offset = 0;
+ String response = connector.getResponse("GET /R1 HTTP/1.0\n" + "Connection: close\n" + "\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200", "200") + 1;
+
+ offset = 0;
+ response = connector.getResponse("GET /R1 HTTP/1.1\n" + "Connection: close\n" + "\n");
+ offset = checkContains(response, offset, "HTTP/1.1 400", "400") + 1;
+
+ offset = 0;
+ response = connector.getResponse("GET /R1 HTTP/1.1\n" + "Host: localhost\n" + "Connection: close\n" + "\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200", "200") + 1;
+
+ offset = 0;
+ response = connector.getResponse("GET /R1 HTTP/1.1\n" + "Host:\n" + "Connection: close\n" + "\n");
+ offset = checkContains(response, offset, "HTTP/1.1 200", "200") + 1;
+ }
+ }
+
+ @Test
+ public void test196()
+ {
+ try
+ {
+ int offset = 0;
+ String response = connector.getResponse("GET /R1 HTTP/1.0\n" + "\n");
+ //@checkstyle-disable-check : IllegalTokenText
+ offset = checkContains(response, offset, "HTTP/1.1 200 OK\015\012", "19.6.2 default close") + 10;
+ checkNotContained(response, offset, "Connection: close", "19.6.2 not assumed");
+
+ LocalEndPoint endp = connector.executeRequest(
+ "GET /R1 HTTP/1.0\n" + "Host: localhost\n" + "Connection: keep-alive\n" + "\n" +
+ "GET /R2 HTTP/1.0\n" + "Host: localhost\n" + "Connection: close\n" + "\n" +
+ "GET /R3 HTTP/1.0\n" + "Host: localhost\n" + "Connection: close\n" + "\n");
+
+ offset = 0;
+ response = endp.getResponse();
+ offset = checkContains(response, offset, "HTTP/1.1 200 OK\015\012", "19.6.2 Keep-alive 1") + 1;
+ offset = checkContains(response, offset, "Connection: keep-alive", "19.6.2 Keep-alive 1") + 1;
+
+ offset = checkContains(response, offset, "<html>", "19.6.2 Keep-alive 1") + 1;
+
+ offset = checkContains(response, offset, "/R1", "19.6.2 Keep-alive 1") + 1;
+
+ offset = 0;
+ response = endp.getResponse();
+ offset = checkContains(response, offset, "HTTP/1.1 200 OK\015\012", "19.6.2 Keep-alive 2") + 11;
+ offset = checkContains(response, offset, "/R2", "19.6.2 Keep-alive close") + 3;
+
+ offset = 0;
+ response = endp.getResponse();
+ assertThat("19.6.2 closed", response, nullValue());
+
+ offset = 0;
+ endp = connector.executeRequest(
+ "GET /R1 HTTP/1.0\n" + "Host: localhost\n" + "Connection: keep-alive\n" + "Content-Length: 10\n" + "\n" + "1234567890\n" +
+ "GET /RA HTTP/1.0\n" + "Host: localhost\n" + "Connection: keep-alive\n" + "Content-Length: 10\n" + "\n" + "ABCDEFGHIJ\n" +
+ "GET /R2 HTTP/1.0\n" + "Host: localhost\n" + "Connection: close\n" + "\n" +
+ "GET /R3 HTTP/1.0\n" + "Host: localhost\n" + "Connection: close\n" + "\n");
+
+ offset = 0;
+ response = endp.getResponse();
+ offset = checkContains(response, offset, "HTTP/1.1 200 OK\015\012", "19.6.2 Keep-alive 1") + 1;
+ offset = checkContains(response, offset, "Connection: keep-alive", "19.6.2 Keep-alive 1") + 1;
+ offset = checkContains(response, offset, "<html>", "19.6.2 Keep-alive 1") + 1;
+ offset = checkContains(response, offset, "1234567890", "19.6.2 Keep-alive 1") + 1;
+
+ offset = 0;
+ response = endp.getResponse();
+ offset = checkContains(response, offset, "HTTP/1.1 200 OK\015\012", "19.6.2 Keep-alive 1") + 1;
+ offset = checkContains(response, offset, "Connection: keep-alive", "19.6.2 Keep-alive 1") + 1;
+ offset = checkContains(response, offset, "<html>", "19.6.2 Keep-alive 1") + 1;
+ offset = checkContains(response, offset, "ABCDEFGHIJ", "19.6.2 Keep-alive 1") + 1;
+
+ offset = 0;
+ response = endp.getResponse();
+ offset = checkContains(response, offset, "HTTP/1.1 200 OK\015\012", "19.6.2 Keep-alive 2") + 11;
+ offset = checkContains(response, offset, "/R2", "19.6.2 Keep-alive close") + 3;
+ //@checkstyle-enable-check : IllegalTokenText
+ offset = 0;
+ response = endp.getResponse();
+ assertThat("19.6.2 closed", response, nullValue());
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+
+ private int checkContains(String s, int offset, String c, String test)
+ {
+ assertThat(test, s.substring(offset), containsString(c));
+ return s.indexOf(c, offset);
+ }
+
+ private void checkNotContained(String s, int offset, String c, String test)
+ {
+ assertThat(test, s.substring(offset), not(containsString(c)));
+ }
+
+ private void checkNotContained(String s, String c, String test)
+ {
+ checkNotContained(s, 0, c, test);
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java
new file mode 100644
index 0000000..58d2f12
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java
@@ -0,0 +1,417 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.toolchain.test.Net;
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class ProxyConnectionTest
+{
+ @ParameterizedTest
+ @MethodSource("requestProcessors")
+ public void testBadCRLF(RequestProcessor p) throws Exception
+ {
+ String request = "PROXY TCP 1.2.3.4 5.6.7.8 111 222\r \n" +
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = p.sendRequestWaitingForResponse(request);
+ assertNull(response);
+ }
+
+ @ParameterizedTest
+ @MethodSource("requestProcessors")
+ public void testBadChar(RequestProcessor p) throws Exception
+ {
+ String request = "PROXY\tTCP 1.2.3.4 5.6.7.8 111 222\r\n" +
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = p.sendRequestWaitingForResponse(request);
+ assertNull(response);
+ }
+
+ @ParameterizedTest
+ @MethodSource("requestProcessors")
+ public void testBadPort(RequestProcessor p) throws Exception
+ {
+ try (StacklessLogging stackless = new StacklessLogging(ProxyConnectionFactory.class))
+ {
+ String request = "PROXY TCP 1.2.3.4 5.6.7.8 9999999999999 222\r\n" +
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = p.sendRequestWaitingForResponse(request);
+ assertNull(response);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("requestProcessors")
+ public void testHttp(RequestProcessor p) throws Exception
+ {
+ String request =
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = p.sendRequestWaitingForResponse(request);
+ assertThat(response, Matchers.containsString("HTTP/1.1 200"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("requestProcessors")
+ public void testIPv6(RequestProcessor p) throws Exception
+ {
+ Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable());
+ String request = "PROXY TCP6 eeee:eeee:eeee:eeee:eeee:eeee:eeee:eeee ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 65535 65535\r\n" +
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+
+ String response = p.sendRequestWaitingForResponse(request);
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 200"));
+ assertThat(response, Matchers.containsString("pathInfo=/path"));
+ assertThat(response, Matchers.containsString("remote=[eeee:eeee:eeee:eeee:eeee:eeee:eeee:eeee]:65535"));
+ assertThat(response, Matchers.containsString("local=[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]:65535"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("requestProcessors")
+ public void testIPv6V2(RequestProcessor p) throws Exception
+ {
+ Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable());
+
+ String proxy =
+ // Preamble
+ "0D0A0D0A000D0A515549540A" +
+
+ // V2, PROXY
+ "21" +
+
+ // 0x1 : AF_INET6 0x1 : STREAM.
+ "21" +
+
+ // Address length is 2*16 + 2*2 = 36 bytes.
+ // length of remaining header (16+16+2+2 = 36)
+ "0024" +
+
+ // uint8_t src_addr[16]; uint8_t dst_addr[16]; uint16_t src_port; uint16_t dst_port;
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + // ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
+ "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE" + // eeee:eeee:eeee:eeee:eeee:eeee:eeee:eeee
+ "3039" + // 12345
+ "1F90"; // 8080
+ String http = "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+
+ String response = p.sendRequestWaitingForResponse(TypeUtil.fromHexString(proxy), http.getBytes(StandardCharsets.US_ASCII));
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 200"));
+ assertThat(response, Matchers.containsString("pathInfo=/path"));
+ assertThat(response, Matchers.containsString("local=[eeee:eeee:eeee:eeee:eeee:eeee:eeee:eeee]:8080"));
+ assertThat(response, Matchers.containsString("remote=[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]:12345"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("requestProcessors")
+ public void testLocalV2(RequestProcessor p) throws Exception
+ {
+ String proxy =
+ // Preamble
+ "0D0A0D0A000D0A515549540A" +
+
+ // V2, LOCAL
+ "20" +
+
+ // 0x1 : AF_INET 0x1 : STREAM.
+ "11" +
+
+ // Address length is 16.
+ "0010" +
+
+ // gibberish
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+ ;
+ String http = "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+
+ String response = p.sendRequestWaitingForResponse(TypeUtil.fromHexString(proxy), http.getBytes(StandardCharsets.US_ASCII));
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 200"));
+ assertThat(response, Matchers.containsString("pathInfo=/path"));
+ assertThat(response, Matchers.containsString("local=0.0.0.0:0"));
+ assertThat(response, Matchers.containsString("remote=0.0.0.0:0"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("requestProcessors")
+ public void testMissingField(RequestProcessor p) throws Exception
+ {
+ String request = "PROXY TCP 1.2.3.4 5.6.7.8 222\r\n" +
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = p.sendRequestWaitingForResponse(request);
+ assertNull(response);
+ }
+
+ @ParameterizedTest
+ @MethodSource("requestProcessors")
+ public void testNotComplete(RequestProcessor p) throws Exception
+ {
+ String response = p.customize(connector -> connector.setIdleTimeout(100)).sendRequestWaitingForResponse("PROXY TIMEOUT");
+ assertNull(response);
+ }
+
+ @ParameterizedTest
+ @MethodSource("requestProcessors")
+ public void testTooLong(RequestProcessor p) throws Exception
+ {
+ String request = "PROXY TOOLONG!!! eeee:eeee:eeee:eeee:0000:0000:0000:0000 ffff:ffff:ffff:ffff:0000:0000:0000:0000 65535 65535\r\n" +
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+
+ String response = p.sendRequestWaitingForResponse(request);
+
+ assertNull(response);
+ }
+
+ @ParameterizedTest
+ @MethodSource("requestProcessors")
+ public void testSimple(RequestProcessor p) throws Exception
+ {
+ String request = "PROXY TCP 1.2.3.4 5.6.7.8 111 222\r\n" +
+ "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+
+ String response = p.sendRequestWaitingForResponse(request);
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 200"));
+ assertThat(response, Matchers.containsString("pathInfo=/path"));
+ assertThat(response, Matchers.containsString("local=5.6.7.8:222"));
+ assertThat(response, Matchers.containsString("remote=1.2.3.4:111"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("requestProcessors")
+ public void testSimpleV2(RequestProcessor p) throws Exception
+ {
+ String proxy =
+ // Preamble
+ "0D0A0D0A000D0A515549540A" +
+
+ // V2, PROXY
+ "21" +
+
+ // 0x1 : AF_INET 0x1 : STREAM.
+ "11" +
+
+ // Address length is 2*4 + 2*2 = 12 bytes.
+ // length of remaining header (4+4+2+2 = 12)
+ "000C" +
+
+ // uint32_t src_addr; uint32_t dst_addr; uint16_t src_port; uint16_t dst_port;
+ "C0A80001" + // 192.168.0.1
+ "7f000001" + // 127.0.0.1
+ "3039" + // 12345
+ "1F90"; // 8080
+ String http = "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+
+ String response = p.sendRequestWaitingForResponse(TypeUtil.fromHexString(proxy), http.getBytes(StandardCharsets.US_ASCII));
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 200"));
+ assertThat(response, Matchers.containsString("pathInfo=/path"));
+ assertThat(response, Matchers.containsString("local=127.0.0.1:8080"));
+ assertThat(response, Matchers.containsString("remote=192.168.0.1:12345"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("requestProcessors")
+ public void testMaxHeaderLengthV2(RequestProcessor p) throws Exception
+ {
+ p.customize((connector) ->
+ {
+ ProxyConnectionFactory factory = (ProxyConnectionFactory)connector.getConnectionFactory("[proxy]");
+ factory.setMaxProxyHeader(11); // just one byte short
+ });
+ String proxy =
+ // Preamble
+ "0D0A0D0A000D0A515549540A" +
+
+ // V2, PROXY
+ "21" +
+
+ // 0x1 : AF_INET 0x1 : STREAM.
+ "11" +
+
+ // Address length is 2*4 + 2*2 = 12 bytes.
+ // length of remaining header (4+4+2+2 = 12)
+ "000C" +
+
+ // uint32_t src_addr; uint32_t dst_addr; uint16_t src_port; uint16_t dst_port;
+ "C0A80001" +
+ "7f000001" +
+ "3039" +
+ "1F90";
+ String http = "GET /path HTTP/1.1\n" +
+ "Host: server:80\n" +
+ "Connection: close\n" +
+ "\n";
+
+ String response = p.sendRequestWaitingForResponse(TypeUtil.fromHexString(proxy), http.getBytes(StandardCharsets.US_ASCII));
+
+ assertThat(response, Matchers.is(Matchers.nullValue()));
+ }
+
+ abstract static class RequestProcessor
+ {
+ protected LocalConnector _connector;
+ private Server _server;
+
+ public RequestProcessor()
+ {
+ _server = new Server();
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ http.getHttpConfiguration().setRequestHeaderSize(1024);
+ http.getHttpConfiguration().setResponseHeaderSize(1024);
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory(HttpVersion.HTTP_1_1.asString());
+
+ _connector = new LocalConnector(_server, null, null, null, 1, proxy, http);
+ _connector.setIdleTimeout(1000);
+ _server.addConnector(_connector);
+ _server.setHandler(new DumpHandler());
+ ErrorHandler eh = new ErrorHandler();
+ eh.setServer(_server);
+ _server.addBean(eh);
+ }
+
+ public RequestProcessor customize(Consumer<LocalConnector> consumer)
+ {
+ consumer.accept(_connector);
+ return this;
+ }
+
+ public final String sendRequestWaitingForResponse(String request) throws Exception
+ {
+ return sendRequestWaitingForResponse(request.getBytes(StandardCharsets.US_ASCII));
+ }
+
+ public final String sendRequestWaitingForResponse(byte[]... requests) throws Exception
+ {
+ try
+ {
+ _server.start();
+ return process(requests);
+ }
+ finally
+ {
+ destroy();
+ }
+ }
+
+ protected abstract String process(byte[]... requests) throws Exception;
+
+ private void destroy() throws Exception
+ {
+ _server.stop();
+ _server.join();
+ }
+ }
+
+ static Stream<Arguments> requestProcessors()
+ {
+ return Stream.of(
+ Arguments.of(new RequestProcessor()
+ {
+ @Override
+ public String process(byte[]... requests) throws Exception
+ {
+ LocalConnector.LocalEndPoint endPoint = _connector.connect();
+ for (byte[] request : requests)
+ {
+ endPoint.addInput(ByteBuffer.wrap(request));
+ }
+ return endPoint.getResponse();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "All bytes at once";
+ }
+ }),
+ Arguments.of(new RequestProcessor()
+ {
+ @Override
+ public String process(byte[]... requests) throws Exception
+ {
+ LocalConnector.LocalEndPoint endPoint = _connector.connect();
+ for (byte[] request : requests)
+ {
+ for (byte b : request)
+ {
+ endPoint.addInput(ByteBuffer.wrap(new byte[]{b}));
+ }
+ }
+ return endPoint.getResponse();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Byte by byte";
+ }
+ })
+ );
+ }
+
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyCustomizerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyCustomizerTest.java
new file mode 100644
index 0000000..4589c53
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyCustomizerTest.java
@@ -0,0 +1,178 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.TypeUtil;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class ProxyCustomizerTest
+{
+ private Server server;
+
+ private ProxyResponse sendProxyRequest(String proxyAsHexString, String rawHttp) throws IOException
+ {
+ try (Socket socket = new Socket(server.getURI().getHost(), server.getURI().getPort()))
+ {
+ OutputStream output = socket.getOutputStream();
+ output.write(TypeUtil.fromHexString(proxyAsHexString));
+ output.write(rawHttp.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+ socket.shutdownOutput();
+
+ StringBuilder sb = new StringBuilder();
+
+ InputStream input = socket.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
+ while (true)
+ {
+ String line = reader.readLine();
+ if (line == null)
+ break;
+ sb.append(line).append("\r\n");
+ }
+
+ return new ProxyResponse((InetSocketAddress)socket.getLocalSocketAddress(), (InetSocketAddress)socket.getRemoteSocketAddress(), sb.toString());
+ }
+ }
+
+ private static class ProxyResponse
+ {
+ private final InetSocketAddress localSocketAddress;
+ private final InetSocketAddress remoteSocketAddress;
+ private final String httpResponse;
+
+ public ProxyResponse(InetSocketAddress localSocketAddress, InetSocketAddress remoteSocketAddress, String httpResponse)
+ {
+ this.localSocketAddress = localSocketAddress;
+ this.remoteSocketAddress = remoteSocketAddress;
+ this.httpResponse = httpResponse;
+ }
+ }
+
+ @BeforeEach
+ void setUp() throws Exception
+ {
+ Handler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ {
+ response.addHeader("preexisting.attribute", request.getAttribute("some.attribute").toString());
+ ArrayList<String> attributeNames = Collections.list(request.getAttributeNames());
+ Collections.sort(attributeNames);
+ response.addHeader("attributeNames", String.join(",", attributeNames));
+
+ response.addHeader("localAddress", request.getLocalAddr() + ":" + request.getLocalPort());
+ response.addHeader("remoteAddress", request.getRemoteAddr() + ":" + request.getRemotePort());
+ Object localAddress = request.getAttribute(ProxyCustomizer.LOCAL_ADDRESS_ATTRIBUTE_NAME);
+ if (localAddress != null)
+ response.addHeader("proxyLocalAddress", localAddress.toString() + ":" + request.getAttribute(ProxyCustomizer.LOCAL_PORT_ATTRIBUTE_NAME));
+ Object remoteAddress = request.getAttribute(ProxyCustomizer.REMOTE_ADDRESS_ATTRIBUTE_NAME);
+ if (remoteAddress != null)
+ response.addHeader("proxyRemoteAddress", remoteAddress.toString() + ":" + request.getAttribute(ProxyCustomizer.REMOTE_PORT_ATTRIBUTE_NAME));
+
+ baseRequest.setHandled(true);
+ }
+ };
+
+ server = new Server();
+ HttpConfiguration httpConfiguration = new HttpConfiguration();
+ httpConfiguration.addCustomizer((connector, channelConfig, request) -> request.setAttribute("some.attribute", "some value"));
+ httpConfiguration.addCustomizer(new ProxyCustomizer());
+ ServerConnector connector = new ServerConnector(server, new ProxyConnectionFactory(), new HttpConnectionFactory(httpConfiguration));
+ server.addConnector(connector);
+ server.setHandler(handler);
+ server.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception
+ {
+ server.stop();
+ server = null;
+ }
+
+ @Test
+ public void testProxyCustomizerWithProxyData() throws Exception
+ {
+ String proxy =
+ // Preamble
+ "0D0A0D0A000D0A515549540A" +
+ // V2, PROXY
+ "21" +
+ // 0x1 : AF_INET 0x1 : STREAM. Address length is 2*4 + 2*2 = 12 bytes.
+ "11" +
+ // length of remaining header (4+4+2+2 = 12)
+ "000C" +
+ // uint32_t src_addr; uint32_t dst_addr; uint16_t src_port; uint16_t dst_port;
+ "01010001" +
+ "010100FE" +
+ "3039" +
+ "1F90";
+ String http = "GET /1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+
+ ProxyResponse response = sendProxyRequest(proxy, http);
+
+ assertThat(response.httpResponse, Matchers.containsString("localAddress: 1.1.0.254:8080"));
+ assertThat(response.httpResponse, Matchers.containsString("remoteAddress: 1.1.0.1:12345"));
+ assertThat(response.httpResponse, Matchers.containsString("proxyLocalAddress: " + response.remoteSocketAddress.getAddress().getHostAddress() + ":" + response.remoteSocketAddress.getPort()));
+ assertThat(response.httpResponse, Matchers.containsString("proxyRemoteAddress: " + response.localSocketAddress.getAddress().getHostAddress() + ":" + response.localSocketAddress.getPort()));
+ assertThat(response.httpResponse, Matchers.containsString("preexisting.attribute: some value"));
+ assertThat(response.httpResponse, Matchers.containsString("attributeNames: org.eclipse.jetty.proxy.local.address,org.eclipse.jetty.proxy.local.port,org.eclipse.jetty.proxy.remote.address,org.eclipse.jetty.proxy.remote.port,some.attribute"));
+ }
+
+ @Test
+ public void testProxyCustomizerWithoutProxyData() throws Exception
+ {
+ String proxy = "";
+ String http = "GET /1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+
+ ProxyResponse response = sendProxyRequest(proxy, http);
+
+ assertThat(response.httpResponse, Matchers.containsString("localAddress: " + response.remoteSocketAddress.getAddress().getHostAddress() + ":" + response.remoteSocketAddress.getPort()));
+ assertThat(response.httpResponse, Matchers.containsString("remoteAddress: " + response.localSocketAddress.getAddress().getHostAddress() + ":" + response.localSocketAddress.getPort()));
+ assertThat(response.httpResponse, Matchers.not(Matchers.containsString("proxyLocalAddress: ")));
+ assertThat(response.httpResponse, Matchers.not(Matchers.containsString("proxyRemoteAddress: ")));
+ assertThat(response.httpResponse, Matchers.containsString("preexisting.attribute: some value"));
+ assertThat(response.httpResponse, Matchers.containsString("attributeNames: some.attribute"));
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyProtocolTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyProtocolTest.java
new file mode 100644
index 0000000..7059dc4
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyProtocolTest.java
@@ -0,0 +1,273 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.TypeUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ProxyProtocolTest
+{
+ private Server server;
+ private ServerConnector connector;
+
+ private void start(Handler handler) throws Exception
+ {
+ server = new Server();
+ connector = new ServerConnector(server, new ProxyConnectionFactory(), new HttpConnectionFactory());
+ server.addConnector(connector);
+ server.setHandler(handler);
+ server.start();
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ if (server != null)
+ server.stop();
+ }
+
+ @Test
+ public void testProxyProtocolV1() throws Exception
+ {
+ final String remoteAddr = "192.168.0.0";
+ final int remotePort = 12345;
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (remoteAddr.equals(request.getRemoteAddr()) &&
+ remotePort == request.getRemotePort())
+ baseRequest.setHandled(true);
+ }
+ });
+
+ try (Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ String request1 =
+ "PROXY TCP4 " + remoteAddr + " 127.0.0.0 " + remotePort + " 8080\r\n" +
+ "GET /1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ OutputStream output = socket.getOutputStream();
+ output.write(request1.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ InputStream input = socket.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
+ String response1 = reader.readLine();
+ assertTrue(response1.startsWith("HTTP/1.1 200 "));
+ while (true)
+ {
+ if (reader.readLine().isEmpty())
+ break;
+ }
+
+ // Send a second request to verify that the proxied IP is retained.
+ String request2 =
+ "GET /2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ output.write(request2.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ String response2 = reader.readLine();
+ assertTrue(response2.startsWith("HTTP/1.1 200 "));
+ while (true)
+ {
+ if (reader.readLine() == null)
+ break;
+ }
+ }
+ }
+
+ @Test
+ public void testProxyProtocolV2() throws Exception
+ {
+ final String remoteAddr = "192.168.0.1";
+ final int remotePort = 12345;
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (remoteAddr.equals(request.getRemoteAddr()) &&
+ remotePort == request.getRemotePort())
+ baseRequest.setHandled(true);
+ }
+ });
+
+ try (Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ String proxy =
+ // Preamble
+ "0D0A0D0A000D0A515549540A" +
+
+ // V2, PROXY
+ "21" +
+
+ // 0x1 : AF_INET 0x1 : STREAM. Address length is 2*4 + 2*2 = 12 bytes.
+ "11" +
+
+ // length of remaining header (4+4+2+2+6+3 = 21)
+ "0015" +
+
+ // uint32_t src_addr; uint32_t dst_addr; uint16_t src_port; uint16_t dst_port;
+ "C0A80001" +
+ "7f000001" +
+ "3039" +
+ "1F90" +
+
+ // NOOP value 0
+ "040000" +
+
+ // NOOP value ABCDEF
+ "040003ABCDEF";
+
+ String request1 =
+ "GET /1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ OutputStream output = socket.getOutputStream();
+ output.write(TypeUtil.fromHexString(proxy));
+ output.write(request1.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ InputStream input = socket.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
+ String response1 = reader.readLine();
+ assertTrue(response1.startsWith("HTTP/1.1 200 "));
+ while (true)
+ {
+ if (reader.readLine().isEmpty())
+ break;
+ }
+
+ // Send a second request to verify that the proxied IP is retained.
+ String request2 =
+ "GET /2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ output.write(request2.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ String response2 = reader.readLine();
+ assertTrue(response2.startsWith("HTTP/1.1 200 "));
+ while (true)
+ {
+ if (reader.readLine() == null)
+ break;
+ }
+ }
+ }
+
+ @Test
+ public void testProxyProtocolV2Local() throws Exception
+ {
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ }
+ });
+
+ try (Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ String proxy =
+ // Preamble
+ "0D0A0D0A000D0A515549540A" +
+
+ // V2, LOCAL
+ "20" +
+
+ // 0x1 : AF_INET 0x1 : STREAM. Address length is 2*4 + 2*2 = 12 bytes.
+ "11" +
+
+ // length of remaining header (4+4+2+2+6+3 = 21)
+ "0015" +
+
+ // uint32_t src_addr; uint32_t dst_addr; uint16_t src_port; uint16_t dst_port;
+ "C0A80001" +
+ "7f000001" +
+ "3039" +
+ "1F90" +
+
+ // NOOP value 0
+ "040000" +
+
+ // NOOP value ABCDEF
+ "040003ABCDEF";
+
+ String request1 =
+ "GET /1 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ OutputStream output = socket.getOutputStream();
+ output.write(TypeUtil.fromHexString(proxy));
+ output.write(request1.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ InputStream input = socket.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
+ String response1 = reader.readLine();
+ assertTrue(response1.startsWith("HTTP/1.1 200 "));
+ while (true)
+ {
+ if (reader.readLine().isEmpty())
+ break;
+ }
+
+ // Send a second request to verify that the proxied IP is retained.
+ String request2 =
+ "GET /2 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ output.write(request2.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ String response2 = reader.readLine();
+ assertTrue(response2.startsWith("HTTP/1.1 200 "));
+ while (true)
+ {
+ if (reader.readLine() == null)
+ break;
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
new file mode 100644
index 0000000..02ff5ba
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
@@ -0,0 +1,2113 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
+import javax.servlet.DispatcherType;
+import javax.servlet.MultipartConfigElement;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletRequestEvent;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.Part;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HttpCompliance;
+import org.eclipse.jetty.http.HttpComplianceSection;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.server.LocalConnector.LocalEndPoint;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+@ExtendWith(WorkDirExtension.class)
+public class RequestTest
+{
+ private static final Logger LOG = Log.getLogger(RequestTest.class);
+ public WorkDir workDir;
+ private Server _server;
+ private LocalConnector _connector;
+ private RequestHandler _handler;
+ private boolean _normalizeAddress = true;
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ _server = new Server();
+ HttpConnectionFactory http = new HttpConnectionFactory()
+ {
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endPoint)
+ {
+ HttpConnection conn = new HttpConnection(getHttpConfiguration(), connector, endPoint, getHttpCompliance(), isRecordHttpComplianceViolations())
+ {
+ @Override
+ protected HttpChannelOverHttp newHttpChannel()
+ {
+ return new HttpChannelOverHttp(this, getConnector(), getHttpConfiguration(), getEndPoint(), this)
+ {
+ @Override
+ protected String formatAddrOrHost(String addr)
+ {
+ if (_normalizeAddress)
+ return super.formatAddrOrHost(addr);
+ return addr;
+ }
+ };
+ }
+ };
+ return configure(conn, connector, endPoint);
+ }
+ };
+ http.setInputBufferSize(1024);
+ http.getHttpConfiguration().setRequestHeaderSize(512);
+ http.getHttpConfiguration().setResponseHeaderSize(512);
+ http.getHttpConfiguration().setOutputBufferSize(2048);
+ http.getHttpConfiguration().addCustomizer(new ForwardedRequestCustomizer());
+ _connector = new LocalConnector(_server, http);
+ _server.addConnector(_connector);
+ _connector.setIdleTimeout(500);
+ _handler = new RequestHandler();
+ _server.setHandler(_handler);
+
+ ErrorHandler errors = new ErrorHandler();
+ errors.setServer(_server);
+ errors.setShowStacks(true);
+ _server.addBean(errors);
+ _server.start();
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ _server.stop();
+ _server.join();
+ }
+
+ @Test
+ public void testParamExtraction() throws Exception
+ {
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response)
+ {
+ try
+ {
+ // do the parse
+ request.getParameterMap();
+ return false;
+ }
+ catch (BadMessageException e)
+ {
+ // Should be able to retrieve the raw query
+ String rawQuery = request.getQueryString();
+ return rawQuery.equals("param=aaa%ZZbbb&other=value");
+ }
+ }
+ };
+
+ //Send a request with query string with illegal hex code to cause
+ //an exception parsing the params
+ String request = "GET /?param=aaa%ZZbbb&other=value HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: text/html;charset=utf8\n" +
+ "Connection: close\n" +
+ "\n";
+
+ String responses = _connector.getResponse(request);
+ assertTrue(responses.startsWith("HTTP/1.1 200"));
+ }
+
+ @Test
+ public void testParamExtractionBadSequence() throws Exception
+ {
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response)
+ {
+ request.getParameterMap();
+ // should have thrown a BadMessageException
+ return false;
+ }
+ };
+
+ //Send a request with query string with illegal hex code to cause
+ //an exception parsing the params
+ String request = "GET /?test_%e0%x8%81=missing HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: text/html;charset=utf8\n" +
+ "Connection: close\n" +
+ "\n";
+
+ String responses = _connector.getResponse(request);
+ assertThat("Responses", responses, startsWith("HTTP/1.1 400"));
+ }
+
+ @Test
+ public void testParamExtractionTimeout() throws Exception
+ {
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response)
+ {
+ request.getParameterMap();
+ // should have thrown a BadMessageException
+ return false;
+ }
+ };
+
+ //Send a request with query string with illegal hex code to cause
+ //an exception parsing the params
+ String request = "POST / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: " + MimeTypes.Type.FORM_ENCODED.asString() + "\n" +
+ "Connection: close\n" +
+ "Content-Length: 100\n" +
+ "\n" +
+ "name=value";
+
+ LocalEndPoint endp = _connector.connect();
+ endp.addInput(request);
+
+ String response = BufferUtil.toString(endp.waitForResponse(false, 1, TimeUnit.SECONDS));
+ assertThat("Responses", response, startsWith("HTTP/1.1 500"));
+ }
+
+ @Test
+ public void testEmptyHeaders() throws Exception
+ {
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response)
+ {
+ assertNotNull(request.getLocale());
+ assertTrue(request.getLocales().hasMoreElements()); // Default locale
+ assertEquals("", request.getContentType());
+ assertNull(request.getCharacterEncoding());
+ assertEquals(0, request.getQueryString().length());
+ assertEquals(-1, request.getContentLength());
+ assertNull(request.getCookies());
+ assertEquals("", request.getHeader("Name"));
+ assertTrue(request.getHeaders("Name").hasMoreElements()); // empty
+ assertThrows(IllegalArgumentException.class, () -> request.getDateHeader("Name"));
+ assertEquals(-1, request.getDateHeader("Other"));
+ return true;
+ }
+ };
+
+ String request = "GET /? HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Connection: close\n" +
+ "Content-Type: \n" +
+ "Accept-Language: \n" +
+ "Cookie: \n" +
+ "Name: \n" +
+ "\n";
+
+ String responses = _connector.getResponse(request);
+ assertTrue(responses.startsWith("HTTP/1.1 200"));
+ }
+
+ @Test
+ public void testMultiPartNoConfig() throws Exception
+ {
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response)
+ {
+ try
+ {
+ request.getPart("stuff");
+ return false;
+ }
+ catch (IllegalStateException e)
+ {
+ //expected exception because no multipart config is set up
+ assertTrue(e.getMessage().startsWith("No multipart config"));
+ return true;
+ }
+ catch (Exception e)
+ {
+ return false;
+ }
+ }
+ };
+
+ String multipart = "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"field1\"\r\n" +
+ "\r\n" +
+ "Joe Blow\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"\r\n" +
+ "Content-Type: text/plain;charset=ISO-8859-1\r\n" +
+ "\r\n" +
+ "000000000000000000000000000000000000000000000000000\r\n" +
+ "--AaB03x--\r\n";
+
+ String request = "GET / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: multipart/form-data; boundary=\"AaB03x\"\r\n" +
+ "Content-Length: " + multipart.getBytes().length + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ multipart;
+
+ String responses = _connector.getResponse(request);
+ assertTrue(responses.startsWith("HTTP/1.1 200"));
+ }
+
+ @Test
+ public void testLocale() throws Exception
+ {
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ assertThat(request.getLocale().getLanguage(), is("da"));
+ Enumeration<Locale> locales = request.getLocales();
+ Locale locale = locales.nextElement();
+ assertThat(locale.getLanguage(), is("da"));
+ assertThat(locale.getCountry(), is(""));
+ locale = locales.nextElement();
+ assertThat(locale.getLanguage(), is("en"));
+ assertThat(locale.getCountry(), is("AU"));
+ locale = locales.nextElement();
+ assertThat(locale.getLanguage(), is("en"));
+ assertThat(locale.getCountry(), is("GB"));
+ locale = locales.nextElement();
+ assertThat(locale.getLanguage(), is("en"));
+ assertThat(locale.getCountry(), is(""));
+ assertFalse(locales.hasMoreElements());
+ return true;
+ }
+ };
+
+ String request = "GET / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Connection: close\r\n" +
+ "Accept-Language: da, en-gb;q=0.8, en;q=0.7\r\n" +
+ "Accept-Language: XX;q=0, en-au;q=0.9\r\n" +
+ "\r\n";
+ String response = _connector.getResponse(request);
+ assertThat(response, containsString(" 200 OK"));
+ }
+
+ @Test
+ public void testMultiPart() throws Exception
+ {
+ Path testTmpDir = workDir.getEmptyPathDir();
+
+ ContextHandler contextHandler = new ContextHandler();
+ contextHandler.setContextPath("/foo");
+ contextHandler.setResourceBase(".");
+ contextHandler.setHandler(new MultiPartRequestHandler(testTmpDir.toFile()));
+ contextHandler.addEventListener(new MultiPartCleanerListener()
+ {
+ @Override
+ public void requestDestroyed(ServletRequestEvent sre)
+ {
+ MultiParts m = (MultiParts)sre.getServletRequest().getAttribute(Request.MULTIPARTS);
+ assertNotNull(m);
+ ContextHandler.Context c = m.getContext();
+ assertNotNull(c);
+ assertSame(c, sre.getServletContext());
+ assertFalse(m.isEmpty());
+ assertThat("File count in temp dir", getFileCount(testTmpDir), is(2L));
+ super.requestDestroyed(sre);
+ assertThat("File count in temp dir", getFileCount(testTmpDir), is(0L));
+ }
+ });
+ _server.stop();
+ _server.setHandler(contextHandler);
+ _server.start();
+
+ String multipart = "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"field1\"\r\n" +
+ "\r\n" +
+ "Joe Blow\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"foo.upload\"\r\n" +
+ "Content-Type: text/plain;charset=ISO-8859-1\r\n" +
+ "\r\n" +
+ "000000000000000000000000000000000000000000000000000\r\n" +
+ "--AaB03x--\r\n";
+
+ String request = "GET /foo/x.html HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: multipart/form-data; boundary=\"AaB03x\"\r\n" +
+ "Content-Length: " + multipart.getBytes().length + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ multipart;
+
+ String responses = _connector.getResponse(request);
+ //System.err.println(responses);
+ assertTrue(responses.startsWith("HTTP/1.1 200"));
+ }
+
+ @Test
+ public void testUtilMultiPart() throws Exception
+ {
+ Path testTmpDir = workDir.getEmptyPathDir();
+
+ ContextHandler contextHandler = new ContextHandler();
+ contextHandler.setContextPath("/foo");
+ contextHandler.setResourceBase(".");
+ contextHandler.setHandler(new MultiPartRequestHandler(testTmpDir.toFile()));
+ contextHandler.addEventListener(new MultiPartCleanerListener()
+ {
+ @Override
+ public void requestDestroyed(ServletRequestEvent sre)
+ {
+ MultiParts m = (MultiParts)sre.getServletRequest().getAttribute(Request.MULTIPARTS);
+ assertNotNull(m);
+ ContextHandler.Context c = m.getContext();
+ assertNotNull(c);
+ assertSame(c, sre.getServletContext());
+ assertFalse(m.isEmpty());
+ assertThat("File count in temp dir", getFileCount(testTmpDir), is(2L));
+ super.requestDestroyed(sre);
+ assertThat("File count in temp dir", getFileCount(testTmpDir), is(0L));
+ }
+ });
+ _server.stop();
+ _server.setHandler(contextHandler);
+ _connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setMultiPartFormDataCompliance(MultiPartFormDataCompliance.LEGACY);
+ _server.start();
+
+ String multipart = " --AaB03x\r" +
+ "content-disposition: form-data; name=\"field1\"\r" +
+ "\r" +
+ "Joe Blow\r" +
+ "--AaB03x\r" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"foo.upload\"\r" +
+ "Content-Type: text/plain;charset=ISO-8859-1\r" +
+ "\r" +
+ "000000000000000000000000000000000000000000000000000\r" +
+ "--AaB03x--\r";
+
+ String request = "GET /foo/x.html HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: multipart/form-data; boundary=\"AaB03x\"\r\n" +
+ "Content-Length: " + multipart.getBytes().length + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ multipart;
+
+ String responses = _connector.getResponse(request);
+ //System.err.println(responses);
+ assertThat(responses, Matchers.startsWith("HTTP/1.1 200"));
+ assertThat(responses, Matchers.containsString("Violation: CR_LINE_TERMINATION"));
+ assertThat(responses, Matchers.containsString("Violation: NO_CRLF_AFTER_PREAMBLE"));
+ }
+
+ @Test
+ public void testHttpMultiPart() throws Exception
+ {
+ Path testTmpDir = workDir.getEmptyPathDir();
+
+ ContextHandler contextHandler = new ContextHandler();
+ contextHandler.setContextPath("/foo");
+ contextHandler.setResourceBase(".");
+ contextHandler.setHandler(new MultiPartRequestHandler(testTmpDir.toFile()));
+
+ _server.stop();
+ _server.setHandler(contextHandler);
+ _connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setMultiPartFormDataCompliance(MultiPartFormDataCompliance.RFC7578);
+ _server.start();
+
+ String multipart = " --AaB03x\r" +
+ "content-disposition: form-data; name=\"field1\"\r" +
+ "\r" +
+ "Joe Blow\r" +
+ "--AaB03x\r" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"foo.upload\"\r" +
+ "Content-Type: text/plain;charset=ISO-8859-1\r" +
+ "\r" +
+ "000000000000000000000000000000000000000000000000000\r" +
+ "--AaB03x--\r";
+
+ String request = "GET /foo/x.html HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: multipart/form-data; boundary=\"AaB03x\"\r\n" +
+ "Content-Length: " + multipart.getBytes().length + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ multipart;
+
+ String responses = _connector.getResponse(request);
+ //System.err.println(responses);
+ assertThat(responses, Matchers.startsWith("HTTP/1.1 500"));
+ }
+
+ @Test
+ public void testBadMultiPart() throws Exception
+ {
+ //a bad multipart where one of the fields has no name
+ Path testTmpDir = workDir.getEmptyPathDir();
+
+ ContextHandler contextHandler = new ContextHandler();
+ contextHandler.setContextPath("/foo");
+ contextHandler.setResourceBase(".");
+ contextHandler.setHandler(new BadMultiPartRequestHandler(testTmpDir.toFile()));
+ contextHandler.addEventListener(new MultiPartCleanerListener()
+ {
+
+ @Override
+ public void requestDestroyed(ServletRequestEvent sre)
+ {
+ MultiParts m = (MultiParts)sre.getServletRequest().getAttribute(Request.MULTIPARTS);
+ assertNotNull(m);
+ ContextHandler.Context c = m.getContext();
+ assertNotNull(c);
+ assertSame(c, sre.getServletContext());
+ super.requestDestroyed(sre);
+ assertThat("File count in temp dir", getFileCount(testTmpDir), is(0L));
+ }
+ });
+ _server.stop();
+ _server.setHandler(contextHandler);
+ _server.start();
+
+ String multipart = "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"xxx\"\r\n" +
+ "\r\n" +
+ "Joe Blow\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; filename=\"foo.upload\"\r\n" +
+ "Content-Type: text/plain;charset=ISO-8859-1\r\n" +
+ "\r\n" +
+ "000000000000000000000000000000000000000000000000000\r\n" +
+ "--AaB03x--\r\n";
+
+ String request = "GET /foo/x.html HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: multipart/form-data; boundary=\"AaB03x\"\r\n" +
+ "Content-Length: " + multipart.getBytes().length + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ multipart;
+
+ try (StacklessLogging stackless = new StacklessLogging(HttpChannel.class))
+ {
+ String responses = _connector.getResponse(request);
+ //System.err.println(responses);
+ assertTrue(responses.startsWith("HTTP/1.1 500"));
+ }
+ }
+
+ @Test
+ public void testBadUtf8ParamExtraction() throws Exception
+ {
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response)
+ {
+ try
+ {
+ // This throws an exception if attempted
+ request.getParameter("param");
+ return false;
+ }
+ catch (BadMessageException e)
+ {
+ // Should still be able to get the raw query.
+ String rawQuery = request.getQueryString();
+ return rawQuery.equals("param=aaa%E7bbb");
+ }
+ }
+ };
+
+ //Send a request with query string with illegal hex code to cause
+ //an exception parsing the params
+ String request = "GET /?param=aaa%E7bbb HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: text/html;charset=utf8\n" +
+ "Connection: close\n" +
+ "\n";
+
+ LOG.info("Expecting NotUtf8Exception in state 36...");
+ String responses = _connector.getResponse(request);
+ assertThat(responses, startsWith("HTTP/1.1 200"));
+ }
+
+ @Test
+ public void testEncodedParamExtraction() throws Exception
+ {
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response)
+ {
+ try
+ {
+ // This throws an exception if attempted
+ request.getParameter("param");
+ return false;
+ }
+ catch (BadMessageException e)
+ {
+ return e.getCode() == 415;
+ }
+ }
+ };
+
+ //Send a request with encoded form content
+ String request = "POST / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: application/x-www-form-urlencoded; charset=utf-8\n" +
+ "Content-Length: 10\n" +
+ "Content-Encoding: gzip\n" +
+ "Connection: close\n" +
+ "\n" +
+ "0123456789\n";
+
+ String responses = _connector.getResponse(request);
+ assertThat(responses, startsWith("HTTP/1.1 200"));
+ }
+
+ @Test
+ public void testContentLengthExceedsMaxInteger() throws Exception
+ {
+ final long HUGE_LENGTH = (long)Integer.MAX_VALUE * 10L;
+
+ _handler._checker = (request, response) ->
+ request.getContentLength() == (-1) && // per HttpServletRequest javadoc this must return (-1);
+ request.getContentLengthLong() == HUGE_LENGTH;
+
+ //Send a request with encoded form content
+ String request = "POST / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: application/octet-stream\n" +
+ "Content-Length: " + HUGE_LENGTH + "\n" +
+ "Connection: close\n" +
+ "\n" +
+ "<insert huge amount of content here>\n";
+
+ System.out.println(request);
+
+ String responses = _connector.getResponse(request);
+ assertThat(responses, startsWith("HTTP/1.1 200"));
+ }
+
+ /**
+ * The Servlet spec and API cannot parse Content-Length that exceeds Long.MAX_VALUE
+ */
+ @Test
+ public void testContentLengthExceedsMaxLong() throws Exception
+ {
+ String hugeLength = Long.MAX_VALUE + "0";
+
+ _handler._checker = (request, response) ->
+ request.getHeader("Content-Length").equals(hugeLength) &&
+ request.getContentLength() == (-1) && // per HttpServletRequest javadoc this must return (-1);
+ request.getContentLengthLong() == (-1); // exact behavior here not specified in Servlet javadoc
+
+ //Send a request with encoded form content
+ String request = "POST / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: application/octet-stream\n" +
+ "Content-Length: " + hugeLength + "\n" +
+ "Connection: close\n" +
+ "\n" +
+ "<insert huge amount of content here>\n";
+
+ String responses = _connector.getResponse(request);
+ assertThat(responses, startsWith("HTTP/1.1 400"));
+ }
+
+ @Test
+ public void testIdentityParamExtraction() throws Exception
+ {
+ _handler._checker = (request, response) -> "bar".equals(request.getParameter("foo"));
+
+ //Send a request with encoded form content
+ String request = "POST / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: application/x-www-form-urlencoded; charset=utf-8\n" +
+ "Content-Length: 7\n" +
+ "Content-Encoding: identity\n" +
+ "Connection: close\n" +
+ "\n" +
+ "foo=bar\n";
+
+ String responses = _connector.getResponse(request);
+ assertThat(responses, startsWith("HTTP/1.1 200"));
+ }
+
+ @Test
+ public void testEncodedNotParams() throws Exception
+ {
+ _handler._checker = (request, response) -> request.getParameter("param") == null;
+
+ //Send a request with encoded form content
+ String request = "POST / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: application/octet-stream\n" +
+ "Content-Length: 10\n" +
+ "Content-Encoding: gzip\n" +
+ "Connection: close\n" +
+ "\n" +
+ "0123456789\n";
+
+ String responses = _connector.getResponse(request);
+ assertThat(responses, startsWith("HTTP/1.1 200"));
+ }
+
+ @Test
+ public void testInvalidHostHeader() throws Exception
+ {
+ // Use a contextHandler with vhosts to force call to Request.getServerName()
+ ContextHandler context = new ContextHandler();
+ context.addVirtualHosts(new String[]{"something"});
+ _server.stop();
+ _server.setHandler(context);
+ _server.start();
+
+ // Request with illegal Host header
+ String request = "GET / HTTP/1.1\n" +
+ "Host: whatever.com:xxxx\n" +
+ "Content-Type: text/html;charset=utf8\n" +
+ "Connection: close\n" +
+ "\n";
+
+ String responses = _connector.getResponse(request);
+ assertThat(responses, Matchers.startsWith("HTTP/1.1 400"));
+ }
+
+ @Test
+ public void testContentTypeEncoding() throws Exception
+ {
+ final ArrayList<String> results = new ArrayList<>();
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response)
+ {
+ results.add(request.getContentType());
+ results.add(request.getCharacterEncoding());
+ return true;
+ }
+ };
+
+ LocalEndPoint endp = _connector.executeRequest(
+ "GET / HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Content-Type: text/test\n" +
+ "\n" +
+
+ "GET / HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Content-Type: text/html;charset=utf8\n" +
+ "\n" +
+
+ "GET / HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Content-Type: text/html; charset=\"utf8\"\n" +
+ "\n" +
+
+ "GET / HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Content-Type: text/html; other=foo ; blah=\"charset=wrong;\" ; charset = \" x=z; \" ; more=values \n" +
+ "Connection: close\n" +
+ "\n"
+ );
+
+ endp.getResponse();
+ endp.getResponse();
+ endp.getResponse();
+ endp.getResponse();
+
+ int i = 0;
+ assertEquals("text/test", results.get(i++));
+ assertEquals(null, results.get(i++));
+
+ assertEquals("text/html;charset=utf8", results.get(i++));
+ assertEquals("utf-8", results.get(i++));
+
+ assertEquals("text/html; charset=\"utf8\"", results.get(i++));
+ assertEquals("utf-8", results.get(i++));
+
+ assertTrue(results.get(i++).startsWith("text/html"));
+ assertEquals(" x=z; ", results.get(i++));
+ }
+
+ @Test
+ public void testHostPort() throws Exception
+ {
+ final ArrayList<String> results = new ArrayList<>();
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response)
+ {
+ results.add(request.getRequestURL().toString());
+ results.add(request.getRemoteAddr());
+ results.add(request.getServerName());
+ results.add(String.valueOf(request.getServerPort()));
+ return true;
+ }
+ };
+
+ results.clear();
+ String response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: myhost\n" +
+ "Connection: close\n" +
+ "\n");
+ int i = 0;
+ assertThat(response, containsString("200 OK"));
+ assertEquals("http://myhost/", results.get(i++));
+ assertEquals("0.0.0.0", results.get(i++));
+ assertEquals("myhost", results.get(i++));
+ assertEquals("80", results.get(i++));
+
+ results.clear();
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: myhost:8888\n" +
+ "Connection: close\n" +
+ "\n");
+ i = 0;
+ assertThat(response, containsString("200 OK"));
+ assertEquals("http://myhost:8888/", results.get(i++));
+ assertEquals("0.0.0.0", results.get(i++));
+ assertEquals("myhost", results.get(i++));
+ assertEquals("8888", results.get(i++));
+
+ results.clear();
+ response = _connector.getResponse(
+ "GET http://myhost:8888/ HTTP/1.0\n" +
+ "\n");
+ i = 0;
+ assertThat(response, containsString("200 OK"));
+ assertEquals("http://myhost:8888/", results.get(i++));
+ assertEquals("0.0.0.0", results.get(i++));
+ assertEquals("myhost", results.get(i++));
+ assertEquals("8888", results.get(i++));
+
+ results.clear();
+ response = _connector.getResponse(
+ "GET http://myhost:8888/ HTTP/1.1\n" +
+ "Host: wrong:666\n" +
+ "Connection: close\n" +
+ "\n");
+ i = 0;
+ assertThat(response, containsString("200 OK"));
+ assertEquals("http://myhost:8888/", results.get(i++));
+ assertEquals("0.0.0.0", results.get(i++));
+ assertEquals("myhost", results.get(i++));
+ assertEquals("8888", results.get(i++));
+
+ results.clear();
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: 1.2.3.4\n" +
+ "Connection: close\n" +
+ "\n");
+ i = 0;
+
+ assertThat(response, containsString("200 OK"));
+ assertEquals("http://1.2.3.4/", results.get(i++));
+ assertEquals("0.0.0.0", results.get(i++));
+ assertEquals("1.2.3.4", results.get(i++));
+ assertEquals("80", results.get(i++));
+
+ results.clear();
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: 1.2.3.4:8888\n" +
+ "Connection: close\n" +
+ "\n");
+ i = 0;
+ assertThat(response, containsString("200 OK"));
+ assertEquals("http://1.2.3.4:8888/", results.get(i++));
+ assertEquals("0.0.0.0", results.get(i++));
+ assertEquals("1.2.3.4", results.get(i++));
+ assertEquals("8888", results.get(i++));
+
+ results.clear();
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: [::1]\n" +
+ "Connection: close\n" +
+ "\n");
+ i = 0;
+ assertThat(response, containsString("200 OK"));
+ assertEquals("http://[::1]/", results.get(i++));
+ assertEquals("0.0.0.0", results.get(i++));
+ assertEquals("[::1]", results.get(i++));
+ assertEquals("80", results.get(i++));
+
+ results.clear();
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: [::1]:8888\n" +
+ "Connection: close\n" +
+ "\n");
+ i = 0;
+ assertThat(response, containsString("200 OK"));
+ assertEquals("http://[::1]:8888/", results.get(i++));
+ assertEquals("0.0.0.0", results.get(i++));
+ assertEquals("[::1]", results.get(i++));
+ assertEquals("8888", results.get(i++));
+
+ results.clear();
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: [::1]\n" +
+ "x-forwarded-for: remote\n" +
+ "x-forwarded-proto: https\n" +
+ "Connection: close\n" +
+ "\n");
+ i = 0;
+ assertThat(response, containsString("200 OK"));
+ assertEquals("https://[::1]/", results.get(i++));
+ assertEquals("remote", results.get(i++));
+ assertEquals("[::1]", results.get(i++));
+ assertEquals("443", results.get(i++));
+
+ results.clear();
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: [::1]:8888\n" +
+ "Connection: close\n" +
+ "x-forwarded-for: remote\n" +
+ "x-forwarded-proto: https\n" +
+ "\n");
+ i = 0;
+ assertThat(response, containsString("200 OK"));
+ assertEquals("https://[::1]:8888/", results.get(i++));
+ assertEquals("remote", results.get(i++));
+ assertEquals("[::1]", results.get(i++));
+ assertEquals("8888", results.get(i++));
+ }
+
+ @Test
+ public void testIPv6() throws Exception
+ {
+ final ArrayList<String> results = new ArrayList<>();
+ final InetAddress local = Inet6Address.getByAddress("localIPv6", new byte[]{
+ 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8
+ });
+ final InetSocketAddress localAddr = new InetSocketAddress(local, 32768);
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response)
+ {
+ ((Request)request).setRemoteAddr(localAddr);
+ results.add(request.getRemoteAddr());
+ results.add(request.getRemoteHost());
+ results.add(Integer.toString(request.getRemotePort()));
+ results.add(request.getServerName());
+ results.add(Integer.toString(request.getServerPort()));
+ results.add(request.getLocalAddr());
+ results.add(Integer.toString(request.getLocalPort()));
+ return true;
+ }
+ };
+
+ _normalizeAddress = true;
+ String response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: [::1]:8888\n" +
+ "Connection: close\n" +
+ "\n");
+ int i = 0;
+ assertThat(response, containsString("200 OK"));
+ assertEquals("[1:2:3:4:5:6:7:8]", results.get(i++));
+ assertEquals("localIPv6", results.get(i++));
+ assertEquals("32768", results.get(i++));
+ assertEquals("[::1]", results.get(i++));
+ assertEquals("8888", results.get(i++));
+ assertEquals("0.0.0.0", results.get(i++));
+ assertEquals("0", results.get(i));
+
+ _normalizeAddress = false;
+ results.clear();
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: [::1]:8888\n" +
+ "Connection: close\n" +
+ "\n");
+ i = 0;
+ assertThat(response, containsString("200 OK"));
+ assertEquals("1:2:3:4:5:6:7:8", results.get(i++));
+ assertEquals("localIPv6", results.get(i++));
+ assertEquals("32768", results.get(i++));
+ assertEquals("[::1]", results.get(i++));
+ assertEquals("8888", results.get(i++));
+ assertEquals("0.0.0.0", results.get(i++));
+ assertEquals("0", results.get(i));
+ }
+
+ @Test
+ public void testContent() throws Exception
+ {
+ final AtomicInteger length = new AtomicInteger();
+
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ int len = request.getContentLength();
+ ServletInputStream in = request.getInputStream();
+ for (int i = 0; i < len; i++)
+ {
+ int b = in.read();
+ if (b < 0)
+ return false;
+ }
+ if (in.read() > 0)
+ return false;
+
+ length.set(len);
+ return true;
+ }
+ };
+
+ String content = "";
+
+ for (int l = 0; l < 1024; l++)
+ {
+ String request = "POST / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: multipart/form-data-test\r\n" +
+ "Content-Length: " + l + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ content;
+ Log.getRootLogger().debug("test l={}", l);
+ String response = _connector.getResponse(request);
+ Log.getRootLogger().debug(response);
+ assertThat(response, containsString(" 200 OK"));
+ assertEquals(l, length.get());
+ content += "x";
+ }
+ }
+
+ @Test
+ public void testEncodedForm() throws Exception
+ {
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ String actual = request.getParameter("name2");
+ return "test2".equals(actual);
+ }
+ };
+
+ String content = "name1=test&name2=test2&name3=&name4=test";
+ String request = "POST / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: " + MimeTypes.Type.FORM_ENCODED.asString() + "\r\n" +
+ "Content-Length: " + content.length() + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ content;
+ String response = _connector.getResponse(request);
+ assertThat(response, containsString(" 200 OK"));
+ }
+
+ @Test
+ public void testEncodedFormUnknownMethod() throws Exception
+ {
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ return request.getParameter("name1") == null && request.getParameter("name2") == null && request.getParameter("name3") == null;
+ }
+ };
+
+ String content = "name1=test&name2=test2&name3=&name4=test";
+ String request = "UNKNOWN / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: " + MimeTypes.Type.FORM_ENCODED.asString() + "\r\n" +
+ "Content-Length: " + content.length() + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ content;
+ String response = _connector.getResponse(request);
+ assertThat(response, containsString(" 200 OK"));
+ }
+
+ @Test
+ public void testEncodedFormExtraMethod() throws Exception
+ {
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ String actual = request.getParameter("name2");
+ return "test2".equals(actual);
+ }
+ };
+
+ _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().addFormEncodedMethod("Extra");
+ String content = "name1=test&name2=test2&name3=&name4=test";
+ String request = "EXTRA / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: " + MimeTypes.Type.FORM_ENCODED.asString() + "\r\n" +
+ "Content-Length: " + content.length() + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ content;
+ String response = _connector.getResponse(request);
+ assertThat(response, containsString(" 200 OK"));
+ }
+
+ @Test
+ public void test8859EncodedForm() throws Exception
+ {
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ // Should be "testä"
+ // "test" followed by a LATIN SMALL LETTER A WITH DIAERESIS
+ request.setCharacterEncoding(StandardCharsets.ISO_8859_1.name());
+ String actual = request.getParameter("name2");
+ return "test\u00e4".equals(actual);
+ }
+ };
+
+ String content = "name1=test&name2=test%E4&name3=&name4=test";
+ String request = "POST / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: " + MimeTypes.Type.FORM_ENCODED.asString() + "\r\n" +
+ "Content-Length: " + content.length() + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ content;
+ String response = _connector.getResponse(request);
+ assertThat(response, containsString(" 200 OK"));
+ }
+
+ @Test
+ public void testUTF8EncodedForm() throws Exception
+ {
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ // http://www.ltg.ed.ac.uk/~richard/utf-8.cgi?input=00e4&mode=hex
+ // Should be "testä"
+ // "test" followed by a LATIN SMALL LETTER A WITH DIAERESIS
+ String actual = request.getParameter("name2");
+ return "test\u00e4".equals(actual);
+ }
+ };
+
+ String content = "name1=test&name2=test%C3%A4&name3=&name4=test";
+ String request = "POST / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: " + MimeTypes.Type.FORM_ENCODED.asString() + "\r\n" +
+ "Content-Length: " + content.length() + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ content;
+ String response = _connector.getResponse(request);
+ assertThat(response, containsString(" 200 OK"));
+ }
+
+ @Test
+ @Disabled("See issue #1175")
+ public void testMultiPartFormDataReadInputThenParams() throws Exception
+ {
+ final File tmpdir = MavenTestingUtils.getTargetTestingDir("multipart");
+ FS.ensureEmpty(tmpdir);
+
+ Handler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException,
+ ServletException
+ {
+ if (baseRequest.getDispatcherType() != DispatcherType.REQUEST)
+ return;
+
+ // Fake a @MultiPartConfig'd servlet endpoint
+ MultipartConfigElement multipartConfig = new MultipartConfigElement(tmpdir.getAbsolutePath());
+ request.setAttribute(Request.MULTIPART_CONFIG_ELEMENT, multipartConfig);
+
+ // Normal processing
+ baseRequest.setHandled(true);
+
+ // Fake the commons-fileupload behavior
+ int length = request.getContentLength();
+ InputStream in = request.getInputStream();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ IO.copy(in, out, length); // KEY STEP (Don't Change!) commons-fileupload does not read to EOF
+
+ // Record what happened as servlet response headers
+ response.setIntHeader("x-request-content-length", request.getContentLength());
+ response.setIntHeader("x-request-content-read", out.size());
+ String foo = request.getParameter("foo"); // uri query parameter
+ String bar = request.getParameter("bar"); // form-data content parameter
+ response.setHeader("x-foo", foo == null ? "null" : foo);
+ response.setHeader("x-bar", bar == null ? "null" : bar);
+ }
+ };
+
+ _server.stop();
+ _server.setHandler(handler);
+ _server.start();
+
+ String multipart = "--AaBbCc\r\n" +
+ "content-disposition: form-data; name=\"bar\"\r\n" +
+ "\r\n" +
+ "BarContent\r\n" +
+ "--AaBbCc\r\n" +
+ "content-disposition: form-data; name=\"stuff\"\r\n" +
+ "Content-Type: text/plain;charset=ISO-8859-1\r\n" +
+ "\r\n" +
+ "000000000000000000000000000000000000000000000000000\r\n" +
+ "--AaBbCc--\r\n";
+
+ String request = "POST /?foo=FooUri HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: multipart/form-data; boundary=\"AaBbCc\"\r\n" +
+ "Content-Length: " + multipart.getBytes().length + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ multipart;
+
+ HttpTester.Response response = HttpTester.parseResponse(_connector.getResponse(request));
+
+ // It should always be possible to read query string
+ assertThat("response.x-foo", response.get("x-foo"), is("FooUri"));
+ // Not possible to read request content parameters?
+ assertThat("response.x-bar", response.get("x-bar"), is("null")); // TODO: should this work?
+ }
+
+ @Test
+ public void testPartialRead() throws Exception
+ {
+ Handler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException,
+ ServletException
+ {
+ baseRequest.setHandled(true);
+ Reader reader = request.getReader();
+ byte[] b = ("read=" + reader.read() + "\n").getBytes(StandardCharsets.UTF_8);
+ response.setContentLength(b.length);
+ response.getOutputStream().write(b);
+ response.flushBuffer();
+ }
+ };
+ _server.stop();
+ _server.setHandler(handler);
+ _server.start();
+
+ String requests = "GET / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: text/plane\r\n" +
+ "Content-Length: " + 10 + "\r\n" +
+ "\r\n" +
+ "0123456789\r\n" +
+ "GET / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: text/plane\r\n" +
+ "Content-Length: " + 10 + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "ABCDEFGHIJ\r\n";
+
+ LocalEndPoint endp = _connector.executeRequest(requests);
+ String responses = endp.getResponse() + endp.getResponse();
+
+ int index = responses.indexOf("read=" + (int)'0');
+ assertTrue(index > 0);
+
+ index = responses.indexOf("read=" + (int)'A', index + 7);
+ assertTrue(index > 0);
+ }
+
+ @Test
+ public void testQueryAfterRead()
+ throws Exception
+ {
+ Handler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException,
+ ServletException
+ {
+ baseRequest.setHandled(true);
+ Reader reader = request.getReader();
+ String in = IO.toString(reader);
+ String param = request.getParameter("param");
+
+ byte[] b = ("read='" + in + "' param=" + param + "\n").getBytes(StandardCharsets.UTF_8);
+ response.setContentLength(b.length);
+ response.getOutputStream().write(b);
+ response.flushBuffer();
+ }
+ };
+ _server.stop();
+ _server.setHandler(handler);
+ _server.start();
+
+ String request = "POST /?param=right HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: " + 11 + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "param=wrong\r\n";
+
+ String responses = _connector.getResponse(request);
+
+ assertTrue(responses.indexOf("read='param=wrong' param=right") > 0);
+ }
+
+ @Test
+ public void testSessionAfterRedirect() throws Exception
+ {
+ Handler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException,
+ ServletException
+ {
+ baseRequest.setHandled(true);
+ response.sendRedirect("/foo");
+ try
+ {
+ request.getSession(true);
+ fail("Session should not be created after response committed");
+ }
+ catch (IllegalStateException e)
+ {
+ //expected
+ }
+ catch (Exception e)
+ {
+ fail("Session creation after response commit should throw IllegalStateException");
+ }
+ }
+ };
+ _server.stop();
+ _server.setHandler(handler);
+ _server.start();
+ String response = _connector.getResponse("GET / HTTP/1.1\n" +
+ "Host: myhost\n" +
+ "Connection: close\n" +
+ "\n");
+ assertThat(response, containsString(" 302 Found"));
+ assertThat(response, containsString("Location: http://myhost/foo"));
+ }
+
+ @Test
+ public void testPartialInput() throws Exception
+ {
+ Handler handler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException,
+ ServletException
+ {
+ baseRequest.setHandled(true);
+ InputStream in = request.getInputStream();
+ byte[] b = ("read=" + in.read() + "\n").getBytes(StandardCharsets.UTF_8);
+ response.setContentLength(b.length);
+ response.getOutputStream().write(b);
+ response.flushBuffer();
+ }
+ };
+ _server.stop();
+ _server.setHandler(handler);
+ _server.start();
+
+ String requests = "GET / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: text/plane\r\n" +
+ "Content-Length: " + 10 + "\r\n" +
+ "\r\n" +
+ "0123456789\r\n" +
+ "GET / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: text/plane\r\n" +
+ "Content-Length: " + 10 + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ "ABCDEFGHIJ\r\n";
+
+ LocalEndPoint endp = _connector.executeRequest(requests);
+ String responses = endp.getResponse() + endp.getResponse();
+
+ int index = responses.indexOf("read=" + (int)'0');
+ assertTrue(index > 0);
+
+ index = responses.indexOf("read=" + (int)'A', index + 7);
+ assertTrue(index > 0);
+ }
+
+ @Test
+ public void testConnectionClose() throws Exception
+ {
+ String response;
+
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ response.getOutputStream().println("Hello World");
+ return true;
+ }
+ };
+
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "\n",
+ 200, TimeUnit.MILLISECONDS
+ );
+ assertThat(response, containsString("200"));
+ assertThat(response, not(containsString("Connection: close")));
+ assertThat(response, containsString("Hello World"));
+
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Connection: close\n" +
+ "\n"
+ );
+ assertThat(response, containsString("200"));
+ assertThat(response, containsString("Connection: close"));
+ assertThat(response, containsString("Hello World"));
+
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Connection: Other, close\n" +
+ "\n"
+ );
+
+ assertThat(response, containsString("200"));
+ assertThat(response, containsString("Connection: close"));
+ assertThat(response, containsString("Hello World"));
+
+ response = _connector.getResponse(
+ "GET / HTTP/1.0\n" +
+ "Host: whatever\n" +
+ "\n"
+ );
+ assertThat(response, containsString("200"));
+ assertThat(response, not(containsString("Connection: close")));
+ assertThat(response, containsString("Hello World"));
+
+ response = _connector.getResponse(
+ "GET / HTTP/1.0\n" +
+ "Host: whatever\n" +
+ "Connection: Other, close\n" +
+ "\n"
+ );
+ assertThat(response, containsString("200"));
+ assertThat(response, containsString("Hello World"));
+
+ response = _connector.getResponse(
+ "GET / HTTP/1.0\n" +
+ "Host: whatever\n" +
+ "Connection: Other,,keep-alive\n" +
+ "\n",
+ 200, TimeUnit.MILLISECONDS
+ );
+ assertThat(response, containsString("200"));
+ assertThat(response, containsString("Connection: keep-alive"));
+ assertThat(response, containsString("Hello World"));
+
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ response.setHeader("Connection", "TE");
+ response.addHeader("Connection", "Other");
+ response.getOutputStream().println("Hello World");
+ return true;
+ }
+ };
+
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "\n",
+ 200, TimeUnit.MILLISECONDS
+ );
+ assertThat(response, containsString("200"));
+ assertThat(response, containsString("Connection: TE"));
+ assertThat(response, containsString("Connection: Other"));
+
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Connection: close\n" +
+ "\n"
+ );
+ assertThat(response, containsString("200 OK"));
+ assertThat(response, containsString("Connection: close"));
+ assertThat(response, containsString("Hello World"));
+ }
+
+ @Test
+ public void testCookies() throws Exception
+ {
+ final ArrayList<Cookie> cookies = new ArrayList<>();
+
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ javax.servlet.http.Cookie[] ca = request.getCookies();
+ if (ca != null)
+ cookies.addAll(Arrays.asList(ca));
+ response.getOutputStream().println("Hello World");
+ return true;
+ }
+ };
+
+ String response;
+
+ cookies.clear();
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Connection: close\n" +
+ "\n"
+ );
+ assertTrue(response.startsWith("HTTP/1.1 200 OK"));
+ assertEquals(0, cookies.size());
+
+ cookies.clear();
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Cookie: name=quoted=\"\\\"badly\\\"\"\n" +
+ "Connection: close\n" +
+ "\n"
+ );
+ assertTrue(response.startsWith("HTTP/1.1 200 OK"));
+ assertEquals(1, cookies.size());
+ assertEquals("name", cookies.get(0).getName());
+ assertEquals("quoted=\"\\\"badly\\\"\"", cookies.get(0).getValue());
+
+ cookies.clear();
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Cookie: name=value; other=\"quoted=;value\"\n" +
+ "Connection: close\n" +
+ "\n"
+ );
+ assertTrue(response.startsWith("HTTP/1.1 200 OK"));
+ assertEquals(2, cookies.size());
+ assertEquals("name", cookies.get(0).getName());
+ assertEquals("value", cookies.get(0).getValue());
+ assertEquals("other", cookies.get(1).getName());
+ assertEquals("quoted=;value", cookies.get(1).getValue());
+
+ cookies.clear();
+ LocalEndPoint endp = _connector.executeRequest(
+ "GET /other HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Other: header\n" +
+ "Cookie: name=value; other=\"quoted=;value\"\n" +
+ "\n" +
+ "GET /other HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Other: header\n" +
+ "Cookie: name=value; other=\"quoted=;value\"\n" +
+ "Connection: close\n" +
+ "\n"
+ );
+ response = endp.getResponse();
+ assertThat(response, Matchers.startsWith("HTTP/1.1 200 OK"));
+ response = endp.getResponse();
+ assertThat(response, Matchers.startsWith("HTTP/1.1 200 OK"));
+
+ assertEquals(4, cookies.size());
+ assertEquals("name", cookies.get(0).getName());
+ assertEquals("value", cookies.get(0).getValue());
+ assertEquals("other", cookies.get(1).getName());
+ assertEquals("quoted=;value", cookies.get(1).getValue());
+
+ assertSame(cookies.get(0), cookies.get(2));
+ assertSame(cookies.get(1), cookies.get(3));
+
+ cookies.clear();
+ endp = _connector.executeRequest(
+ "GET /other HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Other: header\n" +
+ "Cookie: name=value; other=\"quoted=;value\"\n" +
+ "\n" +
+ "GET /other HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Other: header\n" +
+ "Cookie: name=value; other=\"othervalue\"\n" +
+ "Connection: close\n" +
+ "\n"
+ );
+ response = endp.getResponse();
+ assertThat(response, Matchers.startsWith("HTTP/1.1 200 OK"));
+ response = endp.getResponse();
+ assertThat(response, Matchers.startsWith("HTTP/1.1 200 OK"));
+ assertEquals(4, cookies.size());
+ assertEquals("name", cookies.get(0).getName());
+ assertEquals("value", cookies.get(0).getValue());
+ assertEquals("other", cookies.get(1).getName());
+ assertEquals("quoted=;value", cookies.get(1).getValue());
+
+ assertNotSame(cookies.get(0), cookies.get(2));
+ assertNotSame(cookies.get(1), cookies.get(3));
+
+ cookies.clear();
+ response = _connector.getResponse(
+ "GET /other HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Other: header\n" +
+ "Cookie: __utmz=14316.133020.1.1.utr=gna.de|ucn=(real)|utd=reral|utct=/games/hen-one,gnt-50-ba-keys:key,2072262.html\n" +
+ "Connection: close\n" +
+ "\n"
+ );
+ assertTrue(response.startsWith("HTTP/1.1 200 OK"));
+ assertEquals(1, cookies.size());
+ assertEquals("__utmz", cookies.get(0).getName());
+ assertEquals("14316.133020.1.1.utr=gna.de|ucn=(real)|utd=reral|utct=/games/hen-one,gnt-50-ba-keys:key,2072262.html", cookies.get(0).getValue());
+ }
+
+ @Test
+ public void testBadCookies() throws Exception
+ {
+ final ArrayList<Cookie> cookies = new ArrayList<>();
+
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ javax.servlet.http.Cookie[] ca = request.getCookies();
+ if (ca != null)
+ cookies.addAll(Arrays.asList(ca));
+ response.getOutputStream().println("Hello World");
+ return true;
+ }
+ };
+
+ String response;
+
+ cookies.clear();
+ response = _connector.getResponse(
+ "GET / HTTP/1.1\n" +
+ "Host: whatever\n" +
+ "Cookie: Path=value\n" +
+ "Cookie: name=value\n" +
+ "Connection: close\n" +
+ "\n"
+ );
+ assertTrue(response.startsWith("HTTP/1.1 200 OK"));
+ assertEquals(1, cookies.size());
+ assertEquals("name", cookies.get(0).getName());
+ assertEquals("value", cookies.get(0).getValue());
+ }
+
+ @Test
+ public void testHashDOSKeys() throws Exception
+ {
+ try (StacklessLogging stackless = new StacklessLogging(HttpChannel.class))
+ {
+ // Expecting maxFormKeys limit and Closing HttpParser exceptions...
+ _server.setAttribute("org.eclipse.jetty.server.Request.maxFormContentSize", -1);
+ _server.setAttribute("org.eclipse.jetty.server.Request.maxFormKeys", 1000);
+
+ StringBuilder buf = new StringBuilder(4000000);
+ buf.append("a=b");
+
+ // The evil keys file is not distributed - as it is dangerous
+ File evilKeys = new File("/tmp/keys_mapping_to_zero_2m");
+ if (evilKeys.exists())
+ {
+ // Using real evil keys!
+ try (BufferedReader in = new BufferedReader(new FileReader(evilKeys)))
+ {
+ String key = null;
+ while ((key = in.readLine()) != null)
+ {
+ buf.append("&").append(key).append("=").append("x");
+ }
+ }
+ }
+ else
+ {
+ // we will just create a lot of keys and make sure the limit is applied
+ for (int i = 0; i < 2000; i++)
+ {
+ buf.append("&").append("K").append(i).append("=").append("x");
+ }
+ }
+ buf.append("&c=d");
+
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response)
+ {
+ return "b".equals(request.getParameter("a")) && request.getParameter("c") == null;
+ }
+ };
+
+ String request = "POST / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: " + MimeTypes.Type.FORM_ENCODED.asString() + "\r\n" +
+ "Content-Length: " + buf.length() + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ buf;
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ String rawResponse = _connector.getResponse(request);
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat("Response.status", response.getStatus(), is(400));
+ assertThat("Response body content", response.getContent(), containsString(BadMessageException.class.getName()));
+ assertThat("Response body content", response.getContent(), containsString(IllegalStateException.class.getName()));
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ assertTrue((now - start) < 5000);
+ }
+ }
+
+ @Test
+ public void testHashDOSSize() throws Exception
+ {
+ try (StacklessLogging stackless = new StacklessLogging(HttpChannel.class))
+ {
+ LOG.info("Expecting maxFormSize limit and too much data exceptions...");
+ _server.setAttribute("org.eclipse.jetty.server.Request.maxFormContentSize", 3396);
+ _server.setAttribute("org.eclipse.jetty.server.Request.maxFormKeys", 1000);
+
+ StringBuilder buf = new StringBuilder(4000000);
+ buf.append("a=b");
+ // we will just create a lot of keys and make sure the limit is applied
+ for (int i = 0; i < 500; i++)
+ {
+ buf.append("&").append("K").append(i).append("=").append("x");
+ }
+ buf.append("&c=d");
+
+ _handler._checker = new RequestTester()
+ {
+ @Override
+ public boolean check(HttpServletRequest request, HttpServletResponse response)
+ {
+ return "b".equals(request.getParameter("a")) && request.getParameter("c") == null;
+ }
+ };
+
+ String request = "POST / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "Content-Type: " + MimeTypes.Type.FORM_ENCODED.asString() + "\r\n" +
+ "Content-Length: " + buf.length() + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ buf;
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ String rawResponse = _connector.getResponse(request);
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat("Response.status", response.getStatus(), is(400));
+ assertThat("Response body content", response.getContent(), containsString(BadMessageException.class.getName()));
+ assertThat("Response body content", response.getContent(), containsString(IllegalStateException.class.getName()));
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ assertTrue((now - start) < 5000);
+ }
+ }
+
+ @Test
+ public void testNotSupportedCharacterEncoding() throws UnsupportedEncodingException
+ {
+ Request request = new Request(null, null);
+ assertThrows(UnsupportedEncodingException.class, () -> request.setCharacterEncoding("doesNotExist"));
+ }
+
+ @Test
+ public void testGetterSafeFromNullPointerException()
+ {
+ Request request = new Request(null, null);
+
+ assertNull(request.getAuthType());
+ assertNull(request.getAuthentication());
+
+ assertNull(request.getContentType());
+
+ assertNull(request.getCookies());
+ assertNull(request.getContext());
+ assertNull(request.getContextPath());
+
+ assertNull(request.getHttpFields());
+ assertNull(request.getHttpURI());
+
+ assertNotNull(request.getScheme());
+ assertNotNull(request.getServerName());
+ assertNotNull(request.getServerPort());
+
+ assertNotNull(request.getAttributeNames());
+ assertFalse(request.getAttributeNames().hasMoreElements());
+
+ request.getParameterMap();
+ assertNull(request.getQueryString());
+ assertNotNull(request.getQueryParameters());
+ assertEquals(0, request.getQueryParameters().size());
+ assertNotNull(request.getParameterMap());
+ assertEquals(0, request.getParameterMap().size());
+ }
+
+ @Test
+ public void testEncoding() throws Exception
+ {
+ _handler._checker = (request, response) -> "/foo/bar".equals(request.getPathInfo());
+ String request = "GET /f%6f%6F/b%u0061r HTTP/1.0\r\n" +
+ "Host: whatever\r\n" +
+ "\r\n";
+
+ _connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC7230);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
+
+ HttpCompliance.CUSTOM0.sections().clear();
+ HttpCompliance.CUSTOM0.sections().addAll(HttpCompliance.RFC7230.sections());
+ HttpCompliance.CUSTOM0.sections().add(HttpComplianceSection.NO_UTF16_ENCODINGS);
+ _connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.CUSTOM0);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
+ }
+
+ @Test
+ public void testAmbiguousParameters() throws Exception
+ {
+ _handler._checker = (request, response) -> true;
+
+ String request = "GET /ambiguous/..;/path HTTP/1.0\r\n" +
+ "Host: whatever\r\n" +
+ "\r\n";
+
+ _connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC7230);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
+
+ HttpCompliance.CUSTOM0.sections().clear();
+ HttpCompliance.CUSTOM0.sections().addAll(HttpCompliance.RFC7230.sections());
+ HttpCompliance.CUSTOM0.sections().remove(HttpComplianceSection.NO_AMBIGUOUS_PATH_PARAMETERS);
+
+ _connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.CUSTOM0);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
+ }
+
+ @Test
+ public void testAmbiguousSegments() throws Exception
+ {
+ _handler._checker = (request, response) -> true;
+
+ String request = "GET /ambiguous/%2e%2e/path HTTP/1.0\r\n" +
+ "Host: whatever\r\n" +
+ "\r\n";
+
+ _connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC7230_NO_AMBIGUOUS_URIS);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
+
+ _connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC7230);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
+
+ _connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC2616);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
+ }
+
+ @Test
+ public void testAmbiguousSeparators() throws Exception
+ {
+ _handler._checker = (request, response) -> true;
+
+ String request = "GET /ambiguous/%2f/path HTTP/1.0\r\n" +
+ "Host: whatever\r\n" +
+ "\r\n";
+
+ _connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC7230_NO_AMBIGUOUS_URIS);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
+
+ _connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC7230);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
+
+ _connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC2616);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
+ }
+
+ public void setComplianceModes(HttpComplianceSection... complianceSections)
+ {
+ setComplianceModes(null, complianceSections);
+ }
+
+ public void setComplianceModes(HttpCompliance compliance, HttpComplianceSection... additionalSections)
+ {
+ HttpCompliance.CUSTOM0.sections().clear();
+ if (compliance != null)
+ HttpCompliance.CUSTOM0.sections().addAll(compliance.sections());
+ HttpCompliance.CUSTOM0.sections().addAll(Arrays.asList(additionalSections));
+ _connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.CUSTOM0);
+ }
+
+ @Test
+ public void testAmbiguousPaths() throws Exception
+ {
+ _handler._checker = (request, response) ->
+ {
+ response.getOutputStream().println("servletPath=" + request.getServletPath());
+ response.getOutputStream().println("pathInfo=" + request.getPathInfo());
+ return true;
+ };
+ String request = "GET /unnormal/.././path/ambiguous%2f%2e%2e/%2e;/info HTTP/1.0\r\n" +
+ "Host: whatever\r\n" +
+ "\r\n";
+
+ setComplianceModes(HttpCompliance.RFC7230);
+ assertThat(_connector.getResponse(request), Matchers.allOf(
+ startsWith("HTTP/1.1 200"),
+ containsString("pathInfo=/path/info")));
+
+ setComplianceModes(HttpComplianceSection.NO_AMBIGUOUS_PATH_SEGMENTS);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
+ }
+
+ @Test
+ public void testAmbiguousEncoding() throws Exception
+ {
+ _handler._checker = (request, response) -> true;
+ String request = "GET /ambiguous/encoded/%25/path HTTP/1.0\r\n" +
+ "Host: whatever\r\n" +
+ "\r\n";
+
+ setComplianceModes(HttpCompliance.RFC7230);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
+ setComplianceModes(HttpCompliance.RFC7230, HttpComplianceSection.NO_AMBIGUOUS_PATH_ENCODING);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
+ }
+
+ @Test
+ public void testAmbiguousDoubleSlash() throws Exception
+ {
+ _handler._checker = (request, response) -> true;
+ String request = "GET /ambiguous/doubleSlash// HTTP/1.0\r\n" +
+ "Host: whatever\r\n" +
+ "\r\n";
+
+ setComplianceModes(HttpCompliance.RFC7230);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
+ setComplianceModes(HttpCompliance.RFC7230, HttpComplianceSection.NO_AMBIGUOUS_EMPTY_SEGMENT);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
+ }
+
+ private static long getFileCount(Path path)
+ {
+ try (Stream<Path> s = Files.list(path))
+ {
+ return s.count();
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException("Unable to get file list count: " + path, e);
+ }
+ }
+
+ interface RequestTester
+ {
+ boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException;
+ }
+
+ private class RequestHandler extends AbstractHandler
+ {
+ private RequestTester _checker;
+ @SuppressWarnings("unused")
+ private String _content;
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ ((Request)request).setHandled(true);
+
+ if (request.getContentLength() > 0 &&
+ !request.getContentType().startsWith(MimeTypes.Type.FORM_ENCODED.asString()) &&
+ !request.getContentType().startsWith("multipart/form-data"))
+ _content = IO.toString(request.getInputStream());
+
+ if (_checker != null && _checker.check(request, response))
+ response.setStatus(200);
+ else
+ response.sendError(500);
+ }
+ }
+
+ private class MultiPartRequestHandler extends AbstractHandler
+ {
+ File tmpDir;
+
+ public MultiPartRequestHandler(File tmpDir)
+ {
+ this.tmpDir = tmpDir;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ ((Request)request).setHandled(true);
+ try
+ {
+
+ MultipartConfigElement mpce = new MultipartConfigElement(tmpDir.getAbsolutePath(), -1, -1, 2);
+ request.setAttribute(Request.MULTIPART_CONFIG_ELEMENT, mpce);
+
+ String field1 = request.getParameter("field1");
+ assertNotNull(field1);
+
+ Part foo = request.getPart("stuff");
+ assertNotNull(foo);
+ assertTrue(foo.getSize() > 0);
+ response.setStatus(200);
+ List<String> violations = (List<String>)request.getAttribute(HttpCompliance.VIOLATIONS_ATTR);
+ if (violations != null)
+ {
+ for (String v : violations)
+ {
+ response.addHeader("Violation", v);
+ }
+ }
+ }
+ catch (IllegalStateException e)
+ {
+ //expected exception because no multipart config is set up
+ assertTrue(e.getMessage().startsWith("No multipart config"));
+ response.setStatus(200);
+ }
+ catch (Exception e)
+ {
+ response.sendError(500);
+ }
+ }
+ }
+
+ private class BadMultiPartRequestHandler extends AbstractHandler
+ {
+ File tmpDir;
+
+ public BadMultiPartRequestHandler(File tmpDir)
+ {
+ this.tmpDir = tmpDir;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ ((Request)request).setHandled(true);
+ try
+ {
+ MultipartConfigElement mpce = new MultipartConfigElement(tmpDir.getAbsolutePath(), -1, -1, 2);
+ request.setAttribute(Request.MULTIPART_CONFIG_ELEMENT, mpce);
+
+ //We should get an error when we getParams if there was a problem parsing the multipart
+ request.getPart("xxx");
+ //A 200 response is actually wrong here
+ }
+ catch (RuntimeException e)
+ {
+ response.sendError(500);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ResourceCacheTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ResourceCacheTest.java
new file mode 100644
index 0000000..d50a908
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ResourceCacheTest.java
@@ -0,0 +1,371 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+import org.eclipse.jetty.http.CompressedContentFormat;
+import org.eclipse.jetty.http.HttpContent;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.http.ResourceHttpContent;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.resource.PathResource;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceCollection;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(WorkDirExtension.class)
+public class ResourceCacheTest
+{
+ public WorkDir workDir;
+
+ public Path createUtilTestResources(Path basePath) throws IOException
+ {
+ // root
+ makeFile(basePath.resolve("resource.txt"), "this is test data");
+
+ // - one/
+ Path one = basePath.resolve("one");
+ FS.ensureDirExists(one);
+ makeFile(one.resolve("1.txt"), "1 - one");
+
+ // - one/dir/
+ Path oneDir = one.resolve("dir");
+ FS.ensureDirExists(oneDir);
+ makeFile(oneDir.resolve("1.txt"), "1 - one");
+
+ // - two/
+ Path two = basePath.resolve("two");
+ FS.ensureDirExists(two);
+ makeFile(two.resolve("1.txt"), "1 - two");
+ makeFile(two.resolve("2.txt"), "2 - two");
+
+ // - two/dir/
+ Path twoDir = two.resolve("dir");
+ FS.ensureDirExists(twoDir);
+ makeFile(twoDir.resolve("2.txt"), "2 - two");
+
+ // - three/
+ Path three = basePath.resolve("three");
+ FS.ensureDirExists(three);
+ makeFile(three.resolve("2.txt"), "2 - three");
+ makeFile(three.resolve("3.txt"), "3 - three");
+
+ // - three/dir/
+ Path threeDir = three.resolve("dir");
+ FS.ensureDirExists(threeDir);
+ makeFile(threeDir.resolve("3.txt"), "3 - three");
+
+ // - four/
+ Path four = basePath.resolve("four");
+ FS.ensureDirExists(four);
+ makeFile(four.resolve("four"), "4 - four (no extension)");
+ makeFile(four.resolve("four.txt"), "4 - four");
+
+ return basePath;
+ }
+
+ private void makeFile(Path file, String contents) throws IOException
+ {
+ try (BufferedWriter writer = Files.newBufferedWriter(file, UTF_8, StandardOpenOption.CREATE_NEW))
+ {
+ writer.write(contents);
+ writer.flush();
+ }
+ }
+
+ @Test
+ public void testMutlipleSources1() throws Exception
+ {
+ Path basePath = createUtilTestResources(workDir.getEmptyPathDir());
+
+ ResourceCollection rc = new ResourceCollection(
+ new PathResource(basePath.resolve("one")),
+ new PathResource(basePath.resolve("two")),
+ new PathResource(basePath.resolve("three")));
+
+ Resource[] r = rc.getResources();
+ MimeTypes mime = new MimeTypes();
+
+ CachedContentFactory rc3 = new CachedContentFactory(null, r[2], mime, false, false, CompressedContentFormat.NONE);
+ CachedContentFactory rc2 = new CachedContentFactory(rc3, r[1], mime, false, false, CompressedContentFormat.NONE);
+ CachedContentFactory rc1 = new CachedContentFactory(rc2, r[0], mime, false, false, CompressedContentFormat.NONE);
+
+ assertEquals(getContent(rc1, "1.txt"), "1 - one");
+ assertEquals(getContent(rc1, "2.txt"), "2 - two");
+ assertEquals(getContent(rc1, "3.txt"), "3 - three");
+
+ assertEquals(getContent(rc2, "1.txt"), "1 - two");
+ assertEquals(getContent(rc2, "2.txt"), "2 - two");
+ assertEquals(getContent(rc2, "3.txt"), "3 - three");
+
+ assertEquals(null, getContent(rc3, "1.txt"));
+ assertEquals(getContent(rc3, "2.txt"), "2 - three");
+ assertEquals(getContent(rc3, "3.txt"), "3 - three");
+ }
+
+ @Test
+ public void testUncacheable() throws Exception
+ {
+ Path basePath = createUtilTestResources(workDir.getEmptyPathDir());
+
+ ResourceCollection rc = new ResourceCollection(
+ new PathResource(basePath.resolve("one")),
+ new PathResource(basePath.resolve("two")),
+ new PathResource(basePath.resolve("three")));
+
+ Resource[] r = rc.getResources();
+ MimeTypes mime = new MimeTypes();
+
+ CachedContentFactory rc3 = new CachedContentFactory(null, r[2], mime, false, false, CompressedContentFormat.NONE);
+ CachedContentFactory rc2 = new CachedContentFactory(rc3, r[1], mime, false, false, CompressedContentFormat.NONE)
+ {
+ @Override
+ public boolean isCacheable(Resource resource)
+ {
+ return super.isCacheable(resource) && resource.getName().indexOf("2.txt") < 0;
+ }
+ };
+
+ CachedContentFactory rc1 = new CachedContentFactory(rc2, r[0], mime, false, false, CompressedContentFormat.NONE);
+
+ assertEquals(getContent(rc1, "1.txt"), "1 - one");
+ assertEquals(getContent(rc1, "2.txt"), "2 - two");
+ assertEquals(getContent(rc1, "3.txt"), "3 - three");
+
+ assertEquals(getContent(rc2, "1.txt"), "1 - two");
+ assertEquals(getContent(rc2, "2.txt"), "2 - two");
+ assertEquals(getContent(rc2, "3.txt"), "3 - three");
+
+ assertEquals(null, getContent(rc3, "1.txt"));
+ assertEquals(getContent(rc3, "2.txt"), "2 - three");
+ assertEquals(getContent(rc3, "3.txt"), "3 - three");
+ }
+
+ @Test
+ public void testResourceCache() throws Exception
+ {
+ final Resource directory;
+ File[] files = new File[10];
+ String[] names = new String[files.length];
+ CachedContentFactory cache;
+
+ Path basePath = workDir.getEmptyPathDir();
+
+ for (int i = 0; i < files.length; i++)
+ {
+ Path tmpFile = basePath.resolve("R-" + i + ".txt");
+ try (BufferedWriter writer = Files.newBufferedWriter(tmpFile, UTF_8, StandardOpenOption.CREATE_NEW))
+ {
+ for (int j = 0; j < (i * 10 - 1); j++)
+ {
+ writer.write(' ');
+ }
+ writer.write('\n');
+ }
+ files[i] = tmpFile.toFile();
+ names[i] = tmpFile.getFileName().toString();
+ }
+
+ directory = Resource.newResource(files[0].getParentFile().getAbsolutePath());
+
+ cache = new CachedContentFactory(null, directory, new MimeTypes(), false, false, CompressedContentFormat.NONE);
+
+ cache.setMaxCacheSize(95);
+ cache.setMaxCachedFileSize(85);
+ cache.setMaxCachedFiles(4);
+
+ assertTrue(cache.getContent("does not exist", 4096) == null);
+ assertTrue(cache.getContent(names[9], 4096) instanceof ResourceHttpContent);
+ assertTrue(cache.getContent(names[9], 4096).getIndirectBuffer() != null);
+
+ HttpContent content;
+ content = cache.getContent(names[8], 4096);
+ assertThat(content, is(not(nullValue())));
+ assertEquals(80, content.getContentLengthValue());
+ assertEquals(0, cache.getCachedSize());
+
+ if (org.junit.jupiter.api.condition.OS.LINUX.isCurrentOs())
+ {
+ // Initially not using memory mapped files
+ content.getDirectBuffer();
+ assertEquals(80, cache.getCachedSize());
+
+ // with both types of buffer loaded, this is too large for cache
+ content.getIndirectBuffer();
+ assertEquals(0, cache.getCachedSize());
+ assertEquals(0, cache.getCachedFiles());
+
+ cache = new CachedContentFactory(null, directory, new MimeTypes(), true, false, CompressedContentFormat.NONE);
+ cache.setMaxCacheSize(95);
+ cache.setMaxCachedFileSize(85);
+ cache.setMaxCachedFiles(4);
+
+ content = cache.getContent(names[8], 4096);
+ content.getDirectBuffer();
+ assertEquals(cache.isUseFileMappedBuffer() ? 0 : 80, cache.getCachedSize());
+
+ // with both types of buffer loaded, this is not too large for cache because
+ // mapped buffers don't count, so we can continue
+ }
+
+ content.getIndirectBuffer();
+ assertEquals(80, cache.getCachedSize());
+ assertEquals(1, cache.getCachedFiles());
+
+ Thread.sleep(200);
+
+ content = cache.getContent(names[1], 4096);
+ assertEquals(80, cache.getCachedSize());
+ content.getIndirectBuffer();
+ assertEquals(90, cache.getCachedSize());
+ assertEquals(2, cache.getCachedFiles());
+
+ Thread.sleep(200);
+
+ content = cache.getContent(names[2], 4096);
+ content.getIndirectBuffer();
+ assertEquals(30, cache.getCachedSize());
+ assertEquals(2, cache.getCachedFiles());
+
+ Thread.sleep(200);
+
+ content = cache.getContent(names[3], 4096);
+ content.getIndirectBuffer();
+ assertEquals(60, cache.getCachedSize());
+ assertEquals(3, cache.getCachedFiles());
+
+ Thread.sleep(200);
+
+ content = cache.getContent(names[4], 4096);
+ content.getIndirectBuffer();
+ assertEquals(90, cache.getCachedSize());
+ assertEquals(3, cache.getCachedFiles());
+
+ Thread.sleep(200);
+
+ content = cache.getContent(names[5], 4096);
+ content.getIndirectBuffer();
+ assertEquals(90, cache.getCachedSize());
+ assertEquals(2, cache.getCachedFiles());
+
+ Thread.sleep(200);
+
+ content = cache.getContent(names[6], 4096);
+ content.getIndirectBuffer();
+ assertEquals(60, cache.getCachedSize());
+ assertEquals(1, cache.getCachedFiles());
+
+ Thread.sleep(200);
+
+ try (OutputStream out = new FileOutputStream(files[6]))
+ {
+ out.write(' ');
+ }
+ content = cache.getContent(names[7], 4096);
+ content.getIndirectBuffer();
+ assertEquals(70, cache.getCachedSize());
+ assertEquals(1, cache.getCachedFiles());
+
+ Thread.sleep(200);
+
+ content = cache.getContent(names[6], 4096);
+ content.getIndirectBuffer();
+ assertEquals(71, cache.getCachedSize());
+ assertEquals(2, cache.getCachedFiles());
+
+ Thread.sleep(200);
+
+ content = cache.getContent(names[0], 4096);
+ content.getIndirectBuffer();
+ assertEquals(72, cache.getCachedSize());
+ assertEquals(3, cache.getCachedFiles());
+
+ Thread.sleep(200);
+
+ content = cache.getContent(names[1], 4096);
+ content.getIndirectBuffer();
+ assertEquals(82, cache.getCachedSize());
+ assertEquals(4, cache.getCachedFiles());
+
+ Thread.sleep(200);
+
+ content = cache.getContent(names[2], 4096);
+ content.getIndirectBuffer();
+ assertEquals(32, cache.getCachedSize());
+ assertEquals(4, cache.getCachedFiles());
+
+ Thread.sleep(200);
+
+ content = cache.getContent(names[3], 4096);
+ content.getIndirectBuffer();
+ assertEquals(61, cache.getCachedSize());
+ assertEquals(4, cache.getCachedFiles());
+
+ Thread.sleep(200);
+
+ cache.flushCache();
+ assertEquals(0, cache.getCachedSize());
+ assertEquals(0, cache.getCachedFiles());
+
+ cache.flushCache();
+ }
+
+ @Test
+ public void testNoextension() throws Exception
+ {
+ Path basePath = createUtilTestResources(workDir.getEmptyPathDir());
+
+ Resource resource = new PathResource(basePath.resolve("four"));
+ MimeTypes mime = new MimeTypes();
+
+ CachedContentFactory cache = new CachedContentFactory(null, resource, mime, false, false, CompressedContentFormat.NONE);
+
+ assertEquals(getContent(cache, "four.txt"), "4 - four");
+ assertEquals(getContent(cache, "four"), "4 - four (no extension)");
+ }
+
+ static String getContent(CachedContentFactory rc, String path) throws Exception
+ {
+ HttpContent content = rc.getContent(path, rc.getMaxCachedFileSize());
+ if (content == null)
+ return null;
+
+ return BufferUtil.toString(content.getIndirectBuffer());
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java
new file mode 100644
index 0000000..5ba0df3
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java
@@ -0,0 +1,1416 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.PrintWriter;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.URLEncoder;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Stream;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.eclipse.jetty.http.CookieCompliance;
+import org.eclipse.jetty.http.HttpCookie;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.http.MetaData;
+import org.eclipse.jetty.io.AbstractEndPoint;
+import org.eclipse.jetty.io.ByteArrayEndPoint;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.server.session.DefaultSessionCache;
+import org.eclipse.jetty.server.session.DefaultSessionIdManager;
+import org.eclipse.jetty.server.session.NullSessionDataStore;
+import org.eclipse.jetty.server.session.Session;
+import org.eclipse.jetty.server.session.SessionData;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.eclipse.jetty.util.thread.TimerScheduler;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+public class ResponseTest
+{
+
+ static final InetSocketAddress LOCALADDRESS;
+
+ static
+ {
+ InetAddress ip = null;
+ try
+ {
+ ip = Inet4Address.getByName("127.0.0.42");
+ }
+ catch (UnknownHostException e)
+ {
+ e.printStackTrace();
+ }
+ finally
+ {
+ LOCALADDRESS = new InetSocketAddress(ip, 8888);
+ }
+ }
+
+ private Server _server;
+ private HttpChannel _channel;
+ private ByteBuffer _content = BufferUtil.allocate(16 * 1024);
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ BufferUtil.clear(_content);
+
+ _server = new Server();
+ Scheduler scheduler = new TimerScheduler();
+ HttpConfiguration config = new HttpConfiguration();
+ LocalConnector connector = new LocalConnector(_server, null, scheduler, null, 1, new HttpConnectionFactory(config));
+ _server.addConnector(connector);
+ _server.setHandler(new DumpHandler());
+ _server.start();
+
+ AbstractEndPoint endp = new ByteArrayEndPoint(scheduler, 5000)
+ {
+ @Override
+ public InetSocketAddress getLocalAddress()
+ {
+ return LOCALADDRESS;
+ }
+ };
+ _channel = new HttpChannel(connector, new HttpConfiguration(), endp, new HttpTransport()
+ {
+ private Throwable _channelError;
+
+ @Override
+ public void send(MetaData.Response info, boolean head, ByteBuffer content, boolean lastContent, Callback callback)
+ {
+ if (BufferUtil.hasContent(content))
+ {
+ BufferUtil.append(_content, content);
+ }
+
+ if (_channelError == null)
+ callback.succeeded();
+ else
+ callback.failed(_channelError);
+ }
+
+ @Override
+ public boolean isPushSupported()
+ {
+ return false;
+ }
+
+ @Override
+ public void push(org.eclipse.jetty.http.MetaData.Request request)
+ {
+ }
+
+ @Override
+ public void onCompleted()
+ {
+ }
+
+ @Override
+ public void abort(Throwable failure)
+ {
+ _channelError = failure;
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return false;
+ }
+ });
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ _server.stop();
+ _server.join();
+ }
+
+ @SuppressWarnings("InjectedReferences") // to allow for invalid encoding strings in this testcase
+ @Test
+ public void testContentType() throws Exception
+ {
+ Response response = getResponse();
+
+ assertEquals(null, response.getContentType());
+
+ response.setHeader("Content-Type", "text/something");
+ assertEquals("text/something", response.getContentType());
+
+ response.setContentType("foo/bar");
+ assertEquals("foo/bar", response.getContentType());
+ response.getWriter();
+ assertEquals("foo/bar;charset=iso-8859-1", response.getContentType());
+ response.setContentType("foo2/bar2");
+ assertEquals("foo2/bar2;charset=iso-8859-1", response.getContentType());
+ response.setHeader("name", "foo");
+
+ Iterator<String> en = response.getHeaders("name").iterator();
+ assertEquals("foo", en.next());
+ assertFalse(en.hasNext());
+ response.addHeader("name", "bar");
+ en = response.getHeaders("name").iterator();
+ assertEquals("foo", en.next());
+ assertEquals("bar", en.next());
+ assertFalse(en.hasNext());
+
+ response.recycle();
+
+ response.setContentType("text/html");
+ assertEquals("text/html", response.getContentType());
+ response.getWriter();
+ assertEquals("text/html;charset=utf-8", response.getContentType());
+ response.setContentType("foo2/bar2;charset=utf-8");
+ assertEquals("foo2/bar2;charset=utf-8", response.getContentType());
+
+ response.recycle();
+ response.setContentType("text/xml;charset=ISO-8859-7");
+ response.getWriter();
+ assertEquals("text/xml;charset=ISO-8859-7", response.getContentType());
+ response.setContentType("text/html;charset=UTF-8");
+ assertEquals("text/html;charset=ISO-8859-7", response.getContentType());
+
+ response.recycle();
+ response.setContentType("text/html;charset=US-ASCII");
+ response.getWriter();
+ assertEquals("text/html;charset=US-ASCII", response.getContentType());
+
+ response.recycle();
+ response.setContentType("text/html; charset=UTF-8");
+ response.getWriter();
+ assertEquals("text/html;charset=utf-8", response.getContentType());
+
+ response.recycle();
+ response.setContentType("text/json");
+ response.getWriter();
+ assertEquals("text/json", response.getContentType());
+
+ response.recycle();
+ response.setContentType("text/json");
+ response.setCharacterEncoding("UTF-8");
+ response.getWriter();
+ assertEquals("text/json;charset=utf-8", response.getContentType());
+
+ response.recycle();
+ response.setCharacterEncoding("xyz");
+ response.setContentType("foo/bar");
+ assertEquals("foo/bar;charset=xyz", response.getContentType());
+
+ response.recycle();
+ response.setContentType("foo/bar");
+ response.setCharacterEncoding("xyz");
+ assertEquals("foo/bar;charset=xyz", response.getContentType());
+
+ response.recycle();
+ response.setCharacterEncoding("xyz");
+ response.setContentType("foo/bar;charset=abc");
+ assertEquals("foo/bar;charset=abc", response.getContentType());
+
+ response.recycle();
+ response.setContentType("foo/bar;charset=abc");
+ response.setCharacterEncoding("xyz");
+ assertEquals("foo/bar;charset=xyz", response.getContentType());
+
+ response.recycle();
+ response.setCharacterEncoding("xyz");
+ response.setContentType("foo/bar");
+ response.setCharacterEncoding(null);
+ assertEquals("foo/bar", response.getContentType());
+
+ response.recycle();
+ response.setCharacterEncoding("xyz");
+ response.setCharacterEncoding(null);
+ response.setContentType("foo/bar");
+ assertEquals("foo/bar", response.getContentType());
+ response.recycle();
+ response.addHeader("Content-Type", "text/something");
+ assertEquals("text/something", response.getContentType());
+
+ response.recycle();
+ response.addHeader("Content-Type", "application/json");
+ response.getWriter();
+ assertEquals("application/json", response.getContentType());
+ }
+
+ @Test
+ public void testInferredCharset() throws Exception
+ {
+ // Inferred from encoding.properties
+ Response response = getResponse();
+
+ assertEquals(null, response.getContentType());
+
+ response.setHeader("Content-Type", "application/xhtml+xml");
+ assertEquals("application/xhtml+xml", response.getContentType());
+ response.getWriter();
+ assertEquals("application/xhtml+xml;charset=utf-8", response.getContentType());
+ assertEquals("utf-8", response.getCharacterEncoding());
+ }
+
+ @Test
+ public void testAssumedCharset() throws Exception
+ {
+ Response response = getResponse();
+
+ // Assumed from known types
+ assertEquals(null, response.getContentType());
+ response.setHeader("Content-Type", "text/json");
+ assertEquals("text/json", response.getContentType());
+ response.getWriter();
+ assertEquals("text/json", response.getContentType());
+ assertEquals("utf-8", response.getCharacterEncoding());
+
+ response.recycle();
+
+ // Assumed from encoding.properties
+ assertEquals(null, response.getContentType());
+ response.setHeader("Content-Type", "application/vnd.api+json");
+ assertEquals("application/vnd.api+json", response.getContentType());
+ response.getWriter();
+ assertEquals("application/vnd.api+json", response.getContentType());
+ assertEquals("utf-8", response.getCharacterEncoding());
+ }
+
+ @Test
+ public void testStrangeContentType() throws Exception
+ {
+ Response response = getResponse();
+
+ assertEquals(null, response.getContentType());
+
+ response.recycle();
+ response.setContentType("text/html;charset=utf-8;charset=UTF-8");
+ response.getWriter();
+ assertEquals("text/html;charset=utf-8;charset=UTF-8", response.getContentType());
+ assertEquals("utf-8", response.getCharacterEncoding().toLowerCase(Locale.ENGLISH));
+ }
+
+ @Test
+ public void testLocale() throws Exception
+ {
+ Response response = getResponse();
+
+ ContextHandler context = new ContextHandler();
+ context.addLocaleEncoding(Locale.ENGLISH.toString(), "ISO-8859-1");
+ context.addLocaleEncoding(Locale.ITALIAN.toString(), "ISO-8859-2");
+ response.getHttpChannel().getRequest().setContext(context.getServletContext());
+
+ response.setLocale(java.util.Locale.ITALIAN);
+ assertEquals(null, response.getContentType());
+ response.setContentType("text/plain");
+ assertEquals("text/plain;charset=ISO-8859-2", response.getContentType());
+
+ response.recycle();
+ response.setContentType("text/plain");
+ response.setCharacterEncoding("utf-8");
+ response.setLocale(java.util.Locale.ITALIAN);
+ assertEquals("text/plain;charset=utf-8", response.getContentType());
+ assertTrue(response.toString().indexOf("charset=utf-8") > 0);
+ }
+
+ @Test
+ public void testLocaleFormat() throws Exception
+ {
+ Response response = getResponse();
+
+ ContextHandler context = new ContextHandler();
+ context.addLocaleEncoding(Locale.ENGLISH.toString(), "ISO-8859-1");
+ context.addLocaleEncoding(Locale.ITALIAN.toString(), "ISO-8859-2");
+ response.getHttpChannel().getRequest().setContext(context.getServletContext());
+
+ response.setLocale(java.util.Locale.ITALIAN);
+
+ PrintWriter out = response.getWriter();
+
+ out.format("TestA1 %,.2f%n", 1234567.89);
+ out.format("TestA2 %,.2f%n", 1234567.89);
+
+ out.format((java.util.Locale)null, "TestB1 %,.2f%n", 1234567.89);
+ out.format((java.util.Locale)null, "TestB2 %,.2f%n", 1234567.89);
+
+ out.format(Locale.ENGLISH, "TestC1 %,.2f%n", 1234567.89);
+ out.format(Locale.ENGLISH, "TestC2 %,.2f%n", 1234567.89);
+
+ out.format(Locale.ITALIAN, "TestD1 %,.2f%n", 1234567.89);
+ out.format(Locale.ITALIAN, "TestD2 %,.2f%n", 1234567.89);
+
+ out.close();
+
+ /* Test A */
+ assertThat(BufferUtil.toString(_content), Matchers.containsString("TestA1 1.234.567,89"));
+ assertThat(BufferUtil.toString(_content), Matchers.containsString("TestA2 1.234.567,89"));
+
+ /* Test B */
+ assertThat(BufferUtil.toString(_content), Matchers.containsString("TestB1 1.234.567,89"));
+ assertThat(BufferUtil.toString(_content), Matchers.containsString("TestB2 1.234.567,89"));
+
+ /* Test C */
+ assertThat(BufferUtil.toString(_content), Matchers.containsString("TestC1 1,234,567.89"));
+ assertThat(BufferUtil.toString(_content), Matchers.containsString("TestC2 1,234,567.89"));
+
+ /* Test D */
+ assertThat(BufferUtil.toString(_content), Matchers.containsString("TestD1 1.234.567,89"));
+ assertThat(BufferUtil.toString(_content), Matchers.containsString("TestD2 1.234.567,89"));
+ }
+
+ @Test
+ public void testContentTypeCharacterEncoding() throws Exception
+ {
+ Response response = getResponse();
+
+ response.setContentType("foo/bar");
+ response.setCharacterEncoding("utf-8");
+ assertEquals("foo/bar;charset=utf-8", response.getContentType());
+ response.getWriter();
+ assertEquals("foo/bar;charset=utf-8", response.getContentType());
+ response.setContentType("foo2/bar2");
+ assertEquals("foo2/bar2;charset=utf-8", response.getContentType());
+ response.setCharacterEncoding("ISO-8859-1");
+ assertEquals("foo2/bar2;charset=utf-8", response.getContentType());
+
+ response.recycle();
+
+ response.setContentType("text/html");
+ response.setCharacterEncoding("UTF-8");
+ assertEquals("text/html;charset=utf-8", response.getContentType());
+ response.getWriter();
+ assertEquals("text/html;charset=utf-8", response.getContentType());
+ response.setContentType("text/xml");
+ assertEquals("text/xml;charset=utf-8", response.getContentType());
+ response.setCharacterEncoding("ISO-8859-1");
+ assertEquals("text/xml;charset=utf-8", response.getContentType());
+ }
+
+ @Test
+ public void testCharacterEncodingContentType() throws Exception
+ {
+ Response response = getResponse();
+ response.setCharacterEncoding("utf-8");
+ response.setContentType("foo/bar");
+ assertEquals("foo/bar;charset=utf-8", response.getContentType());
+ response.getWriter();
+ assertEquals("foo/bar;charset=utf-8", response.getContentType());
+ response.setContentType("foo2/bar2");
+ assertEquals("foo2/bar2;charset=utf-8", response.getContentType());
+ response.setCharacterEncoding("ISO-8859-1");
+ assertEquals("foo2/bar2;charset=utf-8", response.getContentType());
+
+ response.recycle();
+
+ response.setCharacterEncoding("utf-8");
+ response.setContentType("text/html");
+ assertEquals("text/html;charset=utf-8", response.getContentType());
+ response.getWriter();
+ assertEquals("text/html;charset=utf-8", response.getContentType());
+ response.setContentType("text/xml");
+ assertEquals("text/xml;charset=utf-8", response.getContentType());
+ response.setCharacterEncoding("iso-8859-1");
+ assertEquals("text/xml;charset=utf-8", response.getContentType());
+ }
+
+ @Test
+ public void testContentTypeWithCharacterEncoding() throws Exception
+ {
+ Response response = getResponse();
+
+ response.setCharacterEncoding("utf16");
+ response.setContentType("foo/bar; charset=UTF-8");
+ assertEquals("foo/bar; charset=UTF-8", response.getContentType());
+ response.getWriter();
+ assertEquals("foo/bar; charset=UTF-8", response.getContentType());
+ response.setContentType("foo2/bar2");
+ assertEquals("foo2/bar2;charset=utf-8", response.getContentType());
+ response.setCharacterEncoding("ISO-8859-1");
+ assertEquals("foo2/bar2;charset=utf-8", response.getContentType());
+
+ response.recycle();
+ response.reopen();
+
+ response.setCharacterEncoding("utf16");
+ response.setContentType("text/html; charset=utf-8");
+ assertEquals("text/html;charset=utf-8", response.getContentType());
+ response.getWriter();
+ assertEquals("text/html;charset=utf-8", response.getContentType());
+ response.setContentType("text/xml");
+ assertEquals("text/xml;charset=utf-8", response.getContentType());
+ response.setCharacterEncoding("iso-8859-1");
+ assertEquals("text/xml;charset=utf-8", response.getContentType());
+
+ response.recycle();
+ response.reopen();
+ response.setCharacterEncoding("utf-16");
+ response.setContentType("foo/bar");
+ assertEquals("foo/bar;charset=utf-16", response.getContentType());
+ response.getOutputStream();
+ response.setCharacterEncoding("utf-8");
+ assertEquals("foo/bar;charset=utf-8", response.getContentType());
+ response.flushBuffer();
+ response.setCharacterEncoding("utf-16");
+ assertEquals("foo/bar;charset=utf-8", response.getContentType());
+ }
+
+ @Test
+ public void testResetWithNewSession() throws Exception
+ {
+ Response response = getResponse();
+ Request request = response.getHttpChannel().getRequest();
+
+ SessionHandler sessionHandler = new SessionHandler();
+ sessionHandler.setServer(_server);
+ sessionHandler.setUsingCookies(true);
+ sessionHandler.start();
+ request.setSessionHandler(sessionHandler);
+ HttpSession session = request.getSession(true);
+
+ assertThat(session, not(nullValue()));
+ assertTrue(session.isNew());
+
+ HttpField setCookie = response.getHttpFields().getField(HttpHeader.SET_COOKIE);
+ assertThat(setCookie, not(nullValue()));
+ assertThat(setCookie.getValue(), startsWith("JSESSIONID"));
+ assertThat(setCookie.getValue(), containsString(session.getId()));
+ response.setHeader("Some", "Header");
+ response.addCookie(new Cookie("Some", "Cookie"));
+ response.getOutputStream().print("X");
+ assertThat(response.getHttpFields().size(), is(4));
+
+ response.reset();
+
+ setCookie = response.getHttpFields().getField(HttpHeader.SET_COOKIE);
+ assertThat(setCookie, not(nullValue()));
+ assertThat(setCookie.getValue(), startsWith("JSESSIONID"));
+ assertThat(setCookie.getValue(), containsString(session.getId()));
+ assertThat(response.getHttpFields().size(), is(2));
+ response.getWriter();
+ }
+
+ @Test
+ public void testResetContentTypeWithoutCharacterEncoding() throws Exception
+ {
+ Response response = getResponse();
+
+ response.setCharacterEncoding("utf-8");
+ response.setContentType("wrong/answer");
+ response.setContentType("foo/bar");
+ assertEquals("foo/bar;charset=utf-8", response.getContentType());
+ response.getWriter();
+ response.setContentType("foo2/bar2");
+ assertEquals("foo2/bar2;charset=utf-8", response.getContentType());
+ }
+
+ @Test
+ public void testResetContentTypeWithCharacterEncoding() throws Exception
+ {
+ Response response = getResponse();
+
+ response.setContentType("wrong/answer;charset=utf-8");
+ response.setContentType("foo/bar");
+ assertEquals("foo/bar", response.getContentType());
+ response.setContentType("wrong/answer;charset=utf-8");
+ response.getWriter();
+ response.setContentType("foo2/bar2;charset=utf-16");
+ assertEquals("foo2/bar2;charset=utf-8", response.getContentType());
+ }
+
+ @Test
+ public void testPrintEmpty() throws Exception
+ {
+ Response response = getResponse();
+ response.setCharacterEncoding(UTF_8.name());
+
+ try (ServletOutputStream outputStream = response.getOutputStream())
+ {
+ outputStream.print("ABC");
+ outputStream.print("");
+ outputStream.println();
+ outputStream.flush();
+ }
+
+ String expected = "ABC\r\n";
+ assertEquals(expected, BufferUtil.toString(_content, UTF_8));
+ }
+
+ @Test
+ public void testPrintln() throws Exception
+ {
+ Response response = getResponse();
+ response.setCharacterEncoding(UTF_8.name());
+
+ String expected = "";
+ response.getOutputStream().print("ABC");
+ expected += "ABC";
+ response.getOutputStream().println("XYZ");
+ expected += "XYZ\r\n";
+ String s = "";
+ for (int i = 0; i < 100; i++)
+ {
+ s += "\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC";
+ }
+ response.getOutputStream().println(s);
+ expected += s + "\r\n";
+
+ response.getOutputStream().close();
+ assertEquals(expected, BufferUtil.toString(_content, UTF_8));
+ }
+
+ @Test
+ public void testContentTypeWithOther() throws Exception
+ {
+ Response response = getResponse();
+
+ response.setContentType("foo/bar; other=xyz");
+ assertEquals("foo/bar; other=xyz", response.getContentType());
+ response.getWriter();
+ assertEquals("foo/bar; other=xyz;charset=iso-8859-1", response.getContentType());
+ response.setContentType("foo2/bar2");
+ assertEquals("foo2/bar2;charset=iso-8859-1", response.getContentType());
+
+ response.recycle();
+
+ response.setCharacterEncoding("uTf-8");
+ response.setContentType("text/html; other=xyz");
+ assertEquals("text/html; other=xyz;charset=utf-8", response.getContentType());
+ response.getWriter();
+ assertEquals("text/html; other=xyz;charset=utf-8", response.getContentType());
+ response.setContentType("text/xml");
+ assertEquals("text/xml;charset=utf-8", response.getContentType());
+ }
+
+ @Test
+ public void testContentTypeWithCharacterEncodingAndOther() throws Exception
+ {
+ Response response = getResponse();
+
+ response.setCharacterEncoding("utf16");
+ response.setContentType("foo/bar; charset=utf-8 other=xyz");
+ assertEquals("foo/bar; charset=utf-8 other=xyz", response.getContentType());
+ response.getWriter();
+ assertEquals("foo/bar; charset=utf-8 other=xyz", response.getContentType());
+
+ response.recycle();
+
+ response.setCharacterEncoding("utf16");
+ response.setContentType("text/html; other=xyz charset=utf-8");
+ assertEquals("text/html; other=xyz charset=utf-8;charset=utf-16", response.getContentType());
+ response.getWriter();
+ assertEquals("text/html; other=xyz charset=utf-8;charset=utf-16", response.getContentType());
+
+ response.recycle();
+
+ response.setCharacterEncoding("utf16");
+ response.setContentType("foo/bar; other=pq charset=utf-8 other=xyz");
+ assertEquals("foo/bar; other=pq charset=utf-8 other=xyz;charset=utf-16", response.getContentType());
+ response.getWriter();
+ assertEquals("foo/bar; other=pq charset=utf-8 other=xyz;charset=utf-16", response.getContentType());
+ }
+
+ public static Stream<Object[]> sendErrorTestCodes()
+ {
+ List<Object[]> data = new ArrayList<>();
+ data.add(new Object[]{404, null, "Not Found"});
+ data.add(new Object[]{500, "Database Error", "Database Error"});
+ data.add(new Object[]{406, "Super Nanny", "Super Nanny"});
+ return data.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "sendErrorTestCodes")
+ public void testStatusCodes(int code, String message, String expectedMessage) throws Exception
+ {
+ Response response = getResponse();
+ assertThat(response.getHttpChannel().getState().handling(), is(HttpChannelState.Action.DISPATCH));
+
+ if (message == null)
+ response.sendError(code);
+ else
+ response.sendError(code, message);
+
+ assertTrue(response.getHttpOutput().isClosed());
+ assertEquals(code, response.getStatus());
+ assertEquals(null, response.getReason());
+
+ response.setHeader("Should-Be-Ignored", "value");
+ assertFalse(response.getHttpFields().containsKey("Should-Be-Ignored"));
+
+ assertEquals(expectedMessage, response.getHttpChannel().getRequest().getAttribute(RequestDispatcher.ERROR_MESSAGE));
+ assertThat(response.getHttpChannel().getState().unhandle(), is(HttpChannelState.Action.SEND_ERROR));
+ assertThat(response.getHttpChannel().getState().unhandle(), is(HttpChannelState.Action.COMPLETE));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "sendErrorTestCodes")
+ public void testStatusCodesNoErrorHandler(int code, String message, String expectedMessage) throws Exception
+ {
+ _server.removeBean(_server.getBean(ErrorHandler.class));
+ testStatusCodes(code, message, expectedMessage);
+ }
+
+ @Test
+ public void testWriteCheckError() throws Exception
+ {
+ Response response = getResponse();
+
+ PrintWriter writer = response.getWriter();
+ writer.println("test");
+ writer.flush();
+ assertFalse(writer.checkError());
+
+ Throwable cause = new IOException("problem at mill");
+ _channel.abort(cause);
+ writer.println("test");
+ assertTrue(writer.checkError());
+
+ writer.println("test"); // this should not cause an Exception
+ assertTrue(writer.checkError());
+ }
+
+ @Test
+ public void testEncodeRedirect()
+ throws Exception
+ {
+ Response response = getResponse();
+ Request request = response.getHttpChannel().getRequest();
+ request.setAuthority("myhost", 8888);
+ request.setContextPath("/path");
+
+ assertEquals("http://myhost:8888/path/info;param?query=0&more=1#target", response.encodeURL("http://myhost:8888/path/info;param?query=0&more=1#target"));
+
+ request.setRequestedSessionId("12345");
+ request.setRequestedSessionIdFromCookie(false);
+ SessionHandler handler = new SessionHandler();
+ DefaultSessionCache ss = new DefaultSessionCache(handler);
+ NullSessionDataStore ds = new NullSessionDataStore();
+ ss.setSessionDataStore(ds);
+ DefaultSessionIdManager idMgr = new DefaultSessionIdManager(_server);
+ idMgr.setWorkerName(null);
+ handler.setSessionIdManager(idMgr);
+ request.setSessionHandler(handler);
+ TestSession tsession = new TestSession(handler, "12345");
+ tsession.setExtendedId(handler.getSessionIdManager().getExtendedId("12345", null));
+ request.setSession(tsession);
+
+ handler.setCheckingRemoteSessionIdEncoding(false);
+
+ assertEquals("http://myhost:8888/path/info;param;jsessionid=12345?query=0&more=1#target", response.encodeURL("http://myhost:8888/path/info;param?query=0&more=1#target"));
+ assertEquals("http://other:8888/path/info;param;jsessionid=12345?query=0&more=1#target", response.encodeURL("http://other:8888/path/info;param?query=0&more=1#target"));
+ assertEquals("http://myhost/path/info;param;jsessionid=12345?query=0&more=1#target", response.encodeURL("http://myhost/path/info;param?query=0&more=1#target"));
+ assertEquals("http://myhost:8888/other/info;param;jsessionid=12345?query=0&more=1#target", response.encodeURL("http://myhost:8888/other/info;param?query=0&more=1#target"));
+
+ handler.setCheckingRemoteSessionIdEncoding(true);
+ assertEquals("http://myhost:8888/path/info;param;jsessionid=12345?query=0&more=1#target", response.encodeURL("http://myhost:8888/path/info;param?query=0&more=1#target"));
+ assertEquals("http://other:8888/path/info;param?query=0&more=1#target", response.encodeURL("http://other:8888/path/info;param?query=0&more=1#target"));
+ assertEquals("http://myhost/path/info;param?query=0&more=1#target", response.encodeURL("http://myhost/path/info;param?query=0&more=1#target"));
+ assertEquals("http://myhost:8888/other/info;param?query=0&more=1#target", response.encodeURL("http://myhost:8888/other/info;param?query=0&more=1#target"));
+
+ request.setContextPath("");
+ assertEquals("http://myhost:8888/;jsessionid=12345", response.encodeURL("http://myhost:8888"));
+ assertEquals("https://myhost:8888/;jsessionid=12345", response.encodeURL("https://myhost:8888"));
+ assertEquals("mailto:/foo", response.encodeURL("mailto:/foo"));
+ assertEquals("http://myhost:8888/;jsessionid=12345", response.encodeURL("http://myhost:8888/"));
+ assertEquals("http://myhost:8888/;jsessionid=12345", response.encodeURL("http://myhost:8888/;jsessionid=7777"));
+ assertEquals("http://myhost:8888/;param;jsessionid=12345?query=0&more=1#target", response.encodeURL("http://myhost:8888/;param?query=0&more=1#target"));
+ assertEquals("http://other:8888/path/info;param?query=0&more=1#target", response.encodeURL("http://other:8888/path/info;param?query=0&more=1#target"));
+ handler.setCheckingRemoteSessionIdEncoding(false);
+ assertEquals("/foo;jsessionid=12345", response.encodeURL("/foo"));
+ assertEquals("/;jsessionid=12345", response.encodeURL("/"));
+ assertEquals("/foo.html;jsessionid=12345#target", response.encodeURL("/foo.html#target"));
+ assertEquals(";jsessionid=12345", response.encodeURL(""));
+ }
+
+ @Test
+ public void testSendRedirect()
+ throws Exception
+ {
+ String[][] tests = {
+ // No cookie
+ {
+ "http://myhost:8888/other/location;jsessionid=12345?name=value",
+ "http://myhost:8888/other/location;jsessionid=12345?name=value"
+ },
+ {"/other/location;jsessionid=12345?name=value", "http://@HOST@@PORT@/other/location;jsessionid=12345?name=value"},
+ {"./location;jsessionid=12345?name=value", "http://@HOST@@PORT@/path/location;jsessionid=12345?name=value"},
+
+ // From cookie
+ {"/other/location", "http://@HOST@@PORT@/other/location"},
+ {"/other/l%20cation", "http://@HOST@@PORT@/other/l%20cation"},
+ {"location", "http://@HOST@@PORT@/path/location"},
+ {"./location", "http://@HOST@@PORT@/path/location"},
+ {"../location", "http://@HOST@@PORT@/location"},
+ {"/other/l%20cation", "http://@HOST@@PORT@/other/l%20cation"},
+ {"l%20cation", "http://@HOST@@PORT@/path/l%20cation"},
+ {"./l%20cation", "http://@HOST@@PORT@/path/l%20cation"},
+ {"../l%20cation", "http://@HOST@@PORT@/l%20cation"},
+ {"../locati%C3%abn", "http://@HOST@@PORT@/locati%C3%abn"},
+ {"../other%2fplace", "http://@HOST@@PORT@/other%2fplace"},
+ {"http://somehost.com/other/location", "http://somehost.com/other/location"},
+ };
+
+ int[] ports = new int[]{8080, 80};
+ String[] hosts = new String[]{null, "myhost", "192.168.0.1", "[0::1]"};
+ for (int port : ports)
+ {
+ for (String host : hosts)
+ {
+ for (int i = 0; i < tests.length; i++)
+ {
+ // System.err.printf("%s %d %s%n",host,port,tests[i][0]);
+
+ Response response = getResponse();
+ Request request = response.getHttpChannel().getRequest();
+
+ request.setScheme("http");
+ if (host != null)
+ request.setAuthority(host, port);
+ request.setURIPathQuery("/path/info;param;jsessionid=12345?query=0&more=1#target");
+ request.setContextPath("/path");
+ request.setRequestedSessionId("12345");
+ request.setRequestedSessionIdFromCookie(i > 2);
+ SessionHandler handler = new SessionHandler();
+
+ NullSessionDataStore ds = new NullSessionDataStore();
+ DefaultSessionCache ss = new DefaultSessionCache(handler);
+ handler.setSessionCache(ss);
+ ss.setSessionDataStore(ds);
+ DefaultSessionIdManager idMgr = new DefaultSessionIdManager(_server);
+ idMgr.setWorkerName(null);
+ handler.setSessionIdManager(idMgr);
+ request.setSessionHandler(handler);
+ request.setSession(new TestSession(handler, "12345"));
+ handler.setCheckingRemoteSessionIdEncoding(false);
+
+ response.sendRedirect(tests[i][0]);
+
+ String location = response.getHeader("Location");
+
+ String expected = tests[i][1]
+ .replace("@HOST@", host == null ? request.getLocalAddr() : host)
+ .replace("@PORT@", host == null ? ":8888" : (port == 80 ? "" : (":" + port)));
+ assertEquals(expected, location, "test-" + i + " " + host + ":" + port);
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testSendRedirectRelative()
+ throws Exception
+ {
+ String[][] tests = {
+ // No cookie
+ {
+ "http://myhost:8888/other/location;jsessionid=12345?name=value",
+ "http://myhost:8888/other/location;jsessionid=12345?name=value"
+ },
+ {"/other/location;jsessionid=12345?name=value", "/other/location;jsessionid=12345?name=value"},
+ {"./location;jsessionid=12345?name=value", "/path/location;jsessionid=12345?name=value"},
+
+ // From cookie
+ {"/other/location", "/other/location"},
+ {"/other/l%20cation", "/other/l%20cation"},
+ {"location", "/path/location"},
+ {"./location", "/path/location"},
+ {"../location", "/location"},
+ {"/other/l%20cation", "/other/l%20cation"},
+ {"l%20cation", "/path/l%20cation"},
+ {"./l%20cation", "/path/l%20cation"},
+ {"../l%20cation", "/l%20cation"},
+ {"../locati%C3%abn", "/locati%C3%abn"},
+ {"../other%2fplace", "/other%2fplace"},
+ {"http://somehost.com/other/location", "http://somehost.com/other/location"},
+ };
+
+ int[] ports = new int[]{8080, 80};
+ String[] hosts = new String[]{null, "myhost", "192.168.0.1", "[0::1]"};
+ for (int port : ports)
+ {
+ for (String host : hosts)
+ {
+ for (int i = 0; i < tests.length; i++)
+ {
+ // System.err.printf("%s %d %s%n",host,port,tests[i][0]);
+
+ Response response = getResponse();
+ Request request = response.getHttpChannel().getRequest();
+ request.getHttpChannel().getHttpConfiguration().setRelativeRedirectAllowed(true);
+
+ request.setScheme("http");
+ if (host != null)
+ request.setAuthority(host, port);
+ request.setURIPathQuery("/path/info;param;jsessionid=12345?query=0&more=1#target");
+ request.setContextPath("/path");
+ request.setRequestedSessionId("12345");
+ request.setRequestedSessionIdFromCookie(i > 2);
+ SessionHandler handler = new SessionHandler();
+
+ NullSessionDataStore ds = new NullSessionDataStore();
+ DefaultSessionCache ss = new DefaultSessionCache(handler);
+ handler.setSessionCache(ss);
+ ss.setSessionDataStore(ds);
+ DefaultSessionIdManager idMgr = new DefaultSessionIdManager(_server);
+ idMgr.setWorkerName(null);
+ handler.setSessionIdManager(idMgr);
+ request.setSessionHandler(handler);
+ request.setSession(new TestSession(handler, "12345"));
+ handler.setCheckingRemoteSessionIdEncoding(false);
+
+ response.sendRedirect(tests[i][0]);
+
+ String location = response.getHeader("Location");
+
+ String expected = tests[i][1]
+ .replace("@HOST@", host == null ? request.getLocalAddr() : host)
+ .replace("@PORT@", host == null ? ":8888" : (port == 80 ? "" : (":" + port)));
+ assertEquals(expected, location, "test-" + i + " " + host + ":" + port);
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testInvalidSendRedirect() throws Exception
+ {
+ // Request is /path/info, so we need 3 ".." for an invalid redirect.
+ Response response = getResponse();
+ assertThrows(IllegalStateException.class, () -> response.sendRedirect("../../../invalid"));
+ }
+
+ @Test
+ public void testSetBufferSizeAfterHavingWrittenContent() throws Exception
+ {
+ Response response = getResponse();
+ response.setBufferSize(20 * 1024);
+ response.getWriter().print("hello");
+
+ assertThrows(IllegalStateException.class, () -> response.setBufferSize(21 * 1024));
+ }
+
+ @Test
+ public void testZeroContent() throws Exception
+ {
+ Response response = getResponse();
+ PrintWriter writer = response.getWriter();
+ response.setContentLength(0);
+ assertTrue(!response.isCommitted());
+ assertTrue(!writer.checkError());
+ writer.print("");
+ // assertTrue(!writer.checkError()); // TODO check if this is correct? checkout does an open check and the print above closes
+ assertTrue(response.isCommitted());
+ }
+
+ @Test
+ public void testHead() throws Exception
+ {
+ Server server = new Server(0);
+ try
+ {
+ server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.setStatus(200);
+ response.setContentType("text/plain");
+ PrintWriter w = response.getWriter();
+ w.flush();
+ w.println("Geht");
+ w.flush();
+ w.println("Doch");
+ w.flush();
+ ((Request)request).setHandled(true);
+ }
+ });
+ server.start();
+
+ try (Socket socket = new Socket("localhost", ((NetworkConnector)server.getConnectors()[0]).getLocalPort()))
+ {
+ socket.setSoTimeout(500000);
+ socket.getOutputStream().write("HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n".getBytes());
+ socket.getOutputStream().write("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n".getBytes());
+ socket.getOutputStream().flush();
+
+ LineNumberReader reader = new LineNumberReader(new InputStreamReader(socket.getInputStream()));
+ String line = reader.readLine();
+ assertThat(line, startsWith("HTTP/1.1 200 OK"));
+ // look for blank line
+ while (line != null && line.length() > 0)
+ {
+ line = reader.readLine();
+ }
+
+ // Read the first line of the GET
+ line = reader.readLine();
+ assertThat(line, startsWith("HTTP/1.1 200 OK"));
+
+ String last = null;
+ while (line != null)
+ {
+ last = line;
+ line = reader.readLine();
+ }
+
+ assertEquals("Doch", last);
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void testAddCookie() throws Exception
+ {
+ Response response = getResponse();
+
+ Cookie cookie = new Cookie("name", "value");
+ cookie.setDomain("domain");
+ cookie.setPath("/path");
+ cookie.setSecure(true);
+ cookie.setComment("comment__HTTP_ONLY__");
+
+ response.addCookie(cookie);
+
+ String set = response.getHttpFields().get("Set-Cookie");
+
+ assertEquals("name=value; Path=/path; Domain=domain; Secure; HttpOnly", set);
+ }
+
+ @Test
+ public void testAddCookieInInclude() throws Exception
+ {
+ Response response = getResponse();
+ response.include();
+
+ Cookie cookie = new Cookie("naughty", "value");
+ cookie.setDomain("domain");
+ cookie.setPath("/path");
+ cookie.setSecure(true);
+ cookie.setComment("comment__HTTP_ONLY__");
+
+ response.addCookie(cookie);
+
+ assertNull(response.getHttpFields().get("Set-Cookie"));
+ }
+
+ @Test
+ public void testAddCookieSameSiteDefault() throws Exception
+ {
+ Response response = getResponse();
+ TestServletContextHandler context = new TestServletContextHandler();
+ _channel.getRequest().setContext(context.getServletContext());
+ context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, HttpCookie.SameSite.STRICT);
+ Cookie cookie = new Cookie("name", "value");
+ cookie.setDomain("domain");
+ cookie.setPath("/path");
+ cookie.setSecure(true);
+ cookie.setComment("comment__HTTP_ONLY__");
+
+ response.addCookie(cookie);
+ String set = response.getHttpFields().get("Set-Cookie");
+ assertEquals("name=value; Path=/path; Domain=domain; Secure; HttpOnly; SameSite=Strict", set);
+
+ response.getHttpFields().remove("Set-Cookie");
+
+ //test bad default samesite value
+ context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "FooBar");
+
+ assertThrows(IllegalStateException.class,
+ () -> response.addCookie(cookie));
+ }
+
+ @Test
+ public void testAddCookieComplianceRFC2965() throws Exception
+ {
+ Response response = getResponse();
+ response.getHttpChannel().getHttpConfiguration().setResponseCookieCompliance(CookieCompliance.RFC2965);
+
+ Cookie cookie = new Cookie("name", "value");
+ cookie.setDomain("domain");
+ cookie.setPath("/path");
+ cookie.setSecure(true);
+ cookie.setComment("comment__HTTP_ONLY__");
+
+ response.addCookie(cookie);
+
+ String set = response.getHttpFields().get("Set-Cookie");
+
+ assertEquals("name=value;Version=1;Path=/path;Domain=domain;Secure;HttpOnly;Comment=comment", set);
+ }
+
+ /**
+ * Testing behavior documented in Chrome bug
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=700618
+ */
+ @Test
+ public void testAddCookieJavaxServletHttp() throws Exception
+ {
+ Response response = getResponse();
+
+ Cookie cookie = new Cookie("foo", URLEncoder.encode("bar;baz", UTF_8.toString()));
+ cookie.setPath("/secure");
+
+ response.addCookie(cookie);
+
+ String set = response.getHttpFields().get("Set-Cookie");
+
+ assertEquals("foo=bar%3Bbaz; Path=/secure", set);
+ }
+
+ /**
+ * Testing behavior documented in Chrome bug
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=700618
+ */
+ @Test
+ public void testAddCookieJavaNet() throws Exception
+ {
+ java.net.HttpCookie cookie = new java.net.HttpCookie("foo", URLEncoder.encode("bar;baz", UTF_8.toString()));
+ cookie.setPath("/secure");
+
+ assertEquals("foo=\"bar%3Bbaz\";$Path=\"/secure\"", cookie.toString());
+ }
+
+ @Test
+ public void testResetContent() throws Exception
+ {
+ Response response = getResponse();
+
+ Cookie cookie = new Cookie("name", "value");
+ cookie.setDomain("domain");
+ cookie.setPath("/path");
+ cookie.setSecure(true);
+ cookie.setComment("comment__HTTP_ONLY__");
+ response.addCookie(cookie);
+
+ Cookie cookie2 = new Cookie("name2", "value2");
+ cookie2.setDomain("domain");
+ cookie2.setPath("/path");
+ response.addCookie(cookie2);
+
+ response.setContentType("some/type");
+ response.setContentLength(3);
+ response.setHeader(HttpHeader.EXPIRES, "never");
+
+ response.setHeader("SomeHeader", "SomeValue");
+
+ response.getOutputStream();
+
+ // reset the content
+ response.resetContent();
+
+ // check content is nulled
+ assertThat(response.getContentType(), nullValue());
+ assertThat(response.getContentLength(), is(-1L));
+ assertThat(response.getHeader(HttpHeader.EXPIRES.asString()), nullValue());
+ response.getWriter();
+
+ // check arbitrary header still set
+ assertThat(response.getHeader("SomeHeader"), is("SomeValue"));
+
+ // check cookies are still there
+ Enumeration<String> set = response.getHttpFields().getValues("Set-Cookie");
+
+ assertNotNull(set);
+ ArrayList<String> list = Collections.list(set);
+ assertThat(list, containsInAnyOrder(
+ "name=value; Path=/path; Domain=domain; Secure; HttpOnly",
+ "name2=value2; Path=/path; Domain=domain"
+ ));
+
+ //get rid of the cookies
+ response.reset();
+
+ set = response.getHttpFields().getValues("Set-Cookie");
+ assertFalse(set.hasMoreElements());
+ }
+
+ @Test
+ public void testReplaceHttpCookie()
+ {
+ Response response = getResponse();
+
+ response.replaceCookie(new HttpCookie("Foo", "123456"));
+ response.replaceCookie(new HttpCookie("Foo", "123456", "A", "/path"));
+ response.replaceCookie(new HttpCookie("Foo", "123456", "B", "/path"));
+
+ response.replaceCookie(new HttpCookie("Bar", "123456"));
+ response.replaceCookie(new HttpCookie("Bar", "123456", null, "/left"));
+ response.replaceCookie(new HttpCookie("Bar", "123456", null, "/right"));
+
+ response.replaceCookie(new HttpCookie("Bar", "value", null, "/right"));
+ response.replaceCookie(new HttpCookie("Bar", "value", null, "/left"));
+ response.replaceCookie(new HttpCookie("Bar", "value"));
+
+ response.replaceCookie(new HttpCookie("Foo", "value", "B", "/path"));
+ response.replaceCookie(new HttpCookie("Foo", "value", "A", "/path"));
+ response.replaceCookie(new HttpCookie("Foo", "value"));
+
+ String[] expected = new String[]{
+ "Foo=value",
+ "Foo=value; Path=/path; Domain=A",
+ "Foo=value; Path=/path; Domain=B",
+ "Bar=value",
+ "Bar=value; Path=/left",
+ "Bar=value; Path=/right"
+ };
+
+ List<String> actual = Collections.list(response.getHttpFields().getValues("Set-Cookie"));
+ assertThat("HttpCookie order", actual, hasItems(expected));
+ }
+
+ @Test
+ public void testReplaceHttpCookieSameSite()
+ {
+ Response response = getResponse();
+ TestServletContextHandler context = new TestServletContextHandler();
+ context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "LAX");
+ _channel.getRequest().setContext(context.getServletContext());
+ //replace with no prior does an add
+ response.replaceCookie(new HttpCookie("Foo", "123456"));
+ String set = response.getHttpFields().get("Set-Cookie");
+ assertEquals("Foo=123456; SameSite=Lax", set);
+ //check replacement
+ response.replaceCookie(new HttpCookie("Foo", "other"));
+ set = response.getHttpFields().get("Set-Cookie");
+ assertEquals("Foo=other; SameSite=Lax", set);
+ }
+
+ @Test
+ public void testReplaceParsedHttpCookie()
+ {
+ Response response = getResponse();
+
+ response.addHeader(HttpHeader.SET_COOKIE.asString(), "Foo=123456");
+ response.replaceCookie(new HttpCookie("Foo", "value"));
+ List<String> actual = Collections.list(response.getHttpFields().getValues("Set-Cookie"));
+ assertThat(actual, hasItems(new String[]{"Foo=value"}));
+
+ response.setHeader(HttpHeader.SET_COOKIE, "Foo=123456; domain=Bah; Path=/path");
+ response.replaceCookie(new HttpCookie("Foo", "other"));
+ actual = Collections.list(response.getHttpFields().getValues("Set-Cookie"));
+ assertThat(actual, hasItems(new String[]{"Foo=123456; domain=Bah; Path=/path", "Foo=other"}));
+
+ response.replaceCookie(new HttpCookie("Foo", "replaced", "Bah", "/path"));
+ actual = Collections.list(response.getHttpFields().getValues("Set-Cookie"));
+ assertThat(actual, hasItems(new String[]{"Foo=replaced; Path=/path; Domain=Bah", "Foo=other"}));
+
+ response.setHeader(HttpHeader.SET_COOKIE, "Foo=123456; domain=Bah; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; Path=/path");
+ response.replaceCookie(new HttpCookie("Foo", "replaced", "Bah", "/path"));
+ actual = Collections.list(response.getHttpFields().getValues("Set-Cookie"));
+ assertThat(actual, hasItems(new String[]{"Foo=replaced; Path=/path; Domain=Bah"}));
+ }
+
+ @Test
+ public void testReplaceParsedHttpCookieSiteDefault()
+ {
+ Response response = getResponse();
+ TestServletContextHandler context = new TestServletContextHandler();
+ context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "LAX");
+ _channel.getRequest().setContext(context.getServletContext());
+
+ response.addHeader(HttpHeader.SET_COOKIE.asString(), "Foo=123456");
+ response.replaceCookie(new HttpCookie("Foo", "value"));
+ String set = response.getHttpFields().get("Set-Cookie");
+ assertEquals("Foo=value; SameSite=Lax", set);
+ }
+
+ @Test
+ public void testFlushAfterFullContent() throws Exception
+ {
+ Response response = getResponse();
+ byte[] data = new byte[]{(byte)0xCA, (byte)0xFE};
+ ServletOutputStream output = response.getOutputStream();
+ response.setContentLength(data.length);
+ // Write the whole content
+ output.write(data);
+ // Must not throw
+ output.flush();
+ }
+
+ @Test
+ public void testEnsureConsumeAllOrNotPersistentHttp10() throws Exception
+ {
+ Response response = getResponse();
+ response.getHttpChannel().getRequest().setHttpVersion(HttpVersion.HTTP_1_0);
+ response.getHttpChannel().ensureConsumeAllOrNotPersistent();
+ assertThat(response.getHttpFields().get(HttpHeader.CONNECTION), nullValue());
+
+ response = getResponse();
+ response.getHttpChannel().getRequest().setHttpVersion(HttpVersion.HTTP_1_0);
+ response.setHeader(HttpHeader.CONNECTION, "keep-alive");
+ response.getHttpChannel().ensureConsumeAllOrNotPersistent();
+ assertThat(response.getHttpFields().get(HttpHeader.CONNECTION), nullValue());
+
+ response = getResponse();
+ response.getHttpChannel().getRequest().setHttpVersion(HttpVersion.HTTP_1_0);
+ response.setHeader(HttpHeader.CONNECTION, "before");
+ response.getHttpFields().add(HttpHeader.CONNECTION, "foo, keep-alive, bar");
+ response.getHttpFields().add(HttpHeader.CONNECTION, "after");
+ response.getHttpChannel().ensureConsumeAllOrNotPersistent();
+ assertThat(response.getHttpFields().get(HttpHeader.CONNECTION), is("before, foo, bar, after"));
+
+ response = getResponse();
+ response.getHttpChannel().getRequest().setHttpVersion(HttpVersion.HTTP_1_0);
+ response.setHeader(HttpHeader.CONNECTION, "close");
+ response.getHttpChannel().ensureConsumeAllOrNotPersistent();
+ assertThat(response.getHttpFields().get(HttpHeader.CONNECTION), is("close"));
+ }
+
+ @Test
+ public void testEnsureConsumeAllOrNotPersistentHttp11() throws Exception
+ {
+ Response response = getResponse();
+ response.getHttpChannel().getRequest().setHttpVersion(HttpVersion.HTTP_1_1);
+ response.getHttpChannel().ensureConsumeAllOrNotPersistent();
+ assertThat(response.getHttpFields().get(HttpHeader.CONNECTION), is("close"));
+
+ response = getResponse();
+ response.getHttpChannel().getRequest().setHttpVersion(HttpVersion.HTTP_1_1);
+ response.setHeader(HttpHeader.CONNECTION, "keep-alive");
+ response.getHttpChannel().ensureConsumeAllOrNotPersistent();
+ assertThat(response.getHttpFields().get(HttpHeader.CONNECTION), is("close"));
+
+ response = getResponse();
+ response.getHttpChannel().getRequest().setHttpVersion(HttpVersion.HTTP_1_1);
+ response.setHeader(HttpHeader.CONNECTION, "close");
+ response.getHttpChannel().ensureConsumeAllOrNotPersistent();
+ assertThat(response.getHttpFields().get(HttpHeader.CONNECTION), is("close"));
+
+ response = getResponse();
+ response.getHttpChannel().getRequest().setHttpVersion(HttpVersion.HTTP_1_1);
+ response.setHeader(HttpHeader.CONNECTION, "before, close, after");
+ response.getHttpChannel().ensureConsumeAllOrNotPersistent();
+ assertThat(response.getHttpFields().get(HttpHeader.CONNECTION), is("before, close, after"));
+
+ response = getResponse();
+ response.getHttpChannel().getRequest().setHttpVersion(HttpVersion.HTTP_1_1);
+ response.setHeader(HttpHeader.CONNECTION, "before");
+ response.getHttpFields().add(HttpHeader.CONNECTION, "middle, close");
+ response.getHttpFields().add(HttpHeader.CONNECTION, "after");
+ response.getHttpChannel().ensureConsumeAllOrNotPersistent();
+ assertThat(response.getHttpFields().get(HttpHeader.CONNECTION), is("before, middle, close, after"));
+
+ response = getResponse();
+ response.getHttpChannel().getRequest().setHttpVersion(HttpVersion.HTTP_1_1);
+ response.setHeader(HttpHeader.CONNECTION, "one");
+ response.getHttpFields().add(HttpHeader.CONNECTION, "two");
+ response.getHttpFields().add(HttpHeader.CONNECTION, "three");
+ response.getHttpChannel().ensureConsumeAllOrNotPersistent();
+ assertThat(response.getHttpFields().get(HttpHeader.CONNECTION), is("one, two, three, close"));
+ }
+
+ private Response getResponse()
+ {
+ _channel.recycle();
+ _channel.getRequest().setMetaData(new MetaData.Request("GET", new HttpURI("/path/info"), HttpVersion.HTTP_1_0, new HttpFields()));
+ return _channel.getResponse();
+ }
+
+ private static class TestSession extends Session
+ {
+ protected TestSession(SessionHandler handler, String id)
+ {
+ super(handler, new SessionData(id, "", "0.0.0.0", 0, 0, 0, 300));
+ }
+ }
+
+ private static class TestServletContextHandler extends ContextHandler
+ {
+ private class Context extends ContextHandler.Context
+ {
+ private Map<String, Object> _attributes = new HashMap<>();
+
+ @Override
+ public Object getAttribute(String name)
+ {
+ return _attributes.get(name);
+ }
+
+ @Override
+ public Enumeration<String> getAttributeNames()
+ {
+ return Collections.enumeration(_attributes.keySet());
+ }
+
+ @Override
+ public void setAttribute(String name, Object object)
+ {
+ _attributes.put(name, object);
+ }
+
+ @Override
+ public void removeAttribute(String name)
+ {
+ _attributes.remove(name);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorAsyncContextTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorAsyncContextTest.java
new file mode 100644
index 0000000..d7f8728
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorAsyncContextTest.java
@@ -0,0 +1,42 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jetty.util.IO;
+
+public class ServerConnectorAsyncContextTest extends LocalAsyncContextTest
+{
+ @Override
+ protected Connector initConnector()
+ {
+ return new ServerConnector(_server);
+ }
+
+ @Override
+ protected String getResponse(String request) throws Exception
+ {
+ ServerConnector connector = (ServerConnector)_connector;
+ Socket socket = new Socket((String)null, connector.getLocalPort());
+ socket.getOutputStream().write(request.getBytes(StandardCharsets.UTF_8));
+ return IO.toString(socket.getInputStream());
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorCloseTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorCloseTest.java
new file mode 100644
index 0000000..83b7d10
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorCloseTest.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+public class ServerConnectorCloseTest extends ConnectorCloseTestBase
+{
+ @BeforeEach
+ public void init() throws Exception
+ {
+ startServer(new ServerConnector(_server));
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _server.stop();
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorHttpServerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorHttpServerTest.java
new file mode 100644
index 0000000..09d7780
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorHttpServerTest.java
@@ -0,0 +1,34 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * HttpServer Tester.
+ */
+public class ServerConnectorHttpServerTest extends HttpServerTestBase
+{
+ @BeforeEach
+ public void init() throws Exception
+ {
+ // Run this test with 0 acceptors. Other tests already check the acceptors >0
+ startServer(new ServerConnector(_server, 0, 1));
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTest.java
new file mode 100644
index 0000000..ade6192
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTest.java
@@ -0,0 +1,325 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
+import java.net.BindException;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.SocketChannelEndPoint;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.DefaultHandler;
+import org.eclipse.jetty.server.handler.HandlerList;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ServerConnectorTest
+{
+ public static class ReuseInfoHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.setContentType("text/plain");
+
+ EndPoint endPoint = baseRequest.getHttpChannel().getEndPoint();
+ assertThat("Endpoint", endPoint, instanceOf(SocketChannelEndPoint.class));
+ SocketChannelEndPoint channelEndPoint = (SocketChannelEndPoint)endPoint;
+ Socket socket = channelEndPoint.getSocket();
+ ServerConnector connector = (ServerConnector)baseRequest.getHttpChannel().getConnector();
+
+ PrintWriter out = response.getWriter();
+ out.printf("connector.getReuseAddress() = %b%n", connector.getReuseAddress());
+
+ try
+ {
+ Field fld = connector.getClass().getDeclaredField("_reuseAddress");
+ assertThat("Field[_reuseAddress]", fld, notNullValue());
+ fld.setAccessible(true);
+ Object val = fld.get(connector);
+ out.printf("connector._reuseAddress() = %b%n", val);
+ }
+ catch (Throwable t)
+ {
+ t.printStackTrace(out);
+ }
+
+ out.printf("socket.getReuseAddress() = %b%n", socket.getReuseAddress());
+
+ baseRequest.setHandled(true);
+ }
+ }
+
+ private URI toServerURI(ServerConnector connector) throws URISyntaxException
+ {
+ String host = connector.getHost();
+ if (host == null)
+ {
+ host = "localhost";
+ }
+ int port = connector.getLocalPort();
+ return new URI(String.format("http://%s:%d/", host, port));
+ }
+
+ private String getResponse(URI uri) throws IOException
+ {
+ HttpURLConnection http = (HttpURLConnection)uri.toURL().openConnection();
+ assertThat("Valid Response Code", http.getResponseCode(), anyOf(is(200), is(404)));
+
+ try (InputStream in = http.getInputStream())
+ {
+ return IO.toString(in, StandardCharsets.UTF_8);
+ }
+ }
+
+ @Test
+ public void testReuseAddressDefault() throws Exception
+ {
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(0);
+ server.addConnector(connector);
+
+ HandlerList handlers = new HandlerList();
+ handlers.addHandler(new ReuseInfoHandler());
+ handlers.addHandler(new DefaultHandler());
+
+ server.setHandler(handlers);
+
+ try
+ {
+ server.start();
+
+ URI uri = toServerURI(connector);
+ String response = getResponse(uri);
+ assertThat("Response", response, containsString("connector.getReuseAddress() = true"));
+ assertThat("Response", response, containsString("connector._reuseAddress() = true"));
+
+ // Java on Windows is incapable of propagating reuse-address this to the opened socket.
+ if (!org.junit.jupiter.api.condition.OS.WINDOWS.isCurrentOs())
+ {
+ assertThat("Response", response, containsString("socket.getReuseAddress() = true"));
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void testReuseAddressTrue() throws Exception
+ {
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(0);
+ connector.setReuseAddress(true);
+ server.addConnector(connector);
+
+ HandlerList handlers = new HandlerList();
+ handlers.addHandler(new ReuseInfoHandler());
+ handlers.addHandler(new DefaultHandler());
+
+ server.setHandler(handlers);
+
+ try
+ {
+ server.start();
+
+ URI uri = toServerURI(connector);
+ String response = getResponse(uri);
+ assertThat("Response", response, containsString("connector.getReuseAddress() = true"));
+ assertThat("Response", response, containsString("connector._reuseAddress() = true"));
+
+ // Java on Windows is incapable of propagating reuse-address this to the opened socket.
+ if (!org.junit.jupiter.api.condition.OS.WINDOWS.isCurrentOs())
+ {
+ assertThat("Response", response, containsString("socket.getReuseAddress() = true"));
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void testReuseAddressFalse() throws Exception
+ {
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(0);
+ connector.setReuseAddress(false);
+ server.addConnector(connector);
+
+ HandlerList handlers = new HandlerList();
+ handlers.addHandler(new ReuseInfoHandler());
+ handlers.addHandler(new DefaultHandler());
+
+ server.setHandler(handlers);
+
+ try
+ {
+ server.start();
+
+ URI uri = toServerURI(connector);
+ String response = getResponse(uri);
+ assertThat("Response", response, containsString("connector.getReuseAddress() = false"));
+ assertThat("Response", response, containsString("connector._reuseAddress() = false"));
+
+ // Java on Windows is incapable of propagating reuse-address this to the opened socket.
+ if (!org.junit.jupiter.api.condition.OS.WINDOWS.isCurrentOs())
+ {
+ assertThat("Response", response, containsString("socket.getReuseAddress() = false"));
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void testAddFirstConnectionFactory() throws Exception
+ {
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ server.addConnector(connector);
+
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ connector.addConnectionFactory(http);
+ ProxyConnectionFactory proxy = new ProxyConnectionFactory();
+ connector.addFirstConnectionFactory(proxy);
+
+ Collection<ConnectionFactory> factories = connector.getConnectionFactories();
+ assertEquals(2, factories.size());
+ assertSame(proxy, factories.iterator().next());
+ assertEquals(2, connector.getBeans(ConnectionFactory.class).size());
+ assertEquals(proxy.getProtocol(), connector.getDefaultProtocol());
+ }
+
+ @Test
+ public void testExceptionWhileAccepting() throws Exception
+ {
+ Server server = new Server();
+ try (StacklessLogging stackless = new StacklessLogging(AbstractConnector.class))
+ {
+ AtomicLong spins = new AtomicLong();
+ ServerConnector connector = new ServerConnector(server, 1, 1)
+ {
+ @Override
+ public void accept(int acceptorID) throws IOException
+ {
+ spins.incrementAndGet();
+ throw new IOException("explicitly_thrown_by_test");
+ }
+ };
+ server.addConnector(connector);
+ server.start();
+
+ Thread.sleep(1500);
+ assertThat(spins.get(), Matchers.lessThan(5L));
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void testOpenWithServerSocketChannel() throws Exception
+ {
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ server.addConnector(connector);
+
+ ServerSocketChannel channel = ServerSocketChannel.open();
+ channel.bind(new InetSocketAddress(0));
+
+ assertTrue(channel.isOpen());
+ int port = channel.socket().getLocalPort();
+ assertThat(port, greaterThan(0));
+
+ connector.open(channel);
+
+ assertThat(connector.getLocalPort(), is(port));
+
+ server.start();
+
+ assertThat(connector.getLocalPort(), is(port));
+ assertThat(connector.getTransport(), is(channel));
+
+ server.stop();
+
+ assertThat(connector.getTransport(), Matchers.nullValue());
+ }
+
+ @Test
+ public void testBindToAddressWhichIsInUse() throws Exception
+ {
+ try (ServerSocket socket = new ServerSocket(0))
+ {
+ final int port = socket.getLocalPort();
+
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(port);
+ server.addConnector(connector);
+
+ HandlerList handlers = new HandlerList();
+ handlers.addHandler(new DefaultHandler());
+
+ server.setHandler(handlers);
+
+ IOException x = assertThrows(IOException.class, () -> server.start());
+ assertThat(x.getCause(), instanceOf(BindException.class));
+ assertThat(x.getMessage(), containsString("0.0.0.0:" + port));
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTimeoutTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTimeoutTest.java
new file mode 100644
index 0000000..f8f789b
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTimeoutTest.java
@@ -0,0 +1,224 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static java.time.Duration.ofSeconds;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+
+public class ServerConnectorTimeoutTest extends ConnectorTimeoutTest
+{
+ @BeforeEach
+ public void init() throws Exception
+ {
+ ServerConnector connector = new ServerConnector(_server, 1, 1);
+ connector.setIdleTimeout(MAX_IDLE_TIME);
+ startServer(connector);
+ }
+
+ @Test
+ public void testStartStopStart() throws Exception
+ {
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ _server.stop();
+ _server.start();
+ });
+ }
+
+ @Test
+ public void testIdleTimeoutAfterSuspend() throws Exception
+ {
+ _server.stop();
+ SuspendHandler handler = new SuspendHandler();
+ SessionHandler session = new SessionHandler();
+ session.setHandler(handler);
+ _server.setHandler(session);
+ _server.start();
+
+ handler.setSuspendFor(100);
+ handler.setResumeAfter(25);
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ String process = process(null).toUpperCase(Locale.ENGLISH);
+ assertThat(process, containsString("RESUMED"));
+ });
+ }
+
+ @Test
+ public void testIdleTimeoutAfterTimeout() throws Exception
+ {
+ SuspendHandler handler = new SuspendHandler();
+ _server.stop();
+ SessionHandler session = new SessionHandler();
+ session.setHandler(handler);
+ _server.setHandler(session);
+ _server.start();
+
+ handler.setSuspendFor(50);
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ String process = process(null).toUpperCase(Locale.ENGLISH);
+ assertThat(process, containsString("TIMEOUT"));
+ });
+ }
+
+ @Test
+ public void testIdleTimeoutAfterComplete() throws Exception
+ {
+ SuspendHandler handler = new SuspendHandler();
+ _server.stop();
+ SessionHandler session = new SessionHandler();
+ session.setHandler(handler);
+ _server.setHandler(session);
+ _server.start();
+
+ handler.setSuspendFor(100);
+ handler.setCompleteAfter(25);
+ assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ String process = process(null).toUpperCase(Locale.ENGLISH);
+ assertThat(process, containsString("COMPLETED"));
+ });
+ }
+
+ private synchronized String process(String content) throws IOException, InterruptedException
+ {
+ String request = "GET / HTTP/1.1\r\n" + "Host: localhost\r\n";
+
+ if (content == null)
+ request += "\r\n";
+ else
+ request += "Content-Length: " + content.length() + "\r\n" + "\r\n" + content;
+ return getResponse(request);
+ }
+
+ private String getResponse(String request) throws IOException, InterruptedException
+ {
+ try (Socket socket = new Socket((String)null, _connector.getLocalPort()))
+ {
+ socket.setSoTimeout(10 * MAX_IDLE_TIME);
+ socket.getOutputStream().write(request.getBytes(StandardCharsets.UTF_8));
+ InputStream inputStream = socket.getInputStream();
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ String response = IO.toString(inputStream);
+ long timeElapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start;
+ assertThat(timeElapsed, greaterThanOrEqualTo(MAX_IDLE_TIME - 100L));
+ return response;
+ }
+ }
+
+ @Test
+ public void testHttpWriteIdleTimeout() throws Exception
+ {
+ _httpConfiguration.setIdleTimeout(500);
+ configureServer(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ IO.copy(request.getInputStream(), response.getOutputStream());
+ }
+ });
+ Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
+ client.setSoTimeout(10000);
+
+ assertFalse(client.isClosed());
+
+ final OutputStream os = client.getOutputStream();
+ final InputStream is = client.getInputStream();
+ final StringBuilder response = new StringBuilder();
+
+ CompletableFuture<Void> responseFuture = CompletableFuture.runAsync(() ->
+ {
+ try (InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8))
+ {
+ int c;
+ while ((c = reader.read()) != -1)
+ {
+ response.append((char)c);
+ }
+ }
+ catch (IOException e)
+ {
+ // Valid path (as connection is forcibly closed)
+ // t.printStackTrace(System.err);
+ }
+ });
+
+ CompletableFuture<Void> requestFuture = CompletableFuture.runAsync(() ->
+ {
+ try
+ {
+ os.write((
+ "POST /echo HTTP/1.0\r\n" +
+ "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
+ "content-type: text/plain; charset=utf-8\r\n" +
+ "content-length: 20\r\n" +
+ "\r\n").getBytes("utf-8"));
+ os.flush();
+
+ os.write("123456789\n".getBytes("utf-8"));
+ os.flush();
+ TimeUnit.SECONDS.sleep(1);
+ os.write("=========\n".getBytes("utf-8"));
+ os.flush();
+ }
+ catch (InterruptedException | IOException e)
+ {
+ // Valid path, as write of second half of content can fail
+ // e.printStackTrace(System.err);
+ }
+ });
+
+ try (StacklessLogging ignore = new StacklessLogging(HttpChannel.class))
+ {
+ requestFuture.get(2, TimeUnit.SECONDS);
+ responseFuture.get(3, TimeUnit.SECONDS);
+
+ assertThat(response.toString(), containsString(" 500 "));
+ assertThat(response.toString(), not(containsString("=========")));
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServletRequestWrapperTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServletRequestWrapperTest.java
new file mode 100644
index 0000000..a454774
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServletRequestWrapperTest.java
@@ -0,0 +1,86 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletRequestWrapper;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+
+public class ServletRequestWrapperTest
+{
+ private Server server;
+ private LocalConnector connector;
+ private RequestHandler handler;
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ server = new Server();
+ connector = new LocalConnector(server, new HttpConnectionFactory());
+ server.addConnector(connector);
+
+ handler = new RequestHandler();
+ server.setHandler(handler);
+ server.start();
+ }
+
+ @Test
+ public void testServletRequestWrapper() throws Exception
+ {
+ String request = "GET / HTTP/1.1\r\n" +
+ "Host: whatever\r\n" +
+ "\n";
+
+ String response = connector.getResponse(request);
+ assertThat("Response", response, containsString("200"));
+ }
+
+ private class RequestWrapper extends ServletRequestWrapper
+ {
+ public RequestWrapper(ServletRequest request)
+ {
+ super(request);
+ }
+ }
+
+ private class RequestHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ RequestWrapper requestWrapper = new RequestWrapper(request);
+ AsyncContext context = request.startAsync(requestWrapper, response);
+ context.complete();
+ baseRequest.setHandled(true);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServletWriterTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServletWriterTest.java
new file mode 100644
index 0000000..2b1e94e
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ServletWriterTest.java
@@ -0,0 +1,128 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ServletWriterTest
+{
+ private Server server;
+ private ServerConnector connector;
+
+ private void start(int aggregationSize, Handler handler) throws Exception
+ {
+ server = new Server();
+ HttpConfiguration httpConfig = new HttpConfiguration();
+ httpConfig.setOutputBufferSize(2 * aggregationSize);
+ httpConfig.setOutputAggregationSize(2 * aggregationSize);
+ connector = new ServerConnector(server, 1, 1, new HttpConnectionFactory(httpConfig));
+ server.addConnector(connector);
+ server.setHandler(handler);
+ server.start();
+ }
+
+ @AfterEach
+ public void dispose() throws Exception
+ {
+ server.stop();
+ }
+
+ @Test
+ public void testTCPCongestedCloseDoesNotDeadlock() throws Exception
+ {
+ // Write a large content so it gets TCP congested when calling close().
+ char[] chars = new char[128 * 1024 * 1024];
+ CountDownLatch latch = new CountDownLatch(1);
+ AtomicReference<Thread> serverThreadRef = new AtomicReference<>();
+ start(chars.length, new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ serverThreadRef.set(Thread.currentThread());
+ jettyRequest.setHandled(true);
+ response.setContentType("text/plain; charset=utf-8");
+ PrintWriter writer = response.getWriter();
+ Arrays.fill(chars, '0');
+ // The write is entirely buffered.
+ writer.write(chars);
+ latch.countDown();
+ // Closing will trigger the write over the network.
+ writer.close();
+ }
+ });
+
+ try (Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ String request = "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ OutputStream output = socket.getOutputStream();
+ output.write(request.getBytes(UTF_8));
+ output.flush();
+
+ // Wait until the response is buffered, so close() will write it.
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ // Don't read the response yet to trigger TCP congestion.
+ Thread.sleep(1000);
+
+ // Now read the response.
+ socket.setSoTimeout(5000);
+ InputStream input = socket.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(input, UTF_8));
+ String line = reader.readLine();
+ assertThat(line, containsString(" 200 "));
+ // Consume all the content, we should see EOF.
+ while (line != null)
+ {
+ line = reader.readLine();
+ }
+ }
+ catch (Throwable x)
+ {
+ Thread thread = serverThreadRef.get();
+ if (thread != null)
+ thread.interrupt();
+ throw x;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ShutdownMonitorTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ShutdownMonitorTest.java
new file mode 100644
index 0000000..7bbdabd
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ShutdownMonitorTest.java
@@ -0,0 +1,282 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.BufferedReader;
+import java.io.Closeable;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+
+import org.eclipse.jetty.util.thread.ShutdownThread;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ShutdownMonitorTest
+{
+ @AfterEach
+ public void dispose()
+ {
+ ShutdownMonitor.reset();
+ }
+
+ @Test
+ public void testStatus() throws Exception
+ {
+ ShutdownMonitor monitor = ShutdownMonitor.getInstance();
+ // monitor.setDebug(true);
+ monitor.setPort(0);
+ monitor.setExitVm(false);
+ monitor.start();
+ String key = monitor.getKey();
+ int port = monitor.getPort();
+
+ // Try more than once to be sure that the ServerSocket has not been closed.
+ for (int i = 0; i < 2; ++i)
+ {
+ try (Socket socket = new Socket("localhost", port))
+ {
+ OutputStream output = socket.getOutputStream();
+ String command = "status";
+ output.write((key + "\r\n" + command + "\r\n").getBytes());
+ output.flush();
+
+ BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ String reply = input.readLine();
+ assertEquals("OK", reply);
+ // Socket must be closed afterwards.
+ assertNull(input.readLine());
+ }
+ }
+ }
+
+ @Disabled("Issue #2626")
+ @Test
+ public void testStartStopDifferentPortDifferentKey() throws Exception
+ {
+ testStartStop(false);
+ }
+
+ @Disabled("Issue #2626")
+ @Test
+ public void testStartStopSamePortDifferentKey() throws Exception
+ {
+ testStartStop(true);
+ }
+
+ private void testStartStop(boolean reusePort) throws Exception
+ {
+ ShutdownMonitor monitor = ShutdownMonitor.getInstance();
+ // monitor.setDebug(true);
+ monitor.setPort(0);
+ monitor.setExitVm(false);
+ monitor.start();
+ String key = monitor.getKey();
+ int port = monitor.getPort();
+
+ // try starting a 2nd time (should be ignored)
+ monitor.start();
+
+ stop("stop", port, key, true);
+ monitor.await();
+ assertTrue(!monitor.isAlive());
+
+ // Should be able to change port and key because it is stopped.
+ monitor.setPort(reusePort ? port : 0);
+ String newKey = "foo";
+ monitor.setKey(newKey);
+ monitor.start();
+
+ key = monitor.getKey();
+ assertEquals(newKey, key);
+ port = monitor.getPort();
+ assertTrue(monitor.isAlive());
+
+ stop("stop", port, key, true);
+ monitor.await();
+ assertTrue(!monitor.isAlive());
+ }
+
+ @Test
+ public void testForceStopCommand() throws Exception
+ {
+ ShutdownMonitor monitor = ShutdownMonitor.getInstance();
+ // monitor.setDebug(true);
+ monitor.setPort(0);
+ monitor.setExitVm(false);
+ monitor.start();
+
+ try (CloseableServer server = new CloseableServer())
+ {
+ server.start();
+
+ //shouldn't be registered for shutdown on jvm
+ assertTrue(!ShutdownThread.isRegistered(server));
+ assertTrue(ShutdownMonitor.isRegistered(server));
+
+ String key = monitor.getKey();
+ int port = monitor.getPort();
+
+ stop("forcestop", port, key, true);
+ monitor.await();
+
+ assertTrue(!monitor.isAlive());
+ assertTrue(server.stopped);
+ assertTrue(!server.destroyed);
+ assertTrue(!ShutdownThread.isRegistered(server));
+ assertTrue(!ShutdownMonitor.isRegistered(server));
+ }
+ }
+
+ @Test
+ public void testOldStopCommandWithStopOnShutdownTrue() throws Exception
+ {
+ ShutdownMonitor monitor = ShutdownMonitor.getInstance();
+ // monitor.setDebug(true);
+ monitor.setPort(0);
+ monitor.setExitVm(false);
+ monitor.start();
+
+ try (CloseableServer server = new CloseableServer())
+ {
+ server.setStopAtShutdown(true);
+ server.start();
+
+ //should be registered for shutdown on exit
+ assertTrue(ShutdownThread.isRegistered(server));
+ assertTrue(ShutdownMonitor.isRegistered(server));
+
+ String key = monitor.getKey();
+ int port = monitor.getPort();
+
+ stop("stop", port, key, true);
+ monitor.await();
+
+ assertTrue(!monitor.isAlive());
+ assertTrue(server.stopped);
+ assertTrue(!server.destroyed);
+ assertTrue(!ShutdownThread.isRegistered(server));
+ assertTrue(!ShutdownMonitor.isRegistered(server));
+ }
+ }
+
+ @Test
+ public void testOldStopCommandWithStopOnShutdownFalse() throws Exception
+ {
+ ShutdownMonitor monitor = ShutdownMonitor.getInstance();
+ // monitor.setDebug(true);
+ monitor.setPort(0);
+ monitor.setExitVm(false);
+ monitor.start();
+
+ try (CloseableServer server = new CloseableServer())
+ {
+ server.setStopAtShutdown(false);
+ server.start();
+
+ assertTrue(!ShutdownThread.isRegistered(server));
+ assertTrue(ShutdownMonitor.isRegistered(server));
+
+ String key = monitor.getKey();
+ int port = monitor.getPort();
+
+ stop("stop", port, key, true);
+ monitor.await();
+
+ assertTrue(!monitor.isAlive());
+ assertTrue(!server.stopped);
+ assertTrue(!server.destroyed);
+ assertTrue(!ShutdownThread.isRegistered(server));
+ assertTrue(ShutdownMonitor.isRegistered(server));
+ }
+ }
+
+ public void stop(String command, int port, String key, boolean check) throws Exception
+ {
+ // System.out.printf("Attempting to send " + command + " to localhost:%d (%b)%n", port, check);
+ try (Socket s = new Socket(InetAddress.getByName("127.0.0.1"), port))
+ {
+ // send stop command
+ try (OutputStream out = s.getOutputStream())
+ {
+ out.write((key + "\r\n" + command + "\r\n").getBytes());
+ out.flush();
+
+ if (check)
+ {
+ // check for stop confirmation
+ LineNumberReader lin = new LineNumberReader(new InputStreamReader(s.getInputStream()));
+ String response;
+ if ((response = lin.readLine()) != null)
+ assertEquals("Stopped", response);
+ else
+ throw new IllegalStateException("No stop confirmation");
+ }
+ }
+ }
+ }
+
+ public class CloseableServer extends Server implements Closeable
+ {
+ boolean destroyed = false;
+ boolean stopped = false;
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ stopped = true;
+ super.doStop();
+ }
+
+ @Override
+ public void destroy()
+ {
+ destroyed = true;
+ super.destroy();
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ stopped = false;
+ destroyed = false;
+ super.doStart();
+ }
+
+ @Override
+ public void close()
+ {
+ try
+ {
+ stop();
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/SlowClientWithPipelinedRequestTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/SlowClientWithPipelinedRequestTest.java
new file mode 100644
index 0000000..7397d6c
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/SlowClientWithPipelinedRequestTest.java
@@ -0,0 +1,178 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SlowClientWithPipelinedRequestTest
+{
+ private final AtomicInteger handles = new AtomicInteger();
+ private Server server;
+ private ServerConnector connector;
+
+ public void startServer(Handler handler) throws Exception
+ {
+ server = new Server();
+ connector = new ServerConnector(server, new HttpConnectionFactory()
+ {
+ @Override
+ public Connection newConnection(Connector connector, EndPoint endPoint)
+ {
+ return configure(new HttpConnection(getHttpConfiguration(), connector, endPoint, getHttpCompliance(), isRecordHttpComplianceViolations())
+ {
+ @Override
+ public void onFillable()
+ {
+ handles.incrementAndGet();
+ super.onFillable();
+ }
+ }, connector, endPoint);
+ }
+ });
+
+ server.addConnector(connector);
+ connector.setPort(0);
+ server.setHandler(handler);
+ server.start();
+ }
+
+ @AfterEach
+ public void stopServer() throws Exception
+ {
+ if (server != null)
+ {
+ server.stop();
+ server.join();
+ }
+ }
+
+ @Test
+ public void testSlowClientWithPipelinedRequest() throws Exception
+ {
+ final int contentLength = 512 * 1024;
+ startServer(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ if ("/content".equals(target))
+ {
+ // We simulate what the DefaultServlet does, bypassing the blocking
+ // write mechanism otherwise the test does not reproduce the bug
+ OutputStream outputStream = response.getOutputStream();
+ HttpOutput output = (HttpOutput)outputStream;
+ // Since the test is via localhost, we need a really big buffer to stall the write
+ byte[] bytes = new byte[contentLength];
+ Arrays.fill(bytes, (byte)'9');
+ ByteBuffer buffer = ByteBuffer.wrap(bytes);
+ // Do a non blocking write
+ output.sendContent(buffer);
+ }
+ }
+ });
+
+ Socket client = new Socket("localhost", connector.getLocalPort());
+ OutputStream output = client.getOutputStream();
+ output.write((
+ "GET /content HTTP/1.1\r\n" +
+ "Host: localhost:" + connector.getLocalPort() + "\r\n" +
+ "\r\n" +
+ "").getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ InputStream input = client.getInputStream();
+
+ int read = input.read();
+ assertTrue(read >= 0);
+ // As soon as we can read the response, send a pipelined request
+ // so it is a different read for the server and it will trigger NIO
+ output.write((
+ "GET /pipelined HTTP/1.1\r\n" +
+ "Host: localhost:" + connector.getLocalPort() + "\r\n" +
+ "\r\n" +
+ "").getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ // Simulate a slow reader
+ Thread.sleep(1000);
+ assertThat(handles.get(), lessThan(10));
+
+ // We are sure we are not spinning, read the content
+ StringBuilder lines = new StringBuilder().append((char)read);
+ int crlfs = 0;
+ while (true)
+ {
+ read = input.read();
+ lines.append((char)read);
+ if (read == '\r' || read == '\n')
+ ++crlfs;
+ else
+ crlfs = 0;
+ if (crlfs == 4)
+ break;
+ }
+ assertThat(lines.toString(), containsString(" 200 "));
+ // Read the body
+ for (int i = 0; i < contentLength; ++i)
+ {
+ input.read();
+ }
+
+ // Read the pipelined response
+ lines.setLength(0);
+ crlfs = 0;
+ while (true)
+ {
+ read = input.read();
+ lines.append((char)read);
+ if (read == '\r' || read == '\n')
+ ++crlfs;
+ else
+ crlfs = 0;
+ if (crlfs == 4)
+ break;
+ }
+ assertThat(lines.toString(), containsString(" 200 "));
+
+ client.close();
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/StressTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/StressTest.java
new file mode 100644
index 0000000..4547de2
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/StressTest.java
@@ -0,0 +1,486 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.Queue;
+import java.util.Random;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.handler.HandlerWrapper;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Tag("stress")
+public class StressTest
+{
+ private static final Logger LOG = Log.getLogger(StressTest.class);
+
+ private static QueuedThreadPool _threads;
+ private static Server _server;
+ private static ServerConnector _connector;
+ private static final AtomicInteger _handled = new AtomicInteger(0);
+ private static final ConcurrentLinkedQueue[] _latencies = {
+ new ConcurrentLinkedQueue<Long>(),
+ new ConcurrentLinkedQueue<Long>(),
+ new ConcurrentLinkedQueue<Long>(),
+ new ConcurrentLinkedQueue<Long>(),
+ new ConcurrentLinkedQueue<Long>(),
+ new ConcurrentLinkedQueue<Long>()
+ };
+
+ private volatile AtomicInteger[] _loops;
+ private final Random _random = new Random();
+ private static final String[] __tests =
+ {
+ "/path/0",
+ "/path/1",
+ "/path/2",
+ "/path/3",
+ "/path/4",
+ "/path/5",
+ "/path/6",
+ "/path/7",
+ "/path/8",
+ "/path/9",
+ "/path/a",
+ "/path/b",
+ "/path/c",
+ "/path/d",
+ "/path/e",
+ "/path/f",
+ };
+
+ @BeforeAll
+ public static void init() throws Exception
+ {
+ _threads = new QueuedThreadPool();
+ _threads.setMaxThreads(200);
+
+ _server = new Server(_threads);
+ _server.manage(_threads);
+ _connector = new ServerConnector(_server, null, null, null, 1, 1, new HttpConnectionFactory());
+ _connector.setAcceptQueueSize(5000);
+ _connector.setIdleTimeout(30000);
+ _server.addConnector(_connector);
+
+ TestHandler handler = new TestHandler();
+ _server.setHandler(handler);
+
+ _server.start();
+ }
+
+ @AfterAll
+ public static void destroy() throws Exception
+ {
+ _server.stop();
+ _server.join();
+ }
+
+ @BeforeEach
+ public void reset()
+ {
+ _handled.set(0);
+ for (Queue q : _latencies)
+ {
+ q.clear();
+ }
+ }
+
+ @Test
+ public void testMinNonPersistent() throws Throwable
+ {
+ doThreads(10, 10, false);
+ }
+
+ @Test
+ @Tag("Slow")
+ public void testNonPersistent() throws Throwable
+ {
+ doThreads(20, 20, false);
+ Thread.sleep(1000);
+ doThreads(200, 10, false);
+ Thread.sleep(1000);
+ doThreads(200, 200, false);
+ }
+
+ @Test
+ public void testMinPersistent() throws Throwable
+ {
+ doThreads(10, 10, true);
+ }
+
+ @Test
+ @Tag("Slow")
+ public void testPersistent() throws Throwable
+ {
+ doThreads(40, 40, true);
+ Thread.sleep(1000);
+ doThreads(200, 10, true);
+ Thread.sleep(1000);
+ doThreads(200, 200, true);
+ }
+
+ private void doThreads(int threadCount, final int loops, final boolean persistent) throws Throwable
+ {
+ final Throwable[] throwables = new Throwable[threadCount];
+ final Thread[] threads = new Thread[threadCount];
+
+ try
+ {
+ for (int i = 0; i < threadCount; i++)
+ {
+ final int id = i;
+ final String name = "T" + i;
+ Thread.sleep(_random.nextInt(100));
+ threads[i] = new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ doLoops(id, name, loops, persistent);
+ }
+ catch (Throwable th)
+ {
+ th.printStackTrace();
+ throwables[id] = th;
+ }
+ }
+ };
+ }
+
+ _loops = new AtomicInteger[threadCount];
+ for (int i = 0; i < threadCount; i++)
+ {
+ _loops[i] = new AtomicInteger(0);
+ threads[i].start();
+ }
+
+ String last = null;
+ int same = 0;
+
+ while (true)
+ {
+ Thread.sleep(1000L);
+ int finished = 0;
+ int errors = 0;
+ int min = loops;
+ int max = 0;
+ int total = 0;
+ for (int i = 0; i < threadCount; i++)
+ {
+ int l = _loops[i].get();
+ if (l < 0)
+ {
+ errors++;
+ total -= l;
+ }
+ else
+ {
+ if (l < min)
+ min = l;
+ if (l > max)
+ max = l;
+ total += l;
+ if (l == loops)
+ finished++;
+ }
+ }
+ String status = "min/ave/max/target=" + min + "/" + (total / threadCount) + "/" + max + "/" + loops + " errors/finished/loops=" + errors + "/" + finished + "/" + threadCount + " idle/threads=" + (_threads.getIdleThreads()) + "/" + _threads.getThreads();
+ if (status.equals(last))
+ {
+ if (same++ > 5)
+ {
+ System.err.println("STALLED!!!");
+ System.err.println(_server.getThreadPool().toString());
+ Thread.sleep(5000);
+ System.exit(1);
+ }
+ }
+ else
+ same = 0;
+ last = status;
+ LOG.info(_server.getThreadPool().toString() + " " + status);
+ if ((finished + errors) == threadCount)
+ break;
+ }
+
+ for (Thread thread : threads)
+ {
+ thread.join();
+ }
+
+ for (Throwable throwable : throwables)
+ {
+ if (throwable != null)
+ throw throwable;
+ }
+
+ for (ConcurrentLinkedQueue latency : _latencies)
+ {
+ assertEquals(_handled.get(), latency.size());
+ }
+ }
+ finally
+ {
+ // System.err.println();
+ final int quantums = 48;
+ final int[][] count = new int[_latencies.length][quantums];
+ final int[] length = new int[_latencies.length];
+ final int[] other = new int[_latencies.length];
+
+ long total = 0;
+
+ for (int i = 0; i < _latencies.length; i++)
+ {
+ Queue<Long> latencies = _latencies[i];
+ length[i] = latencies.size();
+
+ loop:
+ for (long latency : latencies)
+ {
+ if (i == 4)
+ total += latency;
+ for (int q = 0; q < quantums; q++)
+ {
+ if (latency >= (q * 100) && latency < ((q + 1) * 100))
+ {
+ count[i][q]++;
+ continue loop;
+ }
+ }
+ other[i]++;
+ }
+ }
+
+ System.out.println(" stage:\tbind\twrite\trecv\tdispatch\twrote\ttotal");
+ for (int q = 0; q < quantums; q++)
+ {
+ System.out.printf("%02d00<=l<%02d00", q, (q + 1));
+ for (int i = 0; i < _latencies.length; i++)
+ {
+ System.out.print("\t" + count[i][q]);
+ }
+ System.out.println();
+ }
+
+ System.out.print("other ");
+ for (int i = 0; i < _latencies.length; i++)
+ {
+ System.out.print("\t" + other[i]);
+ }
+ System.out.println();
+
+ System.out.print("HANDLED ");
+ for (int i = 0; i < _latencies.length; i++)
+ {
+ System.out.print("\t" + _handled.get());
+ }
+ System.out.println();
+ System.out.print("TOTAL ");
+ for (int i = 0; i < _latencies.length; i++)
+ {
+ System.out.print("\t" + length[i]);
+ }
+ System.out.println();
+ long ave = total / _latencies[4].size();
+ System.out.println("ave=" + ave);
+ }
+ }
+
+ private void doLoops(int thread, String name, int loops, boolean persistent) throws Exception
+ {
+ try
+ {
+ for (int i = 0; i < loops; i++)
+ {
+ _loops[thread].set(i);
+ doPaths(thread, name + "-" + i, persistent);
+ Thread.sleep(1 + _random.nextInt(20) * _random.nextInt(20));
+ Thread.sleep(20);
+ }
+ _loops[thread].set(loops);
+ }
+ catch (Exception e)
+ {
+ System.err.println(e);
+ _loops[thread].set(-_loops[thread].get());
+ throw e;
+ }
+ }
+
+ private void doPaths(int thread, String name, boolean persistent) throws Exception
+ {
+ if (persistent)
+ {
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ Socket socket = new Socket("localhost", _connector.getLocalPort());
+ socket.setSoTimeout(30000);
+
+ long connected = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+
+ for (int i = 0; i < __tests.length; i++)
+ {
+ String uri = __tests[i] + "/" + name + "/" + i;
+
+ String close = ((i + 1) < __tests.length) ? "" : "Connection: close\r\n";
+ String request =
+ "GET " + uri + " HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "start: " + start + "\r\n" +
+ close + "\r\n";
+
+ socket.getOutputStream().write(request.getBytes());
+ socket.getOutputStream().flush();
+ Thread.yield();
+ }
+
+ long written = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+
+ String response = IO.toString(socket.getInputStream());
+ socket.close();
+
+ long end = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+
+ int bodies = count(response, "HTTP/1.1 200 OK");
+ if (__tests.length != bodies)
+ System.err.println("responses=\n" + response + "\n---");
+ assertEquals(__tests.length, bodies, name);
+
+ long bind = connected - start;
+ long flush = (written - connected) / __tests.length;
+ long read = (end - written) / __tests.length;
+
+ int offset = 0;
+ for (int i = 0; i < __tests.length; i++)
+ {
+ offset = response.indexOf("DATA " + __tests[i], offset);
+ assertTrue(offset >= 0);
+ offset += __tests[i].length() + 5;
+
+ if (bind < 0 || flush < 0 || read < 0)
+ {
+ System.err.println(bind + "," + flush + "," + read);
+ }
+
+ _latencies[0].add((i == 0) ? new Long(bind) : 0);
+ _latencies[1].add((i == 0) ? new Long(bind + flush) : flush);
+ _latencies[5].add((i == 0) ? new Long(bind + flush + read) : (flush + read));
+ }
+ }
+ else
+ {
+ for (int i = 0; i < __tests.length; i++)
+ {
+ String uri = __tests[i] + "/" + name + "/" + i;
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ String close = "Connection: close\r\n";
+ String request =
+ "GET " + uri + " HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "start: " + start + "\r\n" +
+ close + "\r\n";
+
+ Socket socket = new Socket("localhost", _connector.getLocalPort());
+ socket.setSoTimeout(10000);
+
+ _latencies[0].add(new Long(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start));
+
+ socket.getOutputStream().write(request.getBytes());
+ socket.getOutputStream().flush();
+
+ _latencies[1].add(new Long(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start));
+
+ String response = IO.toString(socket.getInputStream());
+ socket.close();
+ long end = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+
+ String endOfResponse = "\r\n\r\n";
+ assertThat(response, containsString(endOfResponse));
+ response = response.substring(response.indexOf(endOfResponse) + endOfResponse.length());
+
+ assertThat(uri, response, startsWith("DATA " + __tests[i]));
+ long latency = end - start;
+
+ _latencies[5].add(new Long(latency));
+ }
+ }
+ }
+
+ private int count(String s, String sub)
+ {
+ int count = 0;
+ int index = s.indexOf(sub);
+
+ while (index >= 0)
+ {
+ count++;
+ index = s.indexOf(sub, index + sub.length());
+ }
+ return count;
+ }
+
+ private static class TestHandler extends HandlerWrapper
+ {
+ @Override
+ public void handle(String target, final Request baseRequest, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException
+ {
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ long start = Long.parseLong(baseRequest.getHeader("start"));
+ long received = baseRequest.getTimeStamp();
+
+ _handled.incrementAndGet();
+ long delay = received - start;
+ if (delay < 0)
+ delay = 0;
+ _latencies[2].add(new Long(delay));
+ _latencies[3].add(new Long(now - start));
+
+ response.setStatus(200);
+ response.getOutputStream().print("DATA " + request.getPathInfo() + "\n\n");
+ baseRequest.setHandled(true);
+
+ _latencies[4].add(new Long(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start));
+
+ return;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/SuspendHandler.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/SuspendHandler.java
new file mode 100644
index 0000000..ce345b5
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/SuspendHandler.java
@@ -0,0 +1,198 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import javax.servlet.AsyncContext;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.handler.HandlerWrapper;
+
+class SuspendHandler extends HandlerWrapper implements AsyncListener
+{
+ private int _read;
+ private long _suspendFor = -1;
+ private long _resumeAfter = -1;
+ private long _completeAfter = -1;
+
+ public SuspendHandler()
+ {
+ }
+
+ public int getRead()
+ {
+ return _read;
+ }
+
+ public void setRead(int read)
+ {
+ _read = read;
+ }
+
+ public long getSuspendFor()
+ {
+ return _suspendFor;
+ }
+
+ public void setSuspendFor(long suspendFor)
+ {
+ _suspendFor = suspendFor;
+ }
+
+ public long getResumeAfter()
+ {
+ return _resumeAfter;
+ }
+
+ public void setResumeAfter(long resumeAfter)
+ {
+ _resumeAfter = resumeAfter;
+ }
+
+ public long getCompleteAfter()
+ {
+ return _completeAfter;
+ }
+
+ public void setCompleteAfter(long completeAfter)
+ {
+ _completeAfter = completeAfter;
+ }
+
+ @Override
+ public void handle(String target, final Request baseRequest, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException
+ {
+ if (DispatcherType.REQUEST.equals(baseRequest.getDispatcherType()))
+ {
+ if (_read > 0)
+ {
+ byte[] buf = new byte[_read];
+ request.getInputStream().read(buf);
+ }
+ else if (_read < 0)
+ {
+ InputStream in = request.getInputStream();
+ int b = in.read();
+ while (b != -1)
+ {
+ b = in.read();
+ }
+ }
+
+ final AsyncContext asyncContext = baseRequest.startAsync();
+ asyncContext.addListener(this);
+ if (_suspendFor > 0)
+ asyncContext.setTimeout(_suspendFor);
+
+ if (_completeAfter > 0)
+ {
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ Thread.sleep(_completeAfter);
+ response.getOutputStream().println("COMPLETED");
+ response.setStatus(200);
+ baseRequest.setHandled(true);
+ asyncContext.complete();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+ }
+ else if (_completeAfter == 0)
+ {
+ response.getOutputStream().println("COMPLETED");
+ response.setStatus(200);
+ baseRequest.setHandled(true);
+ asyncContext.complete();
+ }
+
+ if (_resumeAfter > 0)
+ {
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ Thread.sleep(_resumeAfter);
+ asyncContext.dispatch();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+ }
+ else if (_resumeAfter == 0)
+ {
+ asyncContext.dispatch();
+ }
+ }
+ else if (request.getAttribute("TIMEOUT") != null)
+ {
+ response.setStatus(200);
+ response.getOutputStream().print("TIMEOUT");
+ baseRequest.setHandled(true);
+ }
+ else
+ {
+ response.setStatus(200);
+ response.getOutputStream().print("RESUMED");
+ baseRequest.setHandled(true);
+ }
+ }
+
+ @Override
+ public void onComplete(AsyncEvent asyncEvent) throws IOException
+ {
+ }
+
+ @Override
+ public void onTimeout(AsyncEvent asyncEvent) throws IOException
+ {
+ asyncEvent.getSuppliedRequest().setAttribute("TIMEOUT", Boolean.TRUE);
+ asyncEvent.getAsyncContext().dispatch();
+ }
+
+ @Override
+ public void onError(AsyncEvent asyncEvent) throws IOException
+ {
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent asyncEvent) throws IOException
+ {
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java
new file mode 100644
index 0000000..29cba34
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java
@@ -0,0 +1,404 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.LeakTrackingByteBufferPool;
+import org.eclipse.jetty.io.MappedByteBufferPool;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ThreadStarvationTest
+{
+ static final int BUFFER_SIZE = 1024 * 1024;
+ static final int BUFFERS = 64;
+ static final int THREADS = 5;
+ static final int CLIENTS = THREADS + 2;
+
+ interface ConnectorProvider
+ {
+ ServerConnector newConnector(Server server, int acceptors, int selectors);
+ }
+
+ interface ClientSocketProvider
+ {
+ Socket newSocket(String host, int port) throws IOException;
+ }
+
+ public static Stream<Arguments> scenarios()
+ {
+ List<Scenario> params = new ArrayList<>();
+
+ // HTTP
+ ConnectorProvider http = (server, acceptors, selectors) -> new ServerConnector(server, acceptors, selectors);
+ ClientSocketProvider httpClient = (host, port) -> new Socket(host, port);
+ params.add(new Scenario("http", http, httpClient));
+
+ // HTTPS/SSL/TLS
+ ConnectorProvider https = (server, acceptors, selectors) ->
+ {
+ Path keystorePath = MavenTestingUtils.getTestResourcePath("keystore");
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystorePath.toString());
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+ sslContextFactory.setTrustStorePath(keystorePath.toString());
+ sslContextFactory.setTrustStorePassword("storepwd");
+ ByteBufferPool pool = new LeakTrackingByteBufferPool(new MappedByteBufferPool.Tagged());
+
+ HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory();
+ ServerConnector connector = new ServerConnector(server, (Executor)null, (Scheduler)null,
+ pool, acceptors, selectors,
+ AbstractConnectionFactory.getFactories(sslContextFactory, httpConnectionFactory));
+ SecureRequestCustomizer secureRequestCustomer = new SecureRequestCustomizer();
+ secureRequestCustomer.setSslSessionAttribute("SSL_SESSION");
+ httpConnectionFactory.getHttpConfiguration().addCustomizer(secureRequestCustomer);
+ return connector;
+ };
+ ClientSocketProvider httpsClient = new ClientSocketProvider()
+ {
+ private SSLContext sslContext;
+
+ {
+ try
+ {
+ HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
+ sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, SslContextFactory.TRUST_ALL_CERTS, new java.security.SecureRandom());
+ HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public Socket newSocket(String host, int port) throws IOException
+ {
+ return sslContext.getSocketFactory().createSocket(host, port);
+ }
+ };
+ params.add(new Scenario("https/ssl/tls", https, httpsClient));
+
+ return params.stream().map(Arguments::of);
+ }
+
+ private QueuedThreadPool _threadPool;
+ private Server _server;
+ private ServerConnector _connector;
+
+ private Server prepareServer(Scenario scenario, Handler handler)
+ {
+ _threadPool = new QueuedThreadPool();
+ _threadPool.setMinThreads(THREADS);
+ _threadPool.setMaxThreads(THREADS);
+ _threadPool.setDetailedDump(true);
+ _server = new Server(_threadPool);
+ int acceptors = 1;
+ int selectors = 1;
+ _connector = scenario.connectorProvider.newConnector(_server, acceptors, selectors);
+ _server.addConnector(_connector);
+ _server.setHandler(handler);
+ return _server;
+ }
+
+ @AfterEach
+ public void dispose() throws Exception
+ {
+ _server.stop();
+ }
+
+ @ParameterizedTest
+ @MethodSource("scenarios")
+ public void testReadInput(Scenario scenario) throws Exception
+ {
+ prepareServer(scenario, new ReadHandler()).start();
+
+ try (Socket client = scenario.clientSocketProvider.newSocket("localhost", _connector.getLocalPort()))
+ {
+ client.setSoTimeout(10000);
+ OutputStream os = client.getOutputStream();
+ InputStream is = client.getInputStream();
+
+ String request =
+ "GET / HTTP/1.0\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: 10\r\n" +
+ "\r\n" +
+ "0123456789\r\n";
+ os.write(request.getBytes(StandardCharsets.UTF_8));
+ os.flush();
+
+ String response = IO.toString(is);
+ assertEquals(-1, is.read());
+ assertThat(response, containsString("200 OK"));
+ assertThat(response, containsString("Read Input 10"));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("scenarios")
+ public void testReadStarvation(Scenario scenario) throws Exception
+ {
+ prepareServer(scenario, new ReadHandler());
+ _server.start();
+
+ ExecutorService clientExecutors = Executors.newFixedThreadPool(CLIENTS);
+
+ List<Callable<String>> clientTasks = new ArrayList<>();
+
+ for (int i = 0; i < CLIENTS; i++)
+ {
+ clientTasks.add(() ->
+ {
+ try (Socket client = scenario.clientSocketProvider.newSocket("localhost", _connector.getLocalPort());
+ OutputStream out = client.getOutputStream();
+ InputStream in = client.getInputStream())
+ {
+ client.setSoTimeout(10000);
+
+ String request =
+ "PUT / HTTP/1.0\r\n" +
+ "host: localhost\r\n" +
+ "content-length: 10\r\n" +
+ "\r\n" +
+ "1";
+
+ // Write partial request
+ out.write(request.getBytes(StandardCharsets.UTF_8));
+ out.flush();
+
+ // Finish Request
+ Thread.sleep(1500);
+ out.write(("234567890\r\n").getBytes(StandardCharsets.UTF_8));
+ out.flush();
+
+ // Read Response
+ String response = IO.toString(in);
+ assertEquals(-1, in.read());
+ return response;
+ }
+ });
+ }
+
+ try
+ {
+ List<Future<String>> responses = clientExecutors.invokeAll(clientTasks, 60, TimeUnit.SECONDS);
+
+ for (Future<String> responseFut : responses)
+ {
+ String response = responseFut.get();
+ assertThat(response, containsString("200 OK"));
+ assertThat(response, containsString("Read Input 10"));
+ }
+ }
+ finally
+ {
+ clientExecutors.shutdownNow();
+ }
+ }
+
+ protected static class ReadHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+
+ if (request.getDispatcherType() == DispatcherType.REQUEST)
+ {
+ response.setStatus(200);
+
+ int l = request.getContentLength();
+ int r = 0;
+ while (r < l)
+ {
+ if (request.getInputStream().read() >= 0)
+ r++;
+ }
+
+ response.getOutputStream().write(("Read Input " + r + "\r\n").getBytes());
+ }
+ else
+ {
+ response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500);
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("scenarios")
+ public void testWriteStarvation(Scenario scenario) throws Exception
+ {
+ prepareServer(scenario, new WriteHandler());
+ _server.start();
+
+ ExecutorService clientExecutors = Executors.newFixedThreadPool(CLIENTS);
+
+ List<Callable<Long>> clientTasks = new ArrayList<>();
+
+ for (int i = 0; i < CLIENTS; i++)
+ {
+ clientTasks.add(() ->
+ {
+ try (Socket client = scenario.clientSocketProvider.newSocket("localhost", _connector.getLocalPort());
+ OutputStream out = client.getOutputStream();
+ InputStream in = client.getInputStream())
+ {
+ client.setSoTimeout(30000);
+
+ String request =
+ "GET / HTTP/1.0\r\n" +
+ "host: localhost\r\n" +
+ "\r\n";
+
+ // Write GET request
+ out.write(request.getBytes(StandardCharsets.UTF_8));
+ out.flush();
+
+ TimeUnit.MILLISECONDS.sleep(1500);
+
+ // Read Response
+ long bodyCount = 0;
+ long len;
+
+ byte[] buf = new byte[1024];
+
+ try
+ {
+ while ((len = in.read(buf, 0, buf.length)) != -1)
+ {
+ for (int x = 0; x < len; x++)
+ {
+ if (buf[x] == '!')
+ bodyCount++;
+ }
+ }
+ }
+ catch (Throwable th)
+ {
+ _server.dumpStdErr();
+ throw th;
+ }
+ return bodyCount;
+ }
+ });
+ }
+
+ try
+ {
+ List<Future<Long>> responses = clientExecutors.invokeAll(clientTasks, 60, TimeUnit.SECONDS);
+
+ long expected = BUFFERS * BUFFER_SIZE;
+ for (Future<Long> responseFut : responses)
+ {
+ Long bodyCount = responseFut.get();
+ assertThat(bodyCount.longValue(), is(expected));
+ }
+ }
+ finally
+ {
+ clientExecutors.shutdownNow();
+ }
+ }
+
+ protected static class WriteHandler extends AbstractHandler
+ {
+ byte[] content = new byte[BUFFER_SIZE];
+
+ {
+ // Using a character that will not show up in an HTTP response header
+ Arrays.fill(content, (byte)'!');
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+
+ response.setContentLength(BUFFERS * BUFFER_SIZE);
+ OutputStream out = response.getOutputStream();
+ for (int i = 0; i < BUFFERS; i++)
+ {
+ out.write(content);
+ out.flush();
+ }
+ }
+ }
+
+ public static class Scenario
+ {
+ public final String testType;
+ public final ConnectorProvider connectorProvider;
+ public final ClientSocketProvider clientSocketProvider;
+
+ public Scenario(String testType, ConnectorProvider connectorProvider, ClientSocketProvider clientSocketProvider)
+ {
+ this.testType = testType;
+ this.connectorProvider = connectorProvider;
+ this.clientSocketProvider = clientSocketProvider;
+ }
+
+ @Override
+ public String toString()
+ {
+ return this.testType;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/AllowAllVerifier.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/AllowAllVerifier.java
new file mode 100644
index 0000000..cc71e40
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/AllowAllVerifier.java
@@ -0,0 +1,31 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+
+public class AllowAllVerifier implements HostnameVerifier
+{
+ @Override
+ public boolean verify(String hostname, SSLSession session)
+ {
+ return true;
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasCheckerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasCheckerTest.java
new file mode 100644
index 0000000..91c201b
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasCheckerTest.java
@@ -0,0 +1,211 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystemException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.resource.PathResource;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.opentest4j.TestAbortedException;
+
+import static java.time.Duration.ofSeconds;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+
+public class AllowSymLinkAliasCheckerTest
+{
+ public static Stream<Arguments> params()
+ {
+ List<Arguments> data = new ArrayList<>();
+
+ String[] dirs = {
+ "/workDir/", "/testdirlnk/", "/testdirprefixlnk/", "/testdirsuffixlnk/",
+ "/testdirwraplnk/"
+ };
+
+ for (String dirname : dirs)
+ {
+ data.add(Arguments.of(dirname, 200, "text/html", "Directory: " + dirname));
+ data.add(Arguments.of(dirname + "testfile.txt", 200, "text/plain", "Hello TestFile"));
+ data.add(Arguments.of(dirname + "testfilelnk.txt", 200, "text/plain", "Hello TestFile"));
+ data.add(Arguments.of(dirname + "testfileprefixlnk.txt", 200, "text/plain", "Hello TestFile"));
+ }
+
+ return data.stream();
+ }
+
+ private Server server;
+ private LocalConnector localConnector;
+ private Path rootPath;
+
+ @BeforeEach
+ public void setup() throws Exception
+ {
+ setupRoot();
+ setupServer();
+ }
+
+ @AfterEach
+ public void teardown() throws Exception
+ {
+ if (server != null)
+ {
+ server.stop();
+ }
+ }
+
+ private void setupRoot() throws IOException
+ {
+ rootPath = MavenTestingUtils.getTargetTestingPath(AllowSymLinkAliasCheckerTest.class.getSimpleName());
+ FS.ensureEmpty(rootPath);
+
+ Path testdir = rootPath.resolve("workDir");
+ FS.ensureDirExists(testdir);
+
+ try
+ {
+ // If we used workDir (Path) from above, these symlinks
+ // would point to an absolute path.
+
+ // Create a relative symlink testdirlnk -> workDir
+ Files.createSymbolicLink(rootPath.resolve("testdirlnk"), new File("workDir").toPath());
+ // Create a relative symlink testdirprefixlnk -> ./workDir
+ Files.createSymbolicLink(rootPath.resolve("testdirprefixlnk"), new File("./workDir").toPath());
+ // Create a relative symlink testdirsuffixlnk -> workDir/
+ Files.createSymbolicLink(rootPath.resolve("testdirsuffixlnk"), new File("workDir/").toPath());
+ // Create a relative symlink testdirwraplnk -> ./workDir/
+ Files.createSymbolicLink(rootPath.resolve("testdirwraplnk"), new File("./workDir/").toPath());
+ }
+ catch (UnsupportedOperationException | FileSystemException e)
+ {
+ // If unable to create symlink, no point testing the rest.
+ // This is the path that Microsoft Windows takes.
+ abortNotSupported(e);
+ }
+
+ Path testfileTxt = testdir.resolve("testfile.txt");
+ Files.createFile(testfileTxt);
+ try (OutputStream out = Files.newOutputStream(testfileTxt))
+ {
+ out.write("Hello TestFile".getBytes(StandardCharsets.UTF_8));
+ }
+
+ try
+ {
+ Path testfileTxtLnk = testdir.resolve("testfilelnk.txt");
+ // Create a relative symlink testfilelnk.txt -> testfile.txt
+ // If we used testfileTxt (Path) from above, this symlink
+ // would point to an absolute path.
+ Files.createSymbolicLink(testfileTxtLnk, new File("testfile.txt").toPath());
+ }
+ catch (UnsupportedOperationException | FileSystemException e)
+ {
+ // If unable to create symlink, no point testing the rest.
+ // This is the path that Microsoft Windows takes.
+ abortNotSupported(e);
+ }
+
+ try
+ {
+ Path testfilePrefixTxtLnk = testdir.resolve("testfileprefixlnk.txt");
+ // Create a relative symlink testfileprefixlnk.txt -> ./testfile.txt
+ // If we used testfileTxt (Path) from above, this symlink
+ // would point to an absolute path.
+ Files.createSymbolicLink(testfilePrefixTxtLnk, new File("./testfile.txt").toPath());
+ }
+ catch (UnsupportedOperationException | FileSystemException e)
+ {
+ // If unable to create symlink, no point testing the rest.
+ // This is the path that Microsoft Windows takes.
+ abortNotSupported(e);
+ }
+ }
+
+ private void abortNotSupported(Throwable t)
+ {
+ if (t == null)
+ return;
+ throw new TestAbortedException("Unsupported Behavior", t);
+ }
+
+ private void setupServer() throws Exception
+ {
+ // Setup server
+ server = new Server();
+ localConnector = new LocalConnector(server);
+ server.addConnector(localConnector);
+
+ ResourceHandler fileResourceHandler = new ResourceHandler();
+ fileResourceHandler.setDirectoriesListed(true);
+ fileResourceHandler.setWelcomeFiles(new String[]{"index.html"});
+ fileResourceHandler.setEtags(true);
+
+ ContextHandler fileResourceContext = new ContextHandler();
+ fileResourceContext.setContextPath("/");
+ fileResourceContext.setAllowNullPathInfo(true);
+ fileResourceContext.setHandler(fileResourceHandler);
+ fileResourceContext.setBaseResource(new PathResource(rootPath));
+
+ fileResourceContext.clearAliasChecks();
+ fileResourceContext.addAliasCheck(new AllowSymLinkAliasChecker());
+
+ server.setHandler(fileResourceContext);
+ server.start();
+ }
+
+ @ParameterizedTest
+ @MethodSource("params")
+ public void testAccess(String requestURI, int expectedResponseStatus, String expectedResponseContentType, String expectedResponseContentContains) throws Exception
+ {
+ HttpTester.Request request = HttpTester.newRequest();
+
+ request.setMethod("GET");
+ request.setHeader("Host", "tester");
+ request.setURI(requestURI);
+
+ assertTimeoutPreemptively(ofSeconds(5), () ->
+ {
+ String responseString = localConnector.getResponse(BufferUtil.toString(request.generate()));
+ assertThat("Response status code", responseString, startsWith("HTTP/1.1 " + expectedResponseStatus + " "));
+ assertThat("Response Content-Type", responseString, containsString("\nContent-Type: " + expectedResponseContentType));
+ assertThat("Response", responseString, containsString(expectedResponseContentContains));
+ });
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/BufferedResponseHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/BufferedResponseHandlerTest.java
new file mode 100644
index 0000000..5962a78
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/BufferedResponseHandlerTest.java
@@ -0,0 +1,272 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.util.Arrays;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+
+/**
+ * Resource Handler test
+ */
+public class BufferedResponseHandlerTest
+{
+ private static Server _server;
+ private static HttpConfiguration _config;
+ private static LocalConnector _local;
+ private static ContextHandler _contextHandler;
+ private static BufferedResponseHandler _bufferedHandler;
+ private static TestHandler _test;
+
+ @BeforeAll
+ public static void setUp() throws Exception
+ {
+ _server = new Server();
+ _config = new HttpConfiguration();
+ _config.setOutputBufferSize(1024);
+ _config.setOutputAggregationSize(256);
+ _local = new LocalConnector(_server, new HttpConnectionFactory(_config));
+ _server.addConnector(_local);
+
+ _bufferedHandler = new BufferedResponseHandler();
+ _bufferedHandler.getPathIncludeExclude().include("/include/*");
+ _bufferedHandler.getPathIncludeExclude().exclude("*.exclude");
+ _bufferedHandler.getMimeIncludeExclude().exclude("text/excluded");
+ _bufferedHandler.setHandler(_test = new TestHandler());
+
+ _contextHandler = new ContextHandler("/ctx");
+ _contextHandler.setHandler(_bufferedHandler);
+
+ _server.setHandler(_contextHandler);
+ _server.start();
+
+ // BufferedResponseHandler.LOG.setDebugEnabled(true);
+ }
+
+ @AfterAll
+ public static void tearDown() throws Exception
+ {
+ _server.stop();
+ }
+
+ @BeforeEach
+ public void before()
+ {
+ _test._bufferSize = -1;
+ _test._mimeType = null;
+ _test._content = new byte[128];
+ Arrays.fill(_test._content, (byte)'X');
+ _test._content[_test._content.length - 1] = '\n';
+ _test._writes = 10;
+ _test._flush = false;
+ _test._close = false;
+ _test._reset = false;
+ }
+
+ @Test
+ public void testNormal() throws Exception
+ {
+ String response = _local.getResponse("GET /ctx/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("Write: 0"));
+ assertThat(response, containsString("Write: 7"));
+ assertThat(response, not(containsString("Content-Length: ")));
+ assertThat(response, not(containsString("Write: 8")));
+ assertThat(response, not(containsString("Write: 9")));
+ assertThat(response, not(containsString("Written: true")));
+ }
+
+ @Test
+ public void testIncluded() throws Exception
+ {
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("Write: 0"));
+ assertThat(response, containsString("Write: 9"));
+ assertThat(response, containsString("Written: true"));
+ }
+
+ @Test
+ public void testExcludedByPath() throws Exception
+ {
+ String response = _local.getResponse("GET /ctx/include/path.exclude HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("Write: 0"));
+ assertThat(response, containsString("Write: 7"));
+ assertThat(response, not(containsString("Content-Length: ")));
+ assertThat(response, not(containsString("Write: 8")));
+ assertThat(response, not(containsString("Write: 9")));
+ assertThat(response, not(containsString("Written: true")));
+ }
+
+ @Test
+ public void testExcludedByMime() throws Exception
+ {
+ _test._mimeType = "text/excluded";
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("Write: 0"));
+ assertThat(response, containsString("Write: 7"));
+ assertThat(response, not(containsString("Content-Length: ")));
+ assertThat(response, not(containsString("Write: 8")));
+ assertThat(response, not(containsString("Write: 9")));
+ assertThat(response, not(containsString("Written: true")));
+ }
+
+ @Test
+ public void testFlushed() throws Exception
+ {
+ _test._flush = true;
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("Write: 0"));
+ assertThat(response, containsString("Write: 9"));
+ assertThat(response, containsString("Written: true"));
+ }
+
+ @Test
+ public void testClosed() throws Exception
+ {
+ _test._close = true;
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("Write: 0"));
+ assertThat(response, containsString("Write: 9"));
+ assertThat(response, not(containsString("Written: true")));
+ }
+
+ @Test
+ public void testBufferSizeSmall() throws Exception
+ {
+ _test._bufferSize = 16;
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("Write: 0"));
+ assertThat(response, containsString("Write: 9"));
+ assertThat(response, containsString("Written: true"));
+ }
+
+ @Test
+ public void testBufferSizeBig() throws Exception
+ {
+ _test._bufferSize = 4096;
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("Content-Length: "));
+ assertThat(response, containsString("Write: 0"));
+ assertThat(response, containsString("Write: 9"));
+ assertThat(response, containsString("Written: true"));
+ }
+
+ @Test
+ public void testOne() throws Exception
+ {
+ _test._writes = 1;
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("Content-Length: "));
+ assertThat(response, containsString("Write: 0"));
+ assertThat(response, not(containsString("Write: 1")));
+ assertThat(response, containsString("Written: true"));
+ }
+
+ @Test
+ public void testFlushEmpty() throws Exception
+ {
+ _test._writes = 1;
+ _test._flush = true;
+ _test._close = false;
+ _test._content = new byte[0];
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("Content-Length: "));
+ assertThat(response, containsString("Write: 0"));
+ assertThat(response, not(containsString("Write: 1")));
+ assertThat(response, containsString("Written: true"));
+ }
+
+ @Test
+ public void testReset() throws Exception
+ {
+ _test._reset = true;
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response, containsString(" 200 OK"));
+ assertThat(response, containsString("Write: 0"));
+ assertThat(response, containsString("Write: 9"));
+ assertThat(response, containsString("Written: true"));
+ assertThat(response, not(containsString("RESET")));
+ }
+
+ public static class TestHandler extends AbstractHandler
+ {
+ int _bufferSize;
+ String _mimeType;
+ byte[] _content;
+ int _writes;
+ boolean _flush;
+ boolean _close;
+ boolean _reset;
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+
+ if (_bufferSize > 0)
+ response.setBufferSize(_bufferSize);
+ if (_mimeType != null)
+ response.setContentType(_mimeType);
+
+ if (_reset)
+ {
+ response.getOutputStream().print("THIS WILL BE RESET");
+ response.getOutputStream().flush();
+ response.getOutputStream().print("THIS WILL BE RESET");
+ response.resetBuffer();
+ }
+ for (int i = 0; i < _writes; i++)
+ {
+ response.addHeader("Write", Integer.toString(i));
+ response.getOutputStream().write(_content);
+ if (_flush)
+ response.getOutputStream().flush();
+ }
+
+ if (_close)
+ response.getOutputStream().close();
+ response.addHeader("Written", "true");
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerCollectionTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerCollectionTest.java
new file mode 100644
index 0000000..967918a
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerCollectionTest.java
@@ -0,0 +1,489 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class ContextHandlerCollectionTest
+{
+ @Test
+ public void testVirtualHosts() throws Exception
+ {
+ Server server = new Server();
+ LocalConnector connector0 = new LocalConnector(server);
+ LocalConnector connector1 = new LocalConnector(server);
+ connector1.setName("connector1");
+
+ server.setConnectors(new Connector[]
+ {connector0, connector1});
+
+ ContextHandler contextA = new ContextHandler("/ctx");
+ contextA.setVirtualHosts(new String[]
+ {"www.example.com", "alias.example.com"});
+ IsHandledHandler handlerA = new IsHandledHandler("A");
+ contextA.setHandler(handlerA);
+ contextA.setAllowNullPathInfo(true);
+
+ ContextHandler contextB = new ContextHandler("/ctx");
+ IsHandledHandler handlerB = new IsHandledHandler("B");
+ contextB.setHandler(handlerB);
+ contextB.setVirtualHosts(new String[]
+ {"*.other.com", "@connector1"});
+
+ ContextHandler contextC = new ContextHandler("/ctx");
+ IsHandledHandler handlerC = new IsHandledHandler("C");
+ contextC.setHandler(handlerC);
+
+ ContextHandler contextD = new ContextHandler("/");
+ IsHandledHandler handlerD = new IsHandledHandler("D");
+ contextD.setHandler(handlerD);
+
+ ContextHandler contextE = new ContextHandler("/ctx/foo");
+ IsHandledHandler handlerE = new IsHandledHandler("E");
+ contextE.setHandler(handlerE);
+
+ ContextHandler contextF = new ContextHandler("/ctxlong");
+ IsHandledHandler handlerF = new IsHandledHandler("F");
+ contextF.setHandler(handlerF);
+
+ ContextHandlerCollection c = new ContextHandlerCollection();
+ c.addHandler(contextA);
+ c.addHandler(contextB);
+ c.addHandler(contextC);
+
+ HandlerList list = new HandlerList();
+ list.addHandler(contextE);
+ list.addHandler(contextF);
+ list.addHandler(contextD);
+ c.addHandler(list);
+
+ server.setHandler(c);
+
+ try
+ {
+ server.start();
+
+ Object[][] tests = new Object[][]{
+ {connector0, "www.example.com.", "/ctx", handlerA},
+ {connector0, "www.example.com.", "/ctx/", handlerA},
+ {connector0, "www.example.com.", "/ctx/info", handlerA},
+ {connector0, "www.example.com", "/ctx/info", handlerA},
+ {connector0, "alias.example.com", "/ctx/info", handlerA},
+ {connector1, "www.example.com.", "/ctx/info", handlerA},
+ {connector1, "www.example.com", "/ctx/info", handlerA},
+ {connector1, "alias.example.com", "/ctx/info", handlerA},
+
+ {connector1, "www.other.com", "/ctx", null},
+ {connector1, "www.other.com", "/ctx/", handlerB},
+ {connector1, "www.other.com", "/ctx/info", handlerB},
+ {connector0, "www.other.com", "/ctx/info", handlerC},
+
+ {connector0, "www.example.com", "/ctxinfo", handlerD},
+ {connector1, "unknown.com", "/unknown", handlerD},
+
+ {connector0, "alias.example.com", "/ctx/foo/info", handlerE},
+ {connector0, "alias.example.com", "/ctxlong/info", handlerF},
+ };
+
+ for (int i = 0; i < tests.length; i++)
+ {
+ Object[] test = tests[i];
+ LocalConnector connector = (LocalConnector)test[0];
+ String host = (String)test[1];
+ String uri = (String)test[2];
+ IsHandledHandler handler = (IsHandledHandler)test[3];
+
+ handlerA.reset();
+ handlerB.reset();
+ handlerC.reset();
+ handlerD.reset();
+ handlerE.reset();
+ handlerF.reset();
+
+ String t = String.format("test %d %s@%s --> %s | %s%n", i, uri, host, connector.getName(), handler);
+ String response = connector.getResponse("GET " + uri + " HTTP/1.0\nHost: " + host + "\n\n");
+
+ if (handler == null)
+ {
+ assertThat(t, response, Matchers.containsString(" 302 "));
+ }
+ else
+ {
+ assertThat(t, response, endsWith(handler.toString()));
+ if (!handler.isHandled())
+ {
+ fail("FAILED " + t + "\n" + response);
+ }
+ }
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void testVirtualHostWildcard() throws Exception
+ {
+ Server server = new Server();
+ LocalConnector connector = new LocalConnector(server);
+ server.setConnectors(new Connector[]{connector});
+
+ ContextHandler context = new ContextHandler("/");
+
+ IsHandledHandler handler = new IsHandledHandler("H");
+ context.setHandler(handler);
+
+ ContextHandlerCollection c = new ContextHandlerCollection();
+ c.addHandler(context);
+
+ server.setHandler(c);
+
+ try
+ {
+ server.start();
+ checkWildcardHost(true, server, null, new String[]{"example.com", ".example.com", "vhost.example.com"});
+ checkWildcardHost(false, server, new String[]{null}, new String[]{
+ "example.com", ".example.com", "vhost.example.com"
+ });
+
+ checkWildcardHost(true, server, new String[]{"example.com", "*.example.com"}, new String[]{
+ "example.com", ".example.com", "vhost.example.com"
+ });
+ checkWildcardHost(false, server, new String[]{"example.com", "*.example.com"}, new String[]{
+ "badexample.com", ".badexample.com", "vhost.badexample.com"
+ });
+
+ checkWildcardHost(false, server, new String[]{"*."}, new String[]{"anything.anything"});
+
+ checkWildcardHost(true, server, new String[]{"*.example.com"}, new String[]{"vhost.example.com", ".example.com"});
+ checkWildcardHost(false, server, new String[]{"*.example.com"}, new String[]{
+ "vhost.www.example.com", "example.com", "www.vhost.example.com"
+ });
+
+ checkWildcardHost(true, server, new String[]{"*.sub.example.com"}, new String[]{
+ "vhost.sub.example.com", ".sub.example.com"
+ });
+ checkWildcardHost(false, server, new String[]{"*.sub.example.com"}, new String[]{
+ ".example.com", "sub.example.com", "vhost.example.com"
+ });
+
+ checkWildcardHost(false, server, new String[]{"example.*.com", "example.com.*"}, new String[]{
+ "example.vhost.com", "example.com.vhost", "example.com"
+ });
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ private void checkWildcardHost(boolean succeed, Server server, String[] contextHosts, String[] requestHosts) throws Exception
+ {
+ LocalConnector connector = (LocalConnector)server.getConnectors()[0];
+ ContextHandlerCollection handlerCollection = (ContextHandlerCollection)server.getHandler();
+ ContextHandler context = (ContextHandler)handlerCollection.getHandlers()[0];
+ IsHandledHandler handler = (IsHandledHandler)context.getHandler();
+
+ context.setVirtualHosts(contextHosts);
+ // trigger this manually
+ handlerCollection.mapContexts();
+
+ for (String host : requestHosts)
+ {
+ // System.err.printf("host=%s in %s%n",host,contextHosts==null?Collections.emptyList():Arrays.asList(contextHosts));
+
+ String response = connector.getResponse("GET / HTTP/1.0\n" + "Host: " + host + "\nConnection:close\n\n");
+ // System.err.println(response);
+ if (succeed)
+ assertTrue(handler.isHandled(), "'" + host + "' should have been handled.");
+ else
+ assertFalse(handler.isHandled(), "'" + host + "' should not have been handled.");
+ handler.reset();
+ }
+ }
+
+ @Test
+ public void testFindContainer() throws Exception
+ {
+ Server server = new Server();
+
+ ContextHandler contextA = new ContextHandler("/a");
+ IsHandledHandler handlerA = new IsHandledHandler("A");
+ contextA.setHandler(handlerA);
+
+ ContextHandler contextB = new ContextHandler("/b");
+ IsHandledHandler handlerB = new IsHandledHandler("B");
+ HandlerWrapper wrapperB = new HandlerWrapper();
+ wrapperB.setHandler(handlerB);
+ contextB.setHandler(wrapperB);
+
+ ContextHandler contextC = new ContextHandler("/c");
+ IsHandledHandler handlerC = new IsHandledHandler("C");
+ contextC.setHandler(handlerC);
+
+ ContextHandlerCollection collection = new ContextHandlerCollection();
+
+ collection.addHandler(contextA);
+ collection.addHandler(contextB);
+ collection.addHandler(contextC);
+
+ HandlerWrapper wrapper = new HandlerWrapper();
+ wrapper.setHandler(collection);
+ server.setHandler(wrapper);
+
+ assertEquals(wrapper, AbstractHandlerContainer.findContainerOf(server, HandlerWrapper.class, handlerA));
+ assertEquals(contextA, AbstractHandlerContainer.findContainerOf(server, ContextHandler.class, handlerA));
+ assertEquals(contextB, AbstractHandlerContainer.findContainerOf(server, ContextHandler.class, handlerB));
+ assertEquals(wrapper, AbstractHandlerContainer.findContainerOf(server, HandlerWrapper.class, handlerB));
+ assertEquals(contextB, AbstractHandlerContainer.findContainerOf(collection, HandlerWrapper.class, handlerB));
+ assertEquals(wrapperB, AbstractHandlerContainer.findContainerOf(contextB, HandlerWrapper.class, handlerB));
+ }
+
+ @Test
+ public void testWrappedContext() throws Exception
+ {
+ Server server = new Server();
+ LocalConnector connector = new LocalConnector(server);
+ server.setConnectors(new Connector[]{connector});
+
+ ContextHandler root = new ContextHandler("/");
+ root.setHandler(new IsHandledHandler("root"));
+
+ ContextHandler left = new ContextHandler("/left");
+ left.setHandler(new IsHandledHandler("left"));
+
+ HandlerList centre = new HandlerList();
+ ContextHandler centreLeft = new ContextHandler("/leftcentre");
+ centreLeft.setHandler(new IsHandledHandler("left of centre"));
+ ContextHandler centreRight = new ContextHandler("/rightcentre");
+ centreRight.setHandler(new IsHandledHandler("right of centre"));
+ centre.setHandlers(new Handler[]{centreLeft, new WrappedHandler(centreRight)});
+
+ ContextHandler right = new ContextHandler("/right");
+ right.setHandler(new IsHandledHandler("right"));
+
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+ contexts.setHandlers(new Handler[]{root, left, centre, new WrappedHandler(right)});
+
+ server.setHandler(contexts);
+ server.start();
+
+ String response = connector.getResponse("GET / HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, endsWith("root"));
+ assertThat(response, not(containsString("Wrapped: TRUE")));
+
+ response = connector.getResponse("GET /foobar/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, endsWith("root"));
+ assertThat(response, not(containsString("Wrapped: TRUE")));
+
+ response = connector.getResponse("GET /left/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, endsWith("left"));
+ assertThat(response, not(containsString("Wrapped: TRUE")));
+
+ response = connector.getResponse("GET /leftcentre/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, endsWith("left of centre"));
+ assertThat(response, not(containsString("Wrapped: TRUE")));
+
+ response = connector.getResponse("GET /rightcentre/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, endsWith("right of centre"));
+ assertThat(response, containsString("Wrapped: TRUE"));
+
+ response = connector.getResponse("GET /right/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, endsWith("right"));
+ assertThat(response, containsString("Wrapped: TRUE"));
+ }
+
+ @Test
+ public void testAsyncWrappedContext() throws Exception
+ {
+ Server server = new Server();
+ LocalConnector connector = new LocalConnector(server);
+ server.setConnectors(new Connector[]{connector});
+
+ ContextHandler root = new ContextHandler("/");
+ root.setHandler(new AsyncHandler("root"));
+
+ ContextHandler left = new ContextHandler("/left");
+ left.setHandler(new AsyncHandler("left"));
+
+ HandlerList centre = new HandlerList();
+ ContextHandler centreLeft = new ContextHandler("/leftcentre");
+ centreLeft.setHandler(new AsyncHandler("left of centre"));
+ ContextHandler centreRight = new ContextHandler("/rightcentre");
+ centreRight.setHandler(new AsyncHandler("right of centre"));
+ centre.setHandlers(new Handler[]{centreLeft, new WrappedHandler(centreRight)});
+
+ ContextHandler right = new ContextHandler("/right");
+ right.setHandler(new AsyncHandler("right"));
+
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+ contexts.setHandlers(new Handler[]{root, left, centre, new WrappedHandler(right)});
+
+ server.setHandler(contexts);
+ server.start();
+
+ String response = connector.getResponse("GET / HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, endsWith("root"));
+ assertThat(response, not(containsString("Wrapped: TRUE")));
+
+ response = connector.getResponse("GET /foobar/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, endsWith("root"));
+ assertThat(response, not(containsString("Wrapped: TRUE")));
+
+ response = connector.getResponse("GET /left/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, endsWith("left"));
+ assertThat(response, not(containsString("Wrapped: TRUE")));
+
+ response = connector.getResponse("GET /leftcentre/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, endsWith("left of centre"));
+ assertThat(response, not(containsString("Wrapped: TRUE")));
+
+ response = connector.getResponse("GET /rightcentre/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, endsWith("right of centre"));
+ assertThat(response, containsString("Wrapped: ASYNC"));
+
+ response = connector.getResponse("GET /right/info HTTP/1.0\r\n\r\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, endsWith("right"));
+ assertThat(response, containsString("Wrapped: ASYNC"));
+ }
+
+ private static final class WrappedHandler extends HandlerWrapper
+ {
+ WrappedHandler(Handler handler)
+ {
+ setHandler(handler);
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (response.containsHeader("Wrapped"))
+ response.setHeader("Wrapped", "ASYNC");
+ else
+ response.setHeader("Wrapped", "TRUE");
+ super.handle(target, baseRequest, request, response);
+ }
+ }
+
+ private static final class IsHandledHandler extends AbstractHandler
+ {
+ private boolean handled;
+ private final String name;
+
+ public IsHandledHandler(String string)
+ {
+ name = string;
+ }
+
+ public boolean isHandled()
+ {
+ return handled;
+ }
+
+ @Override
+ public void handle(String s, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ this.handled = true;
+ response.getWriter().print(name);
+ }
+
+ public void reset()
+ {
+ handled = false;
+ }
+
+ @Override
+ public String toString()
+ {
+ return name;
+ }
+ }
+
+ private static final class AsyncHandler extends AbstractHandler
+ {
+ private final String name;
+
+ public AsyncHandler(String string)
+ {
+ name = string;
+ }
+
+ @Override
+ public void handle(String s, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+
+ String n = (String)baseRequest.getAttribute("async");
+ if (n == null)
+ {
+ AsyncContext async = baseRequest.startAsync();
+ async.setTimeout(1000);
+ baseRequest.setAttribute("async", name);
+ async.dispatch();
+ }
+ else
+ {
+ response.getWriter().print(n);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return name;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java
new file mode 100644
index 0000000..a82c14a
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java
@@ -0,0 +1,445 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.resource.Resource;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import static org.junit.jupiter.api.condition.OS.LINUX;
+import static org.junit.jupiter.api.condition.OS.MAC;
+
+public class ContextHandlerGetResourceTest
+{
+ private static boolean OS_ALIAS_SUPPORTED;
+ private static Server server;
+ private static ContextHandler context;
+ private static File docroot;
+ private static File otherroot;
+ private static final AtomicBoolean allowAliases = new AtomicBoolean(false);
+ private static final AtomicBoolean allowSymlinks = new AtomicBoolean(false);
+
+ @BeforeAll
+ public static void beforeClass() throws Exception
+ {
+ File testRoot = MavenTestingUtils.getTargetTestingDir(ContextHandlerGetResourceTest.class.getSimpleName());
+ FS.ensureEmpty(testRoot);
+ docroot = new File(testRoot, "docroot").getCanonicalFile().getAbsoluteFile();
+ FS.ensureEmpty(docroot);
+ File index = new File(docroot, "index.html");
+ index.createNewFile();
+ File sub = new File(docroot, "subdir");
+ sub.mkdir();
+ File data = new File(sub, "data.txt");
+ data.createNewFile();
+ File verylong = new File(sub, "TextFile.Long.txt");
+ verylong.createNewFile();
+
+ otherroot = new File(testRoot, "otherroot").getCanonicalFile().getAbsoluteFile();
+ FS.ensureEmpty(otherroot);
+ File other = new File(otherroot, "other.txt");
+ other.createNewFile();
+
+ File transit = new File(docroot.getParentFile(), "transit");
+ transit.delete();
+
+ if (!OS.WINDOWS.isCurrentOs())
+ {
+ // Create alias as 8.3 name so same test will produce an alias on both windows an unix/normal systems
+ File eightDotThree = new File(sub, "TEXTFI~1.TXT");
+ Files.createSymbolicLink(eightDotThree.toPath(), verylong.toPath());
+
+ Files.createSymbolicLink(new File(docroot, "other").toPath(), new File("../transit").toPath());
+ Files.createSymbolicLink(transit.toPath(), otherroot.toPath());
+
+ // /web/logs -> /var/logs -> /media/internal/logs
+ // where /media/internal -> /media/internal-physical/
+ new File(docroot, "media/internal-physical/logs").mkdirs();
+ Files.createSymbolicLink(new File(docroot, "media/internal").toPath(), new File(docroot, "media/internal-physical").toPath());
+ new File(docroot, "var").mkdir();
+ Files.createSymbolicLink(new File(docroot, "var/logs").toPath(), new File(docroot, "media/internal/logs").toPath());
+ new File(docroot, "web").mkdir();
+ Files.createSymbolicLink(new File(docroot, "web/logs").toPath(), new File(docroot, "var/logs").toPath());
+ new File(docroot, "media/internal-physical/logs/file.log").createNewFile();
+
+ System.err.println("docroot=" + docroot);
+ }
+
+ OS_ALIAS_SUPPORTED = new File(sub, "TEXTFI~1.TXT").exists();
+
+ server = new Server();
+ context = new ContextHandler("/");
+ context.clearAliasChecks();
+ context.addAliasCheck(new ContextHandler.ApproveNonExistentDirectoryAliases());
+ context.setBaseResource(Resource.newResource(docroot));
+ context.addAliasCheck(new ContextHandler.AliasCheck()
+ {
+ final AllowSymLinkAliasChecker symlinkcheck = new AllowSymLinkAliasChecker();
+
+ @Override
+ public boolean check(String path, Resource resource)
+ {
+ if (allowAliases.get())
+ return true;
+ if (allowSymlinks.get())
+ return symlinkcheck.check(path, resource);
+ return allowAliases.get();
+ }
+ });
+
+ server.setHandler(context);
+ server.start();
+ }
+
+ @AfterAll
+ public static void afterClass() throws Exception
+ {
+ server.stop();
+ }
+
+ @Test
+ public void testBadPath() throws Exception
+ {
+ final String path = "bad";
+ assertThrows(MalformedURLException.class, () -> context.getResource(path));
+ assertThrows(MalformedURLException.class, () -> context.getServletContext().getResource(path));
+ }
+
+ @Test
+ public void testGetUnknown() throws Exception
+ {
+ final String path = "/unknown.txt";
+ Resource resource = context.getResource(path);
+ assertEquals("unknown.txt", resource.getFile().getName());
+ assertEquals(docroot, resource.getFile().getParentFile());
+ assertFalse(resource.exists());
+
+ URL url = context.getServletContext().getResource(path);
+ assertNull(url);
+ }
+
+ @Test
+ public void testGetUnknownDir() throws Exception
+ {
+ final String path = "/unknown/";
+ Resource resource = context.getResource(path);
+ assertEquals("unknown", resource.getFile().getName());
+ assertEquals(docroot, resource.getFile().getParentFile());
+ assertFalse(resource.exists());
+
+ URL url = context.getServletContext().getResource(path);
+ assertNull(url);
+ }
+
+ @Test
+ public void testRoot() throws Exception
+ {
+ final String path = "/";
+ Resource resource = context.getResource(path);
+ assertEquals(docroot, resource.getFile());
+ assertTrue(resource.exists());
+ assertTrue(resource.isDirectory());
+
+ URL url = context.getServletContext().getResource(path);
+ assertEquals(docroot, new File(url.toURI()));
+ }
+
+ @Test
+ public void testSubdir() throws Exception
+ {
+ final String path = "/subdir";
+ Resource resource = context.getResource(path);
+ assertEquals(docroot, resource.getFile().getParentFile());
+ assertTrue(resource.exists());
+ assertTrue(resource.isDirectory());
+ assertTrue(resource.toString().endsWith("/"));
+
+ URL url = context.getServletContext().getResource(path);
+ assertEquals(docroot, new File(url.toURI()).getParentFile());
+ }
+
+ @Test
+ public void testSubdirSlash() throws Exception
+ {
+ final String path = "/subdir/";
+ Resource resource = context.getResource(path);
+ assertEquals(docroot, resource.getFile().getParentFile());
+ assertTrue(resource.exists());
+ assertTrue(resource.isDirectory());
+ assertTrue(resource.toString().endsWith("/"));
+
+ URL url = context.getServletContext().getResource(path);
+ assertEquals(docroot, new File(url.toURI()).getParentFile());
+ }
+
+ @Test
+ public void testGetKnown() throws Exception
+ {
+ final String path = "/index.html";
+ Resource resource = context.getResource(path);
+ assertEquals("index.html", resource.getFile().getName());
+ assertEquals(docroot, resource.getFile().getParentFile());
+ assertTrue(resource.exists());
+
+ URL url = context.getServletContext().getResource(path);
+ assertEquals(docroot, new File(url.toURI()).getParentFile());
+ }
+
+ @Test
+ public void testDoesNotExistResource() throws Exception
+ {
+ Resource resource = context.getResource("/doesNotExist.html");
+ assertNotNull(resource);
+ assertFalse(resource.exists());
+ }
+
+ @Test
+ public void testAlias() throws Exception
+ {
+ String path = "/./index.html";
+ Resource resource = context.getResource(path);
+ assertNull(resource);
+ URL resourceURL = context.getServletContext().getResource(path);
+ assertFalse(resourceURL.getPath().contains("/./"));
+
+ path = "/down/../index.html";
+ resource = context.getResource(path);
+ assertNull(resource);
+ resourceURL = context.getServletContext().getResource(path);
+ assertFalse(resourceURL.getPath().contains("/../"));
+
+ path = "//index.html";
+ resource = context.getResource(path);
+ assertNull(resource);
+ resourceURL = context.getServletContext().getResource(path);
+ assertNull(resourceURL);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"/down/.././../", "/../down/"})
+ public void testNormalize(String path) throws Exception
+ {
+ URL url = context.getServletContext().getResource(path);
+ assertNull(url);
+ }
+
+ @Test
+ public void testDeep() throws Exception
+ {
+ final String path = "/subdir/data.txt";
+ Resource resource = context.getResource(path);
+ assertEquals("data.txt", resource.getFile().getName());
+ assertEquals(docroot, resource.getFile().getParentFile().getParentFile());
+ assertTrue(resource.exists());
+
+ URL url = context.getServletContext().getResource(path);
+ assertEquals(docroot, new File(url.toURI()).getParentFile().getParentFile());
+ }
+
+ @Test
+ public void testEncodedSlash() throws Exception
+ {
+ final String path = "/subdir%2Fdata.txt";
+
+ Resource resource = context.getResource(path);
+ assertEquals("subdir%2Fdata.txt", resource.getFile().getName());
+ assertEquals(docroot, resource.getFile().getParentFile());
+ assertFalse(resource.exists());
+
+ URL url = context.getServletContext().getResource(path);
+ assertNull(url);
+ }
+
+ @Test
+ public void testEncodedSlosh() throws Exception
+ {
+ final String path = "/subdir%5Cdata.txt";
+
+ Resource resource = context.getResource(path);
+ assertEquals("subdir%5Cdata.txt", resource.getFile().getName());
+ assertEquals(docroot, resource.getFile().getParentFile());
+ assertFalse(resource.exists());
+
+ URL url = context.getServletContext().getResource(path);
+ assertNull(url);
+ }
+
+ @Test
+ public void testEncodedNull() throws Exception
+ {
+ final String path = "/subdir/data.txt%00";
+
+ Resource resource = context.getResource(path);
+ assertEquals("data.txt%00", resource.getFile().getName());
+ assertEquals(docroot, resource.getFile().getParentFile().getParentFile());
+ assertFalse(resource.exists());
+
+ URL url = context.getServletContext().getResource(path);
+ assertNull(url);
+ }
+
+ @Test
+ public void testSlashSlash() throws Exception
+ {
+ File expected = new File(docroot, FS.separators("subdir/data.txt"));
+ URL expectedUrl = expected.toURI().toURL();
+
+ String path = "//subdir/data.txt";
+ Resource resource = context.getResource(path);
+ assertThat("Resource: " + resource, resource, nullValue());
+ URL url = context.getServletContext().getResource(path);
+ assertThat("Resource: " + url, url, nullValue());
+
+ path = "/subdir//data.txt";
+ resource = context.getResource(path);
+ assertThat("Resource: " + resource, resource, nullValue());
+ url = context.getServletContext().getResource(path);
+ assertThat("Resource: " + url, url, nullValue());
+ }
+
+ @Test
+ public void testAliasedFile() throws Exception
+ {
+ assumeTrue(OS_ALIAS_SUPPORTED, "OS Supports 8.3 Aliased / Alternate References");
+ final String path = "/subdir/TEXTFI~1.TXT";
+
+ Resource resource = context.getResource(path);
+ assertNull(resource);
+
+ URL url = context.getServletContext().getResource(path);
+ assertNull(url);
+ }
+
+ @Test
+ public void testAliasedFileAllowed() throws Exception
+ {
+ assumeTrue(OS_ALIAS_SUPPORTED, "OS Supports 8.3 Aliased / Alternate References");
+ try
+ {
+ allowAliases.set(true);
+ final String path = "/subdir/TEXTFI~1.TXT";
+
+ Resource resource = context.getResource(path);
+ assertNotNull(resource);
+ assertEquals(context.getResource("/subdir/TextFile.Long.txt").getURI(), resource.getAlias());
+
+ URL url = context.getServletContext().getResource(path);
+ assertNotNull(url);
+ assertEquals(docroot, new File(url.toURI()).getParentFile().getParentFile());
+ }
+ finally
+ {
+ allowAliases.set(false);
+ }
+ }
+
+ @Test
+ @EnabledOnOs({LINUX, MAC})
+ public void testSymlinkKnown() throws Exception
+ {
+ try
+ {
+ allowSymlinks.set(true);
+
+ final String path = "/other/other.txt";
+
+ Resource resource = context.getResource(path);
+ assertNotNull(resource);
+ assertEquals("other.txt", resource.getFile().getName());
+ assertEquals(docroot, resource.getFile().getParentFile().getParentFile());
+ assertTrue(resource.exists());
+
+ URL url = context.getServletContext().getResource(path);
+ assertEquals(docroot, new File(url.toURI()).getParentFile().getParentFile());
+ }
+ finally
+ {
+ allowSymlinks.set(false);
+ }
+ }
+
+ @Test
+ @EnabledOnOs({LINUX, MAC})
+ public void testSymlinkNested() throws Exception
+ {
+ try
+ {
+ allowSymlinks.set(true);
+
+ final String path = "/web/logs/file.log";
+
+ Resource resource = context.getResource(path);
+ assertNotNull(resource);
+ assertEquals("file.log", resource.getFile().getName());
+ assertTrue(resource.exists());
+ }
+ finally
+ {
+ allowSymlinks.set(false);
+ }
+ }
+
+ @Test
+ @EnabledOnOs({LINUX, MAC})
+ public void testSymlinkUnknown() throws Exception
+ {
+ try
+ {
+ allowSymlinks.set(true);
+
+ final String path = "/other/unknown.txt";
+
+ Resource resource = context.getResource(path);
+ assertNotNull(resource);
+ assertEquals("unknown.txt", resource.getFile().getName());
+ assertEquals(docroot, resource.getFile().getParentFile().getParentFile());
+ assertFalse(resource.exists());
+
+ URL url = context.getServletContext().getResource(path);
+ assertNull(url);
+ }
+ finally
+ {
+ allowSymlinks.set(false);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerTest.java
new file mode 100644
index 0000000..246b500
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerTest.java
@@ -0,0 +1,887 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.resource.Resource;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(WorkDirExtension.class)
+public class ContextHandlerTest
+{
+ public WorkDir workDir;
+
+ @Test
+ public void testGetResourcePathsWhenSuppliedPathEndsInSlash() throws Exception
+ {
+ checkResourcePathsForExampleWebApp("/WEB-INF/");
+ }
+
+ @Test
+ public void testGetResourcePathsWhenSuppliedPathDoesNotEndInSlash() throws Exception
+ {
+ checkResourcePathsForExampleWebApp("/WEB-INF");
+ }
+
+ @Test
+ public void testVirtualHostNormalization() throws Exception
+ {
+ Server server = new Server();
+ LocalConnector connector = new LocalConnector(server);
+ server.setConnectors(new Connector[]
+ {connector});
+
+ ContextHandler contextA = new ContextHandler("/");
+ contextA.setVirtualHosts(new String[]
+ {"www.example.com"});
+ IsHandledHandler handlerA = new IsHandledHandler();
+ contextA.setHandler(handlerA);
+
+ ContextHandler contextB = new ContextHandler("/");
+ IsHandledHandler handlerB = new IsHandledHandler();
+ contextB.setHandler(handlerB);
+ contextB.setVirtualHosts(new String[]
+ {"www.example2.com."});
+
+ ContextHandler contextC = new ContextHandler("/");
+ IsHandledHandler handlerC = new IsHandledHandler();
+ contextC.setHandler(handlerC);
+
+ HandlerCollection c = new HandlerCollection();
+
+ c.addHandler(contextA);
+ c.addHandler(contextB);
+ c.addHandler(contextC);
+
+ server.setHandler(c);
+
+ try
+ {
+ server.start();
+ connector.getResponse("GET / HTTP/1.0\n" + "Host: www.example.com.\n\n");
+
+ assertTrue(handlerA.isHandled());
+ assertFalse(handlerB.isHandled());
+ assertFalse(handlerC.isHandled());
+
+ handlerA.reset();
+ handlerB.reset();
+ handlerC.reset();
+
+ connector.getResponse("GET / HTTP/1.0\n" + "Host: www.example2.com\n\n");
+
+ assertFalse(handlerA.isHandled());
+ assertTrue(handlerB.isHandled());
+ assertFalse(handlerC.isHandled());
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void testNamedConnector() throws Exception
+ {
+ Server server = new Server();
+ LocalConnector connector = new LocalConnector(server);
+ LocalConnector connectorN = new LocalConnector(server);
+ connectorN.setName("name");
+ server.setConnectors(new Connector[]{connector, connectorN});
+
+ ContextHandler contextA = new ContextHandler("/");
+ contextA.setDisplayName("A");
+ contextA.setVirtualHosts(new String[]{"www.example.com"});
+ IsHandledHandler handlerA = new IsHandledHandler();
+ contextA.setHandler(handlerA);
+
+ ContextHandler contextB = new ContextHandler("/");
+ contextB.setDisplayName("B");
+ IsHandledHandler handlerB = new IsHandledHandler();
+ contextB.setHandler(handlerB);
+ contextB.setVirtualHosts(new String[]{"@name"});
+
+ ContextHandler contextC = new ContextHandler("/");
+ contextC.setDisplayName("C");
+ IsHandledHandler handlerC = new IsHandledHandler();
+ contextC.setHandler(handlerC);
+
+ ContextHandler contextD = new ContextHandler("/");
+ contextD.setDisplayName("D");
+ IsHandledHandler handlerD = new IsHandledHandler();
+ contextD.setHandler(handlerD);
+ contextD.setVirtualHosts(new String[]{"www.example.com@name"});
+
+ ContextHandler contextE = new ContextHandler("/");
+ contextE.setDisplayName("E");
+ IsHandledHandler handlerE = new IsHandledHandler();
+ contextE.setHandler(handlerE);
+ contextE.setVirtualHosts(new String[]{"*.example.com"});
+
+ ContextHandler contextF = new ContextHandler("/");
+ contextF.setDisplayName("F");
+ IsHandledHandler handlerF = new IsHandledHandler();
+ contextF.setHandler(handlerF);
+ contextF.setVirtualHosts(new String[]{"*.example.com@name"});
+
+ ContextHandler contextG = new ContextHandler("/");
+ contextG.setDisplayName("G");
+ IsHandledHandler handlerG = new IsHandledHandler();
+ contextG.setHandler(handlerG);
+ contextG.setVirtualHosts(new String[]{"*.com@name"});
+
+ ContextHandler contextH = new ContextHandler("/");
+ contextH.setDisplayName("H");
+ IsHandledHandler handlerH = new IsHandledHandler();
+ contextH.setHandler(handlerH);
+ contextH.setVirtualHosts(new String[]{"*.com"});
+
+ HandlerCollection c = new HandlerCollection();
+ c.addHandler(contextA);
+ c.addHandler(contextB);
+ c.addHandler(contextC);
+ c.addHandler(contextD);
+ c.addHandler(contextE);
+ c.addHandler(contextF);
+ c.addHandler(contextG);
+ c.addHandler(contextH);
+
+ server.setHandler(c);
+
+ server.start();
+ try
+ {
+ connector.getResponse("GET / HTTP/1.0\n" + "Host: www.example.com.\n\n");
+ assertTrue(handlerA.isHandled());
+ assertFalse(handlerB.isHandled());
+ assertFalse(handlerC.isHandled());
+ assertFalse(handlerD.isHandled());
+ assertFalse(handlerE.isHandled());
+ assertFalse(handlerF.isHandled());
+ assertFalse(handlerG.isHandled());
+ assertFalse(handlerH.isHandled());
+ handlerA.reset();
+ handlerB.reset();
+ handlerC.reset();
+ handlerD.reset();
+ handlerE.reset();
+ handlerF.reset();
+ handlerG.reset();
+ handlerH.reset();
+
+ connector.getResponse("GET / HTTP/1.0\n" + "Host: localhost\n\n");
+ assertFalse(handlerA.isHandled());
+ assertFalse(handlerB.isHandled());
+ assertTrue(handlerC.isHandled());
+ assertFalse(handlerD.isHandled());
+ assertFalse(handlerE.isHandled());
+ assertFalse(handlerF.isHandled());
+ assertFalse(handlerG.isHandled());
+ assertFalse(handlerH.isHandled());
+ handlerA.reset();
+ handlerB.reset();
+ handlerC.reset();
+ handlerD.reset();
+ handlerE.reset();
+ handlerF.reset();
+ handlerG.reset();
+ handlerH.reset();
+
+ connectorN.getResponse("GET / HTTP/1.0\n" + "Host: www.example.com.\n\n");
+ assertTrue(handlerA.isHandled());
+ assertFalse(handlerB.isHandled());
+ assertFalse(handlerC.isHandled());
+ assertFalse(handlerD.isHandled());
+ assertFalse(handlerE.isHandled());
+ assertFalse(handlerF.isHandled());
+ assertFalse(handlerG.isHandled());
+ assertFalse(handlerH.isHandled());
+ handlerA.reset();
+ handlerB.reset();
+ handlerC.reset();
+ handlerD.reset();
+ handlerE.reset();
+ handlerF.reset();
+ handlerG.reset();
+ handlerH.reset();
+
+ connectorN.getResponse("GET / HTTP/1.0\n" + "Host: localhost\n\n");
+ assertFalse(handlerA.isHandled());
+ assertTrue(handlerB.isHandled());
+ assertFalse(handlerC.isHandled());
+ assertFalse(handlerD.isHandled());
+ assertFalse(handlerE.isHandled());
+ assertFalse(handlerF.isHandled());
+ assertFalse(handlerG.isHandled());
+ assertFalse(handlerH.isHandled());
+ handlerA.reset();
+ handlerB.reset();
+ handlerC.reset();
+ handlerD.reset();
+ handlerE.reset();
+ handlerF.reset();
+ handlerG.reset();
+ handlerH.reset();
+ }
+ finally
+ {
+ server.stop();
+ }
+
+ // Reversed order to check priority when multiple matches
+ HandlerCollection d = new HandlerCollection();
+ d.addHandler(contextH);
+ d.addHandler(contextG);
+ d.addHandler(contextF);
+ d.addHandler(contextE);
+ d.addHandler(contextD);
+ d.addHandler(contextC);
+ d.addHandler(contextB);
+ d.addHandler(contextA);
+
+ server.setHandler(d);
+
+ server.start();
+ try
+ {
+ connector.getResponse("GET / HTTP/1.0\n" + "Host: www.example.com.\n\n");
+ assertFalse(handlerA.isHandled());
+ assertFalse(handlerB.isHandled());
+ assertFalse(handlerC.isHandled());
+ assertFalse(handlerD.isHandled());
+ assertTrue(handlerE.isHandled());
+ assertFalse(handlerF.isHandled());
+ assertFalse(handlerG.isHandled());
+ assertFalse(handlerH.isHandled());
+ handlerA.reset();
+ handlerB.reset();
+ handlerC.reset();
+ handlerD.reset();
+ handlerE.reset();
+ handlerF.reset();
+ handlerG.reset();
+ handlerH.reset();
+
+ connector.getResponse("GET / HTTP/1.0\n" + "Host: localhost\n\n");
+ assertFalse(handlerA.isHandled());
+ assertFalse(handlerB.isHandled());
+ assertTrue(handlerC.isHandled());
+ assertFalse(handlerD.isHandled());
+ assertFalse(handlerE.isHandled());
+ assertFalse(handlerF.isHandled());
+ assertFalse(handlerG.isHandled());
+ assertFalse(handlerH.isHandled());
+ handlerA.reset();
+ handlerB.reset();
+ handlerC.reset();
+ handlerD.reset();
+ handlerE.reset();
+ handlerF.reset();
+ handlerG.reset();
+ handlerH.reset();
+
+ connectorN.getResponse("GET / HTTP/1.0\n" + "Host: www.example.com.\n\n");
+ assertFalse(handlerA.isHandled());
+ assertFalse(handlerB.isHandled());
+ assertFalse(handlerC.isHandled());
+ assertFalse(handlerD.isHandled());
+ assertFalse(handlerE.isHandled());
+ assertTrue(handlerF.isHandled());
+ assertFalse(handlerG.isHandled());
+ assertFalse(handlerH.isHandled());
+ handlerA.reset();
+ handlerB.reset();
+ handlerC.reset();
+ handlerD.reset();
+ handlerE.reset();
+ handlerF.reset();
+ handlerG.reset();
+ handlerH.reset();
+
+ connectorN.getResponse("GET / HTTP/1.0\n" + "Host: localhost\n\n");
+ assertFalse(handlerA.isHandled());
+ assertFalse(handlerB.isHandled());
+ assertTrue(handlerC.isHandled());
+ assertFalse(handlerD.isHandled());
+ assertFalse(handlerE.isHandled());
+ assertFalse(handlerF.isHandled());
+ assertFalse(handlerG.isHandled());
+ assertFalse(handlerH.isHandled());
+ handlerA.reset();
+ handlerB.reset();
+ handlerC.reset();
+ handlerD.reset();
+ handlerE.reset();
+ handlerF.reset();
+ handlerG.reset();
+ handlerH.reset();
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void testContextGetContext() throws Exception
+ {
+ Server server = new Server();
+ LocalConnector connector = new LocalConnector(server);
+ server.setConnectors(new Connector[]{connector});
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+ server.setHandler(contexts);
+
+ ContextHandler rootA = new ContextHandler(contexts, "/");
+ ContextHandler fooA = new ContextHandler(contexts, "/foo");
+ ContextHandler foobarA = new ContextHandler(contexts, "/foo/bar");
+
+ server.start();
+
+ // System.err.println(server.dump());
+
+ assertEquals(rootA._scontext, rootA._scontext.getContext("/"));
+ assertEquals(fooA._scontext, rootA._scontext.getContext("/foo"));
+ assertEquals(foobarA._scontext, rootA._scontext.getContext("/foo/bar"));
+ assertEquals(foobarA._scontext, rootA._scontext.getContext("/foo/bar/bob.jsp"));
+ assertEquals(rootA._scontext, rootA._scontext.getContext("/other"));
+ assertEquals(fooA._scontext, rootA._scontext.getContext("/foo/other"));
+
+ assertEquals(rootA._scontext, foobarA._scontext.getContext("/"));
+ assertEquals(fooA._scontext, foobarA._scontext.getContext("/foo"));
+ assertEquals(foobarA._scontext, foobarA._scontext.getContext("/foo/bar"));
+ assertEquals(foobarA._scontext, foobarA._scontext.getContext("/foo/bar/bob.jsp"));
+ assertEquals(rootA._scontext, foobarA._scontext.getContext("/other"));
+ assertEquals(fooA._scontext, foobarA._scontext.getContext("/foo/other"));
+ }
+
+ @Test
+ public void testLifeCycle() throws Exception
+ {
+ Server server = new Server();
+ LocalConnector connector = new LocalConnector(server);
+ server.setConnectors(new Connector[]{connector});
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+ server.setHandler(contexts);
+
+ ContextHandler root = new ContextHandler(contexts, "/");
+ root.setHandler(new ContextPathHandler());
+ ContextHandler foo = new ContextHandler(contexts, "/foo");
+ foo.setHandler(new ContextPathHandler());
+ ContextHandler foobar = new ContextHandler(contexts, "/foo/bar");
+ foobar.setHandler(new ContextPathHandler());
+
+ // check that all contexts start normally
+ server.start();
+ assertThat(connector.getResponse("GET / HTTP/1.0\n\n"), Matchers.containsString("ctx=''"));
+ assertThat(connector.getResponse("GET /foo/xxx HTTP/1.0\n\n"), Matchers.containsString("ctx='/foo'"));
+ assertThat(connector.getResponse("GET /foo/bar/xxx HTTP/1.0\n\n"), Matchers.containsString("ctx='/foo/bar'"));
+
+ // If we make foobar unavailable, then requests will be handled by 503
+ foobar.setAvailable(false);
+ assertThat(connector.getResponse("GET / HTTP/1.0\n\n"), Matchers.containsString("ctx=''"));
+ assertThat(connector.getResponse("GET /foo/xxx HTTP/1.0\n\n"), Matchers.containsString("ctx='/foo'"));
+ assertThat(connector.getResponse("GET /foo/bar/xxx HTTP/1.0\n\n"), Matchers.containsString(" 503 "));
+
+ // If we make foobar available, then requests will be handled normally
+ foobar.setAvailable(true);
+ assertThat(connector.getResponse("GET / HTTP/1.0\n\n"), Matchers.containsString("ctx=''"));
+ assertThat(connector.getResponse("GET /foo/xxx HTTP/1.0\n\n"), Matchers.containsString("ctx='/foo'"));
+ assertThat(connector.getResponse("GET /foo/bar/xxx HTTP/1.0\n\n"), Matchers.containsString("ctx='/foo/bar'"));
+
+ // If we stop foobar, then requests will be handled by foo
+ foobar.stop();
+ assertThat(connector.getResponse("GET / HTTP/1.0\n\n"), Matchers.containsString("ctx=''"));
+ assertThat(connector.getResponse("GET /foo/xxx HTTP/1.0\n\n"), Matchers.containsString("ctx='/foo'"));
+ assertThat(connector.getResponse("GET /foo/bar/xxx HTTP/1.0\n\n"), Matchers.containsString("ctx='/foo'"));
+
+ // If we shutdown foo then requests will be 503'd
+ foo.shutdown().get();
+ assertThat(connector.getResponse("GET / HTTP/1.0\n\n"), Matchers.containsString("ctx=''"));
+ assertThat(connector.getResponse("GET /foo/xxx HTTP/1.0\n\n"), Matchers.containsString("503"));
+ assertThat(connector.getResponse("GET /foo/bar/xxx HTTP/1.0\n\n"), Matchers.containsString("503"));
+
+ // If we stop foo then requests will be handled by root
+ foo.stop();
+ assertThat(connector.getResponse("GET / HTTP/1.0\n\n"), Matchers.containsString("ctx=''"));
+ assertThat(connector.getResponse("GET /foo/xxx HTTP/1.0\n\n"), Matchers.containsString("ctx=''"));
+ assertThat(connector.getResponse("GET /foo/bar/xxx HTTP/1.0\n\n"), Matchers.containsString("ctx=''"));
+
+ // If we start foo then foobar requests will be handled by foo
+ foo.start();
+ assertThat(connector.getResponse("GET / HTTP/1.0\n\n"), Matchers.containsString("ctx=''"));
+ assertThat(connector.getResponse("GET /foo/xxx HTTP/1.0\n\n"), Matchers.containsString("ctx='/foo'"));
+ assertThat(connector.getResponse("GET /foo/bar/xxx HTTP/1.0\n\n"), Matchers.containsString("ctx='/foo'"));
+
+ // If we start foobar then foobar requests will be handled by foobar
+ foobar.start();
+ assertThat(connector.getResponse("GET / HTTP/1.0\n\n"), Matchers.containsString("ctx=''"));
+ assertThat(connector.getResponse("GET /foo/xxx HTTP/1.0\n\n"), Matchers.containsString("ctx='/foo'"));
+ assertThat(connector.getResponse("GET /foo/bar/xxx HTTP/1.0\n\n"), Matchers.containsString("ctx='/foo/bar'"));
+ }
+
+ @Test
+ public void testContextInitializationDestruction() throws Exception
+ {
+ Server server = new Server();
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+ server.setHandler(contexts);
+
+ ContextHandler noServlets = new ContextHandler(contexts, "/noservlets");
+ TestServletContextListener listener = new TestServletContextListener();
+ noServlets.addEventListener(listener);
+ server.start();
+ assertEquals(1, listener.initialized);
+ server.stop();
+ assertEquals(1, listener.destroyed);
+ }
+
+ @Test
+ public void testContextVirtualGetContext() throws Exception
+ {
+ Server server = new Server();
+ LocalConnector connector = new LocalConnector(server);
+ server.setConnectors(new Connector[]{connector});
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+ server.setHandler(contexts);
+
+ ContextHandler rootA = new ContextHandler(contexts, "/");
+ rootA.setVirtualHosts(new String[]{"a.com"});
+
+ ContextHandler rootB = new ContextHandler(contexts, "/");
+ rootB.setVirtualHosts(new String[]{"b.com"});
+
+ ContextHandler rootC = new ContextHandler(contexts, "/");
+ rootC.setVirtualHosts(new String[]{"c.com"});
+
+ ContextHandler fooA = new ContextHandler(contexts, "/foo");
+ fooA.setVirtualHosts(new String[]{"a.com"});
+
+ ContextHandler fooB = new ContextHandler(contexts, "/foo");
+ fooB.setVirtualHosts(new String[]{"b.com"});
+
+ ContextHandler foobarA = new ContextHandler(contexts, "/foo/bar");
+ foobarA.setVirtualHosts(new String[]{"a.com"});
+
+ server.start();
+
+ // System.err.println(server.dump());
+
+ assertEquals(rootA._scontext, rootA._scontext.getContext("/"));
+ assertEquals(fooA._scontext, rootA._scontext.getContext("/foo"));
+ assertEquals(foobarA._scontext, rootA._scontext.getContext("/foo/bar"));
+ assertEquals(foobarA._scontext, rootA._scontext.getContext("/foo/bar/bob"));
+
+ assertEquals(rootA._scontext, rootA._scontext.getContext("/other"));
+ assertEquals(rootB._scontext, rootB._scontext.getContext("/other"));
+ assertEquals(rootC._scontext, rootC._scontext.getContext("/other"));
+
+ assertEquals(fooB._scontext, rootB._scontext.getContext("/foo/other"));
+ assertEquals(rootC._scontext, rootC._scontext.getContext("/foo/other"));
+ }
+
+ @Test
+ public void testVirtualHostWildcard() throws Exception
+ {
+ Server server = new Server();
+ LocalConnector connector = new LocalConnector(server);
+ server.setConnectors(new Connector[]{connector});
+
+ ContextHandler context = new ContextHandler("/");
+
+ IsHandledHandler handler = new IsHandledHandler();
+ context.setHandler(handler);
+
+ server.setHandler(context);
+
+ try
+ {
+ server.start();
+ checkWildcardHost(true, server, null, new String[]{"example.com", ".example.com", "vhost.example.com"});
+ checkWildcardHost(false, server, new String[]{null}, new String[]{
+ "example.com", ".example.com", "vhost.example.com"
+ });
+
+ checkWildcardHost(true, server, new String[]{"example.com", "*.example.com"}, new String[]{
+ "example.com", ".example.com", "vhost.example.com"
+ });
+ checkWildcardHost(false, server, new String[]{"example.com", "*.example.com"}, new String[]{
+ "badexample.com", ".badexample.com", "vhost.badexample.com"
+ });
+
+ checkWildcardHost(false, server, new String[]{"*."}, new String[]{"anything.anything"});
+
+ checkWildcardHost(true, server, new String[]{"*.example.com"}, new String[]{"vhost.example.com", ".example.com"});
+ checkWildcardHost(false, server, new String[]{"*.example.com"}, new String[]{
+ "vhost.www.example.com", "example.com", "www.vhost.example.com"
+ });
+
+ checkWildcardHost(true, server, new String[]{"*.sub.example.com"}, new String[]{
+ "vhost.sub.example.com", ".sub.example.com"
+ });
+ checkWildcardHost(false, server, new String[]{"*.sub.example.com"}, new String[]{
+ ".example.com", "sub.example.com", "vhost.example.com"
+ });
+
+ checkWildcardHost(false, server, new String[]{"example.*.com", "example.com.*"}, new String[]{
+ "example.vhost.com", "example.com.vhost", "example.com"
+ });
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void testVirtualHostManagement()
+ {
+ ContextHandler context = new ContextHandler("/");
+
+ // test singular
+ context.setVirtualHosts(new String[]{"www.example.com"});
+ assertEquals(1, context.getVirtualHosts().length);
+
+ // test adding two more
+ context.addVirtualHosts(new String[]{"foo.com@connector1", "*.example2.com"});
+ assertEquals(3, context.getVirtualHosts().length);
+
+ // test adding existing context
+ context.addVirtualHosts(new String[]{"www.example.com"});
+ assertEquals(3, context.getVirtualHosts().length);
+
+ // test removing existing
+ context.removeVirtualHosts(new String[]{"*.example2.com"});
+ assertEquals(2, context.getVirtualHosts().length);
+
+ // test removing non-existent
+ context.removeVirtualHosts(new String[]{"www.example3.com"});
+ assertEquals(2, context.getVirtualHosts().length);
+
+ // test removing all remaining and resets to null
+ context.removeVirtualHosts(new String[]{"www.example.com", "foo.com@connector1"});
+ assertArrayEquals(null, context.getVirtualHosts());
+ }
+
+ @Test
+ public void testAttributes() throws Exception
+ {
+ ContextHandler handler = new ContextHandler();
+ handler.setServer(new Server());
+ handler.setAttribute("aaa", "111");
+ assertEquals("111", handler.getServletContext().getAttribute("aaa"));
+ assertEquals(null, handler.getAttribute("bbb"));
+
+ handler.start();
+
+ handler.getServletContext().setAttribute("aaa", "000");
+ handler.setAttribute("ccc", "333");
+ handler.getServletContext().setAttribute("ddd", "444");
+ assertEquals("111", handler.getServletContext().getAttribute("aaa"));
+ assertEquals(null, handler.getServletContext().getAttribute("bbb"));
+ handler.getServletContext().setAttribute("bbb", "222");
+ assertEquals("333", handler.getServletContext().getAttribute("ccc"));
+ assertEquals("444", handler.getServletContext().getAttribute("ddd"));
+
+ assertEquals("111", handler.getAttribute("aaa"));
+ assertEquals(null, handler.getAttribute("bbb"));
+ assertEquals("333", handler.getAttribute("ccc"));
+ assertEquals(null, handler.getAttribute("ddd"));
+
+ handler.stop();
+
+ assertEquals("111", handler.getServletContext().getAttribute("aaa"));
+ assertEquals(null, handler.getServletContext().getAttribute("bbb"));
+ assertEquals("333", handler.getServletContext().getAttribute("ccc"));
+ assertEquals(null, handler.getServletContext().getAttribute("ddd"));
+ }
+
+ @Test
+ public void testProtected() throws Exception
+ {
+ ContextHandler handler = new ContextHandler();
+ String[] protectedTargets = {"/foo-inf", "/bar-inf"};
+ handler.setProtectedTargets(protectedTargets);
+
+ assertTrue(handler.isProtectedTarget("/foo-inf/x/y/z"));
+ assertFalse(handler.isProtectedTarget("/foo/x/y/z"));
+ assertTrue(handler.isProtectedTarget("/foo-inf?x=y&z=1"));
+ assertFalse(handler.isProtectedTarget("/foo-inf-bar"));
+
+ protectedTargets = new String[4];
+ System.arraycopy(handler.getProtectedTargets(), 0, protectedTargets, 0, 2);
+ protectedTargets[2] = "/abc";
+ protectedTargets[3] = "/def";
+ handler.setProtectedTargets(protectedTargets);
+
+ assertTrue(handler.isProtectedTarget("/foo-inf/x/y/z"));
+ assertFalse(handler.isProtectedTarget("/foo/x/y/z"));
+ assertTrue(handler.isProtectedTarget("/foo-inf?x=y&z=1"));
+ assertTrue(handler.isProtectedTarget("/abc/124"));
+ assertTrue(handler.isProtectedTarget("//def"));
+
+ assertTrue(handler.isProtectedTarget("/ABC/7777"));
+ }
+
+ @Test
+ public void testIsShutdown()
+ {
+ ContextHandler handler = new ContextHandler();
+ assertEquals(false, handler.isShutdown());
+ }
+
+ @Test
+ public void testLogNameFromDisplayName() throws Exception
+ {
+ ContextHandler handler = new ContextHandler();
+ handler.setServer(new Server());
+ handler.setDisplayName("An Interesting Project: app.tast.ic");
+ try
+ {
+ handler.start();
+ assertThat("handler.get", handler.getLogger().getName(), is(ContextHandler.class.getName() + ".An_Interesting_Project__app_tast_ic"));
+ }
+ finally
+ {
+ handler.stop();
+ }
+ }
+
+ @Test
+ public void testLogNameFromContextPathDeep() throws Exception
+ {
+ ContextHandler handler = new ContextHandler();
+ handler.setServer(new Server());
+ handler.setContextPath("/app/tast/ic");
+ try
+ {
+ handler.start();
+ assertThat("handler.get", handler.getLogger().getName(), is(ContextHandler.class.getName() + ".app_tast_ic"));
+ }
+ finally
+ {
+ handler.stop();
+ }
+ }
+
+ @Test
+ public void testLogNameFromContextPathRoot() throws Exception
+ {
+ ContextHandler handler = new ContextHandler();
+ handler.setServer(new Server());
+ handler.setContextPath("");
+ try
+ {
+ handler.start();
+ assertThat("handler.get", handler.getLogger().getName(), is(ContextHandler.class.getName() + ".ROOT"));
+ }
+ finally
+ {
+ handler.stop();
+ }
+ }
+
+ @Test
+ public void testLogNameFromContextPathUndefined() throws Exception
+ {
+ ContextHandler handler = new ContextHandler();
+ handler.setServer(new Server());
+ try
+ {
+ handler.start();
+ assertThat("handler.get", handler.getLogger().getName(), is(ContextHandler.class.getName() + ".ROOT"));
+ }
+ finally
+ {
+ handler.stop();
+ }
+ }
+
+ @Test
+ public void testLogNameFromContextPathEmpty() throws Exception
+ {
+ ContextHandler handler = new ContextHandler();
+ handler.setServer(new Server());
+ handler.setContextPath("");
+ try
+ {
+ handler.start();
+ assertThat("handler.get", handler.getLogger().getName(), is(ContextHandler.class.getName() + ".ROOT"));
+ }
+ finally
+ {
+ handler.stop();
+ }
+ }
+
+ @Test
+ public void testClassPathWithSpaces() throws IOException
+ {
+ ContextHandler handler = new ContextHandler();
+ handler.setServer(new Server());
+ handler.setContextPath("/");
+
+ Path baseDir = MavenTestingUtils.getTargetTestingPath("testClassPath_WithSpaces");
+ FS.ensureEmpty(baseDir);
+
+ Path spacey = baseDir.resolve("and extra directory");
+ FS.ensureEmpty(spacey);
+
+ Path jar = spacey.resolve("empty.jar");
+ FS.touch(jar);
+
+ URLClassLoader cl = new URLClassLoader(new URL[]{jar.toUri().toURL()});
+ handler.setClassLoader(cl);
+
+ String classpath = handler.getClassPath();
+ assertThat("classpath", classpath, containsString(jar.toString()));
+ }
+
+ private void checkResourcePathsForExampleWebApp(String root) throws IOException
+ {
+ File testDirectory = setupTestDirectory();
+
+ ContextHandler handler = new ContextHandler();
+
+ assertTrue(testDirectory.isDirectory(), "Not a directory " + testDirectory);
+ handler.setBaseResource(Resource.newResource(Resource.toURL(testDirectory)));
+
+ List<String> paths = new ArrayList<>(handler.getResourcePaths(root));
+ assertEquals(2, paths.size());
+
+ Collections.sort(paths);
+ assertEquals("/WEB-INF/jsp/", paths.get(0));
+ assertEquals("/WEB-INF/web.xml", paths.get(1));
+ }
+
+ private File setupTestDirectory() throws IOException
+ {
+ Path root = workDir.getEmptyPathDir();
+
+ Path webInfDir = root.resolve("WEB-INF");
+ FS.ensureDirExists(webInfDir);
+ FS.ensureDirExists(webInfDir.resolve("jsp"));
+ FS.touch(webInfDir.resolve("web.xml"));
+
+ return root.toFile();
+ }
+
+ private void checkWildcardHost(boolean succeed, Server server, String[] contextHosts, String[] requestHosts) throws Exception
+ {
+ LocalConnector connector = (LocalConnector)server.getConnectors()[0];
+ ContextHandler context = (ContextHandler)server.getHandler();
+ context.setVirtualHosts(contextHosts);
+
+ IsHandledHandler handler = (IsHandledHandler)context.getHandler();
+ for (String host : requestHosts)
+ {
+ connector.getResponse("GET / HTTP/1.1\n" + "Host: " + host + "\nConnection:close\n\n");
+ if (succeed)
+ assertTrue(handler.isHandled(), "'" + host + "' should have been handled.");
+ else
+ assertFalse(handler.isHandled(), "'" + host + "' should not have been handled.");
+ handler.reset();
+ }
+ }
+
+ private static final class IsHandledHandler extends AbstractHandler
+ {
+ private boolean handled;
+
+ public boolean isHandled()
+ {
+ return handled;
+ }
+
+ @Override
+ public void handle(String s, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ {
+ baseRequest.setHandled(true);
+ this.handled = true;
+ }
+
+ public void reset()
+ {
+ handled = false;
+ }
+ }
+
+ private static final class ContextPathHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String s, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+
+ response.setStatus(200);
+ response.setContentType("text/plain; charset=utf-8");
+ response.setHeader("Connection", "close");
+ PrintWriter writer = response.getWriter();
+ writer.println("ctx='" + request.getContextPath() + "'");
+ }
+ }
+
+ private static class TestServletContextListener implements ServletContextListener
+ {
+ public int initialized = 0;
+ public int destroyed = 0;
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce)
+ {
+ initialized++;
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent sce)
+ {
+ destroyed++;
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DebugHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DebugHandlerTest.java
new file mode 100644
index 0000000..4708d08
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DebugHandlerTest.java
@@ -0,0 +1,184 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.concurrent.Executor;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManagerFactory;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.LeakTrackingByteBufferPool;
+import org.eclipse.jetty.io.MappedByteBufferPool;
+import org.eclipse.jetty.server.AbstractConnectionFactory;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+
+public class DebugHandlerTest
+{
+ public static final HostnameVerifier __hostnameverifier = new HostnameVerifier()
+ {
+ @Override
+ public boolean verify(String hostname, SSLSession session)
+ {
+ return true;
+ }
+ };
+
+ private SSLContext sslContext;
+ private Server server;
+ private URI serverURI;
+ private URI secureServerURI;
+
+ @SuppressWarnings("deprecation")
+ private DebugHandler debugHandler;
+ private ByteArrayOutputStream capturedLog;
+
+ @SuppressWarnings("deprecation")
+ @BeforeEach
+ public void startServer() throws Exception
+ {
+ server = new Server();
+
+ ServerConnector httpConnector = new ServerConnector(server);
+ httpConnector.setPort(0);
+ server.addConnector(httpConnector);
+
+ File keystorePath = MavenTestingUtils.getTestResourceFile("keystore");
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystorePath.getAbsolutePath());
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+ sslContextFactory.setTrustStorePath(keystorePath.getAbsolutePath());
+ sslContextFactory.setTrustStorePassword("storepwd");
+ ByteBufferPool pool = new LeakTrackingByteBufferPool(new MappedByteBufferPool.Tagged());
+ ServerConnector sslConnector = new ServerConnector(server,
+ (Executor)null,
+ (Scheduler)null, pool, 1, 1,
+ AbstractConnectionFactory.getFactories(sslContextFactory, new HttpConnectionFactory()));
+
+ server.addConnector(sslConnector);
+
+ debugHandler = new DebugHandler();
+ capturedLog = new ByteArrayOutputStream();
+ debugHandler.setOutputStream(capturedLog);
+ debugHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(HttpStatus.OK_200);
+ }
+ });
+ server.setHandler(debugHandler);
+ server.start();
+
+ String host = httpConnector.getHost();
+ if (host == null)
+ host = "localhost";
+
+ serverURI = URI.create(String.format("http://%s:%d/", host, httpConnector.getLocalPort()));
+ secureServerURI = URI.create(String.format("https://%s:%d/", host, sslConnector.getLocalPort()));
+
+ KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+ try (InputStream stream = sslContextFactory.getKeyStoreResource().getInputStream())
+ {
+ keystore.load(stream, "storepwd".toCharArray());
+ }
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(keystore);
+ sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
+
+ try
+ {
+ HttpsURLConnection.setDefaultHostnameVerifier(__hostnameverifier);
+ SSLContext sc = SSLContext.getInstance("TLS");
+ sc.init(null, SslContextFactory.TRUST_ALL_CERTS, new java.security.SecureRandom());
+ HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ throw new RuntimeException(e);
+ }
+ }
+
+ @AfterEach
+ public void stopServer() throws Exception
+ {
+ server.stop();
+ }
+
+ @Test
+ public void testThreadName() throws IOException
+ {
+ HttpURLConnection http = (HttpURLConnection)serverURI.resolve("/foo/bar?a=b").toURL().openConnection();
+ assertThat("Response Code", http.getResponseCode(), is(200));
+
+ String log = capturedLog.toString(StandardCharsets.UTF_8.name());
+ String expectedThreadName = String.format("//%s:%s/foo/bar?a=b", serverURI.getHost(), serverURI.getPort());
+ assertThat("ThreadName", log, containsString(expectedThreadName));
+ // Look for bad/mangled/duplicated schemes
+ assertThat("ThreadName", log, not(containsString("http:" + expectedThreadName)));
+ assertThat("ThreadName", log, not(containsString("https:" + expectedThreadName)));
+ }
+
+ @Test
+ public void testSecureThreadName() throws IOException
+ {
+ HttpURLConnection http = (HttpURLConnection)secureServerURI.resolve("/foo/bar?a=b").toURL().openConnection();
+ assertThat("Response Code", http.getResponseCode(), is(200));
+
+ String log = capturedLog.toString(StandardCharsets.UTF_8.name());
+ String expectedThreadName = String.format("https://%s:%s/foo/bar?a=b", secureServerURI.getHost(), secureServerURI.getPort());
+ assertThat("ThreadName", log, containsString(expectedThreadName));
+ // Look for bad/mangled/duplicated schemes
+ assertThat("ThreadName", log, not(containsString("http:" + expectedThreadName)));
+ assertThat("ThreadName", log, not(containsString("https:" + expectedThreadName)));
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DefaultHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DefaultHandlerTest.java
new file mode 100644
index 0000000..935c1dd
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DefaultHandlerTest.java
@@ -0,0 +1,146 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class DefaultHandlerTest
+{
+ private Server server;
+ private ServerConnector connector;
+ private DefaultHandler handler;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ server = new Server();
+ connector = new ServerConnector(server);
+ server.addConnector(connector);
+
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+ handler = new DefaultHandler();
+ HandlerCollection handlers = new HandlerCollection();
+ handlers.setHandlers(new Handler[]{contexts, handler});
+ server.setHandler(handlers);
+
+ handler.setServeIcon(true);
+ handler.setShowContexts(true);
+
+ contexts.addHandler(new ContextHandler("/foo"));
+ contexts.addHandler(new ContextHandler("/bar"));
+
+ server.start();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ server.stop();
+ }
+
+ @Test
+ public void testRoot() throws Exception
+ {
+ try (Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ String request =
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ OutputStream output = socket.getOutputStream();
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ HttpTester.Input input = HttpTester.from(socket.getInputStream());
+ HttpTester.Response response = HttpTester.parseResponse(input);
+
+ assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
+ assertEquals("text/html;charset=UTF-8", response.get(HttpHeader.CONTENT_TYPE));
+
+ String content = new String(response.getContentBytes(), StandardCharsets.UTF_8);
+ assertThat(content, containsString("Contexts known to this server are:"));
+ assertThat(content, containsString("/foo"));
+ assertThat(content, containsString("/bar"));
+ }
+ }
+
+ @Test
+ public void testSomePath() throws Exception
+ {
+ try (Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ String request =
+ "GET /some/path HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ OutputStream output = socket.getOutputStream();
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ HttpTester.Input input = HttpTester.from(socket.getInputStream());
+ HttpTester.Response response = HttpTester.parseResponse(input);
+
+ assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
+ assertEquals("text/html;charset=ISO-8859-1", response.get(HttpHeader.CONTENT_TYPE));
+
+ String content = new String(response.getContentBytes(), StandardCharsets.ISO_8859_1);
+ assertThat(content, not(containsString("Contexts known to this server are:")));
+ assertThat(content, not(containsString("/foo")));
+ assertThat(content, not(containsString("/bar")));
+ }
+ }
+
+ @Test
+ public void testFavIcon() throws Exception
+ {
+ try (Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ String request =
+ "GET /favicon.ico HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ OutputStream output = socket.getOutputStream();
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ HttpTester.Input input = HttpTester.from(socket.getInputStream());
+ HttpTester.Response response = HttpTester.parseResponse(input);
+
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertEquals("image/x-icon", response.get(HttpHeader.CONTENT_TYPE));
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandlerTest.java
new file mode 100644
index 0000000..d7e4854
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandlerTest.java
@@ -0,0 +1,696 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.HttpOutput;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(WorkDirExtension.class)
+public class FileBufferedResponseHandlerTest
+{
+ private static final Logger LOG = Log.getLogger(FileBufferedResponseHandlerTest.class);
+
+ public WorkDir _workDir;
+
+ private final CountDownLatch _disposeLatch = new CountDownLatch(1);
+ private Server _server;
+ private LocalConnector _localConnector;
+ private ServerConnector _serverConnector;
+ private Path _testDir;
+ private FileBufferedResponseHandler _bufferedHandler;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ _testDir = _workDir.getEmptyPathDir();
+
+ _server = new Server();
+ HttpConfiguration config = new HttpConfiguration();
+ config.setOutputBufferSize(1024);
+ config.setOutputAggregationSize(256);
+
+ _localConnector = new LocalConnector(_server, new HttpConnectionFactory(config));
+ _localConnector.setIdleTimeout(Duration.ofMinutes(1).toMillis());
+ _server.addConnector(_localConnector);
+ _serverConnector = new ServerConnector(_server, new HttpConnectionFactory(config));
+ _server.addConnector(_serverConnector);
+
+ _bufferedHandler = new FileBufferedResponseHandler()
+ {
+ @Override
+ protected BufferedInterceptor newBufferedInterceptor(HttpChannel httpChannel, HttpOutput.Interceptor interceptor)
+ {
+ return new FileBufferedInterceptor(httpChannel, interceptor)
+ {
+ @Override
+ protected void dispose()
+ {
+ super.dispose();
+ _disposeLatch.countDown();
+ }
+ };
+ }
+ };
+ _bufferedHandler.setTempDir(_testDir);
+ _bufferedHandler.getPathIncludeExclude().include("/include/*");
+ _bufferedHandler.getPathIncludeExclude().exclude("*.exclude");
+ _bufferedHandler.getMimeIncludeExclude().exclude("text/excluded");
+ _server.setHandler(_bufferedHandler);
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _server.stop();
+ }
+
+ @Test
+ public void testPathNotIncluded() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(10);
+ PrintWriter writer = response.getWriter();
+ writer.println("a string larger than the buffer size");
+ writer.println("Committed: " + response.isCommitted());
+ writer.println("NumFiles: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The response was committed after the first write and we never created a file to buffer the response into.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, containsString("Committed: true"));
+ assertThat(responseContent, containsString("NumFiles: 0"));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testIncludedByPath() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(10);
+ PrintWriter writer = response.getWriter();
+ writer.println("a string larger than the buffer size");
+ writer.println("Committed: " + response.isCommitted());
+ writer.println("NumFiles: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The response was not committed after the first write and a file was created to buffer the response.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, containsString("Committed: false"));
+ assertThat(responseContent, containsString("NumFiles: 1"));
+
+ // Unable to verify file deletion on windows, as immediate delete not possible.
+ // only after a GC has occurred.
+ if (!OS.WINDOWS.isCurrentOs())
+ {
+ assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(getNumFiles(), is(0));
+ }
+ }
+
+ @Test
+ public void testExcludedByPath() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(10);
+ PrintWriter writer = response.getWriter();
+ writer.println("a string larger than the buffer size");
+ writer.println("Committed: " + response.isCommitted());
+ writer.println("NumFiles: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path.exclude HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The response was committed after the first write and we never created a file to buffer the response into.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, containsString("Committed: true"));
+ assertThat(responseContent, containsString("NumFiles: 0"));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testExcludedByMime() throws Exception
+ {
+ String excludedMimeType = "text/excluded";
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setContentType(excludedMimeType);
+ response.setBufferSize(10);
+ PrintWriter writer = response.getWriter();
+ writer.println("a string larger than the buffer size");
+ writer.println("Committed: " + response.isCommitted());
+ writer.println("NumFiles: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The response was committed after the first write and we never created a file to buffer the response into.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, containsString("Committed: true"));
+ assertThat(responseContent, containsString("NumFiles: 0"));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testFlushed() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(1024);
+ PrintWriter writer = response.getWriter();
+ writer.println("a string smaller than the buffer size");
+ writer.println("NumFilesBeforeFlush: " + getNumFiles());
+ writer.flush();
+ writer.println("Committed: " + response.isCommitted());
+ writer.println("NumFiles: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The response was not committed after the buffer was flushed and a file was created to buffer the response.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, containsString("NumFilesBeforeFlush: 0"));
+ assertThat(responseContent, containsString("Committed: false"));
+ assertThat(responseContent, containsString("NumFiles: 1"));
+
+ // Unable to verify file deletion on windows, as immediate delete not possible.
+ // only after a GC has occurred.
+ if (!OS.WINDOWS.isCurrentOs())
+ {
+ assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(getNumFiles(), is(0));
+ }
+ }
+
+ @Test
+ public void testClosed() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(10);
+ PrintWriter writer = response.getWriter();
+ writer.println("a string larger than the buffer size");
+ writer.println("NumFiles: " + getNumFiles());
+ writer.close();
+ writer.println("writtenAfterClose");
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The content written after close was not sent.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, not(containsString("writtenAfterClose")));
+ assertThat(responseContent, containsString("NumFiles: 1"));
+
+ // Unable to verify file deletion on windows, as immediate delete not possible.
+ // only after a GC has occurred.
+ if (!OS.WINDOWS.isCurrentOs())
+ {
+ assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(getNumFiles(), is(0));
+ }
+ }
+
+ @Test
+ public void testBufferSizeBig() throws Exception
+ {
+ int bufferSize = 4096;
+ String largeContent = generateContent(bufferSize - 64);
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(bufferSize);
+ PrintWriter writer = response.getWriter();
+ writer.println(largeContent);
+ writer.println("Committed: " + response.isCommitted());
+ writer.println("NumFiles: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The content written was not buffered as a file as it was less than the buffer size.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, not(containsString("writtenAfterClose")));
+ assertThat(responseContent, containsString("Committed: false"));
+ assertThat(responseContent, containsString("NumFiles: 0"));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testFlushEmpty() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(1024);
+ PrintWriter writer = response.getWriter();
+ writer.flush();
+ int numFiles = getNumFiles();
+ writer.println("NumFiles: " + numFiles);
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The flush should not create the file unless there is content to write.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, containsString("NumFiles: 0"));
+
+ // Unable to verify file deletion on windows, as immediate delete not possible.
+ // only after a GC has occurred.
+ if (!OS.WINDOWS.isCurrentOs())
+ {
+ assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(getNumFiles(), is(0));
+ }
+ }
+
+ @Test
+ public void testReset() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(8);
+ PrintWriter writer = response.getWriter();
+ writer.println("THIS WILL BE RESET");
+ writer.flush();
+ writer.println("THIS WILL BE RESET");
+ int numFilesBeforeReset = getNumFiles();
+ response.resetBuffer();
+ int numFilesAfterReset = getNumFiles();
+
+ writer.println("NumFilesBeforeReset: " + numFilesBeforeReset);
+ writer.println("NumFilesAfterReset: " + numFilesAfterReset);
+ writer.println("a string larger than the buffer size");
+ writer.println("NumFilesAfterWrite: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // Resetting the response buffer will delete the file.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, not(containsString("THIS WILL BE RESET")));
+
+ assertThat(responseContent, containsString("NumFilesBeforeReset: 1"));
+ assertThat(responseContent, containsString("NumFilesAfterReset: 0"));
+ assertThat(responseContent, containsString("NumFilesAfterWrite: 1"));
+
+ // Unable to verify file deletion on windows, as immediate delete not possible.
+ // only after a GC has occurred.
+ if (!OS.WINDOWS.isCurrentOs())
+ {
+ assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(getNumFiles(), is(0));
+ }
+ }
+
+ @Test
+ public void testFileLargerThanMaxInteger() throws Exception
+ {
+ long fileSize = Integer.MAX_VALUE + 1234L;
+ byte[] bytes = randomBytes(1024 * 1024);
+
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ ServletOutputStream outputStream = response.getOutputStream();
+
+ long written = 0;
+ while (written < fileSize)
+ {
+ int length = Math.toIntExact(Math.min(bytes.length, fileSize - written));
+ outputStream.write(bytes, 0, length);
+ written += length;
+ }
+ outputStream.flush();
+
+ response.setHeader("NumFiles", Integer.toString(getNumFiles()));
+ response.setHeader("FileSize", Long.toString(getFileSize()));
+ }
+ });
+
+ _server.start();
+
+ AtomicLong received = new AtomicLong();
+ HttpTester.Response response = new HttpTester.Response()
+ {
+ @Override
+ public boolean content(ByteBuffer ref)
+ {
+ // Verify the content is what was sent.
+ while (ref.hasRemaining())
+ {
+ byte byteFromBuffer = ref.get();
+ long totalReceived = received.getAndIncrement();
+ int bytesIndex = (int)(totalReceived % bytes.length);
+ byte byteFromArray = bytes[bytesIndex];
+
+ if (byteFromBuffer != byteFromArray)
+ {
+ LOG.warn("Mismatch at index {} received bytes {}, {}!={}", bytesIndex, totalReceived, byteFromBuffer, byteFromArray, new IllegalStateException());
+ return true;
+ }
+ }
+
+ return false;
+ }
+ };
+
+ try (Socket socket = new Socket("localhost", _serverConnector.getLocalPort()))
+ {
+ OutputStream output = socket.getOutputStream();
+ String request = "GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n";
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ HttpTester.Input input = HttpTester.from(socket.getInputStream());
+ HttpTester.parseResponse(input, response);
+ }
+
+ assertTrue(response.isComplete());
+ assertThat(response.get("NumFiles"), is("1"));
+ assertThat(response.get("FileSize"), is(Long.toString(fileSize)));
+ assertThat(received.get(), is(fileSize));
+
+ // Unable to verify file deletion on windows, as immediate delete not possible.
+ // only after a GC has occurred.
+ if (!OS.WINDOWS.isCurrentOs())
+ {
+ assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(getNumFiles(), is(0));
+ }
+ }
+
+ @Test
+ public void testNextInterceptorFailed() throws Exception
+ {
+ AbstractHandler failingInterceptorHandler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ HttpOutput httpOutput = baseRequest.getResponse().getHttpOutput();
+ HttpOutput.Interceptor nextInterceptor = httpOutput.getInterceptor();
+ httpOutput.setInterceptor(new HttpOutput.Interceptor()
+ {
+ @Override
+ public void write(ByteBuffer content, boolean last, Callback callback)
+ {
+ callback.failed(new Throwable("intentionally throwing from interceptor"));
+ }
+
+ @Override
+ public HttpOutput.Interceptor getNextInterceptor()
+ {
+ return nextInterceptor;
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return false;
+ }
+ });
+ }
+ };
+
+ _server.setHandler(new HandlerCollection(failingInterceptorHandler, _server.getHandler()));
+ CompletableFuture<Throwable> errorFuture = new CompletableFuture<>();
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ byte[] chunk1 = "this content will ".getBytes();
+ byte[] chunk2 = "be buffered in a file".getBytes();
+ response.setContentLength(chunk1.length + chunk2.length);
+ ServletOutputStream outputStream = response.getOutputStream();
+
+ // Write chunk1 and then flush so it is written to the file.
+ outputStream.write(chunk1);
+ outputStream.flush();
+ assertThat(getNumFiles(), is(1));
+
+ try
+ {
+ // ContentLength is set so it knows this is the last write.
+ // This will cause the file to be written to the next interceptor which will fail.
+ outputStream.write(chunk2);
+ }
+ catch (Throwable t)
+ {
+ errorFuture.complete(t);
+ throw t;
+ }
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ // Response was aborted.
+ assertThat(response.getStatus(), is(0));
+
+ // We failed because of the next interceptor.
+ Throwable error = errorFuture.get(5, TimeUnit.SECONDS);
+ assertThat(error.getMessage(), containsString("intentionally throwing from interceptor"));
+
+ // Unable to verify file deletion on windows, as immediate delete not possible.
+ // only after a GC has occurred.
+ if (!OS.WINDOWS.isCurrentOs())
+ {
+ // All files were deleted.
+ assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(getNumFiles(), is(0));
+ }
+ }
+
+ @Test
+ public void testFileWriteFailed() throws Exception
+ {
+ // Set the temp directory to an empty directory so that the file cannot be created.
+ File tempDir = MavenTestingUtils.getTargetTestingDir(getClass().getSimpleName());
+ FS.ensureDeleted(tempDir);
+ _bufferedHandler.setTempDir(tempDir.toPath());
+
+ CompletableFuture<Throwable> errorFuture = new CompletableFuture<>();
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ ServletOutputStream outputStream = response.getOutputStream();
+ byte[] content = "this content will be buffered in a file".getBytes();
+
+ try
+ {
+ // Write the content and flush it to the file.
+ // This should throw as it cannot create the file to aggregate into.
+ outputStream.write(content);
+ outputStream.flush();
+ }
+ catch (Throwable t)
+ {
+ errorFuture.complete(t);
+ throw t;
+ }
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ // Response was aborted.
+ assertThat(response.getStatus(), is(0));
+
+ // We failed because cannot create the file.
+ Throwable error = errorFuture.get(5, TimeUnit.SECONDS);
+ assertThat(error, instanceOf(NoSuchFileException.class));
+
+ // No files were created.
+ assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ private int getNumFiles()
+ {
+ File[] files = _testDir.toFile().listFiles();
+ if (files == null)
+ return 0;
+
+ return files.length;
+ }
+
+ private long getFileSize()
+ {
+ File[] files = _testDir.toFile().listFiles();
+ assertNotNull(files);
+ assertThat(files.length, is(1));
+ return files[0].length();
+ }
+
+ private static String generateContent(int size)
+ {
+ Random random = new Random();
+ StringBuilder stringBuilder = new StringBuilder(size);
+ for (int i = 0; i < size; i++)
+ {
+ stringBuilder.append((char)Math.abs(random.nextInt(0x7F)));
+ }
+ return stringBuilder.toString();
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private byte[] randomBytes(int size)
+ {
+ byte[] data = new byte[size];
+ new Random().nextBytes(data);
+ return data;
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/HandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/HandlerTest.java
new file mode 100644
index 0000000..fcc5ed3
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/HandlerTest.java
@@ -0,0 +1,283 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class HandlerTest
+{
+
+ @Test
+ public void testWrapperSetServer()
+ {
+ Server s = new Server();
+ HandlerWrapper a = new HandlerWrapper();
+ HandlerWrapper b = new HandlerWrapper();
+ HandlerWrapper c = new HandlerWrapper();
+ a.setHandler(b);
+ b.setHandler(c);
+
+ a.setServer(s);
+ assertThat(b.getServer(), equalTo(s));
+ assertThat(c.getServer(), equalTo(s));
+ }
+
+ @Test
+ public void testWrapperServerSet()
+ {
+ Server s = new Server();
+ HandlerWrapper a = new HandlerWrapper();
+ HandlerWrapper b = new HandlerWrapper();
+ HandlerWrapper c = new HandlerWrapper();
+ a.setServer(s);
+ b.setHandler(c);
+ a.setHandler(b);
+
+ assertThat(b.getServer(), equalTo(s));
+ assertThat(c.getServer(), equalTo(s));
+ }
+
+ @Test
+ public void testWrapperThisLoop()
+ {
+ HandlerWrapper a = new HandlerWrapper();
+
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> a.setHandler(a));
+ assertThat(e.getMessage(), containsString("loop"));
+ }
+
+ @Test
+ public void testWrapperSimpleLoop()
+ {
+ HandlerWrapper a = new HandlerWrapper();
+ HandlerWrapper b = new HandlerWrapper();
+
+ a.setHandler(b);
+
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> b.setHandler(a));
+ assertThat(e.getMessage(), containsString("loop"));
+ }
+
+ @Test
+ public void testWrapperDeepLoop()
+ {
+ HandlerWrapper a = new HandlerWrapper();
+ HandlerWrapper b = new HandlerWrapper();
+ HandlerWrapper c = new HandlerWrapper();
+
+ a.setHandler(b);
+ b.setHandler(c);
+
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> c.setHandler(a));
+ assertThat(e.getMessage(), containsString("loop"));
+ }
+
+ @Test
+ public void testWrapperChainLoop()
+ {
+ HandlerWrapper a = new HandlerWrapper();
+ HandlerWrapper b = new HandlerWrapper();
+ HandlerWrapper c = new HandlerWrapper();
+
+ a.setHandler(b);
+ c.setHandler(a);
+
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> b.setHandler(c));
+ assertThat(e.getMessage(), containsString("loop"));
+ }
+
+ @Test
+ public void testCollectionSetServer()
+ {
+ Server s = new Server();
+ HandlerCollection a = new HandlerCollection();
+ HandlerCollection b = new HandlerCollection();
+ HandlerCollection b1 = new HandlerCollection();
+ HandlerCollection b2 = new HandlerCollection();
+ HandlerCollection c = new HandlerCollection();
+ HandlerCollection c1 = new HandlerCollection();
+ HandlerCollection c2 = new HandlerCollection();
+
+ a.addHandler(b);
+ a.addHandler(c);
+ b.setHandlers(new Handler[]{b1, b2});
+ c.setHandlers(new Handler[]{c1, c2});
+ a.setServer(s);
+
+ assertThat(b.getServer(), equalTo(s));
+ assertThat(c.getServer(), equalTo(s));
+ assertThat(b1.getServer(), equalTo(s));
+ assertThat(b2.getServer(), equalTo(s));
+ assertThat(c1.getServer(), equalTo(s));
+ assertThat(c2.getServer(), equalTo(s));
+ }
+
+ @Test
+ public void testCollectionServerSet()
+ {
+ Server s = new Server();
+ HandlerCollection a = new HandlerCollection();
+ HandlerCollection b = new HandlerCollection();
+ HandlerCollection b1 = new HandlerCollection();
+ HandlerCollection b2 = new HandlerCollection();
+ HandlerCollection c = new HandlerCollection();
+ HandlerCollection c1 = new HandlerCollection();
+ HandlerCollection c2 = new HandlerCollection();
+
+ a.setServer(s);
+ a.addHandler(b);
+ a.addHandler(c);
+ b.setHandlers(new Handler[]{b1, b2});
+ c.setHandlers(new Handler[]{c1, c2});
+
+ assertThat(b.getServer(), equalTo(s));
+ assertThat(c.getServer(), equalTo(s));
+ assertThat(b1.getServer(), equalTo(s));
+ assertThat(b2.getServer(), equalTo(s));
+ assertThat(c1.getServer(), equalTo(s));
+ assertThat(c2.getServer(), equalTo(s));
+ }
+
+ @Test
+ public void testCollectionThisLoop()
+ {
+ HandlerCollection a = new HandlerCollection();
+
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> a.addHandler(a));
+ assertThat(e.getMessage(), containsString("loop"));
+ }
+
+ @Test
+ public void testCollectionDeepLoop()
+ {
+ HandlerCollection a = new HandlerCollection();
+ HandlerCollection b = new HandlerCollection();
+ HandlerCollection b1 = new HandlerCollection();
+ HandlerCollection b2 = new HandlerCollection();
+ HandlerCollection c = new HandlerCollection();
+ HandlerCollection c1 = new HandlerCollection();
+ HandlerCollection c2 = new HandlerCollection();
+
+ a.addHandler(b);
+ a.addHandler(c);
+ b.setHandlers(new Handler[]{b1, b2});
+ c.setHandlers(new Handler[]{c1, c2});
+
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> b2.addHandler(a));
+ assertThat(e.getMessage(), containsString("loop"));
+ }
+
+ @Test
+ public void testCollectionChainLoop()
+ {
+ HandlerCollection a = new HandlerCollection();
+ HandlerCollection b = new HandlerCollection();
+ HandlerCollection b1 = new HandlerCollection();
+ HandlerCollection b2 = new HandlerCollection();
+ HandlerCollection c = new HandlerCollection();
+ HandlerCollection c1 = new HandlerCollection();
+ HandlerCollection c2 = new HandlerCollection();
+
+ a.addHandler(c);
+ b.setHandlers(new Handler[]{b1, b2});
+ c.setHandlers(new Handler[]{c1, c2});
+ b2.addHandler(a);
+
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> a.addHandler(b));
+ assertThat(e.getMessage(), containsString("loop"));
+ }
+
+ @Test
+ public void testInsertWrapperTail()
+ {
+ HandlerWrapper a = new HandlerWrapper();
+ HandlerWrapper b = new HandlerWrapper();
+
+ a.insertHandler(b);
+ assertThat(a.getHandler(), equalTo(b));
+ assertThat(b.getHandler(), nullValue());
+ }
+
+ @Test
+ public void testInsertWrapper()
+ {
+ HandlerWrapper a = new HandlerWrapper();
+ HandlerWrapper b = new HandlerWrapper();
+ HandlerWrapper c = new HandlerWrapper();
+
+ a.insertHandler(c);
+ a.insertHandler(b);
+ assertThat(a.getHandler(), equalTo(b));
+ assertThat(b.getHandler(), equalTo(c));
+ assertThat(c.getHandler(), nullValue());
+ }
+
+ @Test
+ public void testInsertWrapperChain()
+ {
+ HandlerWrapper a = new HandlerWrapper();
+ HandlerWrapper b = new HandlerWrapper();
+ HandlerWrapper c = new HandlerWrapper();
+ HandlerWrapper d = new HandlerWrapper();
+
+ a.insertHandler(d);
+ b.insertHandler(c);
+ a.insertHandler(b);
+ assertThat(a.getHandler(), equalTo(b));
+ assertThat(b.getHandler(), equalTo(c));
+ assertThat(c.getHandler(), equalTo(d));
+ assertThat(d.getHandler(), nullValue());
+ }
+
+ @Test
+ public void testInsertWrapperBadChain()
+ {
+ HandlerWrapper a = new HandlerWrapper();
+ HandlerWrapper b = new HandlerWrapper();
+ HandlerWrapper c = new HandlerWrapper();
+ HandlerWrapper d = new HandlerWrapper();
+
+ a.insertHandler(d);
+ b.insertHandler(c);
+ c.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ }
+ });
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> a.insertHandler(b));
+ assertThat(e.getMessage(), containsString("bad tail"));
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/IPAccessHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/IPAccessHandlerTest.java
new file mode 100644
index 0000000..42397bc
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/IPAccessHandlerTest.java
@@ -0,0 +1,590 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.BufferedReader;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.NetworkConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class IPAccessHandlerTest
+{
+ private Server _server;
+ private NetworkConnector _connector;
+ private IPAccessHandler _handler;
+
+ @BeforeEach
+ public void setUp()
+ throws Exception
+ {
+ _server = new Server();
+ _connector = new ServerConnector(_server);
+ _server.setConnectors(new Connector[]{_connector});
+
+ _handler = new IPAccessHandler();
+ _handler.setHandler(new ScopedHandler()
+ {
+ @Override
+ public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ try
+ {
+ baseRequest.setServletPath(baseRequest.getPathInfo());
+ baseRequest.setPathInfo(null);
+ super.doScope(target, baseRequest, request, response);
+ }
+ finally
+ {
+ baseRequest.setPathInfo(baseRequest.getServletPath());
+ baseRequest.setServletPath(null);
+ }
+ }
+
+ @Override
+ public void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(HttpStatus.OK_200);
+ }
+ });
+ }
+
+ @AfterEach
+ public void tearDown()
+ throws Exception
+ {
+ _server.stop();
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testHandler(String white, String black, String host, String uri, String code, boolean byPath)
+ throws Exception
+ {
+ _server.setHandler(_handler);
+ _server.start();
+
+ _handler.setWhite(white.split(";", -1));
+ _handler.setBlack(black.split(";", -1));
+ _handler.setWhiteListByPath(byPath);
+
+ String request = "GET " + uri + " HTTP/1.1\n" + "Host: " + host + "\n\n";
+ Socket socket = new Socket("127.0.0.1", _connector.getLocalPort());
+ socket.setSoTimeout(5000);
+ try (OutputStream output = socket.getOutputStream();)
+ {
+ BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ Response response = readResponse(input);
+ Object[] params = new Object[]{
+ "Request WBHUC", white, black, host, uri, code,
+ "Response", response.getCode()
+ };
+ assertEquals(code, response.getCode(), Arrays.deepToString(params));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testContext(String white, String black, String host, String uri, String code, boolean byPath)
+ throws Exception
+ {
+ ContextHandler context = new ContextHandler(_server, "/ctx");
+ context.setHandler(_handler);
+ _server.setHandler(context);
+ _server.start();
+
+ white = white.replaceAll("\\|/", "|/ctx/");
+ black = black.replaceAll("\\|/", "|/ctx/");
+
+ Assumptions.assumeFalse(white.endsWith("|"));
+ Assumptions.assumeFalse(black.endsWith("|"));
+ _handler.setWhite(white.split(";", -1));
+ _handler.setBlack(black.split(";", -1));
+ _handler.setWhiteListByPath(byPath);
+
+ String request = "GET /ctx" + uri + " HTTP/1.1\n" + "Host: " + host + "\n\n";
+ Socket socket = new Socket("127.0.0.1", _connector.getLocalPort());
+ socket.setSoTimeout(5000);
+ try (OutputStream output = socket.getOutputStream();)
+ {
+ BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ Response response = readResponse(input);
+ Object[] params = new Object[]{
+ "Request WBHUC", white, black, host, uri, code,
+ "Response", response.getCode()
+ };
+ assertEquals(code, response.getCode(), Arrays.deepToString(params));
+ }
+ }
+
+ protected Response readResponse(BufferedReader reader)
+ throws IOException
+ {
+ // Simplified parser for HTTP responses
+ String line = reader.readLine();
+ if (line == null)
+ throw new EOFException();
+ Matcher responseLine = Pattern.compile("HTTP/1\\.1\\s+(\\d+)").matcher(line);
+ assertTrue(responseLine.lookingAt());
+ String code = responseLine.group(1);
+
+ Map<String, String> headers = new LinkedHashMap<>();
+ while ((line = reader.readLine()) != null)
+ {
+ if (line.trim().length() == 0)
+ break;
+
+ Matcher header = Pattern.compile("([^:]+):\\s*(.*)").matcher(line);
+ assertTrue(header.lookingAt());
+ String headerName = header.group(1);
+ String headerValue = header.group(2);
+ headers.put(headerName.toLowerCase(Locale.ENGLISH), headerValue.toLowerCase(Locale.ENGLISH));
+ }
+
+ StringBuilder body = new StringBuilder();
+ if (headers.containsKey("content-length"))
+ {
+ int length = Integer.parseInt(headers.get("content-length"));
+ for (int i = 0; i < length; ++i)
+ {
+ char c = (char)reader.read();
+ body.append(c);
+ }
+ }
+ else if ("chunked".equals(headers.get("transfer-encoding")))
+ {
+ while ((line = reader.readLine()) != null)
+ {
+ if ("0".equals(line))
+ {
+ line = reader.readLine();
+ assertEquals("", line);
+ break;
+ }
+
+ int length = Integer.parseInt(line, 16);
+ for (int i = 0; i < length; ++i)
+ {
+ char c = (char)reader.read();
+ body.append(c);
+ }
+ line = reader.readLine();
+ assertEquals("", line);
+ }
+ }
+
+ return new Response(code, headers, body.toString().trim());
+ }
+
+ protected class Response
+ {
+ private final String code;
+ private final Map<String, String> headers;
+ private final String body;
+
+ private Response(String code, Map<String, String> headers, String body)
+ {
+ this.code = code;
+ this.headers = headers;
+ this.body = body;
+ }
+
+ public String getCode()
+ {
+ return code;
+ }
+
+ public Map<String, String> getHeaders()
+ {
+ return headers;
+ }
+
+ public String getBody()
+ {
+ return body;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append(code).append("\r\n");
+ for (Map.Entry<String, String> entry : headers.entrySet())
+ {
+ builder.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n");
+ }
+ builder.append("\r\n");
+ builder.append(body);
+ return builder.toString();
+ }
+ }
+
+ public static Stream<Arguments> data()
+ {
+ Object[][] data = new Object[][]{
+ // Empty lists
+ {"", "", "127.0.0.1", "/", "200", false},
+ {"", "", "127.0.0.1", "/dump/info", "200", false},
+
+ // White list
+ {"127.0.0.1", "", "127.0.0.1", "/", "200", false},
+ {"127.0.0.1", "", "127.0.0.1", "/dispatch", "200", false},
+ {"127.0.0.1", "", "127.0.0.1", "/dump/info", "200", false},
+
+ {"127.0.0.1|/", "", "127.0.0.1", "/", "200", false},
+ {"127.0.0.1|/", "", "127.0.0.1", "/dispatch", "403", false},
+ {"127.0.0.1|/", "", "127.0.0.1", "/dump/info", "403", false},
+
+ {"127.0.0.1|/*", "", "127.0.0.1", "/", "200", false},
+ {"127.0.0.1|/*", "", "127.0.0.1", "/dispatch", "200", false},
+ {"127.0.0.1|/*", "", "127.0.0.1", "/dump/info", "200", false},
+
+ {"127.0.0.1|/dump/*", "", "127.0.0.1", "/", "403", false},
+ {"127.0.0.1|/dump/*", "", "127.0.0.1", "/dispatch", "403", false},
+ {"127.0.0.1|/dump/*", "", "127.0.0.1", "/dump/info", "200", false},
+ {"127.0.0.1|/dump/*", "", "127.0.0.1", "/dump/test", "200", false},
+
+ {"127.0.0.1|/dump/info", "", "127.0.0.1", "/", "403", false},
+ {"127.0.0.1|/dump/info", "", "127.0.0.1", "/dispatch", "403", false},
+ {"127.0.0.1|/dump/info", "", "127.0.0.1", "/dump/info", "200", false},
+ {"127.0.0.1|/dump/info", "", "127.0.0.1", "/dump/test", "403", false},
+
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/", "403", false},
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dispatch", "403", false},
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dump/info", "200", false},
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dump/test", "200", false},
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dump/fail", "403", false},
+
+ {"127.0.0.0-2|", "", "127.0.0.1", "/", "200", false},
+ {"127.0.0.0-2|", "", "127.0.0.1", "/dump/info", "403", false},
+
+ {"127.0.0.0-2|/", "", "127.0.0.1", "/", "200", false},
+ {"127.0.0.0-2|/", "", "127.0.0.1", "/dispatch", "403", false},
+ {"127.0.0.0-2|/", "", "127.0.0.1", "/dump/info", "403", false},
+
+ {"127.0.0.0-2|/dump/*", "", "127.0.0.1", "/", "403", false},
+ {"127.0.0.0-2|/dump/*", "", "127.0.0.1", "/dispatch", "403", false},
+ {"127.0.0.0-2|/dump/*", "", "127.0.0.1", "/dump/info", "200", false},
+
+ {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/", "403", false},
+ {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/dispatch", "403", false},
+ {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/dump/info", "200", false},
+ {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/dump/test", "403", false},
+
+ {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/", "403", false},
+ {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dispatch", "403", false},
+ {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dump/info", "200", false},
+ {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dump/test", "200", false},
+ {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dump/fail", "403", false},
+
+ // Black list
+ {"", "127.0.0.1", "127.0.0.1", "/", "403", false},
+ {"", "127.0.0.1", "127.0.0.1", "/dispatch", "403", false},
+ {"", "127.0.0.1", "127.0.0.1", "/dump/info", "403", false},
+
+ {"", "127.0.0.1|/", "127.0.0.1", "/", "403", false},
+ {"", "127.0.0.1|/", "127.0.0.1", "/dispatch", "200", false},
+ {"", "127.0.0.1|/", "127.0.0.1", "/dump/info", "200", false},
+
+ {"", "127.0.0.1|/*", "127.0.0.1", "/", "403", false},
+ {"", "127.0.0.1|/*", "127.0.0.1", "/dispatch", "403", false},
+ {"", "127.0.0.1|/*", "127.0.0.1", "/dump/info", "403", false},
+
+ {"", "127.0.0.1|/dump/*", "127.0.0.1", "/", "200", false},
+ {"", "127.0.0.1|/dump/*", "127.0.0.1", "/dispatch", "200", false},
+ {"", "127.0.0.1|/dump/*", "127.0.0.1", "/dump/info", "403", false},
+ {"", "127.0.0.1|/dump/*", "127.0.0.1", "/dump/test", "403", false},
+
+ {"", "127.0.0.1|/dump/info", "127.0.0.1", "/", "200", false},
+ {"", "127.0.0.1|/dump/info", "127.0.0.1", "/dispatch", "200", false},
+ {"", "127.0.0.1|/dump/info", "127.0.0.1", "/dump/info", "403", false},
+ {"", "127.0.0.1|/dump/info", "127.0.0.1", "/dump/test", "200", false},
+
+ {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/", "200", false},
+ {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dispatch", "200", false},
+ {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dump/info", "403", false},
+ {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dump/test", "403", false},
+ {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dump/fail", "200", false},
+
+ {"", "127.0.0.0-2|", "127.0.0.1", "/", "403", false},
+ {"", "127.0.0.0-2|", "127.0.0.1", "/dump/info", "200", false},
+
+ {"", "127.0.0.0-2|/", "127.0.0.1", "/", "403", false},
+ {"", "127.0.0.0-2|/", "127.0.0.1", "/dispatch", "200", false},
+ {"", "127.0.0.0-2|/", "127.0.0.1", "/dump/info", "200", false},
+
+ {"", "127.0.0.0-2|/dump/*", "127.0.0.1", "/", "200", false},
+ {"", "127.0.0.0-2|/dump/*", "127.0.0.1", "/dispatch", "200", false},
+ {"", "127.0.0.0-2|/dump/*", "127.0.0.1", "/dump/info", "403", false},
+
+ {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/", "200", false},
+ {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/dispatch", "200", false},
+ {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/dump/info", "403", false},
+ {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/dump/test", "200", false},
+
+ {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/", "200", false},
+ {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dispatch", "200", false},
+ {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dump/info", "403", false},
+ {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dump/test", "403", false},
+ {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dump/fail", "200", false},
+
+ // Both lists
+ {"127.0.0.1|/dump", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump", "200", false},
+ {"127.0.0.1|/dump", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/info", "403", false},
+ {"127.0.0.1|/dump", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/fail", "403", false},
+
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump", "200", false},
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/info", "200", false},
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/fail", "403", false},
+
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump", "200", false},
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump/info", "200", false},
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump/test", "403", false},
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump/fail", "403", false},
+
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump", "403", false},
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump/info", "200", false},
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump/test", "403", false},
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump/fail", "403", false},
+
+ {"127.0.0.1|/;127.0.0.0-2|/dump/*", "127.0.0.0,1|/dump/fail", "127.0.0.1", "/", "200", false},
+ {"127.0.0.1|/;127.0.0.0-2|/dump/*", "127.0.0.0,1|/dump/fail", "127.0.0.1", "/dump/info", "200", false},
+ {"127.0.0.1|/;127.0.0.0-2|/dump/*", "127.0.0.0,1|/dump/fail", "127.0.0.1", "/dump/fail", "403", false},
+
+ // Different address
+ {"127.0.0.2", "", "127.0.0.1", "/", "403", false},
+ {"127.0.0.2", "", "127.0.0.1", "/dump/info", "403", false},
+
+ {"127.0.0.2|/dump/*", "", "127.0.0.1", "/", "403", false},
+ {"127.0.0.2|/dump/*", "", "127.0.0.1", "/dump/info", "403", false},
+
+ {"127.0.0.2|/dump/info", "", "127.0.0.1", "/", "403", false},
+ {"127.0.0.2|/dump/info", "", "127.0.0.1", "/dump/info", "403", false},
+ {"127.0.0.2|/dump/info", "", "127.0.0.1", "/dump/test", "403", false},
+
+ {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/", "403", false},
+ {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dispatch", "403", false},
+ {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dump/info", "200", false},
+ {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dump/test", "403", false},
+ {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dump/fail", "403", false},
+
+ {"172.0.0.0-255", "", "127.0.0.1", "/", "403", false},
+ {"172.0.0.0-255", "", "127.0.0.1", "/dump/info", "403", false},
+
+ {"172.0.0.0-255|/dump/*;127.0.0.0-255|/dump/*", "", "127.0.0.1", "/", "403", false},
+ {"172.0.0.0-255|/dump/*;127.0.0.0-255|/dump/*", "", "127.0.0.1", "/dispatch", "403", false},
+ {"172.0.0.0-255|/dump/*;127.0.0.0-255|/dump/*", "", "127.0.0.1", "/dump/info", "200", false},
+
+ // Match by path starts with [117]
+ // test cases affected by _whiteListByPath highlighted accordingly
+
+ {"", "", "127.0.0.1", "/", "200", true},
+ {"", "", "127.0.0.1", "/dump/info", "200", true},
+
+ // White list
+ {"127.0.0.1", "", "127.0.0.1", "/", "200", true},
+ {"127.0.0.1", "", "127.0.0.1", "/dispatch", "200", true},
+ {"127.0.0.1", "", "127.0.0.1", "/dump/info", "200", true},
+
+ {"127.0.0.1|/", "", "127.0.0.1", "/", "200", true},
+ {"127.0.0.1|/", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath
+ {"127.0.0.1|/", "", "127.0.0.1", "/dump/info", "200", true}, // _whiteListByPath
+
+ {"127.0.0.1|/*", "", "127.0.0.1", "/", "200", true},
+ {"127.0.0.1|/*", "", "127.0.0.1", "/dispatch", "200", true},
+ {"127.0.0.1|/*", "", "127.0.0.1", "/dump/info", "200", true},
+
+ {"127.0.0.1|/dump/*", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath
+ {"127.0.0.1|/dump/*", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath
+ {"127.0.0.1|/dump/*", "", "127.0.0.1", "/dump/info", "200", true},
+ {"127.0.0.1|/dump/*", "", "127.0.0.1", "/dump/test", "200", true},
+
+ {"127.0.0.1|/dump/info", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath
+ {"127.0.0.1|/dump/info", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath
+ {"127.0.0.1|/dump/info", "", "127.0.0.1", "/dump/info", "200", true},
+ {"127.0.0.1|/dump/info", "", "127.0.0.1", "/dump/test", "200", true}, // _whiteListByPath
+
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dump/info", "200", true},
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dump/test", "200", true},
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dump/fail", "200", true}, // _whiteListByPath
+
+ {"127.0.0.0-2|", "", "127.0.0.1", "/", "200", true},
+ {"127.0.0.0-2|", "", "127.0.0.1", "/dump/info", "200", true},
+
+ {"127.0.0.0-2|/", "", "127.0.0.1", "/", "200", true},
+ {"127.0.0.0-2|/", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath
+ {"127.0.0.0-2|/", "", "127.0.0.1", "/dump/info", "200", true}, // _whiteListByPath
+
+ {"127.0.0.0-2|/dump/*", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath
+ {"127.0.0.0-2|/dump/*", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath
+ {"127.0.0.0-2|/dump/*", "", "127.0.0.1", "/dump/info", "200", true},
+
+ {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath
+ {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath
+ {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/dump/info", "200", true},
+ {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/dump/test", "200", true}, // _whiteListByPath
+
+ {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath
+ {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath
+ {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dump/info", "200", true},
+ {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dump/test", "200", true},
+ {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dump/fail", "200", true}, // _whiteListByPath
+
+ // Black list
+ {"", "127.0.0.1", "127.0.0.1", "/", "403", true},
+ {"", "127.0.0.1", "127.0.0.1", "/dispatch", "403", true},
+ {"", "127.0.0.1", "127.0.0.1", "/dump/info", "403", true},
+
+ {"", "127.0.0.1|/", "127.0.0.1", "/", "403", true},
+ {"", "127.0.0.1|/", "127.0.0.1", "/dispatch", "200", true},
+ {"", "127.0.0.1|/", "127.0.0.1", "/dump/info", "200", true},
+
+ {"", "127.0.0.1|/*", "127.0.0.1", "/", "403", true},
+ {"", "127.0.0.1|/*", "127.0.0.1", "/dispatch", "403", true},
+ {"", "127.0.0.1|/*", "127.0.0.1", "/dump/info", "403", true},
+
+ {"", "127.0.0.1|/dump/*", "127.0.0.1", "/", "200", true},
+ {"", "127.0.0.1|/dump/*", "127.0.0.1", "/dispatch", "200", true},
+ {"", "127.0.0.1|/dump/*", "127.0.0.1", "/dump/info", "403", true},
+ {"", "127.0.0.1|/dump/*", "127.0.0.1", "/dump/test", "403", true},
+
+ {"", "127.0.0.1|/dump/info", "127.0.0.1", "/", "200", true},
+ {"", "127.0.0.1|/dump/info", "127.0.0.1", "/dispatch", "200", true},
+ {"", "127.0.0.1|/dump/info", "127.0.0.1", "/dump/info", "403", true},
+ {"", "127.0.0.1|/dump/info", "127.0.0.1", "/dump/test", "200", true},
+
+ {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/", "200", true},
+ {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dispatch", "200", true},
+ {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dump/info", "403", true},
+ {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dump/test", "403", true},
+ {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dump/fail", "200", true},
+
+ {"", "127.0.0.0-2|", "127.0.0.1", "/", "403", true},
+ {"", "127.0.0.0-2|", "127.0.0.1", "/dump/info", "200", true},
+
+ {"", "127.0.0.0-2|/", "127.0.0.1", "/", "403", true},
+ {"", "127.0.0.0-2|/", "127.0.0.1", "/dispatch", "200", true},
+ {"", "127.0.0.0-2|/", "127.0.0.1", "/dump/info", "200", true},
+
+ {"", "127.0.0.0-2|/dump/*", "127.0.0.1", "/", "200", true},
+ {"", "127.0.0.0-2|/dump/*", "127.0.0.1", "/dispatch", "200", true},
+ {"", "127.0.0.0-2|/dump/*", "127.0.0.1", "/dump/info", "403", true},
+
+ {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/", "200", true},
+ {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/dispatch", "200", true},
+ {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/dump/info", "403", true},
+ {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/dump/test", "200", true},
+
+ {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/", "200", true},
+ {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dispatch", "200", true},
+ {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dump/info", "403", true},
+ {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dump/test", "403", true},
+ {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dump/fail", "200", true},
+
+ // Both lists
+ {"127.0.0.1|/dump", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump", "200", true},
+ {"127.0.0.1|/dump", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/info", "200", true}, // _whiteListByPath
+ {"127.0.0.1|/dump", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/fail", "403", true},
+
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump", "200", true},
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/info", "200", true},
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/fail", "403", true},
+
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump", "200", true},
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump/info", "200", true},
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump/test", "403", true},
+ {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump/fail", "403", true},
+
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump", "200", true},
+ // _whiteListByPath
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump/info", "200", true},
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump/test", "403", true},
+ {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump/fail", "200", true},
+ // _whiteListByPath
+
+ {"127.0.0.1|/;127.0.0.0-2|/dump/*", "127.0.0.0,1|/dump/fail", "127.0.0.1", "/", "200", true},
+ {"127.0.0.1|/;127.0.0.0-2|/dump/*", "127.0.0.0,1|/dump/fail", "127.0.0.1", "/dump/info", "200", true},
+ {"127.0.0.1|/;127.0.0.0-2|/dump/*", "127.0.0.0,1|/dump/fail", "127.0.0.1", "/dump/fail", "403", true},
+
+ // Different address
+ {"127.0.0.2", "", "127.0.0.1", "/", "403", true},
+ {"127.0.0.2", "", "127.0.0.1", "/dump/info", "403", true},
+
+ {"127.0.0.2|/dump/*", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath
+ {"127.0.0.2|/dump/*", "", "127.0.0.1", "/dump/info", "403", true},
+
+ {"127.0.0.2|/dump/info", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath
+ {"127.0.0.2|/dump/info", "", "127.0.0.1", "/dump/info", "403", true},
+ {"127.0.0.2|/dump/info", "", "127.0.0.1", "/dump/test", "200", true}, // _whiteListByPath
+
+ {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath
+ {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath
+ {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dump/info", "200", true},
+ {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dump/test", "403", true},
+ {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dump/fail", "200", true}, // _whiteListByPath
+
+ {"172.0.0.0-255", "", "127.0.0.1", "/", "403", true},
+ {"172.0.0.0-255", "", "127.0.0.1", "/dump/info", "403", true},
+
+ {"172.0.0.0-255|/dump/*;127.0.0.0-255|/dump/*", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath
+ {"172.0.0.0-255|/dump/*;127.0.0.0-255|/dump/*", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath
+ {"172.0.0.0-255|/dump/*;127.0.0.0-255|/dump/*", "", "127.0.0.1", "/dump/info", "200", true},
+ };
+ return Arrays.asList(data).stream().map(Arguments::of);
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetAccessHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetAccessHandlerTest.java
new file mode 100644
index 0000000..47a6ba1
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetAccessHandlerTest.java
@@ -0,0 +1,225 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class InetAccessHandlerTest
+{
+ private static Server _server;
+ private static ServerConnector _connector1;
+ private static ServerConnector _connector2;
+ private static InetAccessHandler _handler;
+
+ @BeforeAll
+ public static void setUp() throws Exception
+ {
+ _server = new Server();
+ _connector1 = new ServerConnector(_server);
+ _connector1.setName("http_connector1");
+ _connector2 = new ServerConnector(_server);
+ _connector2.setName("http_connector2");
+ _server.setConnectors(new Connector[]
+ {_connector1, _connector2});
+
+ _handler = new InetAccessHandler();
+ _handler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(HttpStatus.OK_200);
+ }
+ });
+ _server.setHandler(_handler);
+ _server.start();
+ }
+
+ @AfterAll
+ public static void tearDown() throws Exception
+ {
+ _server.stop();
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testHandler(String include, String exclude, String includeConnectors, String excludeConnectors, String code)
+ throws Exception
+ {
+ _handler.clear();
+ for (String inc : include.split(";", -1))
+ {
+ if (inc.length() > 0)
+ {
+ _handler.include(inc);
+ }
+ }
+ for (String exc : exclude.split(";", -1))
+ {
+ if (exc.length() > 0)
+ {
+ _handler.exclude(exc);
+ }
+ }
+ for (String inc : includeConnectors.split(";", -1))
+ {
+ if (inc.length() > 0)
+ {
+ _handler.includeConnector(inc);
+ }
+ }
+ for (String exc : excludeConnectors.split(";", -1))
+ {
+ if (exc.length() > 0)
+ {
+ _handler.excludeConnector(exc);
+ }
+ }
+
+ List<String> codePerConnector = new ArrayList<>();
+ for (String nextCode : code.split(";", -1))
+ {
+ if (nextCode.length() > 0)
+ {
+ codePerConnector.add(nextCode);
+ }
+ }
+
+ testConnector(_connector1.getLocalPort(), include, exclude, includeConnectors, excludeConnectors, codePerConnector.get(0));
+ testConnector(_connector2.getLocalPort(), include, exclude, includeConnectors, excludeConnectors, codePerConnector.get(1));
+ }
+
+ private void testConnector(int port, String include, String exclude, String includeConnectors, String excludeConnectors, String code) throws IOException
+ {
+ try (Socket socket = new Socket("127.0.0.1", port);)
+ {
+ socket.setSoTimeout(5000);
+
+ HttpTester.Request request = HttpTester.newRequest();
+ request.setMethod("GET");
+ request.setURI("/path");
+ request.setHeader("Host", "127.0.0.1");
+ request.setVersion(HttpVersion.HTTP_1_0);
+
+ ByteBuffer output = request.generate();
+ socket.getOutputStream().write(output.array(), output.arrayOffset() + output.position(), output.remaining());
+ HttpTester.Input input = HttpTester.from(socket.getInputStream());
+ HttpTester.Response response = HttpTester.parseResponse(input);
+ Object[] params = new Object[]
+ {
+ "Request WBHUC", include, exclude, includeConnectors, excludeConnectors, code, "Response",
+ response.getStatus()
+ };
+ assertEquals(Integer.parseInt(code), response.getStatus(), Arrays.deepToString(params));
+ }
+ }
+
+ /**
+ * Data for this test.
+ *
+ * @return Format of data: include;exclude;includeConnectors;excludeConnectors;assertionStatusCodePerConnector
+ */
+ public static Stream<Arguments> data()
+ {
+ Object[][] data = new Object[][]
+ {
+ // Empty lists 1
+ {"", "", "", "", "200;200"},
+
+ // test simple filters
+ {"127.0.0.1", "", "", "", "200;200"},
+ {"127.0.0.1-127.0.0.254", "", "", "", "200;200"},
+ {"192.0.0.1", "", "", "", "403;403"},
+ {"192.0.0.1-192.0.0.254", "", "", "", "403;403"},
+
+ // test includeConnector
+ {"127.0.0.1", "", "http_connector1", "", "200;200"},
+ {"127.0.0.1-127.0.0.254", "", "http_connector1", "", "200;200"},
+ {"192.0.0.1", "", "http_connector1", "", "403;200"},
+ {"192.0.0.1-192.0.0.254", "", "http_connector1", "", "403;200"},
+ {"192.0.0.1", "", "http_connector2", "", "200;403"},
+ {"192.0.0.1-192.0.0.254", "", "http_connector2", "", "200;403"},
+
+ // test includeConnector names where none of them match
+ {"127.0.0.1", "", "nothttp", "", "200;200"},
+ {"127.0.0.1-127.0.0.254", "", "nothttp", "", "200;200"},
+ {"192.0.0.1", "", "nothttp", "", "200;200"},
+ {"192.0.0.1-192.0.0.254", "", "nothttp", "", "200;200"},
+
+ // text excludeConnector
+ {"127.0.0.1", "", "", "http_connector1", "200;200"},
+ {"127.0.0.1-127.0.0.254", "", "", "http_connector1", "200;200"},
+ {"192.0.0.1", "", "", "http_connector1", "200;403"},
+ {"192.0.0.1-192.0.0.254", "", "", "http_connector1", "200;403"},
+ {"192.0.0.1", "", "", "http_connector2", "403;200"},
+ {"192.0.0.1-192.0.0.254", "", "", "http_connector2", "403;200"},
+
+ // test excludeConnector where none of them match.
+ {"127.0.0.1", "", "", "nothttp", "200;200"},
+ {"127.0.0.1-127.0.0.254", "", "", "nothttp", "200;200"},
+ {"192.0.0.1", "", "", "nothttp", "403;403"},
+ {"192.0.0.1-192.0.0.254", "", "", "nothttp", "403;403"},
+
+ // both connectors are excluded
+ {"127.0.0.1", "", "", "http_connector1;http_connector2", "200;200"},
+ {"127.0.0.1-127.0.0.254", "", "", "http_connector1;http_connector2", "200;200"},
+ {"192.0.0.1", "", "", "http_connector1;http_connector2", "200;200"},
+ {"192.0.0.1-192.0.0.254", "", "", "http_connector1;http_connector2", "200;200"},
+
+ // both connectors are included
+ {"127.0.0.1", "", "http_connector1;http_connector2", "", "200;200"},
+ {"127.0.0.1-127.0.0.254", "", "http_connector1;http_connector2", "", "200;200"},
+ {"192.0.0.1", "", "http_connector1;http_connector2", "", "403;403"},
+ {"192.0.0.1-192.0.0.254", "", "http_connector1;http_connector2", "", "403;403"},
+
+ // exclude takes precedence over include
+ {"127.0.0.1", "", "http_connector1;http_connector2", "http_connector1;http_connector2", "200;200"},
+ {"127.0.0.1-127.0.0.254", "", "http_connector1;http_connector2", "http_connector1;http_connector2", "200;200"},
+ {"192.0.0.1", "", "http_connector1;http_connector2", "http_connector1;http_connector2", "200;200"},
+ {"192.0.0.1-192.0.0.254", "", "http_connector1;http_connector2", "http_connector1;http_connector2", "200;200"}
+ };
+ return Arrays.asList(data).stream().map(Arguments::of);
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java
new file mode 100644
index 0000000..e0da617
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java
@@ -0,0 +1,873 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.server.AbstractNCSARequestLog;
+import org.eclipse.jetty.server.CustomRequestLog;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpChannelState;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.RequestLog;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+
+public class NcsaRequestLogTest
+{
+ RequestLog _log;
+ Server _server;
+ LocalConnector _connector;
+ BlockingQueue<String> _entries = new BlockingArrayQueue<>();
+ StacklessLogging stacklessLogging;
+
+ private void setup(String logType) throws Exception
+ {
+ TestRequestLogWriter writer = new TestRequestLogWriter();
+
+ switch (logType)
+ {
+ case "customNCSA":
+ _log = new CustomRequestLog(writer, CustomRequestLog.EXTENDED_NCSA_FORMAT);
+ break;
+ case "NCSA":
+ {
+ AbstractNCSARequestLog logNCSA = new AbstractNCSARequestLog(writer);
+ logNCSA.setExtended(true);
+ _log = logNCSA;
+ break;
+ }
+ default:
+ throw new IllegalStateException("invalid logType");
+ }
+
+ _server = new Server();
+ _connector = new LocalConnector(_server);
+ _server.addConnector(_connector);
+ }
+
+ void testHandlerServerStart() throws Exception
+ {
+ _server.setRequestLog(_log);
+ _server.setHandler(new TestHandler());
+ _server.start();
+ }
+
+ private void startServer() throws Exception
+ {
+ _server.start();
+ }
+
+ private void makeRequest(String requestPath) throws Exception
+ {
+ _connector.getResponse("GET " + requestPath + " HTTP/1.0\r\n\r\n");
+ }
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ stacklessLogging = new StacklessLogging(HttpChannel.class);
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _server.stop();
+ stacklessLogging.close();
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testNotHandled(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse("GET /foo HTTP/1.0\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET /foo HTTP/1.0\" 404 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testRequestLine(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse("GET /foo?data=1 HTTP/1.0\nhost: host:80\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET /foo?data=1 HTTP/1.0\" 200 "));
+
+ _connector.getResponse("GET //bad/foo?data=1 HTTP/1.0\n\n");
+ log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET //bad/foo?data=1 HTTP/1.0\" 200 "));
+
+ _connector.getResponse("GET http://host:80/foo?data=1 HTTP/1.0\n\n");
+ log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET http://host:80/foo?data=1 HTTP/1.0\" 200 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testHTTP10Host(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse(
+ "GET /foo?name=value HTTP/1.0\n" +
+ "Host: servername\n" +
+ "\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET /foo?name=value"));
+ assertThat(log, containsString(" 200 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testHTTP11(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse(
+ "GET /foo?name=value HTTP/1.1\n" +
+ "Host: servername\n" +
+ "\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET /foo?name=value"));
+ assertThat(log, containsString(" 200 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testAbsolute(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse(
+ "GET http://hostname:8888/foo?name=value HTTP/1.1\n" +
+ "Host: servername\n" +
+ "\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET http://hostname:8888/foo?name=value"));
+ assertThat(log, containsString(" 200 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testQuery(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse("GET /foo?name=value HTTP/1.0\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET /foo?name=value"));
+ assertThat(log, containsString(" 200 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testSmallData(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse("GET /foo?data=42 HTTP/1.0\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET /foo?"));
+ assertThat(log, containsString(" 200 42 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testBigData(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse("GET /foo?data=102400 HTTP/1.0\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET /foo?"));
+ assertThat(log, containsString(" 200 102400 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testStatus(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse("GET /foo?status=206 HTTP/1.0\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET /foo?"));
+ assertThat(log, containsString(" 206 0 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testStatusData(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse("GET /foo?status=206&data=42 HTTP/1.0\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET /foo?"));
+ assertThat(log, containsString(" 206 42 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testBadRequest(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse("XXXXXXXXXXXX\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("\"- - -\""));
+ assertThat(log, containsString(" 400 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testBadCharacter(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse("METHOD /f\00o HTTP/1.0\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("\"- - -\""));
+ assertThat(log, containsString(" 400 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testBadVersion(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse("METHOD /foo HTTP/9\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("\"- - -\""));
+ assertThat(log, containsString(" 505 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testLongURI(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ char[] chars = new char[10000];
+ Arrays.fill(chars, 'o');
+ String ooo = new String(chars);
+ _connector.getResponse("METHOD /f" + ooo + " HTTP/1.0\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("\"- - -\""));
+ assertThat(log, containsString(" 414 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testLongHeader(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ char[] chars = new char[10000];
+ Arrays.fill(chars, 'o');
+ String ooo = new String(chars);
+ _connector.getResponse("METHOD /foo HTTP/1.0\name: f+" + ooo + "\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("\"METHOD /foo HTTP/1.0\""));
+ assertThat(log, containsString(" 431 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testBadRequestNoHost(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse("GET /foo HTTP/1.1\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET /foo "));
+ assertThat(log, containsString(" 400 "));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testUseragentWithout(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse("GET http://[:1]/foo HTTP/1.1\nReferer: http://other.site\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET http://[:1]/foo "));
+ assertThat(log, containsString(" 400 50 \"http://other.site\" \"-\""));
+ }
+
+ @ParameterizedTest()
+ @ValueSource(strings = {"customNCSA", "NCSA"})
+ public void testUseragentWith(String logType) throws Exception
+ {
+ setup(logType);
+ testHandlerServerStart();
+
+ _connector.getResponse("GET http://[:1]/foo HTTP/1.1\nReferer: http://other.site\nUser-Agent: Mozilla/5.0 (test)\n\n");
+ String log = _entries.poll(5, TimeUnit.SECONDS);
+ assertThat(log, containsString("GET http://[:1]/foo "));
+ assertThat(log, containsString(" 400 50 \"http://other.site\" \"Mozilla/5.0 (test)\""));
+ }
+
+ // Tests from here use these parameters
+ public static Stream<Arguments> data()
+ {
+ List<Object[]> data = new ArrayList<>();
+
+ for (String logType : Arrays.asList("customNCSA", "NCSA"))
+ {
+ data.add(new Object[]{logType, new NoopHandler(), "/noop", "\"GET /noop HTTP/1.0\" 404"});
+ data.add(new Object[]{logType, new HelloHandler(), "/hello", "\"GET /hello HTTP/1.0\" 200"});
+ data.add(new Object[]{logType, new ResponseSendErrorHandler(), "/sendError", "\"GET /sendError HTTP/1.0\" 599"});
+ data.add(new Object[]{logType, new ServletExceptionHandler(), "/sex", "\"GET /sex HTTP/1.0\" 500"});
+ data.add(new Object[]{logType, new IOExceptionHandler(), "/ioex", "\"GET /ioex HTTP/1.0\" 500"});
+ data.add(new Object[]{logType, new IOExceptionPartialHandler(), "/ioex", "\"GET /ioex HTTP/1.0\" 200"});
+ data.add(new Object[]{logType, new RuntimeExceptionHandler(), "/rtex", "\"GET /rtex HTTP/1.0\" 500"});
+ data.add(new Object[]{logType, new BadMessageHandler(), "/bad", "\"GET /bad HTTP/1.0\" 499"});
+ data.add(new Object[]{logType, new AbortHandler(), "/bad", "\"GET /bad HTTP/1.0\" 500"});
+ data.add(new Object[]{logType, new AbortPartialHandler(), "/bad", "\"GET /bad HTTP/1.0\" 200"});
+ }
+
+ return data.stream().map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testServerRequestLog(String logType, Handler testHandler, String requestPath, String expectedLogEntry) throws Exception
+ {
+ setup(logType);
+ _server.setRequestLog(_log);
+ _server.setHandler(testHandler);
+ startServer();
+ makeRequest(requestPath);
+ assertRequestLog(expectedLogEntry, _log);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testLogHandlerWrapper(String logType, Handler testHandler, String requestPath, String expectedLogEntry) throws Exception
+ {
+ setup(logType);
+ RequestLogHandler handler = new RequestLogHandler();
+ handler.setRequestLog(_log);
+ handler.setHandler(testHandler);
+ _server.setHandler(handler);
+ startServer();
+ makeRequest(requestPath);
+ assertRequestLog(expectedLogEntry, _log);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testLogHandlerCollectionFirst(String logType, Handler testHandler, String requestPath, String expectedLogEntry) throws Exception
+ {
+ setup(logType);
+ RequestLogHandler handler = new RequestLogHandler();
+ handler.setRequestLog(_log);
+ HandlerCollection handlers = new HandlerCollection();
+ handlers.setHandlers(new Handler[]{handler, testHandler});
+ _server.setHandler(handlers);
+ startServer();
+ makeRequest(requestPath);
+ assertRequestLog(expectedLogEntry, _log);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testLogHandlerCollectionLast(String logType, Handler testHandler, String requestPath, String expectedLogEntry) throws Exception
+ {
+ setup(logType);
+ RequestLogHandler handler = new RequestLogHandler();
+ handler.setRequestLog(_log);
+ // This is the old ordering of request handler and it cannot well handle thrown exception
+ Assumptions.assumeTrue(
+ testHandler instanceof NoopHandler ||
+ testHandler instanceof HelloHandler ||
+ testHandler instanceof ResponseSendErrorHandler
+ );
+
+ HandlerCollection handlers = new HandlerCollection();
+ handlers.setHandlers(new Handler[]{testHandler, handler});
+ _server.setHandler(handlers);
+ startServer();
+ makeRequest(requestPath);
+ assertRequestLog(expectedLogEntry, _log);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testErrorHandler(String logType, Handler testHandler, String requestPath, String expectedLogEntry) throws Exception
+ {
+ setup(logType);
+ _server.setRequestLog(_log);
+ AbstractHandler wrapper = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ testHandler.handle(target, baseRequest, request, response);
+ }
+ };
+
+ _server.setHandler(wrapper);
+
+ List<String> errors = new ArrayList<>();
+ ErrorHandler errorHandler = new ErrorHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ errors.add(baseRequest.getRequestURI());
+ super.handle(target, baseRequest, request, response);
+ }
+ };
+ _server.addBean(errorHandler);
+ startServer();
+ makeRequest(requestPath);
+ assertRequestLog(expectedLogEntry, _log);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testOKErrorHandler(String logType, Handler testHandler, String requestPath, String expectedLogEntry) throws Exception
+ {
+ setup(logType);
+ _server.setRequestLog(_log);
+ AbstractHandler wrapper = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ testHandler.handle(target, baseRequest, request, response);
+ }
+ };
+
+ _server.setHandler(wrapper);
+
+ ErrorHandler errorHandler = new OKErrorHandler();
+ _server.addBean(errorHandler);
+ startServer();
+ makeRequest(requestPath);
+
+ // If we abort, we can't write a 200 error page
+ if (!(testHandler instanceof AbortHandler))
+ expectedLogEntry = expectedLogEntry.replaceFirst(" [1-9][0-9][0-9]", " 200");
+ assertRequestLog(expectedLogEntry, _log);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testAsyncDispatch(String logType, Handler testHandler, String requestPath, String expectedLogEntry) throws Exception
+ {
+ setup(logType);
+ _server.setRequestLog(_log);
+ _server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ if (Boolean.TRUE.equals(request.getAttribute("ASYNC")))
+ testHandler.handle(target, baseRequest, request, response);
+ else
+ {
+ request.setAttribute("ASYNC", Boolean.TRUE);
+ AsyncContext ac = request.startAsync();
+ ac.setTimeout(1000);
+ ac.dispatch();
+ baseRequest.setHandled(true);
+ }
+ }
+ });
+ startServer();
+ makeRequest(requestPath);
+
+ assertRequestLog(expectedLogEntry, _log);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testAsyncComplete(String logType, Handler testHandler, String requestPath, String expectedLogEntry) throws Exception
+ {
+ setup(logType);
+ _server.setRequestLog(_log);
+ _server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ if (Boolean.TRUE.equals(request.getAttribute("ASYNC")))
+ testHandler.handle(target, baseRequest, request, response);
+ else
+ {
+ request.setAttribute("ASYNC", Boolean.TRUE);
+ AsyncContext ac = request.startAsync();
+ ac.setTimeout(1000);
+ baseRequest.setHandled(true);
+ _server.getThreadPool().execute(() ->
+ {
+ try
+ {
+ try
+ {
+ while (baseRequest.getHttpChannel().getState().getState() != HttpChannelState.State.WAITING)
+ {
+ Thread.sleep(10);
+ }
+ baseRequest.setHandled(false);
+ testHandler.handle(target, baseRequest, request, response);
+ if (!baseRequest.isHandled())
+ response.sendError(404);
+ }
+ catch (BadMessageException bad)
+ {
+ response.sendError(bad.getCode(), bad.getReason());
+ }
+ catch (Exception e)
+ {
+ response.sendError(500, e.toString());
+ }
+ }
+ catch (IOException | IllegalStateException th)
+ {
+ Log.getLog().ignore(th);
+ }
+ finally
+ {
+ ac.complete();
+ }
+ });
+ }
+ }
+ });
+ startServer();
+ makeRequest(requestPath);
+ assertRequestLog(expectedLogEntry, _log);
+ }
+
+ private void assertRequestLog(final String expectedLogEntry, RequestLog log) throws Exception
+ {
+ String line = _entries.poll(5, TimeUnit.SECONDS);
+ Assertions.assertNotNull(line);
+ assertThat(line, containsString(expectedLogEntry));
+ Assertions.assertTrue(_entries.isEmpty());
+ }
+
+ public static class CaptureLog extends AbstractLifeCycle implements RequestLog
+ {
+ public BlockingQueue<String> log = new BlockingArrayQueue<>();
+
+ @Override
+ public void log(Request request, Response response)
+ {
+ int status = response.getCommittedMetaData().getStatus();
+ log.add(String.format("%s %s %s %03d", request.getMethod(), request.getRequestURI(), request.getProtocol(), status));
+ }
+ }
+
+ private abstract static class AbstractTestHandler extends AbstractHandler
+ {
+ @Override
+ public String toString()
+ {
+ return this.getClass().getSimpleName();
+ }
+ }
+
+ private static class NoopHandler extends AbstractTestHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ }
+ }
+
+ private static class HelloHandler extends AbstractTestHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.setContentType("text/plain");
+ response.getWriter().print("Hello World");
+ if (baseRequest != null)
+ baseRequest.setHandled(true);
+ }
+ }
+
+ private static class ResponseSendErrorHandler extends AbstractTestHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.sendError(599, "expected");
+ if (baseRequest != null)
+ baseRequest.setHandled(true);
+ }
+ }
+
+ private static class ServletExceptionHandler extends AbstractTestHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ throw new ServletException("expected");
+ }
+ }
+
+ private static class IOExceptionHandler extends AbstractTestHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ throw new IOException("expected");
+ }
+ }
+
+ private static class IOExceptionPartialHandler extends AbstractTestHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setContentType("text/plain");
+ response.setContentLength(100);
+ response.getOutputStream().println("You were expecting maybe a ");
+ response.flushBuffer();
+ throw new IOException("expected");
+ }
+ }
+
+ private static class RuntimeExceptionHandler extends AbstractTestHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ throw new RuntimeException("expected");
+ }
+ }
+
+ private static class BadMessageHandler extends AbstractTestHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ throw new BadMessageException(499);
+ }
+ }
+
+ private static class AbortHandler extends AbstractTestHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ BadMessageException bad = new BadMessageException(488);
+ baseRequest.getHttpChannel().abort(bad);
+ throw bad;
+ }
+ }
+
+ private static class AbortPartialHandler extends AbstractTestHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setContentType("text/plain");
+ response.setContentLength(100);
+ response.getOutputStream().println("You were expecting maybe a ");
+ response.flushBuffer();
+ baseRequest.getHttpChannel().abort(new Throwable("bomb"));
+ }
+ }
+
+ public static class OKErrorHandler extends ErrorHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ if (baseRequest.isHandled() || response.isCommitted())
+ {
+ return;
+ }
+
+ // collect error details
+ String reason = (response instanceof Response) ? ((Response)response).getReason() : null;
+ int status = response.getStatus();
+
+ // intentionally set response status to OK (this is a test to see what is actually logged)
+ response.setStatus(200);
+ response.setContentType("text/plain");
+ PrintWriter out = response.getWriter();
+ out.printf("Error %d: %s%n", status, reason);
+ baseRequest.setHandled(true);
+ }
+ }
+
+ class TestRequestLogWriter implements RequestLog.Writer
+ {
+ @Override
+ public void write(String requestEntry)
+ {
+ try
+ {
+ _entries.add(requestEntry);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private class TestHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ String q = request.getQueryString();
+ if (q == null)
+ return;
+
+ baseRequest.setHandled(true);
+ for (String action : q.split("\\&"))
+ {
+ String[] param = action.split("=");
+ String name = param[0];
+ String value = param.length > 1 ? param[1] : null;
+ switch (name)
+ {
+ case "status":
+ {
+ response.setStatus(Integer.parseInt(value));
+ break;
+ }
+
+ case "data":
+ {
+ int data = Integer.parseInt(value);
+ PrintWriter out = response.getWriter();
+
+ int w = 0;
+ while (w < data)
+ {
+ if ((data - w) > 17)
+ {
+ w += 17;
+ out.print("0123456789ABCDEF\n");
+ }
+ else
+ {
+ w++;
+ out.print("\n");
+ }
+ }
+ break;
+ }
+
+ case "throw":
+ {
+ try
+ {
+ throw (Throwable)(Class.forName(value).getDeclaredConstructor().newInstance());
+ }
+ catch (ServletException | IOException | Error | RuntimeException e)
+ {
+ throw e;
+ }
+ catch (Throwable e)
+ {
+ throw new ServletException(e);
+ }
+ }
+ case "flush":
+ {
+ response.flushBuffer();
+ break;
+ }
+
+ case "read":
+ {
+ InputStream in = request.getInputStream();
+ while (in.read() >= 0)
+ {
+ ;
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerRangeTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerRangeTest.java
new file mode 100644
index 0000000..6aea6b1
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerRangeTest.java
@@ -0,0 +1,112 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.resource.Resource;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+@Disabled("Unfixed range bug - Issue #107")
+public class ResourceHandlerRangeTest
+{
+ private static Server server;
+ private static URI serverUri;
+
+ @BeforeAll
+ public static void startServer() throws Exception
+ {
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(0);
+ server.addConnector(connector);
+
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+
+ File dir = MavenTestingUtils.getTargetTestingDir(ResourceHandlerRangeTest.class.getSimpleName());
+ FS.ensureEmpty(dir);
+ File rangeFile = new File(dir, "range.txt");
+ try (FileWriter writer = new FileWriter(rangeFile))
+ {
+ writer.append("0123456789");
+ writer.flush();
+ }
+
+ ContextHandler contextHandler = new ContextHandler();
+ ResourceHandler contentResourceHandler = new ResourceHandler();
+ contextHandler.setBaseResource(Resource.newResource(dir.getAbsolutePath()));
+ contextHandler.setHandler(contentResourceHandler);
+ contextHandler.setContextPath("/");
+
+ contexts.addHandler(contextHandler);
+
+ server.setHandler(contexts);
+ server.start();
+
+ String host = connector.getHost();
+ if (host == null)
+ {
+ host = "localhost";
+ }
+ int port = connector.getLocalPort();
+ serverUri = new URI(String.format("http://%s:%d/", host, port));
+ }
+
+ @AfterAll
+ public static void stopServer() throws Exception
+ {
+ server.stop();
+ }
+
+ @Test
+ public void testGetRange() throws Exception
+ {
+ URI uri = serverUri.resolve("range.txt");
+
+ HttpURLConnection uconn = (HttpURLConnection)uri.toURL().openConnection();
+ uconn.setRequestMethod("GET");
+ uconn.addRequestProperty("Range", "bytes=" + 5 + "-");
+
+ int contentLength = Integer.parseInt(uconn.getHeaderField("Content-Length"));
+
+ String response;
+ try (InputStream is = uconn.getInputStream())
+ {
+ response = IO.toString(is);
+ }
+
+ assertThat("Content Length", contentLength, is(5));
+ assertThat("Response Content", response, is("56789"));
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerTest.java
new file mode 100644
index 0000000..b7b765a
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerTest.java
@@ -0,0 +1,354 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.IO;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static org.eclipse.jetty.http.HttpHeader.CONTENT_LENGTH;
+import static org.eclipse.jetty.http.HttpHeader.CONTENT_TYPE;
+import static org.eclipse.jetty.http.HttpHeader.LAST_MODIFIED;
+import static org.eclipse.jetty.http.HttpHeader.LOCATION;
+import static org.eclipse.jetty.http.HttpHeader.SERVER;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Resource Handler test
+ *
+ * TODO: increase the testing going on here
+ */
+public class ResourceHandlerTest
+{
+ private static String LN = System.getProperty("line.separator");
+ private static Server _server;
+ private static HttpConfiguration _config;
+ private static ServerConnector _connector;
+ private static LocalConnector _local;
+ private static ContextHandler _contextHandler;
+ private static ResourceHandler _resourceHandler;
+
+ @BeforeAll
+ public static void setUp() throws Exception
+ {
+ File dir = MavenTestingUtils.getTargetFile("test-classes/simple");
+ File bigger = new File(dir, "bigger.txt");
+ File big = new File(dir, "big.txt");
+ try (OutputStream out = new FileOutputStream(bigger))
+ {
+ for (int i = 0; i < 100; i++)
+ {
+ try (InputStream in = new FileInputStream(big))
+ {
+ IO.copy(in, out);
+ }
+ }
+ }
+
+ bigger.deleteOnExit();
+
+ // determine how the SCM of choice checked out the big.txt EOL
+ // we can't just use whatever is the OS default.
+ // because, for example, a windows system using git can be configured for EOL handling using
+ // local, remote, file lists, patterns, etc, rendering assumptions about the OS EOL choice
+ // wrong for unit tests.
+ LN = System.getProperty("line.separator");
+ try (BufferedReader reader = Files.newBufferedReader(big.toPath(), StandardCharsets.UTF_8))
+ {
+ // a buffer large enough to capture at least 1 EOL
+ char[] cbuf = new char[128];
+ reader.read(cbuf);
+ String sample = new String(cbuf);
+ if (sample.contains("\r\n"))
+ {
+ LN = "\r\n";
+ }
+ else if (sample.contains("\n\r"))
+ {
+ LN = "\n\r";
+ }
+ else
+ {
+ LN = "\n";
+ }
+ }
+
+ _server = new Server();
+ _config = new HttpConfiguration();
+ _config.setOutputBufferSize(2048);
+ _connector = new ServerConnector(_server, new HttpConnectionFactory(_config));
+
+ _local = new LocalConnector(_server);
+
+ _server.setConnectors(new Connector[]{_connector, _local});
+
+ _resourceHandler = new ResourceHandler();
+
+ _resourceHandler.setResourceBase(MavenTestingUtils.getTargetFile("test-classes/simple").getAbsolutePath());
+ _resourceHandler.setWelcomeFiles(new String[]{"welcome.txt"});
+
+ _contextHandler = new ContextHandler("/resource");
+ _contextHandler.setHandler(_resourceHandler);
+ _server.setHandler(_contextHandler);
+ _server.start();
+ }
+
+ @AfterAll
+ public static void tearDown() throws Exception
+ {
+ _server.stop();
+ }
+
+ @BeforeEach
+ public void before()
+ {
+ _config.setOutputBufferSize(4096);
+ }
+
+ @Test
+ public void testJettyDirRedirect() throws Exception
+ {
+ HttpTester.Response response = HttpTester.parseResponse(
+ _local.getResponse("GET /resource HTTP/1.0\r\n\r\n"));
+ assertThat(response.getStatus(), equalTo(302));
+ assertThat(response.get(LOCATION), containsString("/resource/"));
+ }
+
+ @Test
+ public void testJettyDirListing() throws Exception
+ {
+ HttpTester.Response response = HttpTester.parseResponse(
+ _local.getResponse("GET /resource/ HTTP/1.0\r\n\r\n"));
+ assertThat(response.getStatus(), equalTo(200));
+ assertThat(response.getContent(), containsString("jetty-dir.css"));
+ assertThat(response.getContent(), containsString("Directory: /resource/"));
+ assertThat(response.getContent(), containsString("big.txt"));
+ assertThat(response.getContent(), containsString("bigger.txt"));
+ assertThat(response.getContent(), containsString("directory"));
+ assertThat(response.getContent(), containsString("simple.txt"));
+ }
+
+ @Test
+ public void testHeaders() throws Exception
+ {
+ HttpTester.Response response = HttpTester.parseResponse(
+ _local.getResponse("GET /resource/simple.txt HTTP/1.0\r\n\r\n"));
+ assertThat(response.getStatus(), equalTo(200));
+ assertThat(response.get(CONTENT_TYPE), equalTo("text/plain"));
+ assertThat(response.get(LAST_MODIFIED), Matchers.notNullValue());
+ assertThat(response.get(CONTENT_LENGTH), equalTo("11"));
+ assertThat(response.get(SERVER), containsString("Jetty"));
+ assertThat(response.getContent(), containsString("simple text"));
+ }
+
+ @Test
+ public void testIfModifiedSince() throws Exception
+ {
+ HttpTester.Response response = HttpTester.parseResponse(
+ _local.getResponse("GET /resource/simple.txt HTTP/1.0\r\n\r\n"));
+ assertThat(response.getStatus(), equalTo(200));
+ assertThat(response.get(LAST_MODIFIED), Matchers.notNullValue());
+ assertThat(response.getContent(), containsString("simple text"));
+ String lastModified = response.get(LAST_MODIFIED);
+
+ response = HttpTester.parseResponse(_local.getResponse(
+ "GET /resource/simple.txt HTTP/1.0\r\n" +
+ "If-Modified-Since: " + lastModified + "\r\n" +
+ "\r\n"));
+
+ assertThat(response.getStatus(), equalTo(304));
+ }
+
+ @Test
+ public void testBigFile() throws Exception
+ {
+ _config.setOutputBufferSize(2048);
+
+ HttpTester.Response response = HttpTester.parseResponse(
+ _local.getResponse("GET /resource/big.txt HTTP/1.0\r\n\r\n"));
+ assertThat(response.getStatus(), equalTo(200));
+ assertThat(response.getContent(), startsWith(" 1\tThis is a big file"));
+ assertThat(response.getContent(), endsWith(" 400\tThis is a big file" + LN));
+ }
+
+ @Test
+ public void testBigFileBigBuffer() throws Exception
+ {
+ _config.setOutputBufferSize(16 * 1024);
+
+ HttpTester.Response response = HttpTester.parseResponse(
+ _local.getResponse("GET /resource/big.txt HTTP/1.0\r\n\r\n"));
+ assertThat(response.getStatus(), equalTo(200));
+ assertThat(response.getContent(), startsWith(" 1\tThis is a big file"));
+ assertThat(response.getContent(), endsWith(" 400\tThis is a big file" + LN));
+ }
+
+ @Test
+ public void testBigFileLittleBuffer() throws Exception
+ {
+ _config.setOutputBufferSize(8);
+
+ HttpTester.Response response = HttpTester.parseResponse(
+ _local.getResponse("GET /resource/big.txt HTTP/1.0\r\n\r\n"));
+ assertThat(response.getStatus(), equalTo(200));
+ assertThat(response.getContent(), startsWith(" 1\tThis is a big file"));
+ assertThat(response.getContent(), endsWith(" 400\tThis is a big file" + LN));
+ }
+
+ @Test
+ public void testBigger() throws Exception
+ {
+ try (Socket socket = new Socket("localhost", _connector.getLocalPort());)
+ {
+ socket.getOutputStream().write("GET /resource/bigger.txt HTTP/1.0\n\n".getBytes());
+ Thread.sleep(1000);
+ String response = IO.toString(socket.getInputStream());
+ assertThat(response, Matchers.startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.containsString(" 400\tThis is a big file" + LN + " 1\tThis is a big file"));
+ assertThat(response, Matchers.endsWith(" 400\tThis is a big file" + LN));
+ }
+ }
+
+ @Test
+ public void testWelcome() throws Exception
+ {
+ HttpTester.Response response = HttpTester.parseResponse(
+ _local.getResponse("GET /resource/directory/ HTTP/1.0\r\n\r\n"));
+ assertThat(response.getStatus(), equalTo(200));
+ assertThat(response.getContent(), containsString("Hello"));
+ }
+
+ @Test
+ public void testWelcomeRedirect() throws Exception
+ {
+ try
+ {
+ _resourceHandler.setRedirectWelcome(true);
+ HttpTester.Response response = HttpTester.parseResponse(
+ _local.getResponse("GET /resource/directory/ HTTP/1.0\r\n\r\n"));
+ assertThat(response.getStatus(), equalTo(302));
+ assertThat(response.get(LOCATION), containsString("/resource/directory/welcome.txt"));
+ }
+ finally
+ {
+ _resourceHandler.setRedirectWelcome(false);
+ }
+ }
+
+ @Test
+ @DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review
+ public void testSlowBiggest() throws Exception
+ {
+ _connector.setIdleTimeout(9000);
+
+ File dir = MavenTestingUtils.getTargetFile("test-classes/simple");
+ File biggest = new File(dir, "biggest.txt");
+ try (OutputStream out = new FileOutputStream(biggest))
+ {
+ for (int i = 0; i < 10; i++)
+ {
+ try (InputStream in = new FileInputStream(new File(dir, "bigger.txt")))
+ {
+ IO.copy(in, out);
+ }
+ }
+ out.write("\nTHE END\n".getBytes(StandardCharsets.ISO_8859_1));
+ }
+ biggest.deleteOnExit();
+
+ try (Socket socket = new Socket("localhost", _connector.getLocalPort());
+ OutputStream out = socket.getOutputStream();
+ InputStream in = socket.getInputStream())
+ {
+ socket.getOutputStream().write("GET /resource/biggest.txt HTTP/1.0\n\n".getBytes());
+
+ byte[] array = new byte[102400];
+ ByteBuffer buffer = null;
+ while (true)
+ {
+ Thread.sleep(25);
+ int len = in.read(array);
+ if (len < 0)
+ break;
+ buffer = BufferUtil.toBuffer(array, 0, len);
+ // System.err.println(++i+": "+BufferUtil.toDetailString(buffer));
+ }
+
+ assertEquals('E', buffer.get(buffer.limit() - 4));
+ assertEquals('N', buffer.get(buffer.limit() - 3));
+ assertEquals('D', buffer.get(buffer.limit() - 2));
+ }
+ }
+
+ @Test
+ public void testConditionalGetResponseCommitted() throws Exception
+ {
+ _config.setOutputBufferSize(8);
+ _resourceHandler.setEtags(true);
+
+ HttpTester.Response response = HttpTester.parseResponse(_local.getResponse("GET /resource/big.txt HTTP/1.0\r\n" +
+ "If-Match: \"NO_MATCH\"\r\n" +
+ "\r\n"));
+
+ assertThat(response.getStatus(), equalTo(HttpStatus.PRECONDITION_FAILED_412));
+ }
+
+ @Test
+ public void testConditionalHeadResponseCommitted() throws Exception
+ {
+ _config.setOutputBufferSize(8);
+ _resourceHandler.setEtags(true);
+
+ HttpTester.Response response = HttpTester.parseResponse(_local.getResponse("HEAD /resource/big.txt HTTP/1.0\r\n" +
+ "If-Match: \"NO_MATCH\"\r\n" +
+ "\r\n"));
+
+ assertThat(response.getStatus(), equalTo(HttpStatus.PRECONDITION_FAILED_412));
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ScopedHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ScopedHandlerTest.java
new file mode 100644
index 0000000..5244f73
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ScopedHandlerTest.java
@@ -0,0 +1,204 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Server;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ScopedHandlerTest
+{
+ private StringBuilder _history = new StringBuilder();
+
+ @BeforeEach
+ public void resetHistory()
+ {
+ _history.setLength(0);
+ }
+
+ @Test
+ public void testSingle() throws Exception
+ {
+ TestHandler handler0 = new TestHandler("0");
+ handler0.setServer(new Server());
+ handler0.start();
+ handler0.handle("target", null, null, null);
+ handler0.stop();
+ String history = _history.toString();
+ assertEquals(">S0>W0<W0<S0", history);
+ }
+
+ @Test
+ public void testSimpleDouble() throws Exception
+ {
+ TestHandler handler0 = new TestHandler("0");
+ TestHandler handler1 = new TestHandler("1");
+ handler0.setServer(new Server());
+ handler1.setServer(handler0.getServer());
+ handler0.setHandler(handler1);
+ handler0.start();
+ handler0.handle("target", null, null, null);
+ handler0.stop();
+ String history = _history.toString();
+ assertEquals(">S0>S1>W0>W1<W1<W0<S1<S0", history);
+ }
+
+ @Test
+ public void testSimpleTriple() throws Exception
+ {
+ TestHandler handler0 = new TestHandler("0");
+ TestHandler handler1 = new TestHandler("1");
+ TestHandler handler2 = new TestHandler("2");
+ handler0.setServer(new Server());
+ handler1.setServer(handler0.getServer());
+ handler2.setServer(handler0.getServer());
+ handler0.setHandler(handler1);
+ handler1.setHandler(handler2);
+ handler0.start();
+ handler0.handle("target", null, null, null);
+ handler0.stop();
+ String history = _history.toString();
+ assertEquals(">S0>S1>S2>W0>W1>W2<W2<W1<W0<S2<S1<S0", history);
+ }
+
+ @Test
+ public void testDouble() throws Exception
+ {
+ Request request = new Request(null, null);
+ Response response = new Response(null, null);
+
+ TestHandler handler0 = new TestHandler("0");
+ OtherHandler handlerA = new OtherHandler("A");
+ TestHandler handler1 = new TestHandler("1");
+ OtherHandler handlerB = new OtherHandler("B");
+ handler0.setServer(new Server());
+ handlerA.setServer(handler0.getServer());
+ handler1.setServer(handler0.getServer());
+ handlerB.setServer(handler0.getServer());
+ handler0.setHandler(handlerA);
+ handlerA.setHandler(handler1);
+ handler1.setHandler(handlerB);
+ handler0.start();
+ handler0.handle("target", request, request, response);
+ handler0.stop();
+ String history = _history.toString();
+ assertEquals(">S0>S1>W0>HA>W1>HB<HB<W1<HA<W0<S1<S0", history);
+ }
+
+ @Test
+ public void testTriple() throws Exception
+ {
+ Request request = new Request(null, null);
+ Response response = new Response(null, null);
+
+ TestHandler handler0 = new TestHandler("0");
+ OtherHandler handlerA = new OtherHandler("A");
+ TestHandler handler1 = new TestHandler("1");
+ OtherHandler handlerB = new OtherHandler("B");
+ TestHandler handler2 = new TestHandler("2");
+ OtherHandler handlerC = new OtherHandler("C");
+ handler0.setServer(new Server());
+ handlerA.setServer(handler0.getServer());
+ handler1.setServer(handler0.getServer());
+ handlerB.setServer(handler0.getServer());
+ handler2.setServer(handler0.getServer());
+ handlerC.setServer(handler0.getServer());
+ handler0.setHandler(handlerA);
+ handlerA.setHandler(handler1);
+ handler1.setHandler(handlerB);
+ handlerB.setHandler(handler2);
+ handler2.setHandler(handlerC);
+ handler0.start();
+ handler0.handle("target", request, request, response);
+ handler0.stop();
+ String history = _history.toString();
+ assertEquals(">S0>S1>S2>W0>HA>W1>HB>W2>HC<HC<W2<HB<W1<HA<W0<S2<S1<S0", history);
+ }
+
+ private class TestHandler extends ScopedHandler
+ {
+ private final String _name;
+
+ private TestHandler(String name)
+ {
+ _name = name;
+ }
+
+ @Override
+ public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ try
+ {
+ _history.append(">S").append(_name);
+ super.nextScope(target, baseRequest, request, response);
+ }
+ finally
+ {
+ _history.append("<S").append(_name);
+ }
+ }
+
+ @Override
+ public void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ try
+ {
+ _history.append(">W").append(_name);
+ super.nextHandle(target, baseRequest, request, response);
+ }
+ finally
+ {
+ _history.append("<W").append(_name);
+ }
+ }
+ }
+
+ private class OtherHandler extends HandlerWrapper
+ {
+ private final String _name;
+
+ private OtherHandler(String name)
+ {
+ _name = name;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ try
+ {
+ _history.append(">H").append(_name);
+ super.handle(target, baseRequest, request, response);
+ }
+ finally
+ {
+ _history.append("<H").append(_name);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java
new file mode 100644
index 0000000..b854c71
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java
@@ -0,0 +1,290 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.toolchain.test.IO;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+
+public class SecuredRedirectHandlerTest
+{
+ private static Server server;
+ private static HostnameVerifier origVerifier;
+ private static SSLSocketFactory origSsf;
+ private static URI serverHttpUri;
+ private static URI serverHttpsUri;
+
+ @BeforeAll
+ public static void startServer() throws Exception
+ {
+ // Setup SSL
+ File keystore = MavenTestingUtils.getTestResourceFile("keystore");
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore.getAbsolutePath());
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+ sslContextFactory.setTrustStorePath(keystore.getAbsolutePath());
+ sslContextFactory.setTrustStorePassword("storepwd");
+
+ server = new Server();
+
+ int port = 32080;
+ int securePort = 32443;
+
+ // Setup HTTP Configuration
+ HttpConfiguration httpConf = new HttpConfiguration();
+ httpConf.setSecurePort(securePort);
+ httpConf.setSecureScheme("https");
+
+ ServerConnector httpConnector = new ServerConnector(server, new HttpConnectionFactory(httpConf));
+ httpConnector.setName("unsecured");
+ httpConnector.setPort(port);
+
+ // Setup HTTPS Configuration
+ HttpConfiguration httpsConf = new HttpConfiguration(httpConf);
+ httpsConf.addCustomizer(new SecureRequestCustomizer());
+
+ ServerConnector httpsConnector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, "http/1.1"), new HttpConnectionFactory(httpsConf));
+ httpsConnector.setName("secured");
+ httpsConnector.setPort(securePort);
+
+ // Add connectors
+ server.setConnectors(new Connector[]{httpConnector, httpsConnector});
+
+ // Wire up contexts
+ String[] secureHosts = new String[]{"@secured"};
+
+ ContextHandler test1Context = new ContextHandler();
+ test1Context.setContextPath("/test1");
+ test1Context.setHandler(new HelloHandler("Hello1"));
+ test1Context.setVirtualHosts(secureHosts);
+
+ ContextHandler test2Context = new ContextHandler();
+ test2Context.setContextPath("/test2");
+ test2Context.setHandler(new HelloHandler("Hello2"));
+ test2Context.setVirtualHosts(secureHosts);
+
+ ContextHandler rootContext = new ContextHandler();
+ rootContext.setContextPath("/");
+ rootContext.setHandler(new RootHandler("/test1", "/test2"));
+ rootContext.setVirtualHosts(secureHosts);
+
+ // Wire up context for unsecure handling to only
+ // the named 'unsecured' connector
+ ContextHandler redirectHandler = new ContextHandler();
+ redirectHandler.setContextPath("/");
+ redirectHandler.setHandler(new SecuredRedirectHandler());
+ redirectHandler.setVirtualHosts(new String[]{"@unsecured"});
+
+ // Establish all handlers that have a context
+ ContextHandlerCollection contextHandlers = new ContextHandlerCollection();
+ contextHandlers.setHandlers(new Handler[]{redirectHandler, rootContext, test1Context, test2Context});
+
+ // Create server level handler tree
+ HandlerList handlers = new HandlerList();
+ handlers.addHandler(contextHandlers);
+ handlers.addHandler(new DefaultHandler()); // round things out
+
+ server.setHandler(handlers);
+
+ server.start();
+
+ // calculate serverUri
+ String host = httpConnector.getHost();
+ if (host == null)
+ {
+ host = "localhost";
+ }
+ serverHttpUri = new URI(String.format("http://%s:%d/", host, httpConnector.getLocalPort()));
+ serverHttpsUri = new URI(String.format("https://%s:%d/", host, httpsConnector.getLocalPort()));
+
+ origVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
+ origSsf = HttpsURLConnection.getDefaultSSLSocketFactory();
+
+ HttpsURLConnection.setDefaultHostnameVerifier(new AllowAllVerifier());
+ HttpsURLConnection.setDefaultSSLSocketFactory(sslContextFactory.getSslContext().getSocketFactory());
+ }
+
+ @AfterAll
+ public static void stopServer() throws Exception
+ {
+ HttpsURLConnection.setDefaultSSLSocketFactory(origSsf);
+ HttpsURLConnection.setDefaultHostnameVerifier(origVerifier);
+
+ server.stop();
+ server.join();
+ }
+
+ @Test
+ public void testRedirectUnsecuredRoot() throws Exception
+ {
+ URL url = serverHttpUri.resolve("/").toURL();
+ HttpURLConnection connection = (HttpURLConnection)url.openConnection();
+ connection.setInstanceFollowRedirects(false);
+ connection.setAllowUserInteraction(false);
+ assertThat("response code", connection.getResponseCode(), is(302));
+ assertThat("location header", connection.getHeaderField("Location"), is(serverHttpsUri.resolve("/").toASCIIString()));
+ connection.disconnect();
+ }
+
+ @Test
+ public void testRedirectSecuredRoot() throws Exception
+ {
+ URL url = serverHttpsUri.resolve("/").toURL();
+ HttpURLConnection connection = (HttpURLConnection)url.openConnection();
+ connection.setInstanceFollowRedirects(false);
+ connection.setAllowUserInteraction(false);
+ assertThat("response code", connection.getResponseCode(), is(200));
+ String content = getContent(connection);
+ assertThat("response content", content, containsString("<a href=\"/test1\">"));
+ connection.disconnect();
+ }
+
+ @Test
+ public void testAccessUnsecuredHandler() throws Exception
+ {
+ URL url = serverHttpUri.resolve("/test1").toURL();
+ HttpURLConnection connection = (HttpURLConnection)url.openConnection();
+ connection.setInstanceFollowRedirects(false);
+ connection.setAllowUserInteraction(false);
+ assertThat("response code", connection.getResponseCode(), is(302));
+ assertThat("location header", connection.getHeaderField("Location"), is(serverHttpsUri.resolve("/test1").toASCIIString()));
+ connection.disconnect();
+ }
+
+ @Test
+ public void testAccessUnsecured404() throws Exception
+ {
+ URL url = serverHttpUri.resolve("/nothing/here").toURL();
+ HttpURLConnection connection = (HttpURLConnection)url.openConnection();
+ connection.setInstanceFollowRedirects(false);
+ connection.setAllowUserInteraction(false);
+ assertThat("response code", connection.getResponseCode(), is(302));
+ assertThat("location header", connection.getHeaderField("Location"), is(serverHttpsUri.resolve("/nothing/here").toASCIIString()));
+ connection.disconnect();
+ }
+
+ @Test
+ public void testAccessSecured404() throws Exception
+ {
+ URL url = serverHttpsUri.resolve("/nothing/here").toURL();
+ HttpURLConnection connection = (HttpURLConnection)url.openConnection();
+ connection.setInstanceFollowRedirects(false);
+ connection.setAllowUserInteraction(false);
+ assertThat("response code", connection.getResponseCode(), is(404));
+ connection.disconnect();
+ }
+
+ private String getContent(HttpURLConnection connection) throws IOException
+ {
+ try (InputStream in = connection.getInputStream();
+ InputStreamReader reader = new InputStreamReader(in))
+ {
+ StringWriter writer = new StringWriter();
+ IO.copy(reader, writer);
+ return writer.toString();
+ }
+ }
+
+ public static class HelloHandler extends AbstractHandler
+ {
+ private final String msg;
+
+ public HelloHandler(String msg)
+ {
+ this.msg = msg;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.setContentType("text/plain");
+ response.getWriter().printf("%s%n", msg);
+ baseRequest.setHandled(true);
+ }
+ }
+
+ public static class RootHandler extends AbstractHandler
+ {
+ private final String[] childContexts;
+
+ public RootHandler(String... children)
+ {
+ this.childContexts = children;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (!"/".equals(target))
+ {
+ baseRequest.setHandled(true);
+ response.sendError(404);
+ return;
+ }
+
+ response.setContentType("text/html");
+ PrintWriter out = response.getWriter();
+ out.println("<html>");
+ out.println("<head><title>Contexts</title></head>");
+ out.println("<body>");
+ out.println("<h4>Child Contexts</h4>");
+ out.println("<ul>");
+ for (String child : childContexts)
+ {
+ out.printf("<li><a href=\"%s\">%s</a></li>%n", child, child);
+ }
+ out.println("</ul>");
+ out.println("</body></html>");
+ baseRequest.setHandled(true);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ShutdownHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ShutdownHandlerTest.java
new file mode 100644
index 0000000..5fa4b5d
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ShutdownHandlerTest.java
@@ -0,0 +1,137 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ShutdownHandlerTest
+{
+ private Server server;
+ private ServerConnector connector;
+ private String shutdownToken = "asdlnsldgnklns";
+
+ public void start(HandlerWrapper wrapper) throws Exception
+ {
+ server = new Server();
+ connector = new ServerConnector(server);
+ server.addConnector(connector);
+ Handler shutdown = new ShutdownHandler(shutdownToken);
+ Handler handler = shutdown;
+ if (wrapper != null)
+ {
+ wrapper.setHandler(shutdown);
+ handler = wrapper;
+ }
+ server.setHandler(handler);
+ server.start();
+ }
+
+ @Test
+ public void testShutdownServerWithCorrectTokenAndIP() throws Exception
+ {
+ start(null);
+
+ CountDownLatch stopLatch = new CountDownLatch(1);
+ server.addLifeCycleListener(new AbstractLifeCycle.AbstractLifeCycleListener()
+ {
+ @Override
+ public void lifeCycleStopped(LifeCycle event)
+ {
+ stopLatch.countDown();
+ }
+ });
+
+ HttpTester.Response response = shutdown(shutdownToken);
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+
+ assertTrue(stopLatch.await(5, TimeUnit.SECONDS));
+ assertEquals(AbstractLifeCycle.STOPPED, server.getState());
+ }
+
+ @Test
+ public void testWrongToken() throws Exception
+ {
+ start(null);
+
+ HttpTester.Response response = shutdown("wrongToken");
+ assertEquals(HttpStatus.UNAUTHORIZED_401, response.getStatus());
+
+ Thread.sleep(1000);
+ assertEquals(AbstractLifeCycle.STARTED, server.getState());
+ }
+
+ @Test
+ public void testShutdownRequestNotFromLocalhost() throws Exception
+ {
+ start(new HandlerWrapper()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setRemoteAddr(new InetSocketAddress("192.168.0.1", 12345));
+ super.handle(target, baseRequest, request, response);
+ }
+ });
+
+ HttpTester.Response response = shutdown(shutdownToken);
+ assertEquals(HttpStatus.UNAUTHORIZED_401, response.getStatus());
+
+ Thread.sleep(1000);
+ assertEquals(AbstractLifeCycle.STARTED, server.getState());
+ }
+
+ private HttpTester.Response shutdown(String shutdownToken) throws IOException
+ {
+ try (Socket socket = new Socket("localhost", connector.getLocalPort()))
+ {
+ String request =
+ "POST /shutdown?token=" + shutdownToken + " HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ OutputStream output = socket.getOutputStream();
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ HttpTester.Input input = HttpTester.from(socket.getInputStream());
+ return HttpTester.parseResponse(input);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java
new file mode 100644
index 0000000..9dbc573
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java
@@ -0,0 +1,856 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.servlet.AsyncContext;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.io.ConnectionStatistics;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class StatisticsHandlerTest
+{
+ private Server _server;
+ private ConnectionStatistics _statistics;
+ private LocalConnector _connector;
+ private LatchHandler _latchHandler;
+ private StatisticsHandler _statsHandler;
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ _server = new Server();
+
+ _connector = new LocalConnector(_server);
+ _statistics = new ConnectionStatistics();
+ _connector.addBean(_statistics);
+ _server.addConnector(_connector);
+
+ _latchHandler = new LatchHandler();
+ _statsHandler = new StatisticsHandler();
+
+ _server.setHandler(_latchHandler);
+ _latchHandler.setHandler(_statsHandler);
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ _server.stop();
+ _server.join();
+ }
+
+ @Test
+ public void testRequest() throws Exception
+ {
+ final CyclicBarrier[] barrier = {new CyclicBarrier(2), new CyclicBarrier(2)};
+
+ _statsHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
+ {
+ request.setHandled(true);
+ try
+ {
+ barrier[0].await();
+ barrier[1].await();
+ }
+ catch (Exception x)
+ {
+ Thread.currentThread().interrupt();
+ throw new IOException(x);
+ }
+ }
+ });
+ _server.start();
+
+ String request = "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ _connector.executeRequest(request);
+
+ barrier[0].await();
+
+ assertEquals(1, _statistics.getConnections());
+
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(1, _statsHandler.getRequestsActive());
+ assertEquals(1, _statsHandler.getRequestsActiveMax());
+
+ assertEquals(1, _statsHandler.getDispatched());
+ assertEquals(1, _statsHandler.getDispatchedActive());
+ assertEquals(1, _statsHandler.getDispatchedActiveMax());
+
+ barrier[1].await();
+ assertTrue(_latchHandler.await());
+
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(0, _statsHandler.getRequestsActive());
+ assertEquals(1, _statsHandler.getRequestsActiveMax());
+
+ assertEquals(1, _statsHandler.getDispatched());
+ assertEquals(0, _statsHandler.getDispatchedActive());
+ assertEquals(1, _statsHandler.getDispatchedActiveMax());
+
+ assertEquals(0, _statsHandler.getAsyncRequests());
+ assertEquals(0, _statsHandler.getAsyncDispatches());
+ assertEquals(0, _statsHandler.getExpires());
+ assertEquals(1, _statsHandler.getResponses2xx());
+
+ _latchHandler.reset();
+ barrier[0].reset();
+ barrier[1].reset();
+
+ _connector.executeRequest(request);
+
+ barrier[0].await();
+
+ assertEquals(2, _statistics.getConnections());
+
+ assertEquals(2, _statsHandler.getRequests());
+ assertEquals(1, _statsHandler.getRequestsActive());
+ assertEquals(1, _statsHandler.getRequestsActiveMax());
+
+ assertEquals(2, _statsHandler.getDispatched());
+ assertEquals(1, _statsHandler.getDispatchedActive());
+ assertEquals(1, _statsHandler.getDispatchedActiveMax());
+
+ barrier[1].await();
+ assertTrue(_latchHandler.await());
+
+ assertEquals(2, _statsHandler.getRequests());
+ assertEquals(0, _statsHandler.getRequestsActive());
+ assertEquals(1, _statsHandler.getRequestsActiveMax());
+
+ assertEquals(2, _statsHandler.getDispatched());
+ assertEquals(0, _statsHandler.getDispatchedActive());
+ assertEquals(1, _statsHandler.getDispatchedActiveMax());
+
+ assertEquals(0, _statsHandler.getAsyncRequests());
+ assertEquals(0, _statsHandler.getAsyncDispatches());
+ assertEquals(0, _statsHandler.getExpires());
+ assertEquals(2, _statsHandler.getResponses2xx());
+ }
+
+ @Test
+ public void testTwoRequests() throws Exception
+ {
+ final CyclicBarrier[] barrier = {new CyclicBarrier(3), new CyclicBarrier(3)};
+ _latchHandler.reset(2);
+ _statsHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
+ {
+ request.setHandled(true);
+ try
+ {
+ barrier[0].await();
+ barrier[1].await();
+ }
+ catch (Exception x)
+ {
+ Thread.currentThread().interrupt();
+ throw new IOException(x);
+ }
+ }
+ });
+ _server.start();
+
+ String request = "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+
+ _connector.executeRequest(request);
+ _connector.executeRequest(request);
+
+ barrier[0].await();
+
+ assertEquals(2, _statistics.getConnections());
+
+ assertEquals(2, _statsHandler.getRequests());
+ assertEquals(2, _statsHandler.getRequestsActive());
+ assertEquals(2, _statsHandler.getRequestsActiveMax());
+
+ assertEquals(2, _statsHandler.getDispatched());
+ assertEquals(2, _statsHandler.getDispatchedActive());
+ assertEquals(2, _statsHandler.getDispatchedActiveMax());
+
+ barrier[1].await();
+ assertTrue(_latchHandler.await());
+
+ assertEquals(2, _statsHandler.getRequests());
+ assertEquals(0, _statsHandler.getRequestsActive());
+ assertEquals(2, _statsHandler.getRequestsActiveMax());
+
+ assertEquals(2, _statsHandler.getDispatched());
+ assertEquals(0, _statsHandler.getDispatchedActive());
+ assertEquals(2, _statsHandler.getDispatchedActiveMax());
+
+ assertEquals(0, _statsHandler.getAsyncRequests());
+ assertEquals(0, _statsHandler.getAsyncDispatches());
+ assertEquals(0, _statsHandler.getExpires());
+ assertEquals(2, _statsHandler.getResponses2xx());
+ }
+
+ @Test
+ public void testSuspendResume() throws Exception
+ {
+ final long dispatchTime = 10;
+ final long requestTime = 50;
+ final AtomicReference<AsyncContext> asyncHolder = new AtomicReference<>();
+ final CyclicBarrier[] barrier = {new CyclicBarrier(2), new CyclicBarrier(2), new CyclicBarrier(2)};
+ _statsHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException
+ {
+ request.setHandled(true);
+ try
+ {
+ barrier[0].await();
+
+ Thread.sleep(dispatchTime);
+
+ if (asyncHolder.get() == null)
+ asyncHolder.set(request.startAsync());
+ }
+ catch (Exception x)
+ {
+ throw new ServletException(x);
+ }
+ finally
+ {
+ try
+ {
+ barrier[1].await();
+ }
+ catch (Exception ignored)
+ {
+ }
+ }
+ }
+ });
+ _server.start();
+
+ String request = "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ _connector.executeRequest(request);
+
+ barrier[0].await();
+
+ assertEquals(1, _statistics.getConnections());
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(1, _statsHandler.getRequestsActive());
+ assertEquals(1, _statsHandler.getDispatched());
+ assertEquals(1, _statsHandler.getDispatchedActive());
+
+ barrier[1].await();
+ assertTrue(_latchHandler.await());
+ assertNotNull(asyncHolder.get());
+
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(1, _statsHandler.getRequestsActive());
+ assertEquals(1, _statsHandler.getDispatched());
+ assertEquals(0, _statsHandler.getDispatchedActive());
+
+ _latchHandler.reset();
+ barrier[0].reset();
+ barrier[1].reset();
+
+ Thread.sleep(requestTime);
+
+ asyncHolder.get().addListener(new AsyncListener()
+ {
+ @Override
+ public void onTimeout(AsyncEvent event)
+ {
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event)
+ {
+ }
+
+ @Override
+ public void onError(AsyncEvent event)
+ {
+ }
+
+ @Override
+ public void onComplete(AsyncEvent event)
+ {
+ try
+ {
+ barrier[2].await();
+ }
+ catch (Exception ignored)
+ {
+ }
+ }
+ });
+ asyncHolder.get().dispatch();
+
+ barrier[0].await(); // entered app handler
+
+ assertEquals(1, _statistics.getConnections());
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(1, _statsHandler.getRequestsActive());
+ assertEquals(2, _statsHandler.getDispatched());
+ assertEquals(1, _statsHandler.getDispatchedActive());
+
+ barrier[1].await(); // exiting app handler
+ assertTrue(_latchHandler.await()); // exited stats handler
+ barrier[2].await(); // onComplete called
+
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(0, _statsHandler.getRequestsActive());
+ assertEquals(2, _statsHandler.getDispatched());
+ assertEquals(0, _statsHandler.getDispatchedActive());
+
+ assertEquals(1, _statsHandler.getAsyncRequests());
+ assertEquals(1, _statsHandler.getAsyncDispatches());
+ assertEquals(0, _statsHandler.getExpires());
+ assertEquals(1, _statsHandler.getResponses2xx());
+
+ assertThat(_statsHandler.getRequestTimeTotal(), greaterThanOrEqualTo(requestTime * 3 / 4));
+ assertEquals(_statsHandler.getRequestTimeTotal(), _statsHandler.getRequestTimeMax());
+ assertEquals(_statsHandler.getRequestTimeTotal(), _statsHandler.getRequestTimeMean(), 0.01);
+
+ assertThat(_statsHandler.getDispatchedTimeTotal(), greaterThanOrEqualTo(dispatchTime * 2 * 3 / 4));
+ assertTrue(_statsHandler.getDispatchedTimeMean() + dispatchTime <= _statsHandler.getDispatchedTimeTotal());
+ assertTrue(_statsHandler.getDispatchedTimeMax() + dispatchTime <= _statsHandler.getDispatchedTimeTotal());
+ }
+
+ @Test
+ public void asyncDispatchTest() throws Exception
+ {
+ final AtomicReference<AsyncContext> asyncHolder = new AtomicReference<>();
+ final CyclicBarrier[] barrier = {new CyclicBarrier(2), new CyclicBarrier(2), new CyclicBarrier(2), new CyclicBarrier(2)};
+ _statsHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException
+ {
+ request.setHandled(true);
+ try
+ {
+ if (asyncHolder.get() == null)
+ {
+ barrier[0].await();
+ barrier[1].await();
+ AsyncContext asyncContext = request.startAsync();
+ asyncHolder.set(asyncContext);
+ asyncContext.dispatch();
+ }
+ else
+ {
+ barrier[2].await();
+ barrier[3].await();
+ }
+ }
+ catch (Exception x)
+ {
+ throw new ServletException(x);
+ }
+ }
+ });
+ _server.start();
+
+ String request = "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ _connector.executeRequest(request);
+
+ // Before we have started async we have one active request.
+ barrier[0].await();
+ assertEquals(1, _statistics.getConnections());
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(1, _statsHandler.getRequestsActive());
+ assertEquals(1, _statsHandler.getDispatched());
+ assertEquals(1, _statsHandler.getDispatchedActive());
+ barrier[1].await();
+
+ // After we are async the same request should still be active even though we have async dispatched.
+ barrier[2].await();
+ assertEquals(1, _statistics.getConnections());
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(1, _statsHandler.getRequestsActive());
+ assertEquals(2, _statsHandler.getDispatched());
+ assertEquals(1, _statsHandler.getDispatchedActive());
+ barrier[3].await();
+ }
+
+ @Test
+ public void waitForSuspendedRequestTest() throws Exception
+ {
+ CyclicBarrier barrier = new CyclicBarrier(3);
+ final AtomicReference<AsyncContext> asyncHolder = new AtomicReference<>();
+ final CountDownLatch dispatched = new CountDownLatch(1);
+ _statsHandler.setGracefulShutdownWaitsForRequests(true);
+ _statsHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException
+ {
+ request.setHandled(true);
+
+ try
+ {
+ if (path.contains("async"))
+ {
+ asyncHolder.set(request.startAsync());
+ barrier.await();
+ }
+ else
+ {
+ barrier.await();
+ dispatched.await();
+ }
+ }
+ catch (Exception e)
+ {
+ throw new ServletException(e);
+ }
+ }
+ });
+ _server.start();
+
+ // One request to block while dispatched other will go async.
+ _connector.executeRequest("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ _connector.executeRequest("GET /async HTTP/1.1\r\nHost: localhost\r\n\r\n");
+
+ // Ensure the requests have been dispatched and async started.
+ barrier.await();
+ AsyncContext asyncContext = Objects.requireNonNull(asyncHolder.get());
+
+ // Shutdown should timeout as there are two active requests.
+ Future<Void> shutdown = _statsHandler.shutdown();
+ assertThrows(TimeoutException.class, () -> shutdown.get(1, TimeUnit.SECONDS));
+
+ // When the dispatched thread exits we should still be waiting on the async request.
+ dispatched.countDown();
+ assertThrows(TimeoutException.class, () -> shutdown.get(1, TimeUnit.SECONDS));
+
+ // Shutdown should complete only now the AsyncContext is completed.
+ asyncContext.complete();
+ shutdown.get(5, TimeUnit.MILLISECONDS);
+ }
+
+ @Test
+ public void doNotWaitForSuspendedRequestTest() throws Exception
+ {
+ CyclicBarrier barrier = new CyclicBarrier(3);
+ final AtomicReference<AsyncContext> asyncHolder = new AtomicReference<>();
+ final CountDownLatch dispatched = new CountDownLatch(1);
+ _statsHandler.setGracefulShutdownWaitsForRequests(false);
+ _statsHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException
+ {
+ request.setHandled(true);
+
+ try
+ {
+ if (path.contains("async"))
+ {
+ asyncHolder.set(request.startAsync());
+ barrier.await();
+ }
+ else
+ {
+ barrier.await();
+ dispatched.await();
+ }
+ }
+ catch (Exception e)
+ {
+ throw new ServletException(e);
+ }
+ }
+ });
+ _server.start();
+
+ // One request to block while dispatched other will go async.
+ _connector.executeRequest("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ _connector.executeRequest("GET /async HTTP/1.1\r\nHost: localhost\r\n\r\n");
+
+ // Ensure the requests have been dispatched and async started.
+ barrier.await();
+ assertNotNull(asyncHolder.get());
+
+ // Shutdown should timeout as there is a request dispatched.
+ Future<Void> shutdown = _statsHandler.shutdown();
+ assertThrows(TimeoutException.class, () -> shutdown.get(1, TimeUnit.SECONDS));
+
+ // When the dispatched thread exits we should shutdown even though we have a waiting async request.
+ dispatched.countDown();
+ shutdown.get(5, TimeUnit.MILLISECONDS);
+ }
+
+ @Test
+ public void testSuspendExpire() throws Exception
+ {
+ final long dispatchTime = 10;
+ final long timeout = 100;
+ final AtomicReference<AsyncContext> asyncHolder = new AtomicReference<>();
+ final CyclicBarrier[] barrier = {new CyclicBarrier(2), new CyclicBarrier(2), new CyclicBarrier(2)};
+ _statsHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException
+ {
+ request.setHandled(true);
+ try
+ {
+ barrier[0].await();
+
+ Thread.sleep(dispatchTime);
+
+ if (asyncHolder.get() == null)
+ {
+ AsyncContext async = request.startAsync();
+ asyncHolder.set(async);
+ async.setTimeout(timeout);
+ }
+ }
+ catch (Exception x)
+ {
+ throw new ServletException(x);
+ }
+ finally
+ {
+ try
+ {
+ barrier[1].await();
+ }
+ catch (Exception ignored)
+ {
+ }
+ }
+ }
+ });
+ _server.start();
+
+ String request = "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ _connector.executeRequest(request);
+
+ barrier[0].await();
+
+ assertEquals(1, _statistics.getConnections());
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(1, _statsHandler.getRequestsActive());
+ assertEquals(1, _statsHandler.getDispatched());
+ assertEquals(1, _statsHandler.getDispatchedActive());
+
+ barrier[1].await();
+ assertTrue(_latchHandler.await());
+ assertNotNull(asyncHolder.get());
+ asyncHolder.get().addListener(new AsyncListener()
+ {
+ @Override
+ public void onTimeout(AsyncEvent event)
+ {
+ event.getAsyncContext().complete();
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event)
+ {
+ }
+
+ @Override
+ public void onError(AsyncEvent event)
+ {
+ }
+
+ @Override
+ public void onComplete(AsyncEvent event)
+ {
+ try
+ {
+ barrier[2].await();
+ }
+ catch (Exception ignored)
+ {
+ }
+ }
+ });
+
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(1, _statsHandler.getRequestsActive());
+ assertEquals(1, _statsHandler.getDispatched());
+ assertEquals(0, _statsHandler.getDispatchedActive());
+
+ barrier[2].await();
+
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(0, _statsHandler.getRequestsActive());
+ assertEquals(1, _statsHandler.getDispatched());
+ assertEquals(0, _statsHandler.getDispatchedActive());
+
+ assertEquals(1, _statsHandler.getAsyncRequests());
+ assertEquals(0, _statsHandler.getAsyncDispatches());
+ assertEquals(1, _statsHandler.getExpires());
+ assertEquals(1, _statsHandler.getResponses2xx());
+
+ assertTrue(_statsHandler.getRequestTimeTotal() >= (timeout + dispatchTime) * 3 / 4);
+ assertEquals(_statsHandler.getRequestTimeTotal(), _statsHandler.getRequestTimeMax());
+ assertEquals(_statsHandler.getRequestTimeTotal(), _statsHandler.getRequestTimeMean(), 0.01);
+
+ assertThat(_statsHandler.getDispatchedTimeTotal(), greaterThanOrEqualTo(dispatchTime * 3 / 4));
+ }
+
+ @Test
+ public void testSuspendComplete() throws Exception
+ {
+ final long dispatchTime = 10;
+ final AtomicReference<AsyncContext> asyncHolder = new AtomicReference<>();
+ final CyclicBarrier[] barrier = {new CyclicBarrier(2), new CyclicBarrier(2)};
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ _statsHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException
+ {
+ request.setHandled(true);
+ try
+ {
+ barrier[0].await();
+
+ Thread.sleep(dispatchTime);
+
+ if (asyncHolder.get() == null)
+ {
+ AsyncContext async = request.startAsync();
+ asyncHolder.set(async);
+ }
+ }
+ catch (Exception x)
+ {
+ throw new ServletException(x);
+ }
+ finally
+ {
+ try
+ {
+ barrier[1].await();
+ }
+ catch (Exception ignored)
+ {
+ }
+ }
+ }
+ });
+ _server.start();
+
+ String request = "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ _connector.executeRequest(request);
+
+ barrier[0].await();
+
+ assertEquals(1, _statistics.getConnections());
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(1, _statsHandler.getRequestsActive());
+ assertEquals(1, _statsHandler.getDispatched());
+ assertEquals(1, _statsHandler.getDispatchedActive());
+
+ barrier[1].await();
+ assertTrue(_latchHandler.await());
+ assertNotNull(asyncHolder.get());
+
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(1, _statsHandler.getRequestsActive());
+ assertEquals(1, _statsHandler.getDispatched());
+ assertEquals(0, _statsHandler.getDispatchedActive());
+
+ asyncHolder.get().addListener(new AsyncListener()
+ {
+ @Override
+ public void onTimeout(AsyncEvent event)
+ {
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event)
+ {
+ }
+
+ @Override
+ public void onError(AsyncEvent event)
+ {
+ }
+
+ @Override
+ public void onComplete(AsyncEvent event)
+ {
+ try
+ {
+ latch.countDown();
+ }
+ catch (Exception ignored)
+ {
+ }
+ }
+ });
+ long requestTime = 20;
+ Thread.sleep(requestTime);
+ asyncHolder.get().complete();
+ latch.await();
+
+ assertEquals(1, _statsHandler.getRequests());
+ assertEquals(0, _statsHandler.getRequestsActive());
+ assertEquals(1, _statsHandler.getDispatched());
+ assertEquals(0, _statsHandler.getDispatchedActive());
+
+ assertEquals(1, _statsHandler.getAsyncRequests());
+ assertEquals(0, _statsHandler.getAsyncDispatches());
+ assertEquals(0, _statsHandler.getExpires());
+ assertEquals(1, _statsHandler.getResponses2xx());
+
+ assertTrue(_statsHandler.getRequestTimeTotal() >= (dispatchTime + requestTime) * 3 / 4);
+ assertEquals(_statsHandler.getRequestTimeTotal(), _statsHandler.getRequestTimeMax());
+ assertEquals(_statsHandler.getRequestTimeTotal(), _statsHandler.getRequestTimeMean(), 0.01);
+
+ assertTrue(_statsHandler.getDispatchedTimeTotal() >= dispatchTime * 3 / 4);
+ assertTrue(_statsHandler.getDispatchedTimeTotal() < _statsHandler.getRequestTimeTotal());
+ assertEquals(_statsHandler.getDispatchedTimeTotal(), _statsHandler.getDispatchedTimeMax());
+ assertEquals(_statsHandler.getDispatchedTimeTotal(), _statsHandler.getDispatchedTimeMean(), 0.01);
+ }
+
+ @Test
+ public void testAsyncRequestWithShutdown() throws Exception
+ {
+ long delay = 500;
+ CountDownLatch serverLatch = new CountDownLatch(1);
+ _statsHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ {
+ AsyncContext asyncContext = request.startAsync();
+ asyncContext.setTimeout(0);
+ new Thread(() ->
+ {
+ try
+ {
+ Thread.sleep(delay);
+ asyncContext.complete();
+ }
+ catch (InterruptedException e)
+ {
+ response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
+ asyncContext.complete();
+ }
+ }).start();
+ serverLatch.countDown();
+ }
+ });
+ _server.start();
+
+ String request = "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+ _connector.executeRequest(request);
+
+ assertTrue(serverLatch.await(5, TimeUnit.SECONDS));
+
+ Future<Void> shutdown = _statsHandler.shutdown();
+ assertFalse(shutdown.isDone());
+
+ Thread.sleep(delay / 2);
+ assertFalse(shutdown.isDone());
+
+ Thread.sleep(delay);
+ assertTrue(shutdown.isDone());
+ }
+
+ /**
+ * This handler is external to the statistics handler and it is used to ensure that statistics handler's
+ * handle() is fully executed before asserting its values in the tests, to avoid race conditions with the
+ * tests' code where the test executes but the statistics handler has not finished yet.
+ */
+ private static class LatchHandler extends HandlerWrapper
+ {
+ private volatile CountDownLatch _latch = new CountDownLatch(1);
+
+ @Override
+ public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
+ {
+ final CountDownLatch latch = _latch;
+ try
+ {
+ super.handle(path, request, httpRequest, httpResponse);
+ }
+ finally
+ {
+ latch.countDown();
+ }
+ }
+
+ private void reset()
+ {
+ _latch = new CountDownLatch(1);
+ }
+
+ private void reset(int count)
+ {
+ _latch = new CountDownLatch(count);
+ }
+
+ private boolean await() throws InterruptedException
+ {
+ return _latch.await(10000, TimeUnit.MILLISECONDS);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java
new file mode 100644
index 0000000..cfe40a0
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java
@@ -0,0 +1,245 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.NetworkConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class ThreadLimitHandlerTest
+{
+ private Server _server;
+ private NetworkConnector _connector;
+ private LocalConnector _local;
+
+ @BeforeEach
+ public void before()
+ throws Exception
+ {
+ _server = new Server();
+ _connector = new ServerConnector(_server);
+ _local = new LocalConnector(_server);
+ _server.setConnectors(new Connector[]{_local, _connector});
+ }
+
+ @AfterEach
+ public void after()
+ throws Exception
+ {
+ _server.stop();
+ }
+
+ @Test
+ public void testNoForwardHeaders() throws Exception
+ {
+ AtomicReference<String> last = new AtomicReference<>();
+ ThreadLimitHandler handler = new ThreadLimitHandler(null, false)
+ {
+ @Override
+ protected int getThreadLimit(String ip)
+ {
+ last.set(ip);
+ return super.getThreadLimit(ip);
+ }
+ };
+ handler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(HttpStatus.OK_200);
+ }
+ });
+ _server.setHandler(handler);
+ _server.start();
+
+ last.set(null);
+ _local.getResponse("GET / HTTP/1.0\r\n\r\n");
+ assertThat(last.get(), Matchers.is("0.0.0.0"));
+
+ last.set(null);
+ _local.getResponse("GET / HTTP/1.0\r\nX-Forwarded-For: 1.2.3.4\r\n\r\n");
+ assertThat(last.get(), Matchers.is("0.0.0.0"));
+
+ last.set(null);
+ _local.getResponse("GET / HTTP/1.0\r\nForwarded: for=1.2.3.4\r\n\r\n");
+ assertThat(last.get(), Matchers.is("0.0.0.0"));
+ }
+
+ @Test
+ public void testXForwardForHeaders() throws Exception
+ {
+ AtomicReference<String> last = new AtomicReference<>();
+ ThreadLimitHandler handler = new ThreadLimitHandler("X-Forwarded-For")
+ {
+ @Override
+ protected int getThreadLimit(String ip)
+ {
+ last.set(ip);
+ return super.getThreadLimit(ip);
+ }
+ };
+ _server.setHandler(handler);
+ _server.start();
+
+ last.set(null);
+ _local.getResponse("GET / HTTP/1.0\r\n\r\n");
+ assertThat(last.get(), Matchers.is("0.0.0.0"));
+
+ last.set(null);
+ _local.getResponse("GET / HTTP/1.0\r\nX-Forwarded-For: 1.2.3.4\r\n\r\n");
+ assertThat(last.get(), Matchers.is("1.2.3.4"));
+
+ last.set(null);
+ _local.getResponse("GET / HTTP/1.0\r\nForwarded: for=1.2.3.4\r\n\r\n");
+ assertThat(last.get(), Matchers.is("0.0.0.0"));
+
+ last.set(null);
+ _local.getResponse("GET / HTTP/1.0\r\nX-Forwarded-For: 1.1.1.1\r\nX-Forwarded-For: 6.6.6.6,1.2.3.4\r\nForwarded: for=1.2.3.4\r\n\r\n");
+ assertThat(last.get(), Matchers.is("1.2.3.4"));
+ }
+
+ @Test
+ public void testForwardHeaders() throws Exception
+ {
+ AtomicReference<String> last = new AtomicReference<>();
+ ThreadLimitHandler handler = new ThreadLimitHandler("Forwarded")
+ {
+ @Override
+ protected int getThreadLimit(String ip)
+ {
+ last.set(ip);
+ return super.getThreadLimit(ip);
+ }
+ };
+ _server.setHandler(handler);
+ _server.start();
+
+ last.set(null);
+ _local.getResponse("GET / HTTP/1.0\r\n\r\n");
+ assertThat(last.get(), Matchers.is("0.0.0.0"));
+
+ last.set(null);
+ _local.getResponse("GET / HTTP/1.0\r\nX-Forwarded-For: 1.2.3.4\r\n\r\n");
+ assertThat(last.get(), Matchers.is("0.0.0.0"));
+
+ last.set(null);
+ _local.getResponse("GET / HTTP/1.0\r\nForwarded: for=1.2.3.4\r\n\r\n");
+ assertThat(last.get(), Matchers.is("1.2.3.4"));
+
+ last.set(null);
+ _local.getResponse("GET / HTTP/1.0\r\nX-Forwarded-For: 1.1.1.1\r\nForwarded: for=6.6.6.6; for=1.2.3.4\r\nX-Forwarded-For: 6.6.6.6\r\nForwarded: proto=https\r\n\r\n");
+ assertThat(last.get(), Matchers.is("1.2.3.4"));
+ }
+
+ @Test
+ public void testLimit() throws Exception
+ {
+ ThreadLimitHandler handler = new ThreadLimitHandler("Forwarded");
+
+ handler.setThreadLimit(4);
+
+ AtomicInteger count = new AtomicInteger(0);
+ AtomicInteger total = new AtomicInteger(0);
+ CountDownLatch latch = new CountDownLatch(1);
+ handler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(HttpStatus.OK_200);
+ if ("/other".equals(target))
+ return;
+
+ try
+ {
+ count.incrementAndGet();
+ total.incrementAndGet();
+ latch.await();
+ }
+ catch (InterruptedException e)
+ {
+ throw new ServletException(e);
+ }
+ finally
+ {
+ count.decrementAndGet();
+ }
+ }
+ });
+ _server.setHandler(handler);
+ _server.start();
+
+ Socket[] client = new Socket[10];
+ for (int i = 0; i < client.length; i++)
+ {
+ client[i] = new Socket("127.0.0.1", _connector.getLocalPort());
+ client[i].getOutputStream().write(("GET /" + i + " HTTP/1.0\r\nForwarded: for=1.2.3.4\r\n\r\n").getBytes());
+ client[i].getOutputStream().flush();
+ }
+
+ long wait = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+ while (count.get() < 4 && System.nanoTime() < wait)
+ {
+ Thread.sleep(1);
+ }
+ assertThat(count.get(), is(4));
+
+ // check that other requests are not blocked
+ assertThat(_local.getResponse("GET /other HTTP/1.0\r\nForwarded: for=6.6.6.6\r\n\r\n"), Matchers.containsString(" 200 OK"));
+
+ // let the other requests go
+ latch.countDown();
+
+ while (total.get() < 10 && System.nanoTime() < wait)
+ {
+ Thread.sleep(10);
+ }
+ assertThat(total.get(), is(10));
+
+ while (count.get() > 0 && System.nanoTime() < wait)
+ {
+ Thread.sleep(10);
+ }
+ assertThat(count.get(), is(0));
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/resource/RangeWriterTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/resource/RangeWriterTest.java
new file mode 100644
index 0000000..595a090
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/resource/RangeWriterTest.java
@@ -0,0 +1,199 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.resource;
+
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.resource.PathResource;
+import org.eclipse.jetty.util.resource.Resource;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class RangeWriterTest
+{
+ public static final String DATA = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWZYZ!@#$%^&*()_+/.,[]";
+ private static FileSystem zipfs;
+
+ @AfterAll
+ public static void closeZipFs() throws IOException
+ {
+ if (zipfs != null)
+ {
+ zipfs.close();
+ }
+ }
+
+ public static Path initDataFile() throws IOException
+ {
+ Path testDir = MavenTestingUtils.getTargetTestingPath(RangeWriterTest.class.getSimpleName());
+ FS.ensureEmpty(testDir);
+
+ Path dataFile = testDir.resolve("data.dat");
+ try (BufferedWriter writer = Files.newBufferedWriter(dataFile, UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))
+ {
+ writer.write(DATA);
+ writer.flush();
+ }
+
+ return dataFile;
+ }
+
+ private static Path initZipFsDataFile() throws URISyntaxException, IOException
+ {
+ Path exampleJar = MavenTestingUtils.getTestResourcePathFile("example.jar");
+
+ URI uri = new URI("jar", exampleJar.toUri().toASCIIString(), null);
+
+ Map<String, Object> env = new HashMap<>();
+ env.put("multi-release", "runtime");
+
+ if (zipfs != null)
+ {
+ // close prior one
+ zipfs.close();
+ }
+
+ zipfs = FileSystems.newFileSystem(uri, env);
+ Path rootPath = zipfs.getRootDirectories().iterator().next();
+ return rootPath.resolve("data.dat");
+ }
+
+ public static Stream<Arguments> impls() throws IOException, URISyntaxException
+ {
+ Resource realFileSystemResource = new PathResource(initDataFile());
+ Resource nonDefaultFileSystemResource = new PathResource(initZipFsDataFile());
+
+ return Stream.of(
+ Arguments.of("Traditional / Direct Buffer", new ByteBufferRangeWriter(BufferUtil.toBuffer(realFileSystemResource, true))),
+ Arguments.of("Traditional / Indirect Buffer", new ByteBufferRangeWriter(BufferUtil.toBuffer(realFileSystemResource, false))),
+ Arguments.of("Traditional / SeekableByteChannel", new SeekableByteChannelRangeWriter(() -> (SeekableByteChannel)realFileSystemResource.getReadableByteChannel())),
+ Arguments.of("Traditional / InputStream", new InputStreamRangeWriter(() -> realFileSystemResource.getInputStream())),
+
+ Arguments.of("Non-Default FS / Direct Buffer", new ByteBufferRangeWriter(BufferUtil.toBuffer(nonDefaultFileSystemResource, true))),
+ Arguments.of("Non-Default FS / Indirect Buffer", new ByteBufferRangeWriter(BufferUtil.toBuffer(nonDefaultFileSystemResource, false))),
+ Arguments.of("Non-Default FS / SeekableByteChannel", new SeekableByteChannelRangeWriter(() -> (SeekableByteChannel)nonDefaultFileSystemResource.getReadableByteChannel())),
+ Arguments.of("Non-Default FS / InputStream", new InputStreamRangeWriter(() -> nonDefaultFileSystemResource.getInputStream()))
+ );
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}")
+ @MethodSource("impls")
+ public void testSimpleRange(String description, RangeWriter rangeWriter) throws IOException
+ {
+ ByteArrayOutputStream outputStream;
+
+ outputStream = new ByteArrayOutputStream();
+ rangeWriter.writeTo(outputStream, 10, 50);
+ assertThat("Range: 10 (len=50)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(10, 60)));
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}")
+ @MethodSource("impls")
+ public void testSameRangeMultipleTimes(String description, RangeWriter rangeWriter) throws IOException
+ {
+ ByteArrayOutputStream outputStream;
+
+ outputStream = new ByteArrayOutputStream();
+ rangeWriter.writeTo(outputStream, 10, 50);
+ assertThat("Range(a): 10 (len=50)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(10, 60)));
+
+ outputStream = new ByteArrayOutputStream();
+ rangeWriter.writeTo(outputStream, 10, 50);
+ assertThat("Range(b): 10 (len=50)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(10, 60)));
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}")
+ @MethodSource("impls")
+ public void testMultipleRangesOrdered(String description, RangeWriter rangeWriter) throws IOException
+ {
+ ByteArrayOutputStream outputStream;
+
+ outputStream = new ByteArrayOutputStream();
+ rangeWriter.writeTo(outputStream, 10, 20);
+ assertThat("Range(a): 10 (len=20)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(10, 10 + 20)));
+
+ outputStream = new ByteArrayOutputStream();
+ rangeWriter.writeTo(outputStream, 35, 10);
+ assertThat("Range(b): 35 (len=10)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(35, 35 + 10)));
+
+ outputStream = new ByteArrayOutputStream();
+ rangeWriter.writeTo(outputStream, 55, 10);
+ assertThat("Range(b): 55 (len=10)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(55, 55 + 10)));
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}")
+ @MethodSource("impls")
+ public void testMultipleRangesOverlapping(String description, RangeWriter rangeWriter) throws IOException
+ {
+ ByteArrayOutputStream outputStream;
+
+ outputStream = new ByteArrayOutputStream();
+ rangeWriter.writeTo(outputStream, 10, 20);
+ assertThat("Range(a): 10 (len=20)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(10, 10 + 20)));
+
+ outputStream = new ByteArrayOutputStream();
+ rangeWriter.writeTo(outputStream, 15, 20);
+ assertThat("Range(b): 15 (len=20)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(15, 15 + 20)));
+
+ outputStream = new ByteArrayOutputStream();
+ rangeWriter.writeTo(outputStream, 20, 20);
+ assertThat("Range(b): 20 (len=20)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(20, 20 + 20)));
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}")
+ @MethodSource("impls")
+ public void testMultipleRangesReverseOrder(String description, RangeWriter rangeWriter) throws IOException
+ {
+ ByteArrayOutputStream outputStream;
+
+ outputStream = new ByteArrayOutputStream();
+ rangeWriter.writeTo(outputStream, 55, 10);
+ assertThat("Range(b): 55 (len=10)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(55, 55 + 10)));
+
+ outputStream = new ByteArrayOutputStream();
+ rangeWriter.writeTo(outputStream, 35, 10);
+ assertThat("Range(b): 35 (len=10)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(35, 35 + 10)));
+
+ outputStream = new ByteArrayOutputStream();
+ rangeWriter.writeTo(outputStream, 10, 20);
+ assertThat("Range(a): 10 (len=20)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(10, 10 + 20)));
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/session/HouseKeeperTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/session/HouseKeeperTest.java
new file mode 100644
index 0000000..b2292ea
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/session/HouseKeeperTest.java
@@ -0,0 +1,135 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * HouseKeeperTest
+ */
+public class HouseKeeperTest
+{
+ public class TestHouseKeeper extends HouseKeeper
+ {
+ public Scheduler getScheduler()
+ {
+ return _scheduler;
+ }
+
+ public Scheduler.Task getTask()
+ {
+ return _task;
+ }
+
+ public Runner getRunner()
+ {
+ return _runner;
+ }
+
+ public boolean isOwnScheduler()
+ {
+ return _ownScheduler;
+ }
+ }
+
+ public class TestSessionIdManager extends DefaultSessionIdManager
+ {
+ public TestSessionIdManager(Server server)
+ {
+ super(server);
+ }
+
+ @Override
+ public Set<SessionHandler> getSessionHandlers()
+ {
+ return Collections.singleton(new SessionHandler());
+ }
+ }
+
+ @Test
+ public void testHouseKeeper() throws Exception
+ {
+ HouseKeeper t = new TestHouseKeeper();
+ assertThrows(IllegalStateException.class, () -> t.start());
+
+ TestHouseKeeper hk = new TestHouseKeeper();
+ hk.setSessionIdManager(new TestSessionIdManager(new Server()));
+ hk.setIntervalSec(-1);
+ hk.start(); //no scavenging
+
+ //check that the housekeeper isn't running
+ assertNull(hk.getRunner());
+ assertNull(hk.getTask());
+ assertNull(hk.getScheduler());
+ assertFalse(hk.isOwnScheduler());
+ hk.stop();
+ assertNull(hk.getRunner());
+ assertNull(hk.getTask());
+ assertNull(hk.getScheduler());
+ assertFalse(hk.isOwnScheduler());
+
+ //set the interval but don't start it
+ hk.setIntervalSec(10000);
+ assertNull(hk.getRunner());
+ assertNull(hk.getTask());
+ assertNull(hk.getScheduler());
+ assertFalse(hk.isOwnScheduler());
+
+ //now start it
+ hk.start();
+ assertNotNull(hk.getRunner());
+ assertNotNull(hk.getTask());
+ assertNotNull(hk.getScheduler());
+ assertTrue(hk.isOwnScheduler());
+
+ //stop it
+ hk.stop();
+ assertNull(hk.getRunner());
+ assertNull(hk.getTask());
+ assertNull(hk.getScheduler());
+ assertFalse(hk.isOwnScheduler());
+
+ //start it, but set a different interval after start
+ hk.start();
+ Scheduler.Task oldTask = hk.getTask();
+ hk.setIntervalSec(50000);
+ assertTrue(hk.getIntervalSec() >= 50000);
+ assertNotNull(hk.getRunner());
+ assertNotNull(hk.getTask());
+ //Note: it would be nice to test if the old task was
+ //cancelled, but the Scheduler.Task interface does not
+ //provide that functionality.
+ assertNotSame(oldTask, hk.getTask());
+ assertNotNull(hk.getScheduler());
+ assertTrue(hk.isOwnScheduler());
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionCookieTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionCookieTest.java
new file mode 100644
index 0000000..7b77e72
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionCookieTest.java
@@ -0,0 +1,169 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import javax.servlet.SessionCookieConfig;
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.http.HttpCookie;
+import org.eclipse.jetty.server.Server;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * SessionCookieTest
+ */
+public class SessionCookieTest
+{
+
+ public class MockSessionCache extends AbstractSessionCache
+ {
+
+ public MockSessionCache(SessionHandler manager)
+ {
+ super(manager);
+ }
+
+ @Override
+ public void shutdown()
+ {
+ }
+
+ @Override
+ public Session newSession(SessionData data)
+ {
+ return null;
+ }
+
+ @Override
+ public Session doGet(String key)
+ {
+ return null;
+ }
+
+ @Override
+ public Session doPutIfAbsent(String key, Session session)
+ {
+ return null;
+ }
+
+ @Override
+ public Session doDelete(String key)
+ {
+ return null;
+ }
+
+ @Override
+ public boolean doReplace(String id, Session oldValue, Session newValue)
+ {
+ return false;
+ }
+
+ @Override
+ public Session newSession(HttpServletRequest request, SessionData data)
+ {
+ return null;
+ }
+
+ @Override
+ protected Session doComputeIfAbsent(String id, Function<String, Session> mappingFunction)
+ {
+ return mappingFunction.apply(id);
+ }
+ }
+
+ public class MockSessionIdManager extends DefaultSessionIdManager
+ {
+ public MockSessionIdManager(Server server)
+ {
+ super(server);
+ }
+
+ @Override
+ public boolean isIdInUse(String id)
+ {
+ return false;
+ }
+
+ @Override
+ public void expireAll(String id)
+ {
+
+ }
+
+ @Override
+ public String renewSessionId(String oldClusterId, String oldNodeId, HttpServletRequest request)
+ {
+ return "";
+ }
+ }
+
+ @Test
+ public void testSecureSessionCookie() throws Exception
+ {
+ Server server = new Server();
+ MockSessionIdManager idMgr = new MockSessionIdManager(server);
+ idMgr.setWorkerName("node1");
+ SessionHandler mgr = new SessionHandler();
+ MockSessionCache cache = new MockSessionCache(mgr);
+ cache.setSessionDataStore(new NullSessionDataStore());
+ mgr.setSessionCache(cache);
+ mgr.setSessionIdManager(idMgr);
+
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+
+ Session session = new Session(mgr, new SessionData("123", "_foo", "0.0.0.0", now, now, now, 30));
+
+ SessionCookieConfig sessionCookieConfig = mgr.getSessionCookieConfig();
+ sessionCookieConfig.setSecure(true);
+
+ //sessionCookieConfig.secure == true, always mark cookie as secure, irrespective of if requestIsSecure
+ HttpCookie cookie = mgr.getSessionCookie(session, "/foo", true);
+ assertTrue(cookie.isSecure());
+ //sessionCookieConfig.secure == true, always mark cookie as secure, irrespective of if requestIsSecure
+ cookie = mgr.getSessionCookie(session, "/foo", false);
+ assertTrue(cookie.isSecure());
+
+ //sessionCookieConfig.secure==false, setSecureRequestOnly==true, requestIsSecure==true
+ //cookie should be secure: see SessionCookieConfig.setSecure() javadoc
+ sessionCookieConfig.setSecure(false);
+ cookie = mgr.getSessionCookie(session, "/foo", true);
+ assertTrue(cookie.isSecure());
+
+ //sessionCookieConfig.secure=false, setSecureRequestOnly==true, requestIsSecure==false
+ //cookie is not secure: see SessionCookieConfig.setSecure() javadoc
+ cookie = mgr.getSessionCookie(session, "/foo", false);
+ assertFalse(cookie.isSecure());
+
+ //sessionCookieConfig.secure=false, setSecureRequestOnly==false, requestIsSecure==false
+ //cookie is not secure: not a secure request
+ mgr.setSecureRequestOnly(false);
+ cookie = mgr.getSessionCookie(session, "/foo", false);
+ assertFalse(cookie.isSecure());
+
+ //sessionCookieConfig.secure=false, setSecureRequestOnly==false, requestIsSecure==true
+ //cookie is not secure: not on secured requests and request is secure
+ cookie = mgr.getSessionCookie(session, "/foo", true);
+ assertFalse(cookie.isSecure());
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java
new file mode 100644
index 0000000..db26baa
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/session/SessionHandlerTest.java
@@ -0,0 +1,41 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import javax.servlet.SessionTrackingMode;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class SessionHandlerTest
+{
+ @Test
+ public void testSessionTrackingMode()
+ {
+ SessionHandler sessionHandler = new SessionHandler();
+ sessionHandler.setSessionTrackingModes(new HashSet<>(Arrays.asList(SessionTrackingMode.COOKIE, SessionTrackingMode.URL)));
+ sessionHandler.setSessionTrackingModes(Collections.singleton(SessionTrackingMode.SSL));
+ assertThrows(IllegalArgumentException.class, () ->
+ sessionHandler.setSessionTrackingModes(new HashSet<>(Arrays.asList(SessionTrackingMode.SSL, SessionTrackingMode.URL))));
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLCloseTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLCloseTest.java
new file mode 100644
index 0000000..9d5f49f
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLCloseTest.java
@@ -0,0 +1,125 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.ssl;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import javax.net.ssl.SSLContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.jupiter.api.Test;
+
+public class SSLCloseTest
+{
+ @Test
+ public void testClose() throws Exception
+ {
+ File keystore = MavenTestingUtils.getTestResourceFile("keystore");
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStoreResource(Resource.newResource(keystore));
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server, sslContextFactory);
+ connector.setPort(0);
+
+ server.addConnector(connector);
+ server.setHandler(new WriteHandler());
+ server.start();
+
+ SSLContext ctx = SSLContext.getInstance("TLSv1.2");
+ ctx.init(null, SslContextFactory.TRUST_ALL_CERTS, new java.security.SecureRandom());
+
+ int port = connector.getLocalPort();
+
+ Socket socket = ctx.getSocketFactory().createSocket("localhost", port);
+ OutputStream os = socket.getOutputStream();
+
+ os.write((
+ "GET /test HTTP/1.1\r\n" +
+ "Host:test\r\n" +
+ "Connection:close\r\n\r\n").getBytes());
+ os.flush();
+
+ BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+
+ String line;
+ while ((line = in.readLine()) != null)
+ {
+ if (line.trim().length() == 0)
+ break;
+ }
+
+ Thread.sleep(2000);
+
+ while (in.readLine() != null)
+ {
+ Thread.yield();
+ }
+ }
+
+ private static class WriteHandler extends AbstractHandler
+ {
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ try
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.setHeader("test", "value");
+
+ OutputStream out = response.getOutputStream();
+
+ String data = "Now is the time for all good men to come to the aid of the party.\n";
+ data += "How now brown cow.\n";
+ data += "The quick brown fox jumped over the lazy dog.\n";
+ // data=data+data+data+data+data+data+data+data+data+data+data+data+data;
+ // data=data+data+data+data+data+data+data+data+data+data+data+data+data;
+ data = data + data + data + data;
+ byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
+
+ for (int i = 0; i < 2; i++)
+ {
+ // System.err.println("Write "+i+" "+bytes.length);
+ out.write(bytes);
+ }
+ }
+ catch (Throwable e)
+ {
+ e.printStackTrace();
+ throw new ServletException(e);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java
new file mode 100644
index 0000000..28cee27
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java
@@ -0,0 +1,491 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+// JettyTest.java --
+//
+// Junit test that shows the Jetty SSL bug.
+//
+
+package org.eclipse.jetty.server.ssl;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.HttpURLConnection;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.net.SocketFactory;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.ssl.SslConnection;
+import org.eclipse.jetty.server.ConnectionFactory;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ *
+ */
+public class SSLEngineTest
+{
+ // Useful constants
+ private static final String HELLO_WORLD = "Hello world. The quick brown fox jumped over the lazy dog. How now brown cow. The rain in spain falls mainly on the plain.\n";
+ private static final String JETTY_VERSION = Server.getVersion();
+ private static final String PROTOCOL_VERSION = "2.0";
+
+ /**
+ * The request.
+ */
+ private static final String REQUEST0_HEADER = "POST /r0 HTTP/1.1\n" + "Host: localhost\n" + "Content-Type: text/xml\n" + "Content-Length: ";
+ private static final String REQUEST1_HEADER = "POST /r1 HTTP/1.1\n" + "Host: localhost\n" + "Content-Type: text/xml\n" + "Connection: close\n" + "Content-Length: ";
+ private static final String REQUEST_CONTENT =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<requests xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
+ " xsi:noNamespaceSchemaLocation=\"commander.xsd\" version=\"" + PROTOCOL_VERSION + "\">\n" +
+ "</requests>";
+
+ private static final String REQUEST0 = REQUEST0_HEADER + REQUEST_CONTENT.getBytes().length + "\n\n" + REQUEST_CONTENT;
+ private static final String REQUEST1 = REQUEST1_HEADER + REQUEST_CONTENT.getBytes().length + "\n\n" + REQUEST_CONTENT;
+
+ /**
+ * The expected response.
+ */
+ private static final String RESPONSE0 = "HTTP/1.1 200 OK\n" +
+ "Content-Length: " + HELLO_WORLD.length() + "\n" +
+ "Server: Jetty(" + JETTY_VERSION + ")\n" +
+ '\n' +
+ HELLO_WORLD;
+
+ private static final String RESPONSE1 = "HTTP/1.1 200 OK\n" +
+ "Connection: close\n" +
+ "Content-Length: " + HELLO_WORLD.length() + "\n" +
+ "Server: Jetty(" + JETTY_VERSION + ")\n" +
+ '\n' +
+ HELLO_WORLD;
+
+ private static final int BODY_SIZE = 300;
+
+ private Server server;
+ private ServerConnector connector;
+ private SslContextFactory.Server sslContextFactory;
+
+ @BeforeEach
+ public void startServer() throws Exception
+ {
+ String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
+ sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ server = new Server();
+ HttpConnectionFactory http = new HttpConnectionFactory();
+ http.setInputBufferSize(512);
+ http.getHttpConfiguration().setRequestHeaderSize(512);
+ connector = new ServerConnector(server, sslContextFactory, http);
+ connector.setPort(0);
+ connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setSendDateHeader(false);
+
+ server.addConnector(connector);
+ }
+
+ @AfterEach
+ public void stopServer() throws Exception
+ {
+ server.stop();
+ server.join();
+ }
+
+ @Test
+ public void testHelloWorld() throws Exception
+ {
+ server.setHandler(new HelloWorldHandler());
+ server.start();
+
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ ctx.init(null, SslContextFactory.TRUST_ALL_CERTS, new java.security.SecureRandom());
+
+ int port = connector.getLocalPort();
+
+ Socket client = ctx.getSocketFactory().createSocket("localhost", port);
+ OutputStream os = client.getOutputStream();
+
+ String request =
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost:" + port + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+
+ os.write(request.getBytes());
+ os.flush();
+
+ String response = IO.toString(client.getInputStream());
+
+ assertThat(response, Matchers.containsString("200 OK"));
+ assertThat(response, Matchers.containsString(HELLO_WORLD));
+ }
+
+ @Test
+ public void testBigResponse() throws Exception
+ {
+ server.setHandler(new HelloWorldHandler());
+ server.start();
+
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ ctx.init(null, SslContextFactory.TRUST_ALL_CERTS, new java.security.SecureRandom());
+
+ int port = connector.getLocalPort();
+
+ Socket client = ctx.getSocketFactory().createSocket("localhost", port);
+ OutputStream os = client.getOutputStream();
+
+ String request =
+ "GET /?dump=102400 HTTP/1.1\r\n" +
+ "Host: localhost:" + port + "\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+
+ os.write(request.getBytes());
+ os.flush();
+
+ String response = IO.toString(client.getInputStream());
+
+ assertThat(response.length(), greaterThan(102400));
+ }
+
+ @Test
+ public void testInvalidLargeTLSFrame() throws Exception
+ {
+ AtomicLong unwraps = new AtomicLong();
+ ConnectionFactory http = connector.getConnectionFactory(HttpConnectionFactory.class);
+ ConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, http.getProtocol())
+ {
+ @Override
+ protected SslConnection newSslConnection(Connector connector, EndPoint endPoint, SSLEngine engine)
+ {
+ return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(), endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption())
+ {
+ @Override
+ protected SSLEngineResult unwrap(SSLEngine sslEngine, ByteBuffer input, ByteBuffer output) throws SSLException
+ {
+ unwraps.incrementAndGet();
+ return super.unwrap(sslEngine, input, output);
+ }
+ };
+ }
+ };
+ ServerConnector tlsConnector = new ServerConnector(server, 1, 1, ssl, http);
+ server.addConnector(tlsConnector);
+ server.setHandler(new HelloWorldHandler());
+ server.start();
+
+ // Create raw TLS record.
+ byte[] bytes = new byte[20005];
+ Arrays.fill(bytes, (byte)1);
+
+ bytes[0] = 22; // record type
+ bytes[1] = 3; // major version
+ bytes[2] = 3; // minor version
+ bytes[3] = 78; // record length 2 bytes / 0x4E20 / decimal 20,000
+ bytes[4] = 32; // record length
+ bytes[5] = 1; // message type
+ bytes[6] = 0; // message length 3 bytes / 0x004E17 / decimal 19,991
+ bytes[7] = 78;
+ bytes[8] = 23;
+
+ SocketFactory socketFactory = SocketFactory.getDefault();
+ try (Socket client = socketFactory.createSocket("localhost", tlsConnector.getLocalPort()))
+ {
+ client.getOutputStream().write(bytes);
+
+ // Sleep to see if the server spins.
+ Thread.sleep(1000);
+ assertThat(unwraps.get(), lessThan(128L));
+
+ // Read until -1 or read timeout.
+ client.setSoTimeout(1000);
+ IO.readBytes(client.getInputStream());
+ }
+ }
+
+ @Test
+ public void testRequestJettyHttps() throws Exception
+ {
+ server.setHandler(new HelloWorldHandler());
+ server.start();
+
+ final int loops = 10;
+ final int numConns = 20;
+
+ Socket[] client = new Socket[numConns];
+
+ SSLContext ctx = SSLContext.getInstance("TLSv1.2");
+ ctx.init(null, SslContextFactory.TRUST_ALL_CERTS, new java.security.SecureRandom());
+
+ int port = connector.getLocalPort();
+
+ try
+ {
+ for (int l = 0; l < loops; l++)
+ {
+ // System.err.print('.');
+ try
+ {
+ for (int i = 0; i < numConns; ++i)
+ {
+ // System.err.println("write:"+i);
+ client[i] = ctx.getSocketFactory().createSocket("localhost", port);
+ OutputStream os = client[i].getOutputStream();
+
+ os.write(REQUEST0.getBytes());
+ os.write(REQUEST0.getBytes());
+ os.flush();
+ }
+
+ for (int i = 0; i < numConns; ++i)
+ {
+ // System.err.println("flush:"+i);
+ OutputStream os = client[i].getOutputStream();
+ os.write(REQUEST1.getBytes());
+ os.flush();
+ }
+
+ for (int i = 0; i < numConns; ++i)
+ {
+ // System.err.println("read:"+i);
+ // Read the response.
+ String responses = readResponse(client[i]);
+ // Check the responses
+ assertThat(String.format("responses loop=%d connection=%d", l, i), RESPONSE0 + RESPONSE0 + RESPONSE1, is(responses));
+ }
+ }
+ finally
+ {
+ for (int i = 0; i < numConns; ++i)
+ {
+ if (client[i] != null)
+ {
+ try
+ {
+ assertThat("Client should read EOF", client[i].getInputStream().read(), is(-1));
+ }
+ catch (SocketException e)
+ {
+ // no op
+ }
+ }
+ }
+ }
+ }
+ }
+ finally
+ {
+ // System.err.println();
+ }
+ }
+
+ @Test
+ public void testURLConnectionChunkedPost() throws Exception
+ {
+ StreamHandler handler = new StreamHandler();
+ server.setHandler(handler);
+ server.start();
+
+ SSLContext context = SSLContext.getInstance("SSL");
+ context.init(null, SslContextFactory.TRUST_ALL_CERTS, new java.security.SecureRandom());
+ HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
+
+ URL url = new URL("https://localhost:" + connector.getLocalPort() + "/test");
+
+ HttpURLConnection conn = (HttpURLConnection)url.openConnection();
+ if (conn instanceof HttpsURLConnection)
+ {
+ ((HttpsURLConnection)conn).setHostnameVerifier(new HostnameVerifier()
+ {
+ @Override
+ public boolean verify(String urlHostName, SSLSession session)
+ {
+ return true;
+ }
+ });
+ }
+
+ conn.setConnectTimeout(10000);
+ conn.setReadTimeout(100000);
+ conn.setDoInput(true);
+ conn.setDoOutput(true);
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Content-Type", "text/plain");
+ conn.setChunkedStreamingMode(128);
+ conn.connect();
+ byte[] b = new byte[BODY_SIZE];
+ for (int i = 0; i < BODY_SIZE; i++)
+ {
+ b[i] = 'x';
+ }
+ OutputStream os = conn.getOutputStream();
+ os.write(b);
+ os.flush();
+
+ int len = 0;
+ InputStream is = conn.getInputStream();
+ int bytes = 0;
+ while ((len = is.read(b)) > -1)
+ {
+ bytes += len;
+ }
+ is.close();
+
+ assertEquals(BODY_SIZE, handler.bytes);
+ assertEquals(BODY_SIZE, bytes);
+ }
+
+ /**
+ * Reads entire response from the client. Close the output.
+ *
+ * @param client Open client socket.
+ * @return The response string.
+ * @throws IOException in case of I/O errors
+ */
+ private static String readResponse(Socket client) throws IOException
+ {
+ BufferedReader br = null;
+ StringBuilder sb = new StringBuilder(1000);
+
+ try
+ {
+ client.setSoTimeout(5000);
+ br = new BufferedReader(new InputStreamReader(client.getInputStream()));
+
+ String line;
+
+ while ((line = br.readLine()) != null)
+ {
+ sb.append(line);
+ sb.append('\n');
+ }
+ }
+ catch (SocketTimeoutException e)
+ {
+ System.err.println("Test timedout: " + e.toString());
+ e.printStackTrace(); // added to see if we can get more info from failures on CI
+ }
+ finally
+ {
+ if (br != null)
+ {
+ br.close();
+ }
+ }
+ return sb.toString();
+ }
+
+ private static class HelloWorldHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ // System.err.println("HANDLE "+request.getRequestURI());
+ String sslId = (String)request.getAttribute("javax.servlet.request.ssl_session_id");
+ assertNotNull(sslId);
+
+ if (request.getParameter("dump") != null)
+ {
+ ServletOutputStream out = response.getOutputStream();
+ byte[] buf = new byte[Integer.parseInt(request.getParameter("dump"))];
+ // System.err.println("DUMP "+buf.length);
+ for (int i = 0; i < buf.length; i++)
+ {
+ buf[i] = (byte)('0' + (i % 10));
+ }
+ out.write(buf);
+ out.close();
+ }
+ else
+ {
+ PrintWriter out = response.getWriter();
+ out.print(HELLO_WORLD);
+ out.close();
+ }
+ }
+ }
+
+ private static class StreamHandler extends AbstractHandler
+ {
+ private int bytes = 0;
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.setContentType("text/plain");
+ response.setBufferSize(128);
+ byte[] b = new byte[BODY_SIZE];
+ int len = 0;
+ InputStream is = request.getInputStream();
+ while ((len = is.read(b)) > -1)
+ {
+ bytes += len;
+ }
+
+ OutputStream os = response.getOutputStream();
+ for (int i = 0; i < BODY_SIZE; i++)
+ {
+ b[i] = 'x';
+ }
+ os.write(b);
+ response.flushBuffer();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLReadEOFAfterResponseTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLReadEOFAfterResponseTest.java
new file mode 100644
index 0000000..8c07ac1
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLReadEOFAfterResponseTest.java
@@ -0,0 +1,162 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.ssl;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import javax.net.ssl.SSLContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnJre;
+import org.junit.jupiter.api.condition.JRE;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+// Only in JDK 11 is possible to use SSLSocket.shutdownOutput().
+@DisabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10})
+public class SSLReadEOFAfterResponseTest
+{
+ @Test
+ public void testReadEOFAfterResponse() throws Exception
+ {
+ File keystore = MavenTestingUtils.getTestResourceFile("keystore");
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStoreResource(Resource.newResource(keystore));
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server, sslContextFactory);
+ int idleTimeout = 1000;
+ connector.setIdleTimeout(idleTimeout);
+ server.addConnector(connector);
+
+ String content = "the quick brown fox jumped over the lazy dog";
+ byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
+ server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ // First: read the whole content.
+ InputStream input = request.getInputStream();
+ int length = bytes.length;
+ while (length > 0)
+ {
+ int read = input.read();
+ if (read < 0)
+ throw new IllegalStateException();
+ --length;
+ }
+
+ // Second: write the response.
+ response.setContentLength(bytes.length);
+ response.getOutputStream().write(bytes);
+ response.flushBuffer();
+
+ sleep(idleTimeout / 2);
+
+ // Third, read the EOF.
+ int read = input.read();
+ if (read >= 0)
+ throw new IllegalStateException();
+ }
+ });
+ server.start();
+
+ try
+ {
+ SSLContext sslContext = sslContextFactory.getSslContext();
+ try (Socket client = sslContext.getSocketFactory().createSocket("localhost", connector.getLocalPort()))
+ {
+ client.setSoTimeout(5 * idleTimeout);
+
+ OutputStream output = client.getOutputStream();
+ String request =
+ "POST / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: " + content.length() + "\r\n" +
+ "\r\n";
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.write(bytes);
+ output.flush();
+
+ // Read the response.
+ InputStream input = client.getInputStream();
+ int crlfs = 0;
+ while (true)
+ {
+ int read = input.read();
+ assertThat(read, Matchers.greaterThanOrEqualTo(0));
+ if (read == '\r' || read == '\n')
+ ++crlfs;
+ else
+ crlfs = 0;
+ if (crlfs == 4)
+ break;
+ }
+ for (byte b : bytes)
+ {
+ assertEquals(b, input.read());
+ }
+
+ // Shutdown the output so the server reads the TLS close_notify.
+ client.shutdownOutput();
+ // client.close();
+
+ // The connection should now be idle timed out by the server.
+ int read = input.read();
+ assertEquals(-1, read);
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ private void sleep(long time) throws IOException
+ {
+ try
+ {
+ Thread.sleep(time);
+ }
+ catch (InterruptedException x)
+ {
+ throw new InterruptedIOException();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLSelectChannelConnectorLoadTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLSelectChannelConnectorLoadTest.java
new file mode 100644
index 0000000..9fef677
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLSelectChannelConnectorLoadTest.java
@@ -0,0 +1,353 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.ssl;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.URI;
+import java.security.KeyStore;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.concurrent.Future;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManagerFactory;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class SSLSelectChannelConnectorLoadTest
+{
+ private static Server server;
+ private static ServerConnector connector;
+ private static SSLContext sslContext;
+
+ @BeforeAll
+ public static void startServer() throws Exception
+ {
+ String keystorePath = System.getProperty("basedir", ".") + "/src/test/resources/keystore";
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystorePath);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+ sslContextFactory.setTrustStorePath(keystorePath);
+ sslContextFactory.setTrustStorePassword("storepwd");
+
+ server = new Server();
+ connector = new ServerConnector(server, sslContextFactory);
+ server.addConnector(connector);
+
+ server.setHandler(new EmptyHandler());
+
+ server.start();
+
+ KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+ try (InputStream stream = new FileInputStream(keystorePath))
+ {
+ keystore.load(stream, "storepwd".toCharArray());
+ }
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(keystore);
+ sslContext = SSLContext.getInstance("SSL");
+ sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
+ }
+
+ @AfterAll
+ public static void stopServer() throws Exception
+ {
+ server.stop();
+ server.join();
+ }
+
+ @Test
+ public void testGetURI()
+ {
+ URI uri = server.getURI();
+ assertThat("Server.uri.scheme", uri.getScheme(), is("https"));
+ assertThat("Server.uri.port", uri.getPort(), is(connector.getLocalPort()));
+ assertThat("Server.uri.path", uri.getPath(), is("/"));
+ }
+
+ @Test
+ public void testLongLivedConnections() throws Exception
+ {
+ Worker.totalIterations.set(0);
+
+ int mebiByte = 1048510;
+ int clients = 1;
+ int iterations = 2;
+ ThreadPoolExecutor threadPool = new ThreadPoolExecutor(clients, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
+ threadPool.prestartAllCoreThreads();
+ Worker[] workers = new Worker[clients];
+ Future[] tasks = new Future[clients];
+ for (int i = 0; i < clients; ++i)
+ {
+ workers[i] = new Worker(sslContext, iterations, false, mebiByte, 64 * mebiByte);
+ workers[i].open();
+ tasks[i] = threadPool.submit(workers[i]);
+ }
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ while (true)
+ {
+ Thread.sleep(1000);
+ boolean done = true;
+ for (Future task : tasks)
+ {
+ done &= task.isDone();
+ }
+ //System.err.print("\rIterations: " + Worker.totalIterations.get() + "/" + clients * iterations);
+ if (done)
+ break;
+ }
+ long end = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ //System.err.println();
+ //System.err.println("Elapsed time: " + TimeUnit.MILLISECONDS.toSeconds(end - start) + "s");
+
+ for (Worker worker : workers)
+ {
+ worker.close();
+ }
+
+ threadPool.shutdown();
+
+ // Throw exceptions if any
+ for (Future task : tasks)
+ {
+ task.get();
+ }
+
+ // Keep the JVM running
+// new CountDownLatch(1).await();
+ }
+
+ @Test
+ public void testShortLivedConnections() throws Exception
+ {
+ Worker.totalIterations.set(0);
+
+ int mebiByte = 1048510;
+ int clients = 1;
+ int iterations = 2;
+ ThreadPoolExecutor threadPool = new ThreadPoolExecutor(clients, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
+ threadPool.prestartAllCoreThreads();
+ Worker[] workers = new Worker[clients];
+ Future[] tasks = new Future[clients];
+ for (int i = 0; i < clients; ++i)
+ {
+ workers[i] = new Worker(sslContext, iterations, true, mebiByte, 64 * mebiByte);
+ tasks[i] = threadPool.submit(workers[i]);
+ }
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ while (true)
+ {
+ Thread.sleep(1000);
+ boolean done = true;
+ for (Future task : tasks)
+ {
+ done &= task.isDone();
+ }
+ // System.err.print("\rIterations: " + Worker.totalIterations.get() + "/" + clients * iterations);
+ if (done)
+ break;
+ }
+ long end = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ // System.err.println();
+ // System.err.println("Elapsed time: " + TimeUnit.MILLISECONDS.toSeconds(end - start) + "s");
+
+ threadPool.shutdown();
+
+ // Throw exceptions if any
+ for (Future task : tasks)
+ {
+ task.get();
+ }
+
+ // Keep the JVM running
+// new CountDownLatch(1).await();
+ }
+
+ private static class Worker implements Runnable
+ {
+ private static final AtomicLong totalIterations = new AtomicLong();
+ private final SSLContext sslContext;
+ private volatile SSLSocket sslSocket;
+ private final int iterations;
+ private final boolean closeConnection;
+ private final int minContent;
+ private final int maxContent;
+
+ public Worker(SSLContext sslContext, int iterations, boolean closeConnection, int minContent, int maxContent)
+ {
+ this.sslContext = sslContext;
+ this.iterations = iterations;
+ this.closeConnection = closeConnection;
+ this.minContent = minContent;
+ this.maxContent = maxContent;
+ }
+
+ public void open() throws IOException
+ {
+ this.sslSocket = (SSLSocket)sslContext.getSocketFactory().createSocket("localhost", connector.getLocalPort());
+ }
+
+ public void close() throws IOException
+ {
+ sslSocket.close();
+ }
+
+ public void run()
+ {
+ try
+ {
+ Random random = new Random();
+
+ StringBuilder builder = new StringBuilder();
+ OutputStream out = null;
+ InputStream in = null;
+ if (!closeConnection)
+ {
+ open();
+ out = sslSocket.getOutputStream();
+ in = sslSocket.getInputStream();
+ }
+
+ for (int i = 0; i < iterations; ++i)
+ {
+ if (closeConnection)
+ {
+ open();
+ out = sslSocket.getOutputStream();
+ in = sslSocket.getInputStream();
+ }
+
+ int contentSize = random.nextInt(maxContent - minContent) + minContent;
+// System.err.println("Writing " + content + " request bytes");
+ out.write("POST / HTTP/1.1\r\n".getBytes());
+ out.write("Host: localhost\r\n".getBytes());
+ out.write(("Content-Length: " + contentSize + "\r\n").getBytes());
+ out.write("Content-Type: application/octect-stream\r\n".getBytes());
+ if (closeConnection)
+ out.write("Connection: close\r\n".getBytes());
+ out.write("\r\n".getBytes());
+ out.flush();
+ byte[] contentChunk = new byte[minContent];
+ int content = contentSize;
+ while (content > 0)
+ {
+ int chunk = Math.min(content, contentChunk.length);
+ Arrays.fill(contentChunk, 0, chunk, (byte)'x');
+ out.write(contentChunk, 0, chunk);
+ content -= chunk;
+ }
+ out.flush();
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ int responseLength = 0;
+ String line;
+ while ((line = reader.readLine()) != null)
+ {
+// System.err.println(line);
+ String contentLength = "Content-Length:";
+ if (line.startsWith(contentLength))
+ {
+ responseLength = Integer.parseInt(line.substring(contentLength.length()).trim());
+ }
+ else if (line.length() == 0)
+ {
+ if (responseLength == 0)
+ line = reader.readLine();
+ break;
+ }
+ }
+
+ builder.setLength(0);
+ if (responseLength > 0)
+ {
+ for (int j = 0; j < responseLength; ++j)
+ {
+ builder.append((char)reader.read());
+ }
+ }
+ else
+ {
+ builder.append(line);
+ }
+
+ if (closeConnection)
+ close();
+
+ if (contentSize != Integer.parseInt(builder.toString()))
+ throw new IllegalStateException();
+
+ Thread.sleep(random.nextInt(1000));
+
+ totalIterations.incrementAndGet();
+ }
+
+ if (!closeConnection)
+ close();
+ }
+ catch (Exception x)
+ {
+ throw new RuntimeException(x);
+ }
+ }
+ }
+
+ private static class EmptyHandler extends AbstractHandler
+ {
+ public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
+ {
+ request.setHandled(true);
+
+ InputStream in = request.getInputStream();
+ int total = 0;
+ byte[] b = new byte[1024 * 1024];
+ int read;
+ while ((read = in.read(b)) >= 0)
+ {
+ total += read;
+ }
+// System.err.println("Read " + total + " request bytes");
+ httpResponse.getOutputStream().write(String.valueOf(total).getBytes());
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/ServerConnectorSslServerTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/ServerConnectorSslServerTest.java
new file mode 100644
index 0000000..79ff421
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/ServerConnectorSslServerTest.java
@@ -0,0 +1,263 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.ssl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManagerFactory;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.LeakTrackingByteBufferPool;
+import org.eclipse.jetty.io.MappedByteBufferPool;
+import org.eclipse.jetty.server.AbstractConnectionFactory;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.HttpServerTestBase;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.emptyOrNullString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * HttpServer Tester for SSL based ServerConnector
+ */
+public class ServerConnectorSslServerTest extends HttpServerTestBase
+{
+ private SSLContext _sslContext;
+
+ public ServerConnectorSslServerTest()
+ {
+ _scheme = "https";
+ }
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ String keystorePath = MavenTestingUtils.getTestResourcePath("keystore").toString();
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystorePath);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+ sslContextFactory.setTrustStorePath(keystorePath);
+ sslContextFactory.setTrustStorePassword("storepwd");
+ ByteBufferPool pool = new LeakTrackingByteBufferPool(new MappedByteBufferPool.Tagged());
+
+ HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory();
+ ServerConnector connector = new ServerConnector(_server, null, null, pool, 1, 1, AbstractConnectionFactory.getFactories(sslContextFactory, httpConnectionFactory));
+ SecureRequestCustomizer secureRequestCustomer = new SecureRequestCustomizer();
+ secureRequestCustomer.setSslSessionAttribute("SSL_SESSION");
+ httpConnectionFactory.getHttpConfiguration().addCustomizer(secureRequestCustomer);
+
+ startServer(connector);
+
+ KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+ try (InputStream stream = sslContextFactory.getKeyStoreResource().getInputStream())
+ {
+ keystore.load(stream, "storepwd".toCharArray());
+ }
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(keystore);
+ _sslContext = SSLContext.getInstance("TLS");
+ _sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
+
+ try
+ {
+ // Client configuration in case we use HttpsURLConnection.
+ HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
+ SSLContext sc = SSLContext.getInstance("TLS");
+ sc.init(null, SslContextFactory.TRUST_ALL_CERTS, null);
+ HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ protected Socket newSocket(String host, int port) throws Exception
+ {
+ Socket socket = _sslContext.getSocketFactory().createSocket(host, port);
+ socket.setSoTimeout(10000);
+ socket.setTcpNoDelay(true);
+ return socket;
+ }
+
+ @Override
+ public void testFullHeader() throws Exception
+ {
+ super.testFullHeader();
+ }
+
+ @Override
+ public void testBlockingWhileReadingRequestContent() throws Exception
+ {
+ super.testBlockingWhileReadingRequestContent();
+ }
+
+ @Override
+ public void testBlockingWhileWritingResponseContent() throws Exception
+ {
+ super.testBlockingWhileWritingResponseContent();
+ }
+
+ @Test
+ public void testRequest2FixedFragments() throws Exception
+ {
+ configureServer(new EchoHandler());
+
+ byte[] bytes = REQUEST2.getBytes();
+ int[] points = new int[]{74, 325};
+
+ // Sort the list
+ Arrays.sort(points);
+
+ URI uri = _server.getURI();
+ Socket client = newSocket(uri.getHost(), uri.getPort());
+ try
+ {
+ OutputStream os = client.getOutputStream();
+
+ int last = 0;
+
+ // Write out the fragments
+ for (int j = 0; j < points.length; ++j)
+ {
+ int point = points[j];
+ os.write(bytes, last, point - last);
+ last = point;
+ os.flush();
+ Thread.sleep(PAUSE);
+ }
+
+ // Write the last fragment
+ os.write(bytes, last, bytes.length - last);
+ os.flush();
+ Thread.sleep(PAUSE);
+
+ // Read the response
+ String response = readResponse(client);
+
+ // Check the response
+ assertEquals(RESPONSE2, response);
+ }
+ finally
+ {
+ client.close();
+ }
+ }
+
+ @Override
+ @Test
+ public void testInterruptedRequest()
+ {
+ Assumptions.assumeFalse(_serverURI.getScheme().equals("https"), "SSLSocket.shutdownOutput() is not supported, but shutdownOutput() is needed by the test");
+ }
+
+ @Override
+ public void testAvailable()
+ {
+ Assumptions.assumeFalse(_serverURI.getScheme().equals("https"), "SSLSocket available() is not supported");
+ }
+
+ @Test
+ public void testSecureRequestCustomizer() throws Exception
+ {
+ configureServer(new SecureRequestHandler());
+
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ os.write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
+ os.flush();
+
+ // Read the response.
+ String response = readResponse(client);
+
+ assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertThat(response, containsString("Hello world"));
+ assertThat(response, containsString("scheme='https'"));
+ assertThat(response, containsString("isSecure='true'"));
+ assertThat(response, containsString("X509Certificate='null'"));
+
+ Matcher matcher = Pattern.compile("cipher_suite='([^']*)'").matcher(response);
+ matcher.find();
+ assertThat(matcher.group(1), Matchers.allOf(not(is(emptyOrNullString()))), not(is("null")));
+
+ matcher = Pattern.compile("key_size='([^']*)'").matcher(response);
+ matcher.find();
+ assertThat(matcher.group(1), Matchers.allOf(not(is(emptyOrNullString())), not(is("null"))));
+
+ matcher = Pattern.compile("ssl_session_id='([^']*)'").matcher(response);
+ matcher.find();
+ assertThat(matcher.group(1), Matchers.allOf(not(is(emptyOrNullString())), not(is("null"))));
+
+ matcher = Pattern.compile("ssl_session='([^']*)'").matcher(response);
+ matcher.find();
+ assertThat(matcher.group(1), Matchers.allOf(not(is(emptyOrNullString())), not(is("null"))));
+ }
+ }
+
+ public static class SecureRequestHandler extends AbstractHandler
+ {
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.getOutputStream().println("Hello world");
+ response.getOutputStream().println("scheme='" + request.getScheme() + "'");
+ response.getOutputStream().println("isSecure='" + request.isSecure() + "'");
+ response.getOutputStream().println("X509Certificate='" + request.getAttribute("javax.servlet.request.X509Certificate") + "'");
+ response.getOutputStream().println("cipher_suite='" + request.getAttribute("javax.servlet.request.cipher_suite") + "'");
+ response.getOutputStream().println("key_size='" + request.getAttribute("javax.servlet.request.key_size") + "'");
+ response.getOutputStream().println("ssl_session_id='" + request.getAttribute("javax.servlet.request.ssl_session_id") + "'");
+ SSLSession sslSession = (SSLSession)request.getAttribute("SSL_SESSION");
+ response.getOutputStream().println("ssl_session='" + sslSession + "'");
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SlowClientsTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SlowClientsTest.java
new file mode 100644
index 0000000..ed96c9d
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SlowClientsTest.java
@@ -0,0 +1,148 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.ssl;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import static java.time.Duration.ofSeconds;
+
+@Tag("Unstable")
+@Disabled
+public class SlowClientsTest
+{
+ private Logger logger = Log.getLogger(getClass());
+
+ @Test
+ public void testSlowClientsWithSmallThreadPool() throws Exception
+ {
+ File keystore = MavenTestingUtils.getTestResourceFile("keystore");
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore.getAbsolutePath());
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+
+ int maxThreads = 6;
+ int contentLength = 8 * 1024 * 1024;
+ QueuedThreadPool serverThreads = new QueuedThreadPool(maxThreads);
+ serverThreads.setDetailedDump(true);
+ Server server = new Server(serverThreads);
+
+ try
+ {
+ ServerConnector connector = new ServerConnector(server, 1, 1, sslContextFactory);
+ connector.setPort(8888);
+ server.addConnector(connector);
+ server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ logger.info("SERVING {}", target);
+ // Write some big content.
+ response.getOutputStream().write(new byte[contentLength]);
+ logger.info("SERVED {}", target);
+ }
+ });
+ server.start();
+
+ SSLContext sslContext = sslContextFactory.getSslContext();
+
+ Assertions.assertTimeoutPreemptively(ofSeconds(10), () ->
+ {
+ CompletableFuture[] futures = new CompletableFuture[2 * maxThreads];
+ ExecutorService executor = Executors.newFixedThreadPool(futures.length);
+ for (int i = 0; i < futures.length; i++)
+ {
+ int k = i;
+ futures[i] = CompletableFuture.runAsync(() ->
+ {
+ try (SSLSocket socket = (SSLSocket)sslContext.getSocketFactory().createSocket("localhost", connector.getLocalPort()))
+ {
+ socket.setSoTimeout(contentLength / 1024);
+ OutputStream output = socket.getOutputStream();
+ String target = "/" + k;
+ String request = "GET " + target + " HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ while (serverThreads.getIdleThreads() > 0)
+ {
+ Thread.sleep(50);
+ }
+
+ InputStream input = socket.getInputStream();
+ while (true)
+ {
+ int read = input.read();
+ if (read < 0)
+ break;
+ }
+ logger.info("FINISHED {}", target);
+ }
+ catch (IOException x)
+ {
+ throw new UncheckedIOException(x);
+ }
+ catch (InterruptedException x)
+ {
+ throw new UncheckedIOException(new InterruptedIOException());
+ }
+ }, executor);
+ }
+ CompletableFuture.allOf(futures).join();
+ });
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java
new file mode 100644
index 0000000..1084dc3
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java
@@ -0,0 +1,599 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.ssl;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.ssl.SslConnection;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SocketCustomizationListener;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.ssl.SniX509ExtendedKeyManager;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.ssl.X509;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class SniSslConnectionFactoryTest
+{
+ private Server _server;
+ private ServerConnector _connector;
+ private HttpConfiguration _httpsConfiguration;
+ private int _port;
+
+ @BeforeEach
+ public void before()
+ {
+ _server = new Server();
+
+ HttpConfiguration httpConfig = new HttpConfiguration();
+ httpConfig.setSecureScheme("https");
+ httpConfig.setSecurePort(8443);
+ httpConfig.setOutputBufferSize(32768);
+ _httpsConfiguration = new HttpConfiguration(httpConfig);
+ SecureRequestCustomizer src = new SecureRequestCustomizer();
+ src.setSniHostCheck(true);
+ _httpsConfiguration.addCustomizer(src);
+ _httpsConfiguration.addCustomizer((connector, hc, request) ->
+ {
+ EndPoint endp = request.getHttpChannel().getEndPoint();
+ if (endp instanceof SslConnection.DecryptedEndPoint)
+ {
+ try
+ {
+ SslConnection.DecryptedEndPoint sslEndp = (SslConnection.DecryptedEndPoint)endp;
+ SslConnection sslConnection = sslEndp.getSslConnection();
+ SSLEngine sslEngine = sslConnection.getSSLEngine();
+ SSLSession session = sslEngine.getSession();
+ for (Certificate c : session.getLocalCertificates())
+ {
+ request.getResponse().getHttpFields().add("X-Cert", ((X509Certificate)c).getSubjectDN().toString());
+ }
+ }
+ catch (Throwable th)
+ {
+ th.printStackTrace();
+ }
+ }
+ });
+ }
+
+ protected void start(String keystorePath) throws Exception
+ {
+ start(ssl ->
+ {
+ ssl.setKeyStorePath(keystorePath);
+ ssl.setKeyManagerPassword("keypwd");
+ });
+ }
+
+ protected void start(Consumer<SslContextFactory.Server> sslConfig) throws Exception
+ {
+ SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
+ if (OS.WINDOWS.isCurrentOs())
+ {
+ // Restrict behavior in this testcase to how TLSv1.2 operates.
+ // This is because of behavior differences in TLS between Linux and Windows.
+ // On Linux TLS on client side will always return a javax.net.ssl.SSLHandshakeException
+ // in those test cases that expect it.
+ // However, on Windows, there are differences between using OpenJDK 8 and OpenJDK 11.
+ // Only the TLSv1.2 implementation will return a javax.net.ssl.SSLHandshakeException,
+ // all other TLS versions will result in a
+ // javax.net.ssl.SSLException: Software caused connection abort: recv failed
+ // sslContextFactory.setIncludeProtocols("TLSv1.2");
+ }
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslConfig.accept(sslContextFactory);
+
+ File keystoreFile = sslContextFactory.getKeyStoreResource().getFile();
+ if (!keystoreFile.exists())
+ throw new FileNotFoundException(keystoreFile.getAbsolutePath());
+
+ ServerConnector https = _connector = new ServerConnector(_server,
+ new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
+ new HttpConnectionFactory(_httpsConfiguration));
+ _server.addConnector(https);
+
+ _server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ {
+ baseRequest.setHandled(true);
+ response.setStatus(200);
+ response.setHeader("X-URL", request.getRequestURI());
+ response.setHeader("X-HOST", request.getServerName());
+ }
+ });
+
+ _server.start();
+ _port = https.getLocalPort();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ if (_server != null)
+ _server.stop();
+ }
+
+ @Test
+ public void testConnect() throws Exception
+ {
+ start("src/test/resources/keystore_sni.p12");
+ String response = getResponse("127.0.0.1", null);
+ assertThat(response, Matchers.containsString("X-HOST: 127.0.0.1"));
+ }
+
+ @Test
+ public void testSNIConnectNoWild() throws Exception
+ {
+ start("src/test/resources/keystore_sni_nowild.p12");
+
+ String response = getResponse("www.acme.org", null);
+ assertThat(response, Matchers.containsString("X-HOST: www.acme.org"));
+ assertThat(response, Matchers.containsString("X-Cert: OU=default"));
+
+ response = getResponse("www.example.com", null);
+ assertThat(response, Matchers.containsString("X-HOST: www.example.com"));
+ assertThat(response, Matchers.containsString("X-Cert: OU=example"));
+ }
+
+ @Test
+ public void testSNIConnect() throws Exception
+ {
+ start(ssl ->
+ {
+ ssl.setKeyStorePath("src/test/resources/keystore_sni.p12");
+ ssl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
+ ssl.setSNISelector((keyType, issuers, session, sniHost, certificates) ->
+ {
+ // Make sure the *.domain.com comes before sub.domain.com
+ // to test that we prefer more specific domains.
+ List<X509> sortedCertificates = certificates.stream()
+ // As sorted() sorts ascending, make *.domain.com the smallest.
+ .sorted((x509a, x509b) ->
+ {
+ if (x509a.matches("domain.com"))
+ return -1;
+ if (x509b.matches("domain.com"))
+ return 1;
+ return 0;
+ })
+ .collect(Collectors.toList());
+ return ssl.sniSelect(keyType, issuers, session, sniHost, sortedCertificates);
+ });
+ });
+
+ String response = getResponse("jetty.eclipse.org", "jetty.eclipse.org");
+ assertThat(response, Matchers.containsString("X-HOST: jetty.eclipse.org"));
+
+ response = getResponse("www.example.com", "www.example.com");
+ assertThat(response, Matchers.containsString("X-HOST: www.example.com"));
+
+ response = getResponse("foo.domain.com", "*.domain.com");
+ assertThat(response, Matchers.containsString("X-HOST: foo.domain.com"));
+
+ response = getResponse("sub.domain.com", "sub.domain.com");
+ assertThat(response, Matchers.containsString("X-HOST: sub.domain.com"));
+
+ response = getResponse("m.san.com", "san example");
+ assertThat(response, Matchers.containsString("X-HOST: m.san.com"));
+
+ response = getResponse("www.san.com", "san example");
+ assertThat(response, Matchers.containsString("X-HOST: www.san.com"));
+ }
+
+ @Test
+ public void testWildSNIConnect() throws Exception
+ {
+ start("src/test/resources/keystore_sni.p12");
+
+ String response = getResponse("domain.com", "www.domain.com", "*.domain.com");
+ assertThat(response, Matchers.containsString("X-HOST: www.domain.com"));
+
+ response = getResponse("domain.com", "domain.com", "*.domain.com");
+ assertThat(response, Matchers.containsString("X-HOST: domain.com"));
+
+ response = getResponse("www.domain.com", "www.domain.com", "*.domain.com");
+ assertThat(response, Matchers.containsString("X-HOST: www.domain.com"));
+ }
+
+ @Test
+ public void testBadSNIConnect() throws Exception
+ {
+ start("src/test/resources/keystore_sni.p12");
+
+ String response = getResponse("www.example.com", "some.other.com", "www.example.com");
+ assertThat(response, Matchers.containsString("HTTP/1.1 400 "));
+ assertThat(response, Matchers.containsString("Host does not match SNI"));
+ }
+
+ @DisabledOnOs(value = OS.WINDOWS, disabledReason = "TLSv1.3 behavior differences between Linux and Windows")
+ @Test
+ public void testWrongSNIRejectedConnection() throws Exception
+ {
+ start(ssl ->
+ {
+ ssl.setKeyStorePath("src/test/resources/keystore_sni.p12");
+ ssl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
+ // Do not allow unmatched SNI.
+ ssl.setSniRequired(true);
+ });
+
+ // Wrong SNI host.
+ assertThrows(SSLHandshakeException.class, () -> getResponse("wrong.com", "wrong.com", null));
+
+ // No SNI host.
+ assertThrows(SSLHandshakeException.class, () -> getResponse(null, "wrong.com", null));
+ }
+
+ @Test
+ public void testWrongSNIRejectedBadRequest() throws Exception
+ {
+ start(ssl ->
+ {
+ ssl.setKeyStorePath("src/test/resources/keystore_sni.p12");
+ ssl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
+ // Do not allow unmatched SNI.
+ ssl.setSniRequired(false);
+ _httpsConfiguration.getCustomizers().stream()
+ .filter(SecureRequestCustomizer.class::isInstance)
+ .map(SecureRequestCustomizer.class::cast)
+ .forEach(src -> src.setSniRequired(true));
+ });
+
+ // Wrong SNI host.
+ HttpTester.Response response = HttpTester.parseResponse(getResponse("wrong.com", "wrong.com", null));
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(400));
+
+ // No SNI host.
+ response = HttpTester.parseResponse(getResponse(null, "wrong.com", null));
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(400));
+ }
+
+ @DisabledOnOs(value = OS.WINDOWS, disabledReason = "TLSv1.3 behavior differences between Linux and Windows")
+ @Test
+ public void testWrongSNIRejectedFunction() throws Exception
+ {
+ start(ssl ->
+ {
+ ssl.setKeyStorePath("src/test/resources/keystore_sni.p12");
+ ssl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
+ // Do not allow unmatched SNI.
+ ssl.setSniRequired(true);
+ ssl.setSNISelector((keyType, issuers, session, sniHost, certificates) ->
+ {
+ if (sniHost == null)
+ return SniX509ExtendedKeyManager.SniSelector.DELEGATE;
+ return ssl.sniSelect(keyType, issuers, session, sniHost, certificates);
+ });
+ _httpsConfiguration.getCustomizers().stream()
+ .filter(SecureRequestCustomizer.class::isInstance)
+ .map(SecureRequestCustomizer.class::cast)
+ .forEach(src -> src.setSniRequired(true));
+ });
+
+ // Wrong SNI host.
+ assertThrows(SSLHandshakeException.class, () -> getResponse("wrong.com", "wrong.com", null));
+
+ // No SNI host.
+ HttpTester.Response response = HttpTester.parseResponse(getResponse(null, "wrong.com", null));
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(400));
+ }
+
+ @DisabledOnOs(value = OS.WINDOWS, disabledReason = "TLSv1.3 behavior differences between Linux and Windows")
+ @Test
+ public void testWrongSNIRejectedConnectionWithNonSNIKeystore() throws Exception
+ {
+ start(ssl ->
+ {
+ // Keystore has only one certificate, but we want to enforce SNI.
+ ssl.setKeyStorePath("src/test/resources/keystore");
+ ssl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
+ ssl.setSniRequired(true);
+ });
+
+ // Wrong SNI host.
+ assertThrows(SSLHandshakeException.class, () -> getResponse("wrong.com", "wrong.com", null));
+
+ // No SNI host.
+ assertThrows(SSLHandshakeException.class, () -> getResponse(null, "wrong.com", null));
+
+ // Good SNI host.
+ HttpTester.Response response = HttpTester.parseResponse(getResponse("jetty.eclipse.org", "jetty.eclipse.org", null));
+
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(200));
+ }
+
+ @Test
+ public void testSameConnectionRequestsForManyDomains() throws Exception
+ {
+ start("src/test/resources/keystore_sni.p12");
+ _server.setErrorHandler(new ErrorHandler());
+
+ SslContextFactory clientContextFactory = new SslContextFactory.Client(true);
+ clientContextFactory.start();
+ SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory();
+ try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port))
+ {
+ SNIHostName serverName = new SNIHostName("m.san.com");
+ SSLParameters params = sslSocket.getSSLParameters();
+ params.setServerNames(Collections.singletonList(serverName));
+ sslSocket.setSSLParameters(params);
+ sslSocket.startHandshake();
+
+ // The first request binds the socket to an alias.
+ String request =
+ "GET /ctx/path HTTP/1.1\r\n" +
+ "Host: m.san.com\r\n" +
+ "\r\n";
+ OutputStream output = sslSocket.getOutputStream();
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ InputStream input = sslSocket.getInputStream();
+ HttpTester.Response response = HttpTester.parseResponse(input);
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(200));
+
+ // Same socket, send a request for a different domain but same alias.
+ request =
+ "GET /ctx/path HTTP/1.1\r\n" +
+ "Host: www.san.com\r\n" +
+ "\r\n";
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+ response = HttpTester.parseResponse(input);
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(200));
+
+ // Same socket, send a request for a different domain but different alias.
+ request =
+ "GET /ctx/path HTTP/1.1\r\n" +
+ "Host: www.example.com\r\n" +
+ "\r\n";
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ response = HttpTester.parseResponse(input);
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(400));
+ assertThat(response.getContent(), containsString("Host does not match SNI"));
+ }
+ finally
+ {
+ clientContextFactory.stop();
+ }
+ }
+
+ @Test
+ public void testSameConnectionRequestsForManyWildDomains() throws Exception
+ {
+ start("src/test/resources/keystore_sni.p12");
+
+ SslContextFactory clientContextFactory = new SslContextFactory.Client(true);
+ clientContextFactory.start();
+ SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory();
+ try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port))
+ {
+ SNIHostName serverName = new SNIHostName("www.domain.com");
+ SSLParameters params = sslSocket.getSSLParameters();
+ params.setServerNames(Collections.singletonList(serverName));
+ sslSocket.setSSLParameters(params);
+ sslSocket.startHandshake();
+
+ String request =
+ "GET /ctx/path HTTP/1.1\r\n" +
+ "Host: www.domain.com\r\n" +
+ "\r\n";
+ OutputStream output = sslSocket.getOutputStream();
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ InputStream input = sslSocket.getInputStream();
+ HttpTester.Response response = HttpTester.parseResponse(input);
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(200));
+
+ // Now, on the same socket, send a request for a different valid domain.
+ request =
+ "GET /ctx/path HTTP/1.1\r\n" +
+ "Host: assets.domain.com\r\n" +
+ "\r\n";
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ response = HttpTester.parseResponse(input);
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(200));
+
+ // Now make a request for an invalid domain for this connection.
+ request =
+ "GET /ctx/path HTTP/1.1\r\n" +
+ "Host: www.example.com\r\n" +
+ "\r\n";
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ response = HttpTester.parseResponse(input);
+ assertNotNull(response);
+ assertThat(response.getStatus(), is(400));
+ assertThat(response.getContent(), containsString("Host does not match SNI"));
+ }
+ finally
+ {
+ clientContextFactory.stop();
+ }
+ }
+
+ @Test
+ public void testSocketCustomization() throws Exception
+ {
+ start("src/test/resources/keystore_sni.p12");
+
+ final Queue<String> history = new LinkedBlockingQueue<>();
+
+ _connector.addBean(new SocketCustomizationListener()
+ {
+ @Override
+ protected void customize(Socket socket, Class<? extends Connection> connection, boolean ssl)
+ {
+ history.add("customize connector " + connection + "," + ssl);
+ }
+ });
+
+ _connector.getBean(SslConnectionFactory.class).addBean(new SocketCustomizationListener()
+ {
+ @Override
+ protected void customize(Socket socket, Class<? extends Connection> connection, boolean ssl)
+ {
+ history.add("customize ssl " + connection + "," + ssl);
+ }
+ });
+
+ _connector.getBean(HttpConnectionFactory.class).addBean(new SocketCustomizationListener()
+ {
+ @Override
+ protected void customize(Socket socket, Class<? extends Connection> connection, boolean ssl)
+ {
+ history.add("customize http " + connection + "," + ssl);
+ }
+ });
+
+ String response = getResponse("127.0.0.1", null);
+ assertThat(response, Matchers.containsString("X-HOST: 127.0.0.1"));
+
+ assertEquals("customize connector class org.eclipse.jetty.io.ssl.SslConnection,false", history.poll());
+ assertEquals("customize ssl class org.eclipse.jetty.io.ssl.SslConnection,false", history.poll());
+ assertEquals("customize connector class org.eclipse.jetty.server.HttpConnection,true", history.poll());
+ assertEquals("customize http class org.eclipse.jetty.server.HttpConnection,true", history.poll());
+ assertEquals(0, history.size());
+ }
+
+ @Test
+ public void testSNIWithDifferentKeyTypes() throws Exception
+ {
+ // This KeyStore contains 2 certificates, one with keyAlg=EC, one with keyAlg=RSA.
+ start(ssl -> ssl.setKeyStorePath("src/test/resources/keystore_sni_key_types.p12"));
+
+ // Make a request with SNI = rsa.domain.com, the RSA certificate should be chosen.
+ HttpTester.Response response1 = HttpTester.parseResponse(getResponse("rsa.domain.com", "rsa.domain.com"));
+ assertEquals(HttpStatus.OK_200, response1.getStatus());
+
+ // Make a request with SNI = ec.domain.com, the EC certificate should be chosen.
+ HttpTester.Response response2 = HttpTester.parseResponse(getResponse("ec.domain.com", "ec.domain.com"));
+ assertEquals(HttpStatus.OK_200, response2.getStatus());
+ }
+
+ private String getResponse(String host, String cn) throws Exception
+ {
+ String response = getResponse(host, host, cn);
+ assertThat(response, Matchers.startsWith("HTTP/1.1 200 "));
+ assertThat(response, Matchers.containsString("X-URL: /ctx/path"));
+ return response;
+ }
+
+ private String getResponse(String sniHost, String reqHost, String cn) throws Exception
+ {
+ SslContextFactory clientContextFactory = new SslContextFactory.Client(true);
+ clientContextFactory.start();
+ SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory();
+ try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port))
+ {
+ if (sniHost != null)
+ {
+ SNIHostName serverName = new SNIHostName(sniHost);
+ List<SNIServerName> serverNames = new ArrayList<>();
+ serverNames.add(serverName);
+
+ SSLParameters params = sslSocket.getSSLParameters();
+ params.setServerNames(serverNames);
+ sslSocket.setSSLParameters(params);
+ }
+ sslSocket.startHandshake();
+
+ if (cn != null)
+ {
+ X509Certificate cert = ((X509Certificate)sslSocket.getSession().getPeerCertificates()[0]);
+ assertThat(cert.getSubjectX500Principal().getName("CANONICAL"), Matchers.startsWith("cn=" + cn));
+ }
+
+ String response = "GET /ctx/path HTTP/1.0\r\nHost: " + reqHost + ":" + _port + "\r\n\r\n";
+ sslSocket.getOutputStream().write(response.getBytes(StandardCharsets.ISO_8859_1));
+ return IO.toString(sslSocket.getInputStream());
+ }
+ finally
+ {
+ clientContextFactory.stop();
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslConnectionFactoryTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslConnectionFactoryTest.java
new file mode 100644
index 0000000..2869109
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslConnectionFactoryTest.java
@@ -0,0 +1,242 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.ssl;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SocketCustomizationListener;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class SslConnectionFactoryTest
+{
+ private Server _server;
+ private ServerConnector _connector;
+ private int _port;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ String keystorePath = "src/test/resources/keystore";
+ File keystoreFile = new File(keystorePath);
+ if (!keystoreFile.exists())
+ throw new FileNotFoundException(keystoreFile.getAbsolutePath());
+
+ _server = new Server();
+
+ HttpConfiguration httpConfig = new HttpConfiguration();
+ httpConfig.setSecureScheme("https");
+ httpConfig.setSecurePort(8443);
+ httpConfig.setOutputBufferSize(32768);
+ HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
+ httpsConfig.addCustomizer(new SecureRequestCustomizer());
+
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystoreFile.getAbsolutePath());
+ sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4");
+ sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
+
+ ServerConnector https = _connector = new ServerConnector(_server,
+ new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
+ new HttpConnectionFactory(httpsConfig));
+ https.setPort(0);
+ https.setIdleTimeout(30000);
+
+ _server.addConnector(https);
+
+ _server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.setStatus(200);
+ response.getWriter().write("url=" + request.getRequestURI() + "\nhost=" + request.getServerName());
+ response.flushBuffer();
+ }
+ });
+
+ _server.start();
+ _port = https.getLocalPort();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _server.stop();
+ _server = null;
+ }
+
+ @Test
+ public void testConnect() throws Exception
+ {
+ String response = getResponse("127.0.0.1", null);
+ assertThat(response, Matchers.containsString("host=127.0.0.1"));
+ }
+
+ @Test
+ public void testSNIConnect() throws Exception
+ {
+ String response = getResponse("localhost", "localhost", "jetty.eclipse.org");
+ assertThat(response, Matchers.containsString("host=localhost"));
+ }
+
+ @Test
+ public void testBadHandshake() throws Exception
+ {
+ try (Socket socket = new Socket("127.0.0.1", _port);
+ OutputStream out = socket.getOutputStream())
+ {
+ out.write("Rubbish".getBytes());
+ out.flush();
+
+ socket.setSoTimeout(1000);
+ // Expect TLS message type == 21: Alert
+ assertThat(socket.getInputStream().read(), Matchers.equalTo(21));
+ }
+ }
+
+ @Test
+ public void testSocketCustomization() throws Exception
+ {
+ final Queue<String> history = new LinkedBlockingQueue<>();
+
+ _connector.addBean(new SocketCustomizationListener()
+ {
+ @Override
+ protected void customize(Socket socket, Class<? extends Connection> connection, boolean ssl)
+ {
+ history.add("customize connector " + connection + "," + ssl);
+ }
+ });
+
+ _connector.getBean(SslConnectionFactory.class).addBean(new SocketCustomizationListener()
+ {
+ @Override
+ protected void customize(Socket socket, Class<? extends Connection> connection, boolean ssl)
+ {
+ history.add("customize ssl " + connection + "," + ssl);
+ }
+ });
+
+ _connector.getBean(HttpConnectionFactory.class).addBean(new SocketCustomizationListener()
+ {
+ @Override
+ protected void customize(Socket socket, Class<? extends Connection> connection, boolean ssl)
+ {
+ history.add("customize http " + connection + "," + ssl);
+ }
+ });
+
+ String response = getResponse("127.0.0.1", null);
+ assertThat(response, Matchers.containsString("host=127.0.0.1"));
+
+ assertEquals("customize connector class org.eclipse.jetty.io.ssl.SslConnection,false", history.poll());
+ assertEquals("customize ssl class org.eclipse.jetty.io.ssl.SslConnection,false", history.poll());
+ assertEquals("customize connector class org.eclipse.jetty.server.HttpConnection,true", history.poll());
+ assertEquals("customize http class org.eclipse.jetty.server.HttpConnection,true", history.poll());
+ assertEquals(0, history.size());
+ }
+
+ @Test
+ public void testServerWithoutHttpConnectionFactory() throws Exception
+ {
+ _server.stop();
+ assertNotNull(_connector.removeConnectionFactory(HttpVersion.HTTP_1_1.asString()));
+ assertThrows(IllegalStateException.class, () -> _server.start());
+ }
+
+ private String getResponse(String host, String cn) throws Exception
+ {
+ String response = getResponse(host, host, cn);
+ assertThat(response, Matchers.startsWith("HTTP/1.1 200 OK"));
+ assertThat(response, Matchers.containsString("url=/ctx/path"));
+ return response;
+ }
+
+ private String getResponse(String sniHost, String reqHost, String cn) throws Exception
+ {
+ SslContextFactory clientContextFactory = new SslContextFactory.Client(true);
+ clientContextFactory.start();
+ SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory();
+
+ SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port);
+
+ if (cn != null)
+ {
+ SNIHostName serverName = new SNIHostName(sniHost);
+ List<SNIServerName> serverNames = new ArrayList<>();
+ serverNames.add(serverName);
+
+ SSLParameters params = sslSocket.getSSLParameters();
+ params.setServerNames(serverNames);
+ sslSocket.setSSLParameters(params);
+ }
+ sslSocket.startHandshake();
+
+ if (cn != null)
+ {
+ X509Certificate cert = ((X509Certificate)sslSocket.getSession().getPeerCertificates()[0]);
+ assertThat(cert.getSubjectX500Principal().getName("CANONICAL"), Matchers.startsWith("cn=" + cn));
+ }
+
+ sslSocket.getOutputStream().write(("GET /ctx/path HTTP/1.0\r\nHost: " + reqHost + ":" + _port + "\r\n\r\n").getBytes(StandardCharsets.ISO_8859_1));
+ String response = IO.toString(sslSocket.getInputStream());
+
+ sslSocket.close();
+ clientContextFactory.stop();
+ return response;
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslContextFactoryReloadTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslContextFactoryReloadTest.java
new file mode 100644
index 0000000..0c1c2e0
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslContextFactoryReloadTest.java
@@ -0,0 +1,267 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.ssl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class SslContextFactoryReloadTest
+{
+ public static final String KEYSTORE_1 = "src/test/resources/reload_keystore_1.jks";
+ public static final String KEYSTORE_2 = "src/test/resources/reload_keystore_2.jks";
+
+ private Server server;
+ private SslContextFactory sslContextFactory;
+ private ServerConnector connector;
+
+ private void start(Handler handler) throws Exception
+ {
+ server = new Server();
+
+ sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(KEYSTORE_1);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyStoreType("JKS");
+ sslContextFactory.setKeyStoreProvider(null);
+
+ HttpConfiguration httpsConfig = new HttpConfiguration();
+ httpsConfig.addCustomizer(new SecureRequestCustomizer());
+ connector = new ServerConnector(server,
+ new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
+ new HttpConnectionFactory(httpsConfig));
+ server.addConnector(connector);
+
+ server.setHandler(handler);
+
+ server.start();
+ }
+
+ @AfterEach
+ public void dispose() throws Exception
+ {
+ if (server != null)
+ server.stop();
+ }
+
+ @Test
+ public void testReload() throws Exception
+ {
+ start(new EchoHandler());
+
+ SSLContext ctx = SSLContext.getInstance("TLSv1.2");
+ ctx.init(null, SslContextFactory.TRUST_ALL_CERTS, null);
+ SSLSocketFactory socketFactory = ctx.getSocketFactory();
+ try (SSLSocket client1 = (SSLSocket)socketFactory.createSocket("localhost", connector.getLocalPort()))
+ {
+ String serverDN1 = client1.getSession().getPeerPrincipal().getName();
+ assertThat(serverDN1, Matchers.startsWith("CN=localhost1"));
+
+ String request =
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+
+ OutputStream output1 = client1.getOutputStream();
+ output1.write(request.getBytes(StandardCharsets.UTF_8));
+ output1.flush();
+
+ HttpTester.Response response1 = HttpTester.parseResponse(HttpTester.from(client1.getInputStream()));
+ assertNotNull(response1);
+ assertThat(response1.getStatus(), Matchers.equalTo(HttpStatus.OK_200));
+
+ // Reconfigure SslContextFactory.
+ sslContextFactory.reload(sslContextFactory ->
+ {
+ sslContextFactory.setKeyStorePath(KEYSTORE_2);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ });
+
+ // New connection should use the new keystore.
+ try (SSLSocket client2 = (SSLSocket)socketFactory.createSocket("localhost", connector.getLocalPort()))
+ {
+ String serverDN2 = client2.getSession().getPeerPrincipal().getName();
+ assertThat(serverDN2, Matchers.startsWith("CN=localhost2"));
+
+ OutputStream output2 = client1.getOutputStream();
+ output2.write(request.getBytes(StandardCharsets.UTF_8));
+ output2.flush();
+
+ HttpTester.Response response2 = HttpTester.parseResponse(HttpTester.from(client1.getInputStream()));
+ assertNotNull(response2);
+ assertThat(response2.getStatus(), Matchers.equalTo(HttpStatus.OK_200));
+ }
+
+ // Must still be possible to make requests with the first connection.
+ output1.write(request.getBytes(StandardCharsets.UTF_8));
+ output1.flush();
+
+ response1 = HttpTester.parseResponse(HttpTester.from(client1.getInputStream()));
+ assertNotNull(response1);
+ assertThat(response1.getStatus(), Matchers.equalTo(HttpStatus.OK_200));
+ }
+ }
+
+ @Test
+ public void testReloadWhileServing() throws Exception
+ {
+ start(new EchoHandler());
+
+ Scheduler scheduler = new ScheduledExecutorScheduler();
+ scheduler.start();
+ try
+ {
+ SSLContext ctx = SSLContext.getInstance("TLSv1.2");
+ ctx.init(null, SslContextFactory.TRUST_ALL_CERTS, null);
+ SSLSocketFactory socketFactory = ctx.getSocketFactory();
+
+ // Perform 4 reloads while connections are being served.
+ AtomicInteger reloads = new AtomicInteger(4);
+ long reloadPeriod = 500;
+ AtomicBoolean running = new AtomicBoolean(true);
+ scheduler.schedule(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ if (reloads.decrementAndGet() == 0)
+ {
+ running.set(false);
+ }
+ else
+ {
+ try
+ {
+ sslContextFactory.reload(sslContextFactory ->
+ {
+ if (sslContextFactory.getKeyStorePath().endsWith(KEYSTORE_1))
+ sslContextFactory.setKeyStorePath(KEYSTORE_2);
+ else
+ sslContextFactory.setKeyStorePath(KEYSTORE_1);
+ });
+ scheduler.schedule(this, reloadPeriod, TimeUnit.MILLISECONDS);
+ }
+ catch (Exception x)
+ {
+ running.set(false);
+ reloads.set(-1);
+ }
+ }
+ }
+ }, reloadPeriod, TimeUnit.MILLISECONDS);
+
+ byte[] content = new byte[16 * 1024];
+ while (running.get())
+ {
+ try (SSLSocket client = (SSLSocket)socketFactory.createSocket("localhost", connector.getLocalPort()))
+ {
+ // We need to invalidate the session every time we open a new SSLSocket.
+ // This is because when the client uses session resumption, it caches
+ // the server certificates and then checks that it is the same during
+ // a new TLS handshake. If the SslContextFactory is reloaded during the
+ // TLS handshake, the client will see the new certificate and blow up.
+ // Note that browsers can handle this case better: they will just not
+ // use session resumption and fallback to the normal TLS handshake.
+ client.getSession().invalidate();
+
+ String request1 =
+ "POST / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: " + content.length + "\r\n" +
+ "\r\n";
+ OutputStream outputStream = client.getOutputStream();
+ outputStream.write(request1.getBytes(StandardCharsets.UTF_8));
+ outputStream.write(content);
+ outputStream.flush();
+
+ InputStream inputStream = client.getInputStream();
+ HttpTester.Response response1 = HttpTester.parseResponse(HttpTester.from(inputStream));
+ assertNotNull(response1);
+ assertThat(response1.getStatus(), Matchers.equalTo(HttpStatus.OK_200));
+
+ String request2 =
+ "GET / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ outputStream.write(request2.getBytes(StandardCharsets.UTF_8));
+ outputStream.flush();
+
+ HttpTester.Response response2 = HttpTester.parseResponse(HttpTester.from(inputStream));
+ assertNotNull(response2);
+ assertThat(response2.getStatus(), Matchers.equalTo(HttpStatus.OK_200));
+ }
+ }
+
+ assertEquals(0, reloads.get());
+ }
+ finally
+ {
+ scheduler.stop();
+ }
+ }
+
+ private static class EchoHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ if (HttpMethod.POST.is(request.getMethod()))
+ IO.copy(request.getInputStream(), response.getOutputStream());
+ else
+ response.setContentLength(0);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslSelectChannelTimeoutTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslSelectChannelTimeoutTest.java
new file mode 100644
index 0000000..10f924a
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslSelectChannelTimeoutTest.java
@@ -0,0 +1,67 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.ssl;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.Socket;
+import java.security.KeyStore;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+
+import org.eclipse.jetty.server.ConnectorTimeoutTest;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.jupiter.api.BeforeEach;
+
+public class SslSelectChannelTimeoutTest extends ConnectorTimeoutTest
+{
+ static SSLContext __sslContext;
+
+ @Override
+ protected Socket newSocket(String host, int port) throws Exception
+ {
+ return __sslContext.getSocketFactory().createSocket(host, port);
+ }
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ String keystorePath = System.getProperty("basedir", ".") + "/src/test/resources/keystore";
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystorePath);
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+ sslContextFactory.setTrustStorePath(keystorePath);
+ sslContextFactory.setTrustStorePassword("storepwd");
+ ServerConnector connector = new ServerConnector(_server, 1, 1, sslContextFactory);
+ connector.setIdleTimeout(MAX_IDLE_TIME); //250 msec max idle
+ startServer(connector);
+
+ KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+ try (InputStream stream = new FileInputStream(keystorePath))
+ {
+ keystore.load(stream, "storepwd".toCharArray());
+ }
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(keystore);
+ __sslContext = SSLContext.getInstance("SSL");
+ __sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
+ }
+}
diff --git a/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslUploadTest.java b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslUploadTest.java
new file mode 100644
index 0000000..ab867dc
--- /dev/null
+++ b/third_party/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslUploadTest.java
@@ -0,0 +1,170 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.ssl;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.KeyStore;
+import java.util.Arrays;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManagerFactory;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ *
+ */
+public class SslUploadTest
+{
+ private static Server server;
+ private static ServerConnector connector;
+ private static int total;
+
+ @BeforeAll
+ public static void startServer() throws Exception
+ {
+ File keystore = MavenTestingUtils.getTestResourceFile("keystore");
+
+ SslContextFactory sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStorePath(keystore.getAbsolutePath());
+ sslContextFactory.setKeyStorePassword("storepwd");
+ sslContextFactory.setKeyManagerPassword("keypwd");
+ sslContextFactory.setTrustStorePath(keystore.getAbsolutePath());
+ sslContextFactory.setTrustStorePassword("storepwd");
+
+ server = new Server();
+ connector = new ServerConnector(server, sslContextFactory);
+ server.addConnector(connector);
+
+ server.setHandler(new EmptyHandler());
+
+ server.start();
+ }
+
+ @AfterAll
+ public static void stopServer() throws Exception
+ {
+ server.stop();
+ server.join();
+ }
+
+ @Test
+ @Disabled
+ public void test() throws Exception
+ {
+ KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+ SslContextFactory ctx = connector.getConnectionFactory(SslConnectionFactory.class).getSslContextFactory();
+ try (InputStream stream = new FileInputStream(ctx.getKeyStorePath()))
+ {
+ keystore.load(stream, "storepwd".toCharArray());
+ }
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(keystore);
+ SSLContext sslContext = SSLContext.getInstance("SSL");
+ sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
+
+ final SSLSocket socket = (SSLSocket)sslContext.getSocketFactory().createSocket("localhost", connector.getLocalPort());
+
+ // Simulate async close
+ /*
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ sleep(100);
+ socket.close();
+ }
+ catch (IOException x)
+ {
+ x.printStackTrace();
+ }
+ catch (InterruptedException x)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }.start();
+ */
+
+ long start = System.nanoTime();
+ OutputStream out = socket.getOutputStream();
+ out.write("POST / HTTP/1.1\r\n".getBytes());
+ out.write("Host: localhost\r\n".getBytes());
+ out.write("Content-Length: 16777216\r\n".getBytes());
+ out.write("Content-Type: bytes\r\n".getBytes());
+ out.write("Connection: close\r\n".getBytes());
+ out.write("\r\n".getBytes());
+ out.flush();
+
+ byte[] requestContent = new byte[16777216];
+ Arrays.fill(requestContent, (byte)120);
+ out.write(requestContent);
+ out.flush();
+
+ InputStream in = socket.getInputStream();
+ String response = IO.toString(in);
+ assertTrue(response.indexOf("200") > 0);
+ // System.err.println(response);
+
+ // long end = System.nanoTime();
+ // System.out.println("upload time: " + TimeUnit.NANOSECONDS.toMillis(end - start));
+ assertEquals(requestContent.length, total);
+ }
+
+ private static class EmptyHandler extends AbstractHandler
+ {
+ @Override
+ public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
+ {
+ request.setHandled(true);
+ InputStream in = request.getInputStream();
+ byte[] b = new byte[4096 * 4];
+ int read;
+ while ((read = in.read(b)) >= 0)
+ {
+ total += read;
+ }
+ System.err.println("Read " + total);
+ }
+ }
+}
diff --git a/third_party/jetty-server/src/test/resources/example.jar b/third_party/jetty-server/src/test/resources/example.jar
new file mode 100644
index 0000000..cf15a24
--- /dev/null
+++ b/third_party/jetty-server/src/test/resources/example.jar
Binary files differ
diff --git a/third_party/jetty-server/src/test/resources/fakeRequests.txt b/third_party/jetty-server/src/test/resources/fakeRequests.txt
new file mode 100644
index 0000000..cf306fe
--- /dev/null
+++ b/third_party/jetty-server/src/test/resources/fakeRequests.txt
@@ -0,0 +1,27 @@
+GET /foo/bah1 HTTP/1.1
+Host: localhost
+
+
+GET /foo/bah2 HTTP/1.1
+Host: localhost
+Content-Length: 0
+SomeOtherHeader: blah
+
+GET /foo/bah3 HTTP/1.1
+Host: localhost
+Transfer-Encoding: chunked
+
+a
+0123456789
+
+7
+abcdef
+
+
+0
+
+GET /foo/bah4 HTTP/1.1
+Host: localhost
+
+
+
diff --git a/third_party/jetty-server/src/test/resources/jetty-logging.properties b/third_party/jetty-server/src/test/resources/jetty-logging.properties
new file mode 100644
index 0000000..184609c
--- /dev/null
+++ b/third_party/jetty-server/src/test/resources/jetty-logging.properties
@@ -0,0 +1,6 @@
+org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
+#org.eclipse.jetty.LEVEL=DEBUG
+#org.eclipse.jetty.server.LEVEL=DEBUG
+#org.eclipse.jetty.servlet.LEVEL=DEBUG
+#org.eclipse.jetty.server.ConnectionLimit.LEVEL=DEBUG
+#org.eclipse.jetty.server.AcceptRateLimit.LEVEL=DEBUG
diff --git a/third_party/jetty-server/src/test/resources/keystore b/third_party/jetty-server/src/test/resources/keystore
new file mode 100644
index 0000000..8325b02
--- /dev/null
+++ b/third_party/jetty-server/src/test/resources/keystore
Binary files differ
diff --git a/third_party/jetty-server/src/test/resources/keystore_sni.p12 b/third_party/jetty-server/src/test/resources/keystore_sni.p12
new file mode 100644
index 0000000..352cbba
--- /dev/null
+++ b/third_party/jetty-server/src/test/resources/keystore_sni.p12
Binary files differ
diff --git a/third_party/jetty-server/src/test/resources/keystore_sni_key_types.p12 b/third_party/jetty-server/src/test/resources/keystore_sni_key_types.p12
new file mode 100644
index 0000000..b000fbe
--- /dev/null
+++ b/third_party/jetty-server/src/test/resources/keystore_sni_key_types.p12
Binary files differ
diff --git a/third_party/jetty-server/src/test/resources/keystore_sni_nowild.p12 b/third_party/jetty-server/src/test/resources/keystore_sni_nowild.p12
new file mode 100644
index 0000000..9fda6a0
--- /dev/null
+++ b/third_party/jetty-server/src/test/resources/keystore_sni_nowild.p12
Binary files differ
diff --git a/third_party/jetty-server/src/test/resources/reload_keystore_1.jks b/third_party/jetty-server/src/test/resources/reload_keystore_1.jks
new file mode 100644
index 0000000..d615c22
--- /dev/null
+++ b/third_party/jetty-server/src/test/resources/reload_keystore_1.jks
Binary files differ
diff --git a/third_party/jetty-server/src/test/resources/reload_keystore_2.jks b/third_party/jetty-server/src/test/resources/reload_keystore_2.jks
new file mode 100644
index 0000000..3707c3a
--- /dev/null
+++ b/third_party/jetty-server/src/test/resources/reload_keystore_2.jks
Binary files differ
diff --git a/third_party/jetty-server/src/test/resources/simple/big.txt b/third_party/jetty-server/src/test/resources/simple/big.txt
new file mode 100644
index 0000000..a6d57f0
--- /dev/null
+++ b/third_party/jetty-server/src/test/resources/simple/big.txt
@@ -0,0 +1,400 @@
+ 1 This is a big file
+ 2 This is a big file
+ 3 This is a big file
+ 4 This is a big file
+ 5 This is a big file
+ 6 This is a big file
+ 7 This is a big file
+ 8 This is a big file
+ 9 This is a big file
+ 10 This is a big file
+ 11 This is a big file
+ 12 This is a big file
+ 13 This is a big file
+ 14 This is a big file
+ 15 This is a big file
+ 16 This is a big file
+ 17 This is a big file
+ 18 This is a big file
+ 19 This is a big file
+ 20 This is a big file
+ 21 This is a big file
+ 22 This is a big file
+ 23 This is a big file
+ 24 This is a big file
+ 25 This is a big file
+ 26 This is a big file
+ 27 This is a big file
+ 28 This is a big file
+ 29 This is a big file
+ 30 This is a big file
+ 31 This is a big file
+ 32 This is a big file
+ 33 This is a big file
+ 34 This is a big file
+ 35 This is a big file
+ 36 This is a big file
+ 37 This is a big file
+ 38 This is a big file
+ 39 This is a big file
+ 40 This is a big file
+ 41 This is a big file
+ 42 This is a big file
+ 43 This is a big file
+ 44 This is a big file
+ 45 This is a big file
+ 46 This is a big file
+ 47 This is a big file
+ 48 This is a big file
+ 49 This is a big file
+ 50 This is a big file
+ 51 This is a big file
+ 52 This is a big file
+ 53 This is a big file
+ 54 This is a big file
+ 55 This is a big file
+ 56 This is a big file
+ 57 This is a big file
+ 58 This is a big file
+ 59 This is a big file
+ 60 This is a big file
+ 61 This is a big file
+ 62 This is a big file
+ 63 This is a big file
+ 64 This is a big file
+ 65 This is a big file
+ 66 This is a big file
+ 67 This is a big file
+ 68 This is a big file
+ 69 This is a big file
+ 70 This is a big file
+ 71 This is a big file
+ 72 This is a big file
+ 73 This is a big file
+ 74 This is a big file
+ 75 This is a big file
+ 76 This is a big file
+ 77 This is a big file
+ 78 This is a big file
+ 79 This is a big file
+ 80 This is a big file
+ 81 This is a big file
+ 82 This is a big file
+ 83 This is a big file
+ 84 This is a big file
+ 85 This is a big file
+ 86 This is a big file
+ 87 This is a big file
+ 88 This is a big file
+ 89 This is a big file
+ 90 This is a big file
+ 91 This is a big file
+ 92 This is a big file
+ 93 This is a big file
+ 94 This is a big file
+ 95 This is a big file
+ 96 This is a big file
+ 97 This is a big file
+ 98 This is a big file
+ 99 This is a big file
+ 100 This is a big file
+ 101 This is a big file
+ 102 This is a big file
+ 103 This is a big file
+ 104 This is a big file
+ 105 This is a big file
+ 106 This is a big file
+ 107 This is a big file
+ 108 This is a big file
+ 109 This is a big file
+ 110 This is a big file
+ 111 This is a big file
+ 112 This is a big file
+ 113 This is a big file
+ 114 This is a big file
+ 115 This is a big file
+ 116 This is a big file
+ 117 This is a big file
+ 118 This is a big file
+ 119 This is a big file
+ 120 This is a big file
+ 121 This is a big file
+ 122 This is a big file
+ 123 This is a big file
+ 124 This is a big file
+ 125 This is a big file
+ 126 This is a big file
+ 127 This is a big file
+ 128 This is a big file
+ 129 This is a big file
+ 130 This is a big file
+ 131 This is a big file
+ 132 This is a big file
+ 133 This is a big file
+ 134 This is a big file
+ 135 This is a big file
+ 136 This is a big file
+ 137 This is a big file
+ 138 This is a big file
+ 139 This is a big file
+ 140 This is a big file
+ 141 This is a big file
+ 142 This is a big file
+ 143 This is a big file
+ 144 This is a big file
+ 145 This is a big file
+ 146 This is a big file
+ 147 This is a big file
+ 148 This is a big file
+ 149 This is a big file
+ 150 This is a big file
+ 151 This is a big file
+ 152 This is a big file
+ 153 This is a big file
+ 154 This is a big file
+ 155 This is a big file
+ 156 This is a big file
+ 157 This is a big file
+ 158 This is a big file
+ 159 This is a big file
+ 160 This is a big file
+ 161 This is a big file
+ 162 This is a big file
+ 163 This is a big file
+ 164 This is a big file
+ 165 This is a big file
+ 166 This is a big file
+ 167 This is a big file
+ 168 This is a big file
+ 169 This is a big file
+ 170 This is a big file
+ 171 This is a big file
+ 172 This is a big file
+ 173 This is a big file
+ 174 This is a big file
+ 175 This is a big file
+ 176 This is a big file
+ 177 This is a big file
+ 178 This is a big file
+ 179 This is a big file
+ 180 This is a big file
+ 181 This is a big file
+ 182 This is a big file
+ 183 This is a big file
+ 184 This is a big file
+ 185 This is a big file
+ 186 This is a big file
+ 187 This is a big file
+ 188 This is a big file
+ 189 This is a big file
+ 190 This is a big file
+ 191 This is a big file
+ 192 This is a big file
+ 193 This is a big file
+ 194 This is a big file
+ 195 This is a big file
+ 196 This is a big file
+ 197 This is a big file
+ 198 This is a big file
+ 199 This is a big file
+ 200 This is a big file
+ 201 This is a big file
+ 202 This is a big file
+ 203 This is a big file
+ 204 This is a big file
+ 205 This is a big file
+ 206 This is a big file
+ 207 This is a big file
+ 208 This is a big file
+ 209 This is a big file
+ 210 This is a big file
+ 211 This is a big file
+ 212 This is a big file
+ 213 This is a big file
+ 214 This is a big file
+ 215 This is a big file
+ 216 This is a big file
+ 217 This is a big file
+ 218 This is a big file
+ 219 This is a big file
+ 220 This is a big file
+ 221 This is a big file
+ 222 This is a big file
+ 223 This is a big file
+ 224 This is a big file
+ 225 This is a big file
+ 226 This is a big file
+ 227 This is a big file
+ 228 This is a big file
+ 229 This is a big file
+ 230 This is a big file
+ 231 This is a big file
+ 232 This is a big file
+ 233 This is a big file
+ 234 This is a big file
+ 235 This is a big file
+ 236 This is a big file
+ 237 This is a big file
+ 238 This is a big file
+ 239 This is a big file
+ 240 This is a big file
+ 241 This is a big file
+ 242 This is a big file
+ 243 This is a big file
+ 244 This is a big file
+ 245 This is a big file
+ 246 This is a big file
+ 247 This is a big file
+ 248 This is a big file
+ 249 This is a big file
+ 250 This is a big file
+ 251 This is a big file
+ 252 This is a big file
+ 253 This is a big file
+ 254 This is a big file
+ 255 This is a big file
+ 256 This is a big file
+ 257 This is a big file
+ 258 This is a big file
+ 259 This is a big file
+ 260 This is a big file
+ 261 This is a big file
+ 262 This is a big file
+ 263 This is a big file
+ 264 This is a big file
+ 265 This is a big file
+ 266 This is a big file
+ 267 This is a big file
+ 268 This is a big file
+ 269 This is a big file
+ 270 This is a big file
+ 271 This is a big file
+ 272 This is a big file
+ 273 This is a big file
+ 274 This is a big file
+ 275 This is a big file
+ 276 This is a big file
+ 277 This is a big file
+ 278 This is a big file
+ 279 This is a big file
+ 280 This is a big file
+ 281 This is a big file
+ 282 This is a big file
+ 283 This is a big file
+ 284 This is a big file
+ 285 This is a big file
+ 286 This is a big file
+ 287 This is a big file
+ 288 This is a big file
+ 289 This is a big file
+ 290 This is a big file
+ 291 This is a big file
+ 292 This is a big file
+ 293 This is a big file
+ 294 This is a big file
+ 295 This is a big file
+ 296 This is a big file
+ 297 This is a big file
+ 298 This is a big file
+ 299 This is a big file
+ 300 This is a big file
+ 301 This is a big file
+ 302 This is a big file
+ 303 This is a big file
+ 304 This is a big file
+ 305 This is a big file
+ 306 This is a big file
+ 307 This is a big file
+ 308 This is a big file
+ 309 This is a big file
+ 310 This is a big file
+ 311 This is a big file
+ 312 This is a big file
+ 313 This is a big file
+ 314 This is a big file
+ 315 This is a big file
+ 316 This is a big file
+ 317 This is a big file
+ 318 This is a big file
+ 319 This is a big file
+ 320 This is a big file
+ 321 This is a big file
+ 322 This is a big file
+ 323 This is a big file
+ 324 This is a big file
+ 325 This is a big file
+ 326 This is a big file
+ 327 This is a big file
+ 328 This is a big file
+ 329 This is a big file
+ 330 This is a big file
+ 331 This is a big file
+ 332 This is a big file
+ 333 This is a big file
+ 334 This is a big file
+ 335 This is a big file
+ 336 This is a big file
+ 337 This is a big file
+ 338 This is a big file
+ 339 This is a big file
+ 340 This is a big file
+ 341 This is a big file
+ 342 This is a big file
+ 343 This is a big file
+ 344 This is a big file
+ 345 This is a big file
+ 346 This is a big file
+ 347 This is a big file
+ 348 This is a big file
+ 349 This is a big file
+ 350 This is a big file
+ 351 This is a big file
+ 352 This is a big file
+ 353 This is a big file
+ 354 This is a big file
+ 355 This is a big file
+ 356 This is a big file
+ 357 This is a big file
+ 358 This is a big file
+ 359 This is a big file
+ 360 This is a big file
+ 361 This is a big file
+ 362 This is a big file
+ 363 This is a big file
+ 364 This is a big file
+ 365 This is a big file
+ 366 This is a big file
+ 367 This is a big file
+ 368 This is a big file
+ 369 This is a big file
+ 370 This is a big file
+ 371 This is a big file
+ 372 This is a big file
+ 373 This is a big file
+ 374 This is a big file
+ 375 This is a big file
+ 376 This is a big file
+ 377 This is a big file
+ 378 This is a big file
+ 379 This is a big file
+ 380 This is a big file
+ 381 This is a big file
+ 382 This is a big file
+ 383 This is a big file
+ 384 This is a big file
+ 385 This is a big file
+ 386 This is a big file
+ 387 This is a big file
+ 388 This is a big file
+ 389 This is a big file
+ 390 This is a big file
+ 391 This is a big file
+ 392 This is a big file
+ 393 This is a big file
+ 394 This is a big file
+ 395 This is a big file
+ 396 This is a big file
+ 397 This is a big file
+ 398 This is a big file
+ 399 This is a big file
+ 400 This is a big file
diff --git a/third_party/jetty-server/src/test/resources/simple/directory/content.txt b/third_party/jetty-server/src/test/resources/simple/directory/content.txt
new file mode 100644
index 0000000..6b584e8
--- /dev/null
+++ b/third_party/jetty-server/src/test/resources/simple/directory/content.txt
@@ -0,0 +1 @@
+content
\ No newline at end of file
diff --git a/third_party/jetty-server/src/test/resources/simple/directory/welcome.txt b/third_party/jetty-server/src/test/resources/simple/directory/welcome.txt
new file mode 100644
index 0000000..5ab2f8a
--- /dev/null
+++ b/third_party/jetty-server/src/test/resources/simple/directory/welcome.txt
@@ -0,0 +1 @@
+Hello
\ No newline at end of file
diff --git a/third_party/jetty-server/src/test/resources/simple/simple.txt b/third_party/jetty-server/src/test/resources/simple/simple.txt
new file mode 100644
index 0000000..f2403ae
--- /dev/null
+++ b/third_party/jetty-server/src/test/resources/simple/simple.txt
@@ -0,0 +1 @@
+simple text
\ No newline at end of file
diff --git a/third_party/jetty-util/.gitignore b/third_party/jetty-util/.gitignore
new file mode 100644
index 0000000..89c493b
--- /dev/null
+++ b/third_party/jetty-util/.gitignore
@@ -0,0 +1,7 @@
+.project
+.classpath
+.settings/
+.pmd
+target/
+*.swp
+*.log
diff --git a/third_party/jetty-util/pom.xml b/third_party/jetty-util/pom.xml
new file mode 100644
index 0000000..4f6fbde
--- /dev/null
+++ b/third_party/jetty-util/pom.xml
@@ -0,0 +1,104 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-project</artifactId>
+ <version>9.4.44.v20210927</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>jetty-util</artifactId>
+ <name>Jetty :: Utilities</name>
+ <description>Utility classes for Jetty</description>
+ <properties>
+ <bundle-symbolic-name>${project.groupId}.util</bundle-symbolic-name>
+ <spotbugs.onlyAnalyze>org.eclipse.jetty.util.*</spotbugs.onlyAnalyze>
+ </properties>
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <filtering>true</filtering>
+ </resource>
+ </resources>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>buildnumber-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>create-buildnumber</id>
+ <goals>
+ <goal>create</goal>
+ </goals>
+ <configuration>
+ <doCheck>false</doCheck>
+ <doUpdate>false</doUpdate>
+ <revisionOnScmFailure>${nonCanonicalRevision}</revisionOnScmFailure>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Export-Package>org.eclipse.jetty.util;version="${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}";uses:="org.eclipse.jetty.util.annotation,org.eclipse.jetty.util.component,org.eclipse.jetty.util.log,org.eclipse.jetty.util.resource,org.eclipse.jetty.util.thread";-noimport:=true,*</Export-Package>
+ <Require-Capability>osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.util.security.CredentialProvider)";resolution:=optional;cardinality:=multiple, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional</Require-Capability>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables>
+ <mavenRepoPath>${settings.localRepository}</mavenRepoPath>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+ <dependencies>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.toolchain</groupId>
+ <artifactId>jetty-perf-helper</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.toolchain</groupId>
+ <artifactId>jetty-test-helper</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <scope>provided</scope>
+ <optional>true</optional>
+ </dependency>
+ <!--
+ This dependency is used to test Slf4jLog.
+ Due to the introduction of src/test/resource/jetty-logging.properties (and the Log.static{} block)
+ the default Log implementation is still StdErrLog during testing.
+ -->
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-jdk14</artifactId>
+ <version>${slf4j.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/third_party/jetty-util/src/main/assembly/config.xml b/third_party/jetty-util/src/main/assembly/config.xml
new file mode 100644
index 0000000..b0592ae
--- /dev/null
+++ b/third_party/jetty-util/src/main/assembly/config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<assembly>
+ <id>config</id>
+ <includeBaseDirectory>false</includeBaseDirectory>
+ <formats>
+ <format>jar</format>
+ </formats>
+ <fileSets>
+
+ <fileSet>
+ <directory>src/main/config</directory>
+ <outputDirectory></outputDirectory>
+ <includes>
+ <include>**</include>
+ </includes>
+ </fileSet>
+ </fileSets>
+
+</assembly>
+
diff --git a/third_party/jetty-util/src/main/assembly/perf-tests.xml b/third_party/jetty-util/src/main/assembly/perf-tests.xml
new file mode 100644
index 0000000..0ac6e21
--- /dev/null
+++ b/third_party/jetty-util/src/main/assembly/perf-tests.xml
@@ -0,0 +1,28 @@
+<assembly
+ xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
+ <id>perf-tests</id>
+ <formats>
+ <format>jar</format>
+ </formats>
+ <includeBaseDirectory>false</includeBaseDirectory>
+ <dependencySets>
+ <dependencySet>
+ <outputDirectory>/</outputDirectory>
+ <useProjectArtifact>true</useProjectArtifact>
+ <unpack>true</unpack>
+ <scope>test</scope>
+ </dependencySet>
+ </dependencySets>
+ <fileSets>
+ <fileSet>
+ <directory>${project.build.directory}/test-classes</directory>
+ <outputDirectory>/</outputDirectory>
+ <includes>
+ <include>**/*</include>
+ </includes>
+ <useDefaultExcludes>true</useDefaultExcludes>
+ </fileSet>
+ </fileSets>
+</assembly>
\ No newline at end of file
diff --git a/third_party/jetty-util/src/main/config/etc/console-capture.xml b/third_party/jetty-util/src/main/config/etc/console-capture.xml
new file mode 100644
index 0000000..806f61b
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/etc/console-capture.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
+
+<Configure id="logging" class="org.eclipse.jetty.util.log.Log">
+ <New id="ServerLog" class="java.io.PrintStream">
+ <Arg>
+ <New class="org.eclipse.jetty.util.RolloverFileOutputStream">
+ <Arg><Property name="jetty.console-capture.dir" deprecated="jetty.logging.dir" default="./logs"/>/yyyy_mm_dd.jetty.log</Arg>
+ <Arg type="boolean"><Property name="jetty.console-capture.append" deprecated="jetty.logging.append" default="false"/></Arg>
+ <Arg type="int"><Property name="jetty.console-capture.retainDays" deprecated="jetty.logging.retainDays" default="90"/></Arg>
+ <Arg>
+ <Call class="java.util.TimeZone" name="getTimeZone">
+ <Arg><Property name="jetty.console-capture.timezone" deprecated="jetty.logging.timezone" default="GMT"/></Arg>
+ </Call>
+ </Arg>
+ <Get id="ServerLogName" name="datedFilename"/>
+ </New>
+ </Arg>
+ </New>
+
+ <Get name="rootLogger">
+ <Call name="info"><Arg>Console stderr/stdout captured to <Ref refid="ServerLogName"/></Arg></Call>
+ </Get>
+ <Call class="java.lang.System" name="setErr"><Arg><Ref refid="ServerLog"/></Arg></Call>
+ <Call class="java.lang.System" name="setOut"><Arg><Ref refid="ServerLog"/></Arg></Call>
+</Configure>
diff --git a/third_party/jetty-util/src/main/config/modules/console-capture.mod b/third_party/jetty-util/src/main/config/modules/console-capture.mod
new file mode 100644
index 0000000..16edf24
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/console-capture.mod
@@ -0,0 +1,30 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Redirects JVMs console stderr and stdout to a log file,
+including output from Jetty's default StdErrLog logging.
+
+[tags]
+logging
+
+[xml]
+etc/console-capture.xml
+
+[files]
+logs/
+
+[lib]
+resources/
+
+[ini-template]
+## Logging directory (relative to $jetty.base)
+# jetty.console-capture.dir=./logs
+
+## Whether to append to existing file
+# jetty.console-capture.append=true
+
+## How many days to retain old log files
+# jetty.console-capture.retainDays=90
+
+## Timezone of the log timestamps
+# jetty.console-capture.timezone=GMT
diff --git a/third_party/jetty-util/src/main/config/modules/jcl-slf4j.mod b/third_party/jetty-util/src/main/config/modules/jcl-slf4j.mod
new file mode 100644
index 0000000..7efaf1c
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/jcl-slf4j.mod
@@ -0,0 +1,25 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Provides a Java Commons Logging (JCL) binding to SLF4J logging.
+
+[tags]
+logging
+jcl
+slf4j
+internal
+
+[depends]
+slf4j-api
+slf4j-impl
+
+[provides]
+jcl-api
+jcl-impl
+slf4j+jcl
+
+[files]
+maven://org.slf4j/jcl-over-slf4j/${slf4j.version}|lib/slf4j/jcl-over-slf4j-${slf4j.version}.jar
+
+[lib]
+lib/slf4j/jcl-over-slf4j-${slf4j.version}.jar
diff --git a/third_party/jetty-util/src/main/config/modules/jul-impl.mod b/third_party/jetty-util/src/main/config/modules/jul-impl.mod
new file mode 100644
index 0000000..b20d8b0
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/jul-impl.mod
@@ -0,0 +1,20 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Configures the Java Util Logging mechanism
+
+[tags]
+logging
+jul
+internal
+
+[provides]
+jul-api
+jul-impl
+
+[files]
+basehome:modules/jul-impl
+
+[exec]
+-Djava.util.logging.config.file?=${jetty.base}/etc/java-util-logging.properties
+
diff --git a/third_party/jetty-util/src/main/config/modules/jul-impl/etc/java-util-logging.properties b/third_party/jetty-util/src/main/config/modules/jul-impl/etc/java-util-logging.properties
new file mode 100644
index 0000000..867df05
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/jul-impl/etc/java-util-logging.properties
@@ -0,0 +1,12 @@
+.level = INFO
+
+handlers = java.util.logging.ConsoleHandler
+java.util.logging.ConsoleHandler.level = INFO
+java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
+
+#handlers = java.util.logging.FileHandler
+#java.util.logging.FileHandler.pattern = ${jetty.logging.dir}/jetty%u.log
+#java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
+
+java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
+
diff --git a/third_party/jetty-util/src/main/config/modules/jul-slf4j.mod b/third_party/jetty-util/src/main/config/modules/jul-slf4j.mod
new file mode 100644
index 0000000..9ff3602
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/jul-slf4j.mod
@@ -0,0 +1,28 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Provides a Java Util Logging binding to SLF4J logging.
+
+[tags]
+logging
+slf4j
+internal
+
+[depends]
+slf4j-api
+slf4j-impl
+
+[provides]
+jul-api
+jul-impl
+slf4j+jul
+
+[files]
+maven://org.slf4j/jul-to-slf4j/${slf4j.version}|lib/slf4j/jul-to-slf4j-${slf4j.version}.jar
+basehome:modules/jul-slf4j
+
+[lib]
+lib/slf4j/jul-to-slf4j-${slf4j.version}.jar
+
+[exec]
+-Djava.util.logging.config.file?=${jetty.base}/etc/java-util-logging.properties
diff --git a/third_party/jetty-util/src/main/config/modules/jul-slf4j/etc/java-util-logging.properties b/third_party/jetty-util/src/main/config/modules/jul-slf4j/etc/java-util-logging.properties
new file mode 100644
index 0000000..9c03ec8
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/jul-slf4j/etc/java-util-logging.properties
@@ -0,0 +1,2 @@
+handlers = org.slf4j.bridge.SLF4JBridgeHandler
+.level = INFO
diff --git a/third_party/jetty-util/src/main/config/modules/log4j-impl.mod b/third_party/jetty-util/src/main/config/modules/log4j-impl.mod
new file mode 100644
index 0000000..bfe9bcb
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/log4j-impl.mod
@@ -0,0 +1,32 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Provides a Log4j v1.2 API and implementation.
+To receive jetty logs enable the jetty-slf4j and slf4j-log4j modules.
+
+[tags]
+logging
+log4j
+internal
+
+[depends]
+resources
+
+[provides]
+log4j-api
+log4j-impl
+
+[files]
+maven://log4j/log4j/${log4j.version}|lib/log4j/log4j-${log4j.version}.jar
+basehome:modules/log4j-impl
+
+[lib]
+lib/log4j/log4j-${log4j.version}.jar
+
+[license]
+Log4j is released under the Apache 2.0 license.
+http://www.apache.org/licenses/LICENSE-2.0.html
+
+[ini]
+log4j.version?=@log4j.version@
+jetty.webapp.addServerClasses+=,${jetty.base.uri}/lib/log4j/
diff --git a/third_party/jetty-util/src/main/config/modules/log4j-impl/resources/log4j.xml b/third_party/jetty-util/src/main/config/modules/log4j-impl/resources/log4j.xml
new file mode 100644
index 0000000..f4554f8
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/log4j-impl/resources/log4j.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
+
+<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
+ <appender name="console" class="org.apache.log4j.ConsoleAppender">
+ <layout class="org.apache.log4j.PatternLayout">
+ <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n" />
+ </layout>
+ </appender>
+
+ <!--
+ <appender name="file" class="org.apache.log4j.RollingFileAppender">
+ <param name="File" value="${jetty.logging.dir}/jetty.log" />
+ <param name="MaxFileSize" value="100MB" />
+ <param name="MaxBackupIndex" value="10" />
+ <layout class="org.apache.log4j.PatternLayout">
+ <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
+ </layout>
+ </appender>
+ -->
+
+ <root>
+ <level value="INFO" />
+ <appender-ref ref="console" />
+ <!--<appender-ref ref="file" />-->
+ </root>
+
+</log4j:configuration>
diff --git a/third_party/jetty-util/src/main/config/modules/log4j2-api.mod b/third_party/jetty-util/src/main/config/modules/log4j2-api.mod
new file mode 100644
index 0000000..cb0878b
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/log4j2-api.mod
@@ -0,0 +1,28 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Provides the Log4j v2 API
+
+[tags]
+logging
+log4j2
+log4j
+internal
+
+[provides]
+log4j2-api
+
+[files]
+maven://org.apache.logging.log4j/log4j-api/${log4j2.version}|lib/log4j2/log4j-api-${log4j2.version}.jar
+
+[lib]
+lib/log4j2/log4j-api-${log4j2.version}.jar
+
+[license]
+Log4j is released under the Apache 2.0 license.
+http://www.apache.org/licenses/LICENSE-2.0.html
+
+[ini]
+log4j2.version?=2.14.0
+disruptor.version=3.4.2
+jetty.webapp.addServerClasses+=,${jetty.base.uri}/lib/log4j2/
diff --git a/third_party/jetty-util/src/main/config/modules/log4j2-impl.mod b/third_party/jetty-util/src/main/config/modules/log4j2-impl.mod
new file mode 100644
index 0000000..e758eb0
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/log4j2-impl.mod
@@ -0,0 +1,27 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Provides a Log4j v2 implementation.
+To receive jetty logs enable the jetty-slf4j, slf4j-log4j and log4j-log4j2 modules.
+
+[tags]
+logging
+log4j2
+log4j
+internal
+
+[depends]
+log4j2-api
+resources
+
+[provides]
+log4j2-impl
+
+[files]
+maven://org.apache.logging.log4j/log4j-core/${log4j2.version}|lib/log4j2/log4j-core-${log4j2.version}.jar
+maven://com.lmax/disruptor/${disruptor.version}|lib/log4j2/disruptor-${disruptor.version}.jar
+basehome:modules/log4j2-impl
+
+[lib]
+lib/log4j2/*.jar
+
diff --git a/third_party/jetty-util/src/main/config/modules/log4j2-impl/resources/log4j2.xml b/third_party/jetty-util/src/main/config/modules/log4j2-impl/resources/log4j2.xml
new file mode 100644
index 0000000..400c779
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/log4j2-impl/resources/log4j2.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="warn" name="Jetty" >
+
+ <properties>
+ <property name="logging.dir">${sys:jetty.logging.dir:-logs}</property>
+ </properties>
+
+ <Appenders>
+ <Console name="console" target="SYSTEM_ERR">
+ <PatternLayout>
+ <Pattern>%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n</Pattern>
+ </PatternLayout>
+ </Console>
+
+
+ <RollingRandomAccessFile name="file"
+ fileName="${logging.dir}/jetty.log"
+ filePattern="${logging.dir}/jetty-%d{MM-dd-yyyy}.log.gz"
+ ignoreExceptions="false">
+ <PatternLayout>
+ <Pattern>%d [%t] %-5p %c %x - %m%n</Pattern>
+ </PatternLayout>
+
+ <Policies>
+ <TimeBasedTriggeringPolicy />
+ <SizeBasedTriggeringPolicy size="10 MB"/>
+ </Policies>
+ </RollingRandomAccessFile>
+
+ </Appenders>
+
+ <Loggers>
+ <Root level="info">
+ <AppenderRef ref="console"/>
+ </Root>
+ <!--
+ To have all logger async
+ <AsyncRoot level="info">
+ <AppenderRef ref="file"/>
+ </AsyncRoot>
+ -->
+ </Loggers>
+
+</Configuration>
+
diff --git a/third_party/jetty-util/src/main/config/modules/log4j2-slf4j.mod b/third_party/jetty-util/src/main/config/modules/log4j2-slf4j.mod
new file mode 100644
index 0000000..dd012fa
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/log4j2-slf4j.mod
@@ -0,0 +1,25 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Provides a Log4j v2 binding to SLF4J logging.
+
+[tags]
+logging
+log4j2
+log4j
+slf4j
+internal
+
+[depends]
+log4j2-api
+slf4j-api
+
+[provides]
+log4j2-impl
+
+[files]
+maven://org.apache.logging.log4j/log4j-to-slf4j/${log4j2.version}|lib/log4j2/log4j-to-slf4j-${log4j2.version}.jar
+
+[lib]
+lib/log4j2/log4j-slf4j-to-${log4j2.version}.jar
+
diff --git a/third_party/jetty-util/src/main/config/modules/logback-impl.mod b/third_party/jetty-util/src/main/config/modules/logback-impl.mod
new file mode 100644
index 0000000..05b6df0
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/logback-impl.mod
@@ -0,0 +1,40 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Provides the logback core implementation
+and logback-access
+
+[tags]
+logging
+internal
+
+[depends]
+resources
+
+[files]
+maven://ch.qos.logback/logback-core/${logback.version}|lib/logback/logback-core-${logback.version}.jar
+basehome:modules/logback-impl
+
+[lib]
+lib/logback/logback-core-${logback.version}.jar
+
+[license]
+Logback: the reliable, generic, fast and flexible logging framework.
+Copyright (C) 1999-2012, QOS.ch. All rights reserved.
+
+This program and the accompanying materials are dual-licensed under
+either:
+
+ the terms of the Eclipse Public License v1.0
+ as published by the Eclipse Foundation:
+ http://www.eclipse.org/legal/epl-v10.html
+
+or (per the licensee's choosing) under
+
+ the terms of the GNU Lesser General Public License version 2.1
+ as published by the Free Software Foundation:
+ http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
+
+[ini]
+logback.version?=@logback.version@
+jetty.webapp.addServerClasses+=,${jetty.base.uri}/lib/logback/
diff --git a/third_party/jetty-util/src/main/config/modules/logback-impl/resources/logback.xml b/third_party/jetty-util/src/main/config/modules/logback-impl/resources/logback.xml
new file mode 100644
index 0000000..552845e
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/logback-impl/resources/logback.xml
@@ -0,0 +1,31 @@
+<configuration>
+ <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
+
+ <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+ <!-- encoders are assigned the type
+ ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
+ <encoder>
+ <pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>
+ </encoder>
+ </appender>
+
+ <!--
+ <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <file>logs/jetty.log</file>
+ <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+ <fileNamePattern>logs/jetty-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+ <maxFileSize>100MB</maxFileSize>
+ <maxHistory>10</maxHistory>
+ <totalSizeCap>2GB</totalSizeCap>
+ </rollingPolicy>
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+ -->
+
+ <root level="INFO">
+ <appender-ref ref="console" />
+ </root>
+</configuration>
+
diff --git a/third_party/jetty-util/src/main/config/modules/logging-jetty.mod b/third_party/jetty-util/src/main/config/modules/logging-jetty.mod
new file mode 100644
index 0000000..1a161b0
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/logging-jetty.mod
@@ -0,0 +1,17 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Configure jetty logging mechanism.
+Provides a ${jetty.base}/resources/jetty-logging.properties.
+
+[tags]
+logging
+
+[depends]
+resources
+
+[provides]
+logging
+
+[files]
+basehome:modules/logging-jetty
diff --git a/third_party/jetty-util/src/main/config/modules/logging-jetty/resources/jetty-logging.properties b/third_party/jetty-util/src/main/config/modules/logging-jetty/resources/jetty-logging.properties
new file mode 100644
index 0000000..07e0435
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/logging-jetty/resources/jetty-logging.properties
@@ -0,0 +1,12 @@
+## Force jetty logging implementation
+#org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
+
+## Set logging levels from: ALL, DEBUG, INFO, WARN, OFF
+#org.eclipse.jetty.LEVEL=INFO
+#com.example.LEVEL=INFO
+
+## Hide stacks traces in logs?
+#com.example.STACKS=false
+
+## Show the source file of a log location?
+#com.example.SOURCE=false
diff --git a/third_party/jetty-util/src/main/config/modules/logging-jul.mod b/third_party/jetty-util/src/main/config/modules/logging-jul.mod
new file mode 100644
index 0000000..b69c712
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/logging-jul.mod
@@ -0,0 +1,18 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Configure jetty logging to use Java Util Logging (jul)
+SLF4J is used as the core logging mechanism.
+
+[tags]
+logging
+
+[depends]
+slf4j-jul
+jul-impl
+
+[provides]
+logging
+
+[exec]
+-Dorg.eclipse.jetty.util.log.class?=org.eclipse.jetty.util.log.Slf4jLog
diff --git a/third_party/jetty-util/src/main/config/modules/logging-log4j.mod b/third_party/jetty-util/src/main/config/modules/logging-log4j.mod
new file mode 100644
index 0000000..4853cb4
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/logging-log4j.mod
@@ -0,0 +1,18 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Configure jetty logging to use Log4j Logging
+SLF4J is used as the core logging mechanism.
+
+[tags]
+logging
+
+[depends]
+slf4j-log4j
+log4j-impl
+
+[provides]
+logging
+
+[exec]
+-Dorg.eclipse.jetty.util.log.class?=org.eclipse.jetty.util.log.Slf4jLog
diff --git a/third_party/jetty-util/src/main/config/modules/logging-log4j2.mod b/third_party/jetty-util/src/main/config/modules/logging-log4j2.mod
new file mode 100644
index 0000000..97d66d8
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/logging-log4j2.mod
@@ -0,0 +1,18 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Configure jetty logging to use log4j version 2
+SLF4J is used as the core logging mechanism.
+
+[tags]
+logging
+
+[depends]
+slf4j-log4j2
+log4j2-impl
+
+[provides]
+logging
+
+[exec]
+-Dorg.eclipse.jetty.util.log.class?=org.eclipse.jetty.util.log.Slf4jLog
diff --git a/third_party/jetty-util/src/main/config/modules/logging-logback.mod b/third_party/jetty-util/src/main/config/modules/logging-logback.mod
new file mode 100644
index 0000000..60a03fe
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/logging-logback.mod
@@ -0,0 +1,18 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Configure jetty logging to use Logback Logging.
+SLF4J is used as the core logging mechanism.
+
+[tags]
+logging
+
+[depends]
+slf4j-logback
+logback-impl
+
+[provides]
+logging
+
+[exec]
+-Dorg.eclipse.jetty.util.log.class?=org.eclipse.jetty.util.log.Slf4jLog
diff --git a/third_party/jetty-util/src/main/config/modules/logging-slf4j.mod b/third_party/jetty-util/src/main/config/modules/logging-slf4j.mod
new file mode 100644
index 0000000..f0e684d
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/logging-slf4j.mod
@@ -0,0 +1,18 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Configure jetty logging to use slf4j.
+Any slf4j-impl implementation is used
+
+[tags]
+logging
+
+[depends]
+slf4j-api
+slf4j-impl
+
+[provides]
+logging
+
+[exec]
+-Dorg.eclipse.jetty.util.log.class?=org.eclipse.jetty.util.log.Slf4jLog
diff --git a/third_party/jetty-util/src/main/config/modules/slf4j-api.mod b/third_party/jetty-util/src/main/config/modules/slf4j-api.mod
new file mode 100644
index 0000000..b7146c3
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/slf4j-api.mod
@@ -0,0 +1,44 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Provides SLF4J API. Requires a slf4j implementation (eg slf4j-simple-impl)
+otherwise a noop implementation is used.
+
+[tags]
+logging
+slf4j
+internal
+
+[files]
+maven://org.slf4j/slf4j-api/${slf4j.version}|lib/slf4j/slf4j-api-${slf4j.version}.jar
+
+[lib]
+lib/slf4j/slf4j-api-${slf4j.version}.jar
+
+[ini]
+slf4j.version?=@slf4j.version@
+jetty.webapp.addServerClasses+=,${jetty.base.uri}/lib/slf4j/
+
+[license]
+SLF4J is distributed under the MIT License.
+Copyright (c) 2004-2013 QOS.ch
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/third_party/jetty-util/src/main/config/modules/slf4j-jul.mod b/third_party/jetty-util/src/main/config/modules/slf4j-jul.mod
new file mode 100644
index 0000000..806bb51
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/slf4j-jul.mod
@@ -0,0 +1,22 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Provides a SLF4J binding to Java Util Logging (JUL) logging.
+
+[tags]
+logging
+slf4j
+internal
+
+[depends]
+slf4j-api
+
+[provides]
+slf4j-impl
+slf4j+jul
+
+[files]
+maven://org.slf4j/slf4j-jdk14/${slf4j.version}|lib/slf4j/slf4j-jdk14-${slf4j.version}.jar
+
+[lib]
+lib/slf4j/slf4j-jdk14-${slf4j.version}.jar
diff --git a/third_party/jetty-util/src/main/config/modules/slf4j-log4j.mod b/third_party/jetty-util/src/main/config/modules/slf4j-log4j.mod
new file mode 100644
index 0000000..8e4dcd2
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/slf4j-log4j.mod
@@ -0,0 +1,25 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Provides a SLF4J binding to the Log4j v1.2 API logging.
+
+[tags]
+logging
+log4j
+slf4j
+internal
+
+[depends]
+slf4j-api
+log4j-api
+log4j-impl
+
+[provides]
+slf4j-impl
+
+[files]
+maven://org.slf4j/slf4j-log4j12/${slf4j.version}|lib/slf4j/slf4j-log4j12-${slf4j.version}.jar
+
+[lib]
+lib/slf4j/slf4j-log4j12-${slf4j.version}.jar
+
diff --git a/third_party/jetty-util/src/main/config/modules/slf4j-log4j2.mod b/third_party/jetty-util/src/main/config/modules/slf4j-log4j2.mod
new file mode 100644
index 0000000..20e2fc8
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/slf4j-log4j2.mod
@@ -0,0 +1,25 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Provides a SLF4J binding to Log4j v2 logging.
+
+[tags]
+logging
+log4j2
+log4j
+slf4j
+internal
+
+[depends]
+slf4j-api
+log4j2-api
+log4j2-impl
+
+[provides]
+slf4j-impl
+
+[files]
+maven://org.apache.logging.log4j/log4j-slf4j-impl/${log4j2.version}|lib/log4j2/log4j-slf4j-impl-${log4j2.version}.jar
+
+[lib]
+lib/log4j2/log4j-slf4j-impl-${log4j2.version}.jar
diff --git a/third_party/jetty-util/src/main/config/modules/slf4j-logback.mod b/third_party/jetty-util/src/main/config/modules/slf4j-logback.mod
new file mode 100644
index 0000000..ca29d7c
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/slf4j-logback.mod
@@ -0,0 +1,24 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Provides a SLF4J binding to Logback logging.
+
+[tags]
+logging
+slf4j
+internal
+
+[depends]
+slf4j-api
+logback-impl
+resources
+
+[provides]
+slf4j-impl
+
+[files]
+maven://ch.qos.logback/logback-classic/${logback.version}|lib/logback/logback-classic-${logback.version}.jar
+
+[lib]
+lib/logback/logback-classic-${logback.version}.jar
+
diff --git a/third_party/jetty-util/src/main/config/modules/slf4j-simple-impl.mod b/third_party/jetty-util/src/main/config/modules/slf4j-simple-impl.mod
new file mode 100644
index 0000000..ca87507
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/slf4j-simple-impl.mod
@@ -0,0 +1,24 @@
+# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
+
+[description]
+Provides SLF4J simple logging implementation.
+To receive jetty logs enable the jetty-slf4j module.
+
+[tags]
+logging
+slf4j
+internal
+
+[depends]
+slf4j-api
+resources
+
+[provides]
+slf4j-impl
+
+[files]
+maven://org.slf4j/slf4j-simple/${slf4j.version}|lib/slf4j/slf4j-simple-${slf4j.version}.jar
+basehome:modules/slf4j-simple-impl
+
+[lib]
+lib/slf4j/slf4j-simple-${slf4j.version}.jar
diff --git a/third_party/jetty-util/src/main/config/modules/slf4j-simple-impl/resources/simplelogger.properties b/third_party/jetty-util/src/main/config/modules/slf4j-simple-impl/resources/simplelogger.properties
new file mode 100644
index 0000000..060cf1c
--- /dev/null
+++ b/third_party/jetty-util/src/main/config/modules/slf4j-simple-impl/resources/simplelogger.properties
@@ -0,0 +1,6 @@
+#org.slf4j.simpleLogger.logFile=logs/jetty.log
+org.slf4j.simpleLogger.defaultLogLevel=info
+org.slf4j.simpleLogger.showDateTime=true
+org.slf4j.simpleLogger.showThreadName=true
+
+#org.slf4j.simpleLogger.log.org.eclipse.jetty=debug
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/AbstractTrie.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/AbstractTrie.java
new file mode 100644
index 0000000..4a45caf
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/AbstractTrie.java
@@ -0,0 +1,84 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Abstract Trie implementation.
+ * <p>Provides some common implementations, which may not be the most
+ * efficient. For byte operations, the assumption is made that the charset
+ * is ISO-8859-1</p>
+ *
+ * @param <V> the type of object that the Trie holds
+ */
+public abstract class AbstractTrie<V> implements Trie<V>
+{
+ final boolean _caseInsensitive;
+
+ protected AbstractTrie(boolean insensitive)
+ {
+ _caseInsensitive = insensitive;
+ }
+
+ @Override
+ public boolean put(V v)
+ {
+ return put(v.toString(), v);
+ }
+
+ @Override
+ public V remove(String s)
+ {
+ V o = get(s);
+ put(s, null);
+ return o;
+ }
+
+ @Override
+ public V get(String s)
+ {
+ return get(s, 0, s.length());
+ }
+
+ @Override
+ public V get(ByteBuffer b)
+ {
+ return get(b, 0, b.remaining());
+ }
+
+ @Override
+ public V getBest(String s)
+ {
+ return getBest(s, 0, s.length());
+ }
+
+ @Override
+ public V getBest(byte[] b, int offset, int len)
+ {
+ return getBest(new String(b, offset, len, StandardCharsets.ISO_8859_1));
+ }
+
+ @Override
+ public boolean isCaseInsensitive()
+ {
+ return _caseInsensitive;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTernaryTrie.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTernaryTrie.java
new file mode 100644
index 0000000..110306c
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTernaryTrie.java
@@ -0,0 +1,716 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.nio.ByteBuffer;
+import java.util.AbstractMap;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * <p>A Ternary Trie String lookup data structure.</p>
+ * <p>
+ * This Trie is of a fixed size and cannot grow (which can be a good thing with regards to DOS when used as a cache).
+ * </p>
+ * <p>
+ * The Trie is stored in 3 arrays:
+ * </p>
+ * <dl>
+ * <dt>char[] _tree</dt><dd>This is semantically 2 dimensional array flattened into a 1 dimensional char array. The second dimension
+ * is that every 4 sequential elements represents a row of: character; hi index; eq index; low index, used to build a
+ * ternary trie of key strings.</dd>
+ * <dt>String[] _key</dt><dd>An array of key values where each element matches a row in the _tree array. A non zero key element
+ * indicates that the _tree row is a complete key rather than an intermediate character of a longer key.</dd>
+ * <dt>V[] _value</dt><dd>An array of values corresponding to the _key array</dd>
+ * </dl>
+ * <p>The lookup of a value will iterate through the _tree array matching characters. If the equal tree branch is followed,
+ * then the _key array is looked up to see if this is a complete match. If a match is found then the _value array is looked up
+ * to return the matching value.
+ * </p>
+ * <p>
+ * This Trie may be instantiated either as case sensitive or insensitive.
+ * </p>
+ * <p>This Trie is not Threadsafe and contains no mutual exclusion
+ * or deliberate memory barriers. It is intended for an ArrayTrie to be
+ * built by a single thread and then used concurrently by multiple threads
+ * and not mutated during that access. If concurrent mutations of the
+ * Trie is required external locks need to be applied.
+ * </p>
+ *
+ * @param <V> the Entry type
+ */
+public class ArrayTernaryTrie<V> extends AbstractTrie<V>
+{
+ private static int LO = 1;
+ private static int EQ = 2;
+ private static int HI = 3;
+
+ /**
+ * The Size of a Trie row is the char, and the low, equal and high
+ * child pointers
+ */
+ private static final int ROW_SIZE = 4;
+
+ /**
+ * The Trie rows in a single array which allows a lookup of row,character
+ * to the next row in the Trie. This is actually a 2 dimensional
+ * array that has been flattened to achieve locality of reference.
+ */
+ private final char[] _tree;
+
+ /**
+ * The key (if any) for a Trie row.
+ * A row may be a leaf, a node or both in the Trie tree.
+ */
+ private final String[] _key;
+
+ /**
+ * The value (if any) for a Trie row.
+ * A row may be a leaf, a node or both in the Trie tree.
+ */
+ private final V[] _value;
+
+ /**
+ * The number of rows allocated
+ */
+ private char _rows;
+
+ /**
+ * Create a case insensitive Trie of default capacity.
+ */
+ public ArrayTernaryTrie()
+ {
+ this(128);
+ }
+
+ /**
+ * Create a Trie of default capacity
+ *
+ * @param insensitive true if the Trie is insensitive to the case of the key.
+ */
+ public ArrayTernaryTrie(boolean insensitive)
+ {
+ this(insensitive, 128);
+ }
+
+ /**
+ * Create a case insensitive Trie
+ *
+ * @param capacity The capacity of the Trie, which is in the worst case
+ * is the total number of characters of all keys stored in the Trie.
+ * The capacity needed is dependent of the shared prefixes of the keys.
+ * For example, a capacity of 6 nodes is required to store keys "foo"
+ * and "bar", but a capacity of only 4 is required to
+ * store "bar" and "bat".
+ */
+ public ArrayTernaryTrie(int capacity)
+ {
+ this(true, capacity);
+ }
+
+ /**
+ * Create a Trie
+ *
+ * @param insensitive true if the Trie is insensitive to the case of the key.
+ * @param capacity The capacity of the Trie, which is in the worst case
+ * is the total number of characters of all keys stored in the Trie.
+ * The capacity needed is dependent of the shared prefixes of the keys.
+ * For example, a capacity of 6 nodes is required to store keys "foo"
+ * and "bar", but a capacity of only 4 is required to
+ * store "bar" and "bat".
+ */
+ public ArrayTernaryTrie(boolean insensitive, int capacity)
+ {
+ super(insensitive);
+ _value = (V[])new Object[capacity];
+ _tree = new char[capacity * ROW_SIZE];
+ _key = new String[capacity];
+ }
+
+ /**
+ * Copy Trie and change capacity by a factor
+ *
+ * @param trie the trie to copy from
+ * @param factor the factor to grow the capacity by
+ */
+ public ArrayTernaryTrie(ArrayTernaryTrie<V> trie, double factor)
+ {
+ super(trie.isCaseInsensitive());
+ int capacity = (int)(trie._value.length * factor);
+ _rows = trie._rows;
+ _value = Arrays.copyOf(trie._value, capacity);
+ _tree = Arrays.copyOf(trie._tree, capacity * ROW_SIZE);
+ _key = Arrays.copyOf(trie._key, capacity);
+ }
+
+ @Override
+ public void clear()
+ {
+ _rows = 0;
+ Arrays.fill(_value, null);
+ Arrays.fill(_tree, (char)0);
+ Arrays.fill(_key, null);
+ }
+
+ @Override
+ public boolean put(String s, V v)
+ {
+ int t = 0;
+ int limit = s.length();
+ int last = 0;
+ for (int k = 0; k < limit; k++)
+ {
+ char c = s.charAt(k);
+ if (isCaseInsensitive() && c < 128)
+ c = StringUtil.lowercases[c];
+
+ while (true)
+ {
+ int row = ROW_SIZE * t;
+
+ // Do we need to create the new row?
+ if (t == _rows)
+ {
+ _rows++;
+ if (_rows >= _key.length)
+ {
+ _rows--;
+ return false;
+ }
+ _tree[row] = c;
+ }
+
+ char n = _tree[row];
+ int diff = n - c;
+ if (diff == 0)
+ t = _tree[last = (row + EQ)];
+ else if (diff < 0)
+ t = _tree[last = (row + LO)];
+ else
+ t = _tree[last = (row + HI)];
+
+ // do we need a new row?
+ if (t == 0)
+ {
+ t = _rows;
+ _tree[last] = (char)t;
+ }
+
+ if (diff == 0)
+ break;
+ }
+ }
+
+ // Do we need to create the new row?
+ if (t == _rows)
+ {
+ _rows++;
+ if (_rows >= _key.length)
+ {
+ _rows--;
+ return false;
+ }
+ }
+
+ // Put the key and value
+ _key[t] = v == null ? null : s;
+ _value[t] = v;
+
+ return true;
+ }
+
+ @Override
+ public V get(String s, int offset, int len)
+ {
+ int t = 0;
+ for (int i = 0; i < len; )
+ {
+ char c = s.charAt(offset + i++);
+ if (isCaseInsensitive() && c < 128)
+ c = StringUtil.lowercases[c];
+
+ while (true)
+ {
+ int row = ROW_SIZE * t;
+ char n = _tree[row];
+ int diff = n - c;
+
+ if (diff == 0)
+ {
+ t = _tree[row + EQ];
+ if (t == 0)
+ return null;
+ break;
+ }
+
+ t = _tree[row + hilo(diff)];
+ if (t == 0)
+ return null;
+ }
+ }
+
+ return _value[t];
+ }
+
+ @Override
+ public V get(ByteBuffer b, int offset, int len)
+ {
+ int t = 0;
+ offset += b.position();
+
+ for (int i = 0; i < len; )
+ {
+ byte c = (byte)(b.get(offset + i++) & 0x7f);
+ if (isCaseInsensitive())
+ c = (byte)StringUtil.lowercases[c];
+
+ while (true)
+ {
+ int row = ROW_SIZE * t;
+ char n = _tree[row];
+ int diff = n - c;
+
+ if (diff == 0)
+ {
+ t = _tree[row + EQ];
+ if (t == 0)
+ return null;
+ break;
+ }
+
+ t = _tree[row + hilo(diff)];
+ if (t == 0)
+ return null;
+ }
+ }
+
+ return _value[t];
+ }
+
+ @Override
+ public V getBest(String s)
+ {
+ return getBest(0, s, 0, s.length());
+ }
+
+ @Override
+ public V getBest(String s, int offset, int length)
+ {
+ return getBest(0, s, offset, length);
+ }
+
+ private V getBest(int t, String s, int offset, int len)
+ {
+ int node = t;
+ int end = offset + len;
+ loop:
+ while (offset < end)
+ {
+ char c = s.charAt(offset++);
+ len--;
+ if (isCaseInsensitive() && c < 128)
+ c = StringUtil.lowercases[c];
+
+ while (true)
+ {
+ int row = ROW_SIZE * t;
+ char n = _tree[row];
+ int diff = n - c;
+
+ if (diff == 0)
+ {
+ t = _tree[row + EQ];
+ if (t == 0)
+ break loop;
+
+ // if this node is a match, recurse to remember
+ if (_key[t] != null)
+ {
+ node = t;
+ V better = getBest(t, s, offset, len);
+ if (better != null)
+ return better;
+ }
+ break;
+ }
+
+ t = _tree[row + hilo(diff)];
+ if (t == 0)
+ break loop;
+ }
+ }
+ return _value[node];
+ }
+
+ @Override
+ public V getBest(ByteBuffer b, int offset, int len)
+ {
+ if (b.hasArray())
+ return getBest(0, b.array(), b.arrayOffset() + b.position() + offset, len);
+ return getBest(0, b, offset, len);
+ }
+
+ @Override
+ public V getBest(byte[] b, int offset, int len)
+ {
+ return getBest(0, b, offset, len);
+ }
+
+ private V getBest(int t, byte[] b, int offset, int len)
+ {
+ int node = t;
+ int end = offset + len;
+ loop:
+ while (offset < end)
+ {
+ byte c = (byte)(b[offset++] & 0x7f);
+ len--;
+ if (isCaseInsensitive())
+ c = (byte)StringUtil.lowercases[c];
+
+ while (true)
+ {
+ int row = ROW_SIZE * t;
+ char n = _tree[row];
+ int diff = n - c;
+
+ if (diff == 0)
+ {
+ t = _tree[row + EQ];
+ if (t == 0)
+ break loop;
+
+ // if this node is a match, recurse to remember
+ if (_key[t] != null)
+ {
+ node = t;
+ V better = getBest(t, b, offset, len);
+ if (better != null)
+ return better;
+ }
+ break;
+ }
+
+ t = _tree[row + hilo(diff)];
+ if (t == 0)
+ break loop;
+ }
+ }
+ return _value[node];
+ }
+
+ private V getBest(int t, ByteBuffer b, int offset, int len)
+ {
+ int node = t;
+ int o = offset + b.position();
+
+ loop:
+ for (int i = 0; i < len; i++)
+ {
+ byte c = (byte)(b.get(o + i) & 0x7f);
+ if (isCaseInsensitive())
+ c = (byte)StringUtil.lowercases[c];
+
+ while (true)
+ {
+ int row = ROW_SIZE * t;
+ char n = _tree[row];
+ int diff = n - c;
+
+ if (diff == 0)
+ {
+ t = _tree[row + EQ];
+ if (t == 0)
+ break loop;
+
+ // if this node is a match, recurse to remember
+ if (_key[t] != null)
+ {
+ node = t;
+ V best = getBest(t, b, offset + i + 1, len - i - 1);
+ if (best != null)
+ return best;
+ }
+ break;
+ }
+
+ t = _tree[row + hilo(diff)];
+ if (t == 0)
+ break loop;
+ }
+ }
+ return _value[node];
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder buf = new StringBuilder();
+ for (int r = 0; r <= _rows; r++)
+ {
+ if (_key[r] != null && _value[r] != null)
+ {
+ buf.append(',');
+ buf.append(_key[r]);
+ buf.append('=');
+ buf.append(_value[r].toString());
+ }
+ }
+ if (buf.length() == 0)
+ return "{}";
+
+ buf.setCharAt(0, '{');
+ buf.append('}');
+ return buf.toString();
+ }
+
+ @Override
+ public Set<String> keySet()
+ {
+ Set<String> keys = new HashSet<>();
+
+ for (int r = 0; r <= _rows; r++)
+ {
+ if (_key[r] != null && _value[r] != null)
+ keys.add(_key[r]);
+ }
+ return keys;
+ }
+
+ public int size()
+ {
+ int s = 0;
+ for (int r = 0; r <= _rows; r++)
+ {
+ if (_key[r] != null && _value[r] != null)
+ s++;
+ }
+ return s;
+ }
+
+ public boolean isEmpty()
+ {
+ for (int r = 0; r <= _rows; r++)
+ {
+ if (_key[r] != null && _value[r] != null)
+ return false;
+ }
+ return true;
+ }
+
+ public Set<Map.Entry<String, V>> entrySet()
+ {
+ Set<Map.Entry<String, V>> entries = new HashSet<>();
+ for (int r = 0; r <= _rows; r++)
+ {
+ if (_key[r] != null && _value[r] != null)
+ entries.add(new AbstractMap.SimpleEntry<>(_key[r], _value[r]));
+ }
+ return entries;
+ }
+
+ @Override
+ public boolean isFull()
+ {
+ return _rows + 1 == _key.length;
+ }
+
+ public static int hilo(int diff)
+ {
+ // branchless equivalent to return ((diff<0)?LO:HI);
+ // return 3+2*((diff&Integer.MIN_VALUE)>>Integer.SIZE-1);
+ return 1 + (diff | Integer.MAX_VALUE) / (Integer.MAX_VALUE / 2);
+ }
+
+ public void dump()
+ {
+ for (int r = 0; r < _rows; r++)
+ {
+ char c = _tree[r * ROW_SIZE + 0];
+ System.err.printf("%4d [%s,%d,%d,%d] '%s':%s%n",
+ r,
+ (c < ' ' || c > 127) ? ("" + (int)c) : "'" + c + "'",
+ (int)_tree[r * ROW_SIZE + LO],
+ (int)_tree[r * ROW_SIZE + EQ],
+ (int)_tree[r * ROW_SIZE + HI],
+ _key[r],
+ _value[r]);
+ }
+ }
+
+ public static class Growing<V> implements Trie<V>
+ {
+ private final int _growby;
+ private ArrayTernaryTrie<V> _trie;
+
+ public Growing()
+ {
+ this(1024, 1024);
+ }
+
+ public Growing(int capacity, int growby)
+ {
+ _growby = growby;
+ _trie = new ArrayTernaryTrie<>(capacity);
+ }
+
+ public Growing(boolean insensitive, int capacity, int growby)
+ {
+ _growby = growby;
+ _trie = new ArrayTernaryTrie<>(insensitive, capacity);
+ }
+
+ @Override
+ public boolean put(V v)
+ {
+ return put(v.toString(), v);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return _trie.hashCode();
+ }
+
+ @Override
+ public V remove(String s)
+ {
+ return _trie.remove(s);
+ }
+
+ @Override
+ public V get(String s)
+ {
+ return _trie.get(s);
+ }
+
+ @Override
+ public V get(ByteBuffer b)
+ {
+ return _trie.get(b);
+ }
+
+ @Override
+ public V getBest(byte[] b, int offset, int len)
+ {
+ return _trie.getBest(b, offset, len);
+ }
+
+ @Override
+ public boolean isCaseInsensitive()
+ {
+ return _trie.isCaseInsensitive();
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ return _trie.equals(obj);
+ }
+
+ @Override
+ public void clear()
+ {
+ _trie.clear();
+ }
+
+ @Override
+ public boolean put(String s, V v)
+ {
+ boolean added = _trie.put(s, v);
+ while (!added && _growby > 0)
+ {
+ ArrayTernaryTrie<V> bigger = new ArrayTernaryTrie<>(_trie.isCaseInsensitive(), _trie._key.length + _growby);
+ for (Map.Entry<String, V> entry : _trie.entrySet())
+ {
+ bigger.put(entry.getKey(), entry.getValue());
+ }
+ _trie = bigger;
+ added = _trie.put(s, v);
+ }
+
+ return added;
+ }
+
+ @Override
+ public V get(String s, int offset, int len)
+ {
+ return _trie.get(s, offset, len);
+ }
+
+ @Override
+ public V get(ByteBuffer b, int offset, int len)
+ {
+ return _trie.get(b, offset, len);
+ }
+
+ @Override
+ public V getBest(String s)
+ {
+ return _trie.getBest(s);
+ }
+
+ @Override
+ public V getBest(String s, int offset, int length)
+ {
+ return _trie.getBest(s, offset, length);
+ }
+
+ @Override
+ public V getBest(ByteBuffer b, int offset, int len)
+ {
+ return _trie.getBest(b, offset, len);
+ }
+
+ @Override
+ public String toString()
+ {
+ return _trie.toString();
+ }
+
+ @Override
+ public Set<String> keySet()
+ {
+ return _trie.keySet();
+ }
+
+ @Override
+ public boolean isFull()
+ {
+ return false;
+ }
+
+ public void dump()
+ {
+ _trie.dump();
+ }
+
+ public boolean isEmpty()
+ {
+ return _trie.isEmpty();
+ }
+
+ public int size()
+ {
+ return _trie.size();
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTrie.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTrie.java
new file mode 100644
index 0000000..04165cf
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTrie.java
@@ -0,0 +1,479 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * <p>A Trie String lookup data structure using a fixed size array.</p>
+ * <p>This implementation is always case insensitive and is optimal for
+ * a small number of fixed strings with few special characters. The
+ * Trie is stored in an array of lookup tables, each indexed by the
+ * next character of the key. Frequently used characters are directly
+ * indexed in each lookup table, whilst infrequently used characters
+ * must use a big character table.
+ * </p>
+ * <p>This Trie is very space efficient if the key characters are
+ * from ' ', '+', '-', ':', ';', '.', 'A' to 'Z' or 'a' to 'z'.
+ * Other ISO-8859-1 characters can be used by the key, but less space
+ * efficiently.
+ * </p>
+ * <p>This Trie is not Threadsafe and contains no mutual exclusion
+ * or deliberate memory barriers. It is intended for an ArrayTrie to be
+ * built by a single thread and then used concurrently by multiple threads
+ * and not mutated during that access. If concurrent mutations of the
+ * Trie is required external locks need to be applied.
+ * </p>
+ *
+ * @param <V> the entry type
+ */
+public class ArrayTrie<V> extends AbstractTrie<V>
+{
+ /**
+ * The Size of a Trie row is how many characters can be looked
+ * up directly without going to a big index. This is set at
+ * 32 to cover case insensitive alphabet and a few other common
+ * characters.
+ */
+ private static final int ROW_SIZE = 32;
+
+ /**
+ * The index lookup table, this maps a character as a byte
+ * (ISO-8859-1 or UTF8) to an index within a Trie row
+ */
+ private static final int[] __lookup =
+ {
+ // 0 1 2 3 4 5 6 7 8 9 A B C D E F
+ /*0*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ /*1*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ /*2*/31, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 26, -1, 27, 30, -1,
+ /*3*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 28, 29, -1, -1, -1, -1,
+ /*4*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ /*5*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
+ /*6*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ /*7*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1
+ };
+
+ /**
+ * The Trie rows in a single array which allows a lookup of row,character
+ * to the next row in the Trie. This is actually a 2 dimensional
+ * array that has been flattened to achieve locality of reference.
+ * The first ROW_SIZE entries are for row 0, then next ROW_SIZE
+ * entries are for row 1 etc. So in general instead of using
+ * _rows[row][index], we use _rows[row*ROW_SIZE+index] to look up
+ * the next row for a given character.
+ *
+ * The array is of characters rather than integers to save space.
+ */
+ private final char[] _rowIndex;
+
+ /**
+ * The key (if any) for a Trie row.
+ * A row may be a leaf, a node or both in the Trie tree.
+ */
+ private final String[] _key;
+
+ /**
+ * The value (if any) for a Trie row.
+ * A row may be a leaf, a node or both in the Trie tree.
+ */
+ private final V[] _value;
+
+ /**
+ * A big index for each row.
+ * If a character outside of the lookup map is needed,
+ * then a big index will be created for the row, with
+ * 256 entries, one for each possible byte.
+ */
+ private char[][] _bigIndex;
+
+ /**
+ * The number of rows allocated
+ */
+ private char _rows;
+
+ public ArrayTrie()
+ {
+ this(128);
+ }
+
+ /**
+ * @param capacity The capacity of the trie, which at the worst case
+ * is the total number of characters of all keys stored in the Trie.
+ * The capacity needed is dependent of the shared prefixes of the keys.
+ * For example, a capacity of 6 nodes is required to store keys "foo"
+ * and "bar", but a capacity of only 4 is required to
+ * store "bar" and "bat".
+ */
+ @SuppressWarnings("unchecked")
+ public ArrayTrie(int capacity)
+ {
+ super(true);
+ _value = (V[])new Object[capacity];
+ _rowIndex = new char[capacity * 32];
+ _key = new String[capacity];
+ }
+
+ @Override
+ public void clear()
+ {
+ _rows = 0;
+ Arrays.fill(_value, null);
+ Arrays.fill(_rowIndex, (char)0);
+ Arrays.fill(_key, null);
+ }
+
+ @Override
+ public boolean put(String s, V v)
+ {
+ int t = 0;
+ int k;
+ int limit = s.length();
+ for (k = 0; k < limit; k++)
+ {
+ char c = s.charAt(k);
+
+ int index = __lookup[c & 0x7f];
+ if (index >= 0)
+ {
+ int idx = t * ROW_SIZE + index;
+ t = _rowIndex[idx];
+ if (t == 0)
+ {
+ if (++_rows >= _value.length)
+ return false;
+ t = _rowIndex[idx] = _rows;
+ }
+ }
+ else if (c > 127)
+ throw new IllegalArgumentException("non ascii character");
+ else
+ {
+ if (_bigIndex == null)
+ _bigIndex = new char[_value.length][];
+ if (t >= _bigIndex.length)
+ return false;
+ char[] big = _bigIndex[t];
+ if (big == null)
+ big = _bigIndex[t] = new char[128];
+ t = big[c];
+ if (t == 0)
+ {
+ if (_rows == _value.length)
+ return false;
+ t = big[c] = ++_rows;
+ }
+ }
+ }
+
+ if (t >= _key.length)
+ {
+ _rows = (char)_key.length;
+ return false;
+ }
+
+ _key[t] = v == null ? null : s;
+ _value[t] = v;
+ return true;
+ }
+
+ @Override
+ public V get(String s, int offset, int len)
+ {
+ int t = 0;
+ for (int i = 0; i < len; i++)
+ {
+ char c = s.charAt(offset + i);
+ if (c > 0x7f)
+ return null;
+ int index = __lookup[c & 0x7f];
+ if (index >= 0)
+ {
+ int idx = t * ROW_SIZE + index;
+ t = _rowIndex[idx];
+ if (t == 0)
+ return null;
+ }
+ else
+ {
+ char[] big = _bigIndex == null ? null : _bigIndex[t];
+ if (big == null)
+ return null;
+ t = big[c & 0x7f];
+ if (t == 0)
+ return null;
+ }
+ }
+ return _value[t];
+ }
+
+ @Override
+ public V get(ByteBuffer b, int offset, int len)
+ {
+ int t = 0;
+ for (int i = 0; i < len; i++)
+ {
+ byte c = b.get(offset + i);
+ int index = __lookup[c & 0x7f];
+ if (index >= 0)
+ {
+ int idx = t * ROW_SIZE + index;
+ t = _rowIndex[idx];
+ if (t == 0)
+ return null;
+ }
+ else
+ {
+ char[] big = _bigIndex == null ? null : _bigIndex[t];
+ if (big == null)
+ return null;
+ t = big[c];
+ if (t == 0)
+ return null;
+ }
+ }
+ return _value[t];
+ }
+
+ @Override
+ public V getBest(byte[] b, int offset, int len)
+ {
+ return getBest(0, b, offset, len);
+ }
+
+ @Override
+ public V getBest(ByteBuffer b, int offset, int len)
+ {
+ if (b.hasArray())
+ return getBest(0, b.array(), b.arrayOffset() + b.position() + offset, len);
+ return getBest(0, b, offset, len);
+ }
+
+ @Override
+ public V getBest(String s, int offset, int len)
+ {
+ return getBest(0, s, offset, len);
+ }
+
+ private V getBest(int t, String s, int offset, int len)
+ {
+ int pos = offset;
+ for (int i = 0; i < len; i++)
+ {
+ char c = s.charAt(pos++);
+ int index = __lookup[c & 0x7f];
+ if (index >= 0)
+ {
+ int idx = t * ROW_SIZE + index;
+ int nt = _rowIndex[idx];
+ if (nt == 0)
+ break;
+ t = nt;
+ }
+ else
+ {
+ char[] big = _bigIndex == null ? null : _bigIndex[t];
+ if (big == null)
+ return null;
+ int nt = big[c];
+ if (nt == 0)
+ break;
+ t = nt;
+ }
+
+ // Is the next Trie is a match
+ if (_key[t] != null)
+ {
+ // Recurse so we can remember this possibility
+ V best = getBest(t, s, offset + i + 1, len - i - 1);
+ if (best != null)
+ return best;
+ return _value[t];
+ }
+ }
+ return _value[t];
+ }
+
+ private V getBest(int t, byte[] b, int offset, int len)
+ {
+ for (int i = 0; i < len; i++)
+ {
+ byte c = b[offset + i];
+ int index = __lookup[c & 0x7f];
+ if (index >= 0)
+ {
+ int idx = t * ROW_SIZE + index;
+ int nt = _rowIndex[idx];
+ if (nt == 0)
+ break;
+ t = nt;
+ }
+ else
+ {
+ char[] big = _bigIndex == null ? null : _bigIndex[t];
+ if (big == null)
+ return null;
+ int nt = big[c];
+ if (nt == 0)
+ break;
+ t = nt;
+ }
+
+ // Is the next Trie is a match
+ if (_key[t] != null)
+ {
+ // Recurse so we can remember this possibility
+ V best = getBest(t, b, offset + i + 1, len - i - 1);
+ if (best != null)
+ return best;
+ break;
+ }
+ }
+ return _value[t];
+ }
+
+ private V getBest(int t, ByteBuffer b, int offset, int len)
+ {
+ int pos = b.position() + offset;
+ for (int i = 0; i < len; i++)
+ {
+ byte c = b.get(pos++);
+ int index = __lookup[c & 0x7f];
+ if (index >= 0)
+ {
+ int idx = t * ROW_SIZE + index;
+ int nt = _rowIndex[idx];
+ if (nt == 0)
+ break;
+ t = nt;
+ }
+ else
+ {
+ char[] big = _bigIndex == null ? null : _bigIndex[t];
+ if (big == null)
+ return null;
+ int nt = big[c];
+ if (nt == 0)
+ break;
+ t = nt;
+ }
+
+ // Is the next Trie is a match
+ if (_key[t] != null)
+ {
+ // Recurse so we can remember this possibility
+ V best = getBest(t, b, offset + i + 1, len - i - 1);
+ if (best != null)
+ return best;
+ break;
+ }
+ }
+ return _value[t];
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder buf = new StringBuilder();
+ toString(buf, 0);
+
+ if (buf.length() == 0)
+ return "{}";
+
+ buf.setCharAt(0, '{');
+ buf.append('}');
+ return buf.toString();
+ }
+
+ private void toString(Appendable out, int t)
+ {
+ if (_value[t] != null)
+ {
+ try
+ {
+ out.append(',');
+ out.append(_key[t]);
+ out.append('=');
+ out.append(_value[t].toString());
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ for (int i = 0; i < ROW_SIZE; i++)
+ {
+ int idx = t * ROW_SIZE + i;
+ if (_rowIndex[idx] != 0)
+ toString(out, _rowIndex[idx]);
+ }
+
+ char[] big = _bigIndex == null ? null : _bigIndex[t];
+ if (big != null)
+ {
+ for (int i : big)
+ {
+ if (i != 0)
+ toString(out, i);
+ }
+ }
+ }
+
+ @Override
+ public Set<String> keySet()
+ {
+ Set<String> keys = new HashSet<>();
+ keySet(keys, 0);
+ return keys;
+ }
+
+ private void keySet(Set<String> set, int t)
+ {
+ if (t < _value.length && _value[t] != null)
+ set.add(_key[t]);
+
+ for (int i = 0; i < ROW_SIZE; i++)
+ {
+ int idx = t * ROW_SIZE + i;
+ if (idx < _rowIndex.length && _rowIndex[idx] != 0)
+ keySet(set, _rowIndex[idx]);
+ }
+
+ char[] big = _bigIndex == null || t >= _bigIndex.length ? null : _bigIndex[t];
+ if (big != null)
+ {
+ for (int i : big)
+ {
+ if (i != 0)
+ keySet(set, i);
+ }
+ }
+ }
+
+ @Override
+ public boolean isFull()
+ {
+ return _rows + 1 >= _key.length;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayUtil.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayUtil.java
new file mode 100644
index 0000000..a8ea8cb
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayUtil.java
@@ -0,0 +1,164 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utility methods for Array manipulation
+ */
+public class ArrayUtil
+ implements Cloneable, Serializable
+{
+
+ public static <T> T[] removeFromArray(T[] array, Object item)
+ {
+ if (item == null || array == null)
+ return array;
+ for (int i = array.length; i-- > 0; )
+ {
+ if (item.equals(array[i]))
+ {
+ Class<?> c = array == null ? item.getClass() : array.getClass().getComponentType();
+ @SuppressWarnings("unchecked")
+ T[] na = (T[])Array.newInstance(c, Array.getLength(array) - 1);
+ if (i > 0)
+ System.arraycopy(array, 0, na, 0, i);
+ if (i + 1 < array.length)
+ System.arraycopy(array, i + 1, na, i, array.length - (i + 1));
+ return na;
+ }
+ }
+ return array;
+ }
+
+ /**
+ * Add arrays
+ *
+ * @param array1 An array to add to (or null)
+ * @param array2 An array to add to (or null)
+ * @param <T> the array entry type
+ * @return new array with contents of both arrays, or null if both arrays are null
+ */
+ public static <T> T[] add(T[] array1, T[] array2)
+ {
+ if (array1 == null || array1.length == 0)
+ return array2;
+ if (array2 == null || array2.length == 0)
+ return array1;
+
+ T[] na = Arrays.copyOf(array1, array1.length + array2.length);
+ System.arraycopy(array2, 0, na, array1.length, array2.length);
+ return na;
+ }
+
+ /**
+ * Add element to an array
+ *
+ * @param array The array to add to (or null)
+ * @param item The item to add
+ * @param type The type of the array (in case of null array)
+ * @param <T> the array entry type
+ * @return new array with contents of array plus item
+ */
+ public static <T> T[] addToArray(T[] array, T item, Class<?> type)
+ {
+ if (array == null)
+ {
+ if (type == null && item != null)
+ type = item.getClass();
+ @SuppressWarnings("unchecked")
+ T[] na = (T[])Array.newInstance(type, 1);
+ na[0] = item;
+ return na;
+ }
+ else
+ {
+ T[] na = Arrays.copyOf(array, array.length + 1);
+ na[array.length] = item;
+ return na;
+ }
+ }
+
+ /**
+ * Add element to the start of an array
+ *
+ * @param array The array to add to (or null)
+ * @param item The item to add
+ * @param type The type of the array (in case of null array)
+ * @param <T> the array entry type
+ * @return new array with contents of array plus item
+ */
+ public static <T> T[] prependToArray(T item, T[] array, Class<?> type)
+ {
+ if (array == null)
+ {
+ if (type == null && item != null)
+ type = item.getClass();
+ @SuppressWarnings("unchecked")
+ T[] na = (T[])Array.newInstance(type, 1);
+ na[0] = item;
+ return na;
+ }
+ else
+ {
+ Class<?> c = array.getClass().getComponentType();
+ @SuppressWarnings("unchecked")
+ T[] na = (T[])Array.newInstance(c, Array.getLength(array) + 1);
+ System.arraycopy(array, 0, na, 1, array.length);
+ na[0] = item;
+ return na;
+ }
+ }
+
+ /**
+ * @param array Any array of object
+ * @param <E> the array entry type
+ * @return A new <i>modifiable</i> list initialised with the elements from <code>array</code>.
+ */
+ public static <E> List<E> asMutableList(E[] array)
+ {
+ if (array == null || array.length == 0)
+ return new ArrayList<E>();
+ return new ArrayList<E>(Arrays.asList(array));
+ }
+
+ public static <T> T[] removeNulls(T[] array)
+ {
+ for (T t : array)
+ {
+ if (t == null)
+ {
+ List<T> list = new ArrayList<>();
+ for (T t2 : array)
+ {
+ if (t2 != null)
+ list.add(t2);
+ }
+ return list.toArray(Arrays.copyOf(array, list.size()));
+ }
+ }
+ return array;
+ }
+}
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/AtomicBiInteger.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/AtomicBiInteger.java
new file mode 100644
index 0000000..7b91c8d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/AtomicBiInteger.java
@@ -0,0 +1,300 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * An AtomicLong with additional methods to treat it as two hi/lo integers.
+ */
+public class AtomicBiInteger extends AtomicLong
+{
+
+ public AtomicBiInteger()
+ {
+ }
+
+ public AtomicBiInteger(long encoded)
+ {
+ super(encoded);
+ }
+
+ public AtomicBiInteger(int hi, int lo)
+ {
+ super(encode(hi, lo));
+ }
+
+ /**
+ * @return the hi value
+ */
+ public int getHi()
+ {
+ return getHi(get());
+ }
+
+ /**
+ * @return the lo value
+ */
+ public int getLo()
+ {
+ return getLo(get());
+ }
+
+ /**
+ * Atomically sets the hi value without changing the lo value.
+ *
+ * @param hi the new hi value
+ * @return the previous hi value
+ */
+ public int getAndSetHi(int hi)
+ {
+ while (true)
+ {
+ long encoded = get();
+ long update = encodeHi(encoded, hi);
+ if (compareAndSet(encoded, update))
+ return getHi(encoded);
+ }
+ }
+
+ /**
+ * Atomically sets the lo value without changing the hi value.
+ *
+ * @param lo the new lo value
+ * @return the previous lo value
+ */
+ public int getAndSetLo(int lo)
+ {
+ while (true)
+ {
+ long encoded = get();
+ long update = encodeLo(encoded, lo);
+ if (compareAndSet(encoded, update))
+ return getLo(encoded);
+ }
+ }
+
+ /**
+ * Sets the hi and lo values.
+ *
+ * @param hi the new hi value
+ * @param lo the new lo value
+ */
+ public void set(int hi, int lo)
+ {
+ set(encode(hi, lo));
+ }
+
+ /**
+ * <p>Atomically sets the hi value to the given updated value
+ * only if the current value {@code ==} the expected value.</p>
+ * <p>Concurrent changes to the lo value result in a retry.</p>
+ *
+ * @param expectHi the expected hi value
+ * @param hi the new hi value
+ * @return {@code true} if successful. False return indicates that
+ * the actual hi value was not equal to the expected hi value.
+ */
+ public boolean compareAndSetHi(int expectHi, int hi)
+ {
+ while (true)
+ {
+ long encoded = get();
+ if (getHi(encoded) != expectHi)
+ return false;
+ long update = encodeHi(encoded, hi);
+ if (compareAndSet(encoded, update))
+ return true;
+ }
+ }
+
+ /**
+ * <p>Atomically sets the lo value to the given updated value
+ * only if the current value {@code ==} the expected value.</p>
+ * <p>Concurrent changes to the hi value result in a retry.</p>
+ *
+ * @param expectLo the expected lo value
+ * @param lo the new lo value
+ * @return {@code true} if successful. False return indicates that
+ * the actual lo value was not equal to the expected lo value.
+ */
+ public boolean compareAndSetLo(int expectLo, int lo)
+ {
+ while (true)
+ {
+ long encoded = get();
+ if (getLo(encoded) != expectLo)
+ return false;
+ long update = encodeLo(encoded, lo);
+ if (compareAndSet(encoded, update))
+ return true;
+ }
+ }
+
+ /**
+ * Atomically sets the values to the given updated values only if
+ * the current encoded value {@code ==} the expected encoded value.
+ *
+ * @param encoded the expected encoded value
+ * @param hi the new hi value
+ * @param lo the new lo value
+ * @return {@code true} if successful. False return indicates that
+ * the actual encoded value was not equal to the expected encoded value.
+ */
+ public boolean compareAndSet(long encoded, int hi, int lo)
+ {
+ long update = encode(hi, lo);
+ return compareAndSet(encoded, update);
+ }
+
+ /**
+ * Atomically sets the hi and lo values to the given updated values only if
+ * the current hi and lo values {@code ==} the expected hi and lo values.
+ *
+ * @param expectHi the expected hi value
+ * @param hi the new hi value
+ * @param expectLo the expected lo value
+ * @param lo the new lo value
+ * @return {@code true} if successful. False return indicates that
+ * the actual hi and lo values were not equal to the expected hi and lo value.
+ */
+ public boolean compareAndSet(int expectHi, int hi, int expectLo, int lo)
+ {
+ long encoded = encode(expectHi, expectLo);
+ long update = encode(hi, lo);
+ return compareAndSet(encoded, update);
+ }
+
+ /**
+ * Atomically adds the given delta to the current hi value, returning the updated hi value.
+ *
+ * @param delta the delta to apply
+ * @return the updated hi value
+ */
+ public int addAndGetHi(int delta)
+ {
+ while (true)
+ {
+ long encoded = get();
+ int hi = getHi(encoded) + delta;
+ long update = encodeHi(encoded, hi);
+ if (compareAndSet(encoded, update))
+ return hi;
+ }
+ }
+
+ /**
+ * Atomically adds the given delta to the current lo value, returning the updated lo value.
+ *
+ * @param delta the delta to apply
+ * @return the updated lo value
+ */
+ public int addAndGetLo(int delta)
+ {
+ while (true)
+ {
+ long encoded = get();
+ int lo = getLo(encoded) + delta;
+ long update = encodeLo(encoded, lo);
+ if (compareAndSet(encoded, update))
+ return lo;
+ }
+ }
+
+ /**
+ * Atomically adds the given deltas to the current hi and lo values.
+ *
+ * @param deltaHi the delta to apply to the hi value
+ * @param deltaLo the delta to apply to the lo value
+ */
+ public void add(int deltaHi, int deltaLo)
+ {
+ while (true)
+ {
+ long encoded = get();
+ long update = encode(getHi(encoded) + deltaHi, getLo(encoded) + deltaLo);
+ if (compareAndSet(encoded, update))
+ return;
+ }
+ }
+
+ /**
+ * Gets a hi value from the given encoded value.
+ *
+ * @param encoded the encoded value
+ * @return the hi value
+ */
+ public static int getHi(long encoded)
+ {
+ return (int)((encoded >> 32) & 0xFFFF_FFFFL);
+ }
+
+ /**
+ * Gets a lo value from the given encoded value.
+ *
+ * @param encoded the encoded value
+ * @return the lo value
+ */
+ public static int getLo(long encoded)
+ {
+ return (int)(encoded & 0xFFFF_FFFFL);
+ }
+
+ /**
+ * Encodes hi and lo values into a long.
+ *
+ * @param hi the hi value
+ * @param lo the lo value
+ * @return the encoded value
+ */
+ public static long encode(int hi, int lo)
+ {
+ long h = ((long)hi) & 0xFFFF_FFFFL;
+ long l = ((long)lo) & 0xFFFF_FFFFL;
+ return (h << 32) + l;
+ }
+
+ /**
+ * Sets the hi value into the given encoded value.
+ *
+ * @param encoded the encoded value
+ * @param hi the hi value
+ * @return the new encoded value
+ */
+ public static long encodeHi(long encoded, int hi)
+ {
+ long h = ((long)hi) & 0xFFFF_FFFFL;
+ long l = encoded & 0xFFFF_FFFFL;
+ return (h << 32) + l;
+ }
+
+ /**
+ * Sets the lo value into the given encoded value.
+ *
+ * @param encoded the encoded value
+ * @param lo the lo value
+ * @return the new encoded value
+ */
+ public static long encodeLo(long encoded, int lo)
+ {
+ long h = (encoded >> 32) & 0xFFFF_FFFFL;
+ long l = ((long)lo) & 0xFFFF_FFFFL;
+ return (h << 32) + l;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Atomics.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Atomics.java
new file mode 100644
index 0000000..a1e5e46
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Atomics.java
@@ -0,0 +1,77 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class Atomics
+{
+ private Atomics()
+ {
+ }
+
+ public static boolean updateMin(AtomicLong currentMin, long newValue)
+ {
+ long oldValue = currentMin.get();
+ while (newValue < oldValue)
+ {
+ if (currentMin.compareAndSet(oldValue, newValue))
+ return true;
+ oldValue = currentMin.get();
+ }
+ return false;
+ }
+
+ public static boolean updateMax(AtomicLong currentMax, long newValue)
+ {
+ long oldValue = currentMax.get();
+ while (newValue > oldValue)
+ {
+ if (currentMax.compareAndSet(oldValue, newValue))
+ return true;
+ oldValue = currentMax.get();
+ }
+ return false;
+ }
+
+ public static boolean updateMin(AtomicInteger currentMin, int newValue)
+ {
+ int oldValue = currentMin.get();
+ while (newValue < oldValue)
+ {
+ if (currentMin.compareAndSet(oldValue, newValue))
+ return true;
+ oldValue = currentMin.get();
+ }
+ return false;
+ }
+
+ public static boolean updateMax(AtomicInteger currentMax, int newValue)
+ {
+ int oldValue = currentMax.get();
+ while (newValue > oldValue)
+ {
+ if (currentMax.compareAndSet(oldValue, newValue))
+ return true;
+ oldValue = currentMax.get();
+ }
+ return false;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Attachable.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Attachable.java
new file mode 100644
index 0000000..762618c
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Attachable.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+/**
+ * Abstract mechanism to support attachment of miscellaneous objects.
+ */
+public interface Attachable
+{
+ /**
+ * @return the object attached to this instance
+ * @see #setAttachment(Object)
+ */
+ Object getAttachment();
+
+ /**
+ * Attaches the given object to this stream for later retrieval.
+ *
+ * @param attachment the object to attach to this instance
+ */
+ void setAttachment(Object attachment);
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Attributes.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Attributes.java
new file mode 100644
index 0000000..f5be7bb
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Attributes.java
@@ -0,0 +1,99 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Set;
+
+/**
+ * Attributes.
+ * Interface commonly used for storing attributes.
+ */
+public interface Attributes
+{
+ void removeAttribute(String name);
+
+ void setAttribute(String name, Object attribute);
+
+ Object getAttribute(String name);
+
+ Set<String> getAttributeNameSet();
+
+ default Enumeration<String> getAttributeNames()
+ {
+ return Collections.enumeration(getAttributeNameSet());
+ }
+
+ void clearAttributes();
+
+ static Attributes unwrap(Attributes attributes)
+ {
+ while (attributes instanceof Wrapper)
+ {
+ attributes = ((Wrapper)attributes).getAttributes();
+ }
+ return attributes;
+ }
+
+ abstract class Wrapper implements Attributes
+ {
+ protected final Attributes _attributes;
+
+ public Wrapper(Attributes attributes)
+ {
+ _attributes = attributes;
+ }
+
+ public Attributes getAttributes()
+ {
+ return _attributes;
+ }
+
+ @Override
+ public void removeAttribute(String name)
+ {
+ _attributes.removeAttribute(name);
+ }
+
+ @Override
+ public void setAttribute(String name, Object attribute)
+ {
+ _attributes.setAttribute(name, attribute);
+ }
+
+ @Override
+ public Object getAttribute(String name)
+ {
+ return _attributes.getAttribute(name);
+ }
+
+ @Override
+ public Set<String> getAttributeNameSet()
+ {
+ return _attributes.getAttributeNameSet();
+ }
+
+ @Override
+ public void clearAttributes()
+ {
+ _attributes.clearAttributes();
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/AttributesMap.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/AttributesMap.java
new file mode 100644
index 0000000..b9f0412
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/AttributesMap.java
@@ -0,0 +1,166 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.util.component.Dumpable;
+
+public class AttributesMap implements Attributes, Dumpable
+{
+ private final AtomicReference<ConcurrentMap<String, Object>> _map = new AtomicReference<>();
+
+ public AttributesMap()
+ {
+ }
+
+ public AttributesMap(AttributesMap attributes)
+ {
+ ConcurrentMap<String, Object> map = attributes.map();
+ if (map != null)
+ _map.set(new ConcurrentHashMap<>(map));
+ }
+
+ private ConcurrentMap<String, Object> map()
+ {
+ return _map.get();
+ }
+
+ private ConcurrentMap<String, Object> ensureMap()
+ {
+ while (true)
+ {
+ ConcurrentMap<String, Object> map = map();
+ if (map != null)
+ return map;
+ map = new ConcurrentHashMap<>();
+ if (_map.compareAndSet(null, map))
+ return map;
+ }
+ }
+
+ @Override
+ public void removeAttribute(String name)
+ {
+ Map<String, Object> map = map();
+ if (map != null)
+ map.remove(name);
+ }
+
+ @Override
+ public void setAttribute(String name, Object attribute)
+ {
+ if (attribute == null)
+ removeAttribute(name);
+ else
+ ensureMap().put(name, attribute);
+ }
+
+ @Override
+ public Object getAttribute(String name)
+ {
+ Map<String, Object> map = map();
+ return map == null ? null : map.get(name);
+ }
+
+ @Override
+ public Enumeration<String> getAttributeNames()
+ {
+ return Collections.enumeration(getAttributeNameSet());
+ }
+
+ @Override
+ public Set<String> getAttributeNameSet()
+ {
+ return keySet();
+ }
+
+ public Set<Map.Entry<String, Object>> getAttributeEntrySet()
+ {
+ Map<String, Object> map = map();
+ return map == null ? Collections.emptySet() : map.entrySet();
+ }
+
+ public static Enumeration<String> getAttributeNamesCopy(Attributes attrs)
+ {
+ if (attrs instanceof AttributesMap)
+ return Collections.enumeration(((AttributesMap)attrs).keySet());
+
+ List<String> names = new ArrayList<>(Collections.list(attrs.getAttributeNames()));
+ return Collections.enumeration(names);
+ }
+
+ @Override
+ public void clearAttributes()
+ {
+ Map<String, Object> map = map();
+ if (map != null)
+ map.clear();
+ }
+
+ public int size()
+ {
+ Map<String, Object> map = map();
+ return map == null ? 0 : map.size();
+ }
+
+ @Override
+ public String toString()
+ {
+ Map<String, Object> map = map();
+ return map == null ? "{}" : map.toString();
+ }
+
+ private Set<String> keySet()
+ {
+ Map<String, Object> map = map();
+ return map == null ? Collections.emptySet() : map.keySet();
+ }
+
+ public void addAll(Attributes attributes)
+ {
+ Enumeration<String> e = attributes.getAttributeNames();
+ while (e.hasMoreElements())
+ {
+ String name = e.nextElement();
+ setAttribute(name, attributes.getAttribute(name));
+ }
+ }
+
+ @Override
+ public String dump()
+ {
+ return Dumpable.dump(this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ Dumpable.dumpObjects(out, indent, String.format("%s@%x", this.getClass().getSimpleName(), hashCode()), map());
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/B64Code.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/B64Code.java
new file mode 100644
index 0000000..8e2265f
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/B64Code.java
@@ -0,0 +1,596 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.Base64;
+
+/**
+ * Fast B64 Encoder/Decoder as described in RFC 1421.
+ * <p>Does not insert or interpret whitespace as described in RFC
+ * 1521. If you require this you must pre/post process your data.
+ * <p> Note that in a web context the usual case is to not want
+ * linebreaks or other white space in the encoded output.
+ *
+ * @deprecated use {@link java.util.Base64} instead
+ */
+@Deprecated
+public class B64Code
+{
+ private static final char __pad = '=';
+ private static final char[] __rfc1421alphabet =
+ {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
+ };
+
+ private static final byte[] __rfc1421nibbles;
+
+ static
+ {
+ __rfc1421nibbles = new byte[256];
+ for (int i = 0; i < 256; i++)
+ {
+ __rfc1421nibbles[i] = -1;
+ }
+ for (byte b = 0; b < 64; b++)
+ {
+ __rfc1421nibbles[(byte)__rfc1421alphabet[b]] = b;
+ }
+ __rfc1421nibbles[(byte)__pad] = 0;
+ }
+
+ private static final char[] __rfc4648urlAlphabet =
+ {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'
+ };
+
+ private static final byte[] __rfc4648urlNibbles;
+
+ static
+ {
+ __rfc4648urlNibbles = new byte[256];
+ for (int i = 0; i < 256; i++)
+ {
+ __rfc4648urlNibbles[i] = -1;
+ }
+ for (byte b = 0; b < 64; b++)
+ {
+ __rfc4648urlNibbles[(byte)__rfc4648urlAlphabet[b]] = b;
+ }
+ __rfc4648urlNibbles[(byte)__pad] = 0;
+ }
+
+ private B64Code()
+ {
+ }
+
+ /**
+ * Base 64 encode as described in RFC 1421.
+ * <p>Does not insert whitespace as described in RFC 1521.
+ *
+ * @param s String to encode.
+ * @return String containing the encoded form of the input.
+ * @deprecated use {@link Base64.Encoder#encodeToString(byte[])}} instead.
+ */
+ public static String encode(String s)
+ {
+ // NOTE: no Jetty mainline code uses this anymore
+ return encode(s, (Charset)null);
+ }
+
+ /**
+ * Base 64 encode as described in RFC 1421.
+ * <p>Does not insert whitespace as described in RFC 1521.
+ *
+ * @param s String to encode.
+ * @param charEncoding String representing the name of
+ * the character encoding of the provided input String.
+ * @return String containing the encoded form of the input.
+ */
+ public static String encode(String s, String charEncoding)
+ {
+ // NOTE: no Jetty mainline code uses this anymore
+ byte[] bytes;
+ if (charEncoding == null)
+ bytes = s.getBytes(StandardCharsets.ISO_8859_1);
+ else
+ bytes = s.getBytes(Charset.forName(charEncoding));
+ return new String(encode(bytes));
+ }
+
+ /**
+ * Base 64 encode as described in RFC 1421.
+ * <p>Does not insert whitespace as described in RFC 1521.
+ *
+ * @param s String to encode.
+ * @param charEncoding The character encoding of the provided input String.
+ * @return String containing the encoded form of the input.
+ */
+ public static String encode(String s, Charset charEncoding)
+ {
+ // NOTE: no Jetty mainline code uses this anymore
+ byte[] bytes = s.getBytes(charEncoding == null ? StandardCharsets.ISO_8859_1 : charEncoding);
+ return new String(encode(bytes));
+ }
+
+ /**
+ * Fast Base 64 encode as described in RFC 1421.
+ * <p>Does not insert whitespace as described in RFC 1521.
+ * <p> Avoids creating extra copies of the input/output.
+ *
+ * @param b byte array to encode.
+ * @return char array containing the encoded form of the input.
+ */
+ public static char[] encode(byte[] b)
+ {
+ // NOTE: no Jetty mainline code uses this anymore
+ if (b == null)
+ return null;
+
+ int bLen = b.length;
+ int cLen = ((bLen + 2) / 3) * 4;
+ char[] c = new char[cLen];
+ int ci = 0;
+ int bi = 0;
+ byte b0;
+ byte b1;
+ byte b2;
+ int stop = (bLen / 3) * 3;
+ while (bi < stop)
+ {
+ b0 = b[bi++];
+ b1 = b[bi++];
+ b2 = b[bi++];
+ c[ci++] = __rfc1421alphabet[(b0 >>> 2) & 0x3f];
+ c[ci++] = __rfc1421alphabet[(b0 << 4) & 0x3f | (b1 >>> 4) & 0x0f];
+ c[ci++] = __rfc1421alphabet[(b1 << 2) & 0x3f | (b2 >>> 6) & 0x03];
+ c[ci++] = __rfc1421alphabet[b2 & 0x3f];
+ }
+
+ if (bLen != bi)
+ {
+ switch (bLen % 3)
+ {
+ case 2:
+ b0 = b[bi++];
+ b1 = b[bi++];
+ c[ci++] = __rfc1421alphabet[(b0 >>> 2) & 0x3f];
+ c[ci++] = __rfc1421alphabet[(b0 << 4) & 0x3f | (b1 >>> 4) & 0x0f];
+ c[ci++] = __rfc1421alphabet[(b1 << 2) & 0x3f];
+ c[ci++] = __pad;
+ break;
+
+ case 1:
+ b0 = b[bi++];
+ c[ci++] = __rfc1421alphabet[(b0 >>> 2) & 0x3f];
+ c[ci++] = __rfc1421alphabet[(b0 << 4) & 0x3f];
+ c[ci++] = __pad;
+ c[ci++] = __pad;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return c;
+ }
+
+ /**
+ * Fast Base 64 encode as described in RFC 1421 and RFC2045
+ * <p>Does not insert whitespace as described in RFC 1521, unless rfc2045 is passed as true.
+ * <p> Avoids creating extra copies of the input/output.
+ *
+ * @param b byte array to encode.
+ * @param rfc2045 If true, break lines at 76 characters with CRLF
+ * @return char array containing the encoded form of the input.
+ */
+ public static char[] encode(byte[] b, boolean rfc2045)
+ {
+ // NOTE: no Jetty mainline code uses this anymore
+ if (b == null)
+ return null;
+ if (!rfc2045)
+ return encode(b);
+
+ int bLen = b.length;
+ int cLen = ((bLen + 2) / 3) * 4;
+ cLen += 2 + 2 * (cLen / 76);
+ char[] c = new char[cLen];
+ int ci = 0;
+ int bi = 0;
+ byte b0;
+ byte b1;
+ byte b2;
+ int stop = (bLen / 3) * 3;
+ int l = 0;
+ while (bi < stop)
+ {
+ b0 = b[bi++];
+ b1 = b[bi++];
+ b2 = b[bi++];
+ c[ci++] = __rfc1421alphabet[(b0 >>> 2) & 0x3f];
+ c[ci++] = __rfc1421alphabet[(b0 << 4) & 0x3f | (b1 >>> 4) & 0x0f];
+ c[ci++] = __rfc1421alphabet[(b1 << 2) & 0x3f | (b2 >>> 6) & 0x03];
+ c[ci++] = __rfc1421alphabet[b2 & 0x3f];
+ l += 4;
+ if (l % 76 == 0)
+ {
+ c[ci++] = 13;
+ c[ci++] = 10;
+ }
+ }
+
+ if (bLen != bi)
+ {
+ switch (bLen % 3)
+ {
+ case 2:
+ b0 = b[bi++];
+ b1 = b[bi++];
+ c[ci++] = __rfc1421alphabet[(b0 >>> 2) & 0x3f];
+ c[ci++] = __rfc1421alphabet[(b0 << 4) & 0x3f | (b1 >>> 4) & 0x0f];
+ c[ci++] = __rfc1421alphabet[(b1 << 2) & 0x3f];
+ c[ci++] = __pad;
+ break;
+
+ case 1:
+ b0 = b[bi++];
+ c[ci++] = __rfc1421alphabet[(b0 >>> 2) & 0x3f];
+ c[ci++] = __rfc1421alphabet[(b0 << 4) & 0x3f];
+ c[ci++] = __pad;
+ c[ci++] = __pad;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ c[ci++] = 13;
+ c[ci++] = 10;
+ return c;
+ }
+
+ /**
+ * Base 64 decode as described in RFC 2045.
+ * <p>Unlike {@link #decode(char[])}, extra whitespace is ignored.
+ *
+ * @param encoded String to decode.
+ * @param charEncoding String representing the character encoding
+ * used to map the decoded bytes into a String. If null
+ * the platforms default charset is used.
+ * @return String decoded byte array.
+ * @throws UnsupportedCharsetException if the encoding is not supported
+ * @throws IllegalArgumentException if the input is not a valid
+ * B64 encoding.
+ */
+ @SuppressWarnings("DefaultCharset")
+ public static String decode(String encoded, String charEncoding)
+ {
+ // FIXME: no Jetty mainline code uses this anymore
+ byte[] decoded = decode(encoded);
+ if (charEncoding == null)
+ return new String(decoded);
+ return new String(decoded, Charset.forName(charEncoding));
+ }
+
+ /**
+ * Base 64 decode as described in RFC 2045.
+ * <p>Unlike {@link #decode(char[])}, extra whitespace is ignored.
+ *
+ * @param encoded String to decode.
+ * @param charEncoding Character encoding
+ * used to map the decoded bytes into a String. If null
+ * the platforms default charset is used.
+ * @return String decoded byte array.
+ * @throws IllegalArgumentException if the input is not a valid
+ * B64 encoding.
+ */
+ @SuppressWarnings("DefaultCharset")
+ public static String decode(String encoded, Charset charEncoding)
+ {
+ // FIXME: no Jetty mainline code uses this anymore
+ byte[] decoded = decode(encoded);
+ if (charEncoding == null)
+ return new String(decoded);
+ return new String(decoded, charEncoding);
+ }
+
+ /**
+ * Fast Base 64 decode as described in RFC 1421.
+ *
+ * <p>Unlike other decode methods, this does not attempt to
+ * cope with extra whitespace as described in RFC 1521/2045.
+ * <p> Avoids creating extra copies of the input/output.
+ * <p> Note this code has been flattened for performance.
+ *
+ * @param b char array to decode.
+ * @return byte array containing the decoded form of the input.
+ * @throws IllegalArgumentException if the input is not a valid
+ * B64 encoding.
+ */
+ public static byte[] decode(char[] b)
+ {
+ // FIXME: no Jetty mainline code uses this anymore
+ if (b == null)
+ return null;
+
+ int bLen = b.length;
+ if (bLen % 4 != 0)
+ throw new IllegalArgumentException("Input block size is not 4");
+
+ int li = bLen - 1;
+ while (li >= 0 && b[li] == (byte)__pad)
+ {
+ li--;
+ }
+
+ if (li < 0)
+ return new byte[0];
+
+ // Create result array of exact required size.
+ int rLen = ((li + 1) * 3) / 4;
+ byte[] r = new byte[rLen];
+ int ri = 0;
+ int bi = 0;
+ int stop = (rLen / 3) * 3;
+ byte b0;
+ byte b1;
+ byte b2;
+ byte b3;
+ try
+ {
+ while (ri < stop)
+ {
+ b0 = __rfc1421nibbles[b[bi++]];
+ b1 = __rfc1421nibbles[b[bi++]];
+ b2 = __rfc1421nibbles[b[bi++]];
+ b3 = __rfc1421nibbles[b[bi++]];
+ if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0)
+ throw new IllegalArgumentException("Not B64 encoded");
+
+ r[ri++] = (byte)(b0 << 2 | b1 >>> 4);
+ r[ri++] = (byte)(b1 << 4 | b2 >>> 2);
+ r[ri++] = (byte)(b2 << 6 | b3);
+ }
+
+ if (rLen != ri)
+ {
+ switch (rLen % 3)
+ {
+ case 2:
+ b0 = __rfc1421nibbles[b[bi++]];
+ b1 = __rfc1421nibbles[b[bi++]];
+ b2 = __rfc1421nibbles[b[bi++]];
+ if (b0 < 0 || b1 < 0 || b2 < 0)
+ throw new IllegalArgumentException("Not B64 encoded");
+ r[ri++] = (byte)(b0 << 2 | b1 >>> 4);
+ r[ri++] = (byte)(b1 << 4 | b2 >>> 2);
+ break;
+
+ case 1:
+ b0 = __rfc1421nibbles[b[bi++]];
+ b1 = __rfc1421nibbles[b[bi++]];
+ if (b0 < 0 || b1 < 0)
+ throw new IllegalArgumentException("Not B64 encoded");
+ r[ri++] = (byte)(b0 << 2 | b1 >>> 4);
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+ catch (IndexOutOfBoundsException e)
+ {
+ throw new IllegalArgumentException("char " + bi + " was not B64 encoded");
+ }
+
+ return r;
+ }
+
+ /**
+ * Base 64 decode as described in RFC 2045.
+ * <p>Unlike {@link #decode(char[])}, extra whitespace is ignored.
+ *
+ * @param encoded String to decode.
+ * @return byte array containing the decoded form of the input.
+ * @throws IllegalArgumentException if the input is not a valid
+ * B64 encoding.
+ */
+ public static byte[] decode(String encoded)
+ {
+ // FIXME: no Jetty mainline code uses this anymore
+ if (encoded == null)
+ return null;
+
+ ByteArrayOutputStream bout = new ByteArrayOutputStream(4 * encoded.length() / 3);
+ decode(encoded, bout);
+ return bout.toByteArray();
+ }
+
+ /**
+ * Base 64 decode as described in RFC 2045.
+ * <p>Unlike {@link #decode(char[])}, extra whitespace is ignored.
+ *
+ * @param encoded String to decode.
+ * @param bout stream for decoded bytes
+ * @throws IllegalArgumentException if the input is not a valid
+ * B64 encoding.
+ */
+ public static void decode(String encoded, ByteArrayOutputStream bout)
+ {
+ // FIXME: no Jetty mainline code uses this anymore
+ if (encoded == null)
+ return;
+
+ if (bout == null)
+ throw new IllegalArgumentException("No outputstream for decoded bytes");
+
+ int ci = 0;
+ byte[] nibbles = new byte[4];
+ int s = 0;
+
+ while (ci < encoded.length())
+ {
+ char c = encoded.charAt(ci++);
+
+ if (c == __pad)
+ break;
+
+ if (Character.isWhitespace(c))
+ continue;
+
+ byte nibble = __rfc1421nibbles[c];
+ if (nibble < 0)
+ throw new IllegalArgumentException("Not B64 encoded");
+
+ nibbles[s++] = __rfc1421nibbles[c];
+
+ switch (s)
+ {
+ case 1:
+ break;
+ case 2:
+ bout.write(nibbles[0] << 2 | nibbles[1] >>> 4);
+ break;
+ case 3:
+ bout.write(nibbles[1] << 4 | nibbles[2] >>> 2);
+ break;
+ case 4:
+ bout.write(nibbles[2] << 6 | nibbles[3]);
+ s = 0;
+ break;
+ }
+ }
+
+ return;
+ }
+
+ public static byte[] decodeRFC4648URL(String encoded)
+ {
+ // FIXME: no Jetty mainline code uses this anymore
+ if (encoded == null)
+ return null;
+
+ ByteArrayOutputStream bout = new ByteArrayOutputStream(4 * encoded.length() / 3);
+ decodeRFC4648URL(encoded, bout);
+ return bout.toByteArray();
+ }
+
+ /**
+ * Base 64 decode as described in RFC 4648 URL.
+ * <p>Unlike {@link #decode(char[])}, extra whitespace is ignored.
+ *
+ * @param encoded String to decode.
+ * @param bout stream for decoded bytes
+ * @throws IllegalArgumentException if the input is not a valid
+ * B64 encoding.
+ */
+ public static void decodeRFC4648URL(String encoded, ByteArrayOutputStream bout)
+ {
+ // FIXME: no Jetty mainline code uses this anymore
+ if (encoded == null)
+ return;
+
+ if (bout == null)
+ throw new IllegalArgumentException("No outputstream for decoded bytes");
+
+ int ci = 0;
+ byte[] nibbles = new byte[4];
+ int s = 0;
+
+ while (ci < encoded.length())
+ {
+ char c = encoded.charAt(ci++);
+
+ if (c == __pad)
+ break;
+
+ if (Character.isWhitespace(c))
+ continue;
+
+ byte nibble = __rfc4648urlNibbles[c];
+ if (nibble < 0)
+ throw new IllegalArgumentException("Not B64 encoded");
+
+ nibbles[s++] = __rfc4648urlNibbles[c];
+
+ switch (s)
+ {
+ case 1:
+ break;
+ case 2:
+ bout.write(nibbles[0] << 2 | nibbles[1] >>> 4);
+ break;
+ case 3:
+ bout.write(nibbles[1] << 4 | nibbles[2] >>> 2);
+ break;
+ case 4:
+ bout.write(nibbles[2] << 6 | nibbles[3]);
+ s = 0;
+ break;
+ }
+ }
+
+ return;
+ }
+
+ public static void encode(int value, Appendable buf) throws IOException
+ {
+ // FIXME: no Jetty mainline code uses this anymore
+ buf.append(__rfc1421alphabet[0x3f & ((0xFC000000 & value) >> 26)]);
+ buf.append(__rfc1421alphabet[0x3f & ((0x03F00000 & value) >> 20)]);
+ buf.append(__rfc1421alphabet[0x3f & ((0x000FC000 & value) >> 14)]);
+ buf.append(__rfc1421alphabet[0x3f & ((0x00003F00 & value) >> 8)]);
+ buf.append(__rfc1421alphabet[0x3f & ((0x000000FC & value) >> 2)]);
+ buf.append(__rfc1421alphabet[0x3f & ((0x00000003 & value) << 4)]);
+ }
+
+ public static void encode(long lvalue, Appendable buf) throws IOException
+ {
+ // FIXME: no Jetty mainline code uses this anymore
+ int value = (int)(0xFFFFFFFC & (lvalue >> 32));
+ buf.append(__rfc1421alphabet[0x3f & ((0xFC000000 & value) >> 26)]);
+ buf.append(__rfc1421alphabet[0x3f & ((0x03F00000 & value) >> 20)]);
+ buf.append(__rfc1421alphabet[0x3f & ((0x000FC000 & value) >> 14)]);
+ buf.append(__rfc1421alphabet[0x3f & ((0x00003F00 & value) >> 8)]);
+ buf.append(__rfc1421alphabet[0x3f & ((0x000000FC & value) >> 2)]);
+
+ buf.append(__rfc1421alphabet[0x3f & ((0x00000003 & value) << 4) + (0xf & (int)(lvalue >> 28))]);
+
+ value = 0x0FFFFFFF & (int)lvalue;
+ buf.append(__rfc1421alphabet[0x3f & ((0x0FC00000 & value) >> 22)]);
+ buf.append(__rfc1421alphabet[0x3f & ((0x003F0000 & value) >> 16)]);
+ buf.append(__rfc1421alphabet[0x3f & ((0x0000FC00 & value) >> 10)]);
+ buf.append(__rfc1421alphabet[0x3f & ((0x000003F0 & value) >> 4)]);
+ buf.append(__rfc1421alphabet[0x3f & ((0x0000000F & value) << 2)]);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/BlockingArrayQueue.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/BlockingArrayQueue.java
new file mode 100644
index 0000000..48b931b
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/BlockingArrayQueue.java
@@ -0,0 +1,905 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.AbstractList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.ListIterator;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * A BlockingQueue backed by a circular array capable or growing.
+ * <p>
+ * This queue is uses a variant of the two lock queue algorithm to provide an efficient queue or list backed by a growable circular array.
+ * </p>
+ * <p>
+ * Unlike {@link java.util.concurrent.ArrayBlockingQueue}, this class is able to grow and provides a blocking put call.
+ * </p>
+ * <p>
+ * The queue has both a capacity (the size of the array currently allocated) and a max capacity (the maximum size that may be allocated), which defaults to
+ * {@link Integer#MAX_VALUE}.
+ * </p>
+ *
+ * @param <E> The element type
+ */
+public class BlockingArrayQueue<E> extends AbstractList<E> implements BlockingQueue<E>
+{
+ /**
+ * The head offset in the {@link #_indexes} array, displaced by 15 slots to avoid false sharing with the array length (stored before the first element of
+ * the array itself).
+ */
+ private static final int HEAD_OFFSET = MemoryUtils.getIntegersPerCacheLine() - 1;
+ /**
+ * The tail offset in the {@link #_indexes} array, displaced by 16 slots from the head to avoid false sharing with it.
+ */
+ private static final int TAIL_OFFSET = HEAD_OFFSET + MemoryUtils.getIntegersPerCacheLine();
+ /**
+ * Default initial capacity, 128.
+ */
+ public static final int DEFAULT_CAPACITY = 128;
+ /**
+ * Default growth factor, 64.
+ */
+ public static final int DEFAULT_GROWTH = 64;
+
+ private final int _maxCapacity;
+ private final int _growCapacity;
+ /**
+ * Array that holds the head and tail indexes, separated by a cache line to avoid false sharing
+ */
+ private final int[] _indexes = new int[TAIL_OFFSET + 1];
+ private final Lock _tailLock = new ReentrantLock();
+ private final AtomicInteger _size = new AtomicInteger();
+ private final Lock _headLock = new ReentrantLock();
+ private final Condition _notEmpty = _headLock.newCondition();
+ private Object[] _elements;
+
+ /**
+ * Creates an unbounded {@link BlockingArrayQueue} with default initial capacity and grow factor.
+ *
+ * @see #DEFAULT_CAPACITY
+ * @see #DEFAULT_GROWTH
+ */
+ public BlockingArrayQueue()
+ {
+ _elements = new Object[DEFAULT_CAPACITY];
+ _growCapacity = DEFAULT_GROWTH;
+ _maxCapacity = Integer.MAX_VALUE;
+ }
+
+ /**
+ * Creates a bounded {@link BlockingArrayQueue} that does not grow. The capacity of the queue is fixed and equal to the given parameter.
+ *
+ * @param maxCapacity the maximum capacity
+ */
+ public BlockingArrayQueue(int maxCapacity)
+ {
+ _elements = new Object[maxCapacity];
+ _growCapacity = -1;
+ _maxCapacity = maxCapacity;
+ }
+
+ /**
+ * Creates an unbounded {@link BlockingArrayQueue} that grows by the given parameter.
+ *
+ * @param capacity the initial capacity
+ * @param growBy the growth factor
+ */
+ public BlockingArrayQueue(int capacity, int growBy)
+ {
+ _elements = new Object[capacity];
+ _growCapacity = growBy;
+ _maxCapacity = Integer.MAX_VALUE;
+ }
+
+ /**
+ * Create a bounded {@link BlockingArrayQueue} that grows by the given parameter.
+ *
+ * @param capacity the initial capacity
+ * @param growBy the growth factor
+ * @param maxCapacity the maximum capacity
+ */
+ public BlockingArrayQueue(int capacity, int growBy, int maxCapacity)
+ {
+ if (capacity > maxCapacity)
+ throw new IllegalArgumentException();
+ _elements = new Object[capacity];
+ _growCapacity = growBy;
+ _maxCapacity = maxCapacity;
+ }
+
+
+ /* Collection methods */
+
+ @Override
+ public void clear()
+ {
+
+ _tailLock.lock();
+ try
+ {
+
+ _headLock.lock();
+ try
+ {
+ _indexes[HEAD_OFFSET] = 0;
+ _indexes[TAIL_OFFSET] = 0;
+ _size.set(0);
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ }
+ finally
+ {
+ _tailLock.unlock();
+ }
+ }
+
+ @Override
+ public int size()
+ {
+ return _size.get();
+ }
+
+ @Override
+ public Iterator<E> iterator()
+ {
+ return listIterator();
+ }
+
+
+ /* Queue methods */
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public E poll()
+ {
+ if (_size.get() == 0)
+ return null;
+
+ E e = null;
+
+ _headLock.lock(); // Size cannot shrink
+ try
+ {
+ if (_size.get() > 0)
+ {
+ final int head = _indexes[HEAD_OFFSET];
+ e = (E)_elements[head];
+ _elements[head] = null;
+ _indexes[HEAD_OFFSET] = (head + 1) % _elements.length;
+ if (_size.decrementAndGet() > 0)
+ _notEmpty.signal();
+ }
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ return e;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public E peek()
+ {
+ if (_size.get() == 0)
+ return null;
+
+ E e = null;
+
+ _headLock.lock(); // Size cannot shrink
+ try
+ {
+ if (_size.get() > 0)
+ e = (E)_elements[_indexes[HEAD_OFFSET]];
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ return e;
+ }
+
+ @Override
+ public E remove()
+ {
+ E e = poll();
+ if (e == null)
+ throw new NoSuchElementException();
+ return e;
+ }
+
+ @Override
+ public E element()
+ {
+ E e = peek();
+ if (e == null)
+ throw new NoSuchElementException();
+ return e;
+ }
+
+
+ /* BlockingQueue methods */
+
+ @Override
+ public boolean offer(E e)
+ {
+ Objects.requireNonNull(e);
+
+ boolean notEmpty = false;
+ _tailLock.lock(); // Size cannot grow... only shrink
+ try
+ {
+ int size = _size.get();
+ if (size >= _maxCapacity)
+ return false;
+
+ // Should we expand array?
+ if (size == _elements.length)
+ {
+ _headLock.lock();
+ try
+ {
+ if (!grow())
+ return false;
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ }
+
+ // Re-read head and tail after a possible grow
+ int tail = _indexes[TAIL_OFFSET];
+ _elements[tail] = e;
+ _indexes[TAIL_OFFSET] = (tail + 1) % _elements.length;
+ notEmpty = _size.getAndIncrement() == 0;
+ }
+ finally
+ {
+ _tailLock.unlock();
+ }
+
+ if (notEmpty)
+ {
+ _headLock.lock();
+ try
+ {
+ _notEmpty.signal();
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean add(E e)
+ {
+ if (offer(e))
+ return true;
+ else
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public void put(E o) throws InterruptedException
+ {
+ // The mechanism to await and signal when the queue is full is not implemented
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean offer(E o, long timeout, TimeUnit unit) throws InterruptedException
+ {
+ // The mechanism to await and signal when the queue is full is not implemented
+ throw new UnsupportedOperationException();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public E take() throws InterruptedException
+ {
+ E e = null;
+
+ _headLock.lockInterruptibly(); // Size cannot shrink
+ try
+ {
+ try
+ {
+ while (_size.get() == 0)
+ {
+ _notEmpty.await();
+ }
+ }
+ catch (InterruptedException ex)
+ {
+ _notEmpty.signal();
+ throw ex;
+ }
+
+ final int head = _indexes[HEAD_OFFSET];
+ e = (E)_elements[head];
+ _elements[head] = null;
+ _indexes[HEAD_OFFSET] = (head + 1) % _elements.length;
+
+ if (_size.decrementAndGet() > 0)
+ _notEmpty.signal();
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ return e;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public E poll(long time, TimeUnit unit) throws InterruptedException
+ {
+ long nanos = unit.toNanos(time);
+ E e = null;
+
+ _headLock.lockInterruptibly(); // Size cannot shrink
+ try
+ {
+ try
+ {
+ while (_size.get() == 0)
+ {
+ if (nanos <= 0)
+ return null;
+ nanos = _notEmpty.awaitNanos(nanos);
+ }
+ }
+ catch (InterruptedException x)
+ {
+ _notEmpty.signal();
+ throw x;
+ }
+
+ int head = _indexes[HEAD_OFFSET];
+ e = (E)_elements[head];
+ _elements[head] = null;
+ _indexes[HEAD_OFFSET] = (head + 1) % _elements.length;
+
+ if (_size.decrementAndGet() > 0)
+ _notEmpty.signal();
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ return e;
+ }
+
+ @Override
+ public boolean remove(Object o)
+ {
+
+ _tailLock.lock();
+ try
+ {
+
+ _headLock.lock();
+ try
+ {
+ if (isEmpty())
+ return false;
+
+ final int head = _indexes[HEAD_OFFSET];
+ final int tail = _indexes[TAIL_OFFSET];
+ final int capacity = _elements.length;
+
+ int i = head;
+ while (true)
+ {
+ if (Objects.equals(_elements[i], o))
+ {
+ remove(i >= head ? i - head : capacity - head + i);
+ return true;
+ }
+ ++i;
+ if (i == capacity)
+ i = 0;
+ if (i == tail)
+ return false;
+ }
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ }
+ finally
+ {
+ _tailLock.unlock();
+ }
+ }
+
+ @Override
+ public int remainingCapacity()
+ {
+
+ _tailLock.lock();
+ try
+ {
+
+ _headLock.lock();
+ try
+ {
+ return getCapacity() - size();
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ }
+ finally
+ {
+ _tailLock.unlock();
+ }
+ }
+
+ @Override
+ public int drainTo(Collection<? super E> c)
+ {
+ return drainTo(c, Integer.MAX_VALUE);
+ }
+
+ @Override
+ public int drainTo(Collection<? super E> c, int maxElements)
+ {
+ int elements = 0;
+ _tailLock.lock();
+ try
+ {
+ _headLock.lock();
+ try
+ {
+ final int head = _indexes[HEAD_OFFSET];
+ final int tail = _indexes[TAIL_OFFSET];
+ final int capacity = _elements.length;
+
+ int i = head;
+ while (i != tail && elements < maxElements)
+ {
+ elements++;
+ c.add((E)_elements[i]);
+ ++i;
+ if (i == capacity)
+ i = 0;
+ }
+
+ if (i == tail)
+ {
+ _indexes[HEAD_OFFSET] = 0;
+ _indexes[TAIL_OFFSET] = 0;
+ _size.set(0);
+ }
+ else
+ {
+ _indexes[HEAD_OFFSET] = i;
+ _size.addAndGet(-elements);
+ }
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ }
+ finally
+ {
+ _tailLock.unlock();
+ }
+ return elements;
+ }
+
+
+ /* List methods */
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public E get(int index)
+ {
+
+ _tailLock.lock();
+ try
+ {
+ _headLock.lock();
+ try
+ {
+ if (index < 0 || index >= _size.get())
+ throw new IndexOutOfBoundsException("!(" + 0 + "<" + index + "<=" + _size + ")");
+ int i = _indexes[HEAD_OFFSET] + index;
+ int capacity = _elements.length;
+ if (i >= capacity)
+ i -= capacity;
+ return (E)_elements[i];
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ }
+ finally
+ {
+ _tailLock.unlock();
+ }
+ }
+
+ @Override
+ public void add(int index, E e)
+ {
+ if (e == null)
+ throw new NullPointerException();
+
+ _tailLock.lock();
+ try
+ {
+
+ _headLock.lock();
+ try
+ {
+ final int size = _size.get();
+
+ if (index < 0 || index > size)
+ throw new IndexOutOfBoundsException("!(" + 0 + "<" + index + "<=" + _size + ")");
+
+ if (index == size)
+ {
+ add(e);
+ }
+ else
+ {
+ if (_indexes[TAIL_OFFSET] == _indexes[HEAD_OFFSET])
+ if (!grow())
+ throw new IllegalStateException("full");
+
+ // Re-read head and tail after a possible grow
+ int i = _indexes[HEAD_OFFSET] + index;
+ int capacity = _elements.length;
+
+ if (i >= capacity)
+ i -= capacity;
+
+ _size.incrementAndGet();
+ int tail = _indexes[TAIL_OFFSET];
+ _indexes[TAIL_OFFSET] = tail = (tail + 1) % capacity;
+
+ if (i < tail)
+ {
+ System.arraycopy(_elements, i, _elements, i + 1, tail - i);
+ _elements[i] = e;
+ }
+ else
+ {
+ if (tail > 0)
+ {
+ System.arraycopy(_elements, 0, _elements, 1, tail);
+ _elements[0] = _elements[capacity - 1];
+ }
+
+ System.arraycopy(_elements, i, _elements, i + 1, capacity - i - 1);
+ _elements[i] = e;
+ }
+ }
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ }
+ finally
+ {
+ _tailLock.unlock();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public E set(int index, E e)
+ {
+ Objects.requireNonNull(e);
+
+ _tailLock.lock();
+ try
+ {
+
+ _headLock.lock();
+ try
+ {
+ if (index < 0 || index >= _size.get())
+ throw new IndexOutOfBoundsException("!(" + 0 + "<" + index + "<=" + _size + ")");
+
+ int i = _indexes[HEAD_OFFSET] + index;
+ int capacity = _elements.length;
+ if (i >= capacity)
+ i -= capacity;
+ E old = (E)_elements[i];
+ _elements[i] = e;
+ return old;
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ }
+ finally
+ {
+ _tailLock.unlock();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public E remove(int index)
+ {
+
+ _tailLock.lock();
+ try
+ {
+
+ _headLock.lock();
+ try
+ {
+ if (index < 0 || index >= _size.get())
+ throw new IndexOutOfBoundsException("!(" + 0 + "<" + index + "<=" + _size + ")");
+
+ int i = _indexes[HEAD_OFFSET] + index;
+ int capacity = _elements.length;
+ if (i >= capacity)
+ i -= capacity;
+ E old = (E)_elements[i];
+
+ int tail = _indexes[TAIL_OFFSET];
+ if (i < tail)
+ {
+ System.arraycopy(_elements, i + 1, _elements, i, tail - i);
+ --_indexes[TAIL_OFFSET];
+ }
+ else
+ {
+ System.arraycopy(_elements, i + 1, _elements, i, capacity - i - 1);
+ _elements[capacity - 1] = _elements[0];
+ if (tail > 0)
+ {
+ System.arraycopy(_elements, 1, _elements, 0, tail);
+ --_indexes[TAIL_OFFSET];
+ }
+ else
+ {
+ _indexes[TAIL_OFFSET] = capacity - 1;
+ }
+ _elements[_indexes[TAIL_OFFSET]] = null;
+ }
+
+ _size.decrementAndGet();
+
+ return old;
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ }
+ finally
+ {
+ _tailLock.unlock();
+ }
+ }
+
+ @Override
+ public ListIterator<E> listIterator(int index)
+ {
+
+ _tailLock.lock();
+ try
+ {
+
+ _headLock.lock();
+ try
+ {
+ Object[] elements = new Object[size()];
+ if (size() > 0)
+ {
+ int head = _indexes[HEAD_OFFSET];
+ int tail = _indexes[TAIL_OFFSET];
+ if (head < tail)
+ {
+ System.arraycopy(_elements, head, elements, 0, tail - head);
+ }
+ else
+ {
+ int chunk = _elements.length - head;
+ System.arraycopy(_elements, head, elements, 0, chunk);
+ System.arraycopy(_elements, 0, elements, chunk, tail);
+ }
+ }
+ return new Itr(elements, index);
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ }
+ finally
+ {
+ _tailLock.unlock();
+ }
+ }
+
+ /**
+ * @return the current capacity of this queue
+ */
+ public int getCapacity()
+ {
+ _tailLock.lock();
+ try
+ {
+ return _elements.length;
+ }
+ finally
+ {
+ _tailLock.unlock();
+ }
+ }
+
+ /**
+ * @return the max capacity of this queue, or -1 if this queue is unbounded
+ */
+ public int getMaxCapacity()
+ {
+ return _maxCapacity;
+ }
+
+ private boolean grow()
+ {
+ if (_growCapacity <= 0)
+ return false;
+
+ _tailLock.lock();
+ try
+ {
+
+ _headLock.lock();
+ try
+ {
+ final int head = _indexes[HEAD_OFFSET];
+ final int tail = _indexes[TAIL_OFFSET];
+ final int newTail;
+ final int capacity = _elements.length;
+
+ Object[] elements = new Object[capacity + _growCapacity];
+
+ if (head < tail)
+ {
+ newTail = tail - head;
+ System.arraycopy(_elements, head, elements, 0, newTail);
+ }
+ else if (head > tail || _size.get() > 0)
+ {
+ newTail = capacity + tail - head;
+ int cut = capacity - head;
+ System.arraycopy(_elements, head, elements, 0, cut);
+ System.arraycopy(_elements, 0, elements, cut, tail);
+ }
+ else
+ {
+ newTail = 0;
+ }
+
+ _elements = elements;
+ _indexes[HEAD_OFFSET] = 0;
+ _indexes[TAIL_OFFSET] = newTail;
+ return true;
+ }
+ finally
+ {
+ _headLock.unlock();
+ }
+ }
+ finally
+ {
+ _tailLock.unlock();
+ }
+ }
+
+ private class Itr implements ListIterator<E>
+ {
+ private final Object[] _elements;
+ private int _cursor;
+
+ public Itr(Object[] elements, int offset)
+ {
+ _elements = elements;
+ _cursor = offset;
+ }
+
+ @Override
+ public boolean hasNext()
+ {
+ return _cursor < _elements.length;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public E next()
+ {
+ return (E)_elements[_cursor++];
+ }
+
+ @Override
+ public boolean hasPrevious()
+ {
+ return _cursor > 0;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public E previous()
+ {
+ return (E)_elements[--_cursor];
+ }
+
+ @Override
+ public int nextIndex()
+ {
+ return _cursor + 1;
+ }
+
+ @Override
+ public int previousIndex()
+ {
+ return _cursor - 1;
+ }
+
+ @Override
+ public void remove()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void set(E e)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void add(E e)
+ {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
new file mode 100644
index 0000000..88e141a
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
@@ -0,0 +1,1281 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.Buffer;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileChannel.MapMode;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Arrays;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.resource.Resource;
+
+/**
+ * Buffer utility methods.
+ * <p>The standard JVM {@link ByteBuffer} can exist in two modes: In fill mode the valid
+ * data is between 0 and pos; In flush mode the valid data is between the pos and the limit.
+ * The various ByteBuffer methods assume a mode and some of them will switch or enforce a mode:
+ * Allocate and clear set fill mode; flip and compact switch modes; read and write assume fill
+ * and flush modes. This duality can result in confusing code such as:
+ * </p>
+ * <pre>
+ * buffer.clear();
+ * channel.write(buffer);
+ * </pre>
+ * <p>
+ * Which looks as if it should write no data, but in fact writes the buffer worth of garbage.
+ * </p>
+ * <p>
+ * The BufferUtil class provides a set of utilities that operate on the convention that ByteBuffers
+ * will always be left, passed in an API or returned from a method in the flush mode - ie with
+ * valid data between the pos and limit. This convention is adopted so as to avoid confusion as to
+ * what state a buffer is in and to avoid excessive copying of data that can result with the usage
+ * of compress.</p>
+ * <p>
+ * Thus this class provides alternate implementations of {@link #allocate(int)},
+ * {@link #allocateDirect(int)} and {@link #clear(ByteBuffer)} that leave the buffer
+ * in flush mode. Thus the following tests will pass:
+ * </p>
+ * <pre>
+ * ByteBuffer buffer = BufferUtil.allocate(1024);
+ * assert(buffer.remaining()==0);
+ * BufferUtil.clear(buffer);
+ * assert(buffer.remaining()==0);
+ * </pre>
+ * <p>If the BufferUtil methods {@link #fill(ByteBuffer, byte[], int, int)},
+ * {@link #append(ByteBuffer, byte[], int, int)} or {@link #put(ByteBuffer, ByteBuffer)} are used,
+ * then the caller does not need to explicitly switch the buffer to fill mode.
+ * If the caller wishes to use other ByteBuffer bases libraries to fill a buffer,
+ * then they can use explicit calls of #flipToFill(ByteBuffer) and #flipToFlush(ByteBuffer, int)
+ * to change modes. Note because this convention attempts to avoid the copies of compact, the position
+ * is not set to zero on each fill cycle and so its value must be remembered:
+ * </p>
+ * <pre>
+ * int pos = BufferUtil.flipToFill(buffer);
+ * try
+ * {
+ * buffer.put(data);
+ * }
+ * finally
+ * {
+ * flipToFlush(buffer, pos);
+ * }
+ * </pre>
+ * <p>
+ * The flipToFill method will effectively clear the buffer if it is empty and will compact the buffer if there is no space.
+ * </p>
+ */
+public class BufferUtil
+{
+ static final int TEMP_BUFFER_SIZE = 4096;
+ static final byte SPACE = 0x20;
+ static final byte MINUS = '-';
+ static final byte[] DIGIT =
+ {
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9',
+ (byte)'A', (byte)'B', (byte)'C', (byte)'D',
+ (byte)'E', (byte)'F'
+ };
+
+ public static final ByteBuffer EMPTY_BUFFER = ByteBuffer.wrap(new byte[0]);
+
+ /**
+ * Allocate ByteBuffer in flush mode.
+ * The position and limit will both be zero, indicating that the buffer is
+ * empty and must be flipped before any data is put to it.
+ *
+ * @param capacity capacity of the allocated ByteBuffer
+ * @return Buffer
+ */
+ public static ByteBuffer allocate(int capacity)
+ {
+ ByteBuffer buf = ByteBuffer.allocate(capacity);
+ buf.limit(0);
+ return buf;
+ }
+
+ /**
+ * Allocate ByteBuffer in flush mode.
+ * The position and limit will both be zero, indicating that the buffer is
+ * empty and in flush mode.
+ *
+ * @param capacity capacity of the allocated ByteBuffer
+ * @return Buffer
+ */
+ public static ByteBuffer allocateDirect(int capacity)
+ {
+ ByteBuffer buf = ByteBuffer.allocateDirect(capacity);
+ buf.limit(0);
+ return buf;
+ }
+
+ /**
+ * Deep copy of a buffer
+ *
+ * @param buffer The buffer to copy
+ * @return A copy of the buffer
+ */
+ public static ByteBuffer copy(ByteBuffer buffer)
+ {
+ if (buffer == null)
+ return null;
+ int p = buffer.position();
+ ByteBuffer clone = buffer.isDirect() ? ByteBuffer.allocateDirect(buffer.remaining()) : ByteBuffer.allocate(buffer.remaining());
+ clone.put(buffer);
+ clone.flip();
+ buffer.position(p);
+ return clone;
+ }
+
+ /**
+ * Clear the buffer to be empty in flush mode.
+ * The position and limit are set to 0;
+ *
+ * @param buffer The buffer to clear.
+ */
+ public static void clear(ByteBuffer buffer)
+ {
+ if (buffer != null)
+ {
+ buffer.position(0);
+ buffer.limit(0);
+ }
+ }
+
+ /**
+ * Clear the buffer to be empty in fill mode.
+ * The position is set to 0 and the limit is set to the capacity.
+ *
+ * @param buffer The buffer to clear.
+ */
+ public static void clearToFill(ByteBuffer buffer)
+ {
+ if (buffer != null)
+ {
+ buffer.position(0);
+ buffer.limit(buffer.capacity());
+ }
+ }
+
+ /**
+ * Flip the buffer to fill mode.
+ * The position is set to the first unused position in the buffer
+ * (the old limit) and the limit is set to the capacity.
+ * If the buffer is empty, then this call is effectively {@link #clearToFill(ByteBuffer)}.
+ * If there is no unused space to fill, a {@link ByteBuffer#compact()} is done to attempt
+ * to create space.
+ * <p>
+ * This method is used as a replacement to {@link ByteBuffer#compact()}.
+ *
+ * @param buffer The buffer to flip
+ * @return The position of the valid data before the flipped position. This value should be
+ * passed to a subsequent call to {@link #flipToFlush(ByteBuffer, int)}
+ */
+ public static int flipToFill(ByteBuffer buffer)
+ {
+ int position = buffer.position();
+ int limit = buffer.limit();
+ if (position == limit)
+ {
+ buffer.position(0);
+ buffer.limit(buffer.capacity());
+ return 0;
+ }
+
+ int capacity = buffer.capacity();
+ if (limit == capacity)
+ {
+ buffer.compact();
+ return 0;
+ }
+
+ buffer.position(limit);
+ buffer.limit(capacity);
+ return position;
+ }
+
+ /**
+ * Flip the buffer to Flush mode.
+ * The limit is set to the first unused byte(the old position) and
+ * the position is set to the passed position.
+ * <p>
+ * This method is used as a replacement of {@link Buffer#flip()}.
+ *
+ * @param buffer the buffer to be flipped
+ * @param position The position of valid data to flip to. This should
+ * be the return value of the previous call to {@link #flipToFill(ByteBuffer)}
+ */
+ public static void flipToFlush(ByteBuffer buffer, int position)
+ {
+ buffer.limit(buffer.position());
+ buffer.position(position);
+ }
+
+ /** Put an integer little endian
+ * @param buffer The buffer to put to
+ * @param value The value to put.
+ */
+ public static void putIntLittleEndian(ByteBuffer buffer, int value)
+ {
+ int p = flipToFill(buffer);
+ buffer.put((byte)(value & 0xFF));
+ buffer.put((byte)((value >>> 8) & 0xFF));
+ buffer.put((byte)((value >>> 16) & 0xFF));
+ buffer.put((byte)((value >>> 24) & 0xFF));
+ flipToFlush(buffer, p);
+ }
+
+ /**
+ * Convert a ByteBuffer to a byte array.
+ *
+ * @param buffer The buffer to convert in flush mode. The buffer is not altered.
+ * @return An array of bytes duplicated from the buffer.
+ */
+ public static byte[] toArray(ByteBuffer buffer)
+ {
+ if (buffer.hasArray())
+ {
+ byte[] array = buffer.array();
+ int from = buffer.arrayOffset() + buffer.position();
+ return Arrays.copyOfRange(array, from, from + buffer.remaining());
+ }
+ else
+ {
+ byte[] to = new byte[buffer.remaining()];
+ buffer.slice().get(to);
+ return to;
+ }
+ }
+
+ /**
+ * @param buf the buffer to check
+ * @return true if buf is equal to EMPTY_BUFFER
+ */
+ public static boolean isTheEmptyBuffer(ByteBuffer buf)
+ {
+ @SuppressWarnings("ReferenceEquality")
+ boolean isTheEmptyBuffer = (buf == EMPTY_BUFFER);
+ return isTheEmptyBuffer;
+ }
+
+ /**
+ * Check for an empty or null buffer.
+ *
+ * @param buf the buffer to check
+ * @return true if the buffer is null or empty.
+ */
+ public static boolean isEmpty(ByteBuffer buf)
+ {
+ return buf == null || buf.remaining() == 0;
+ }
+
+ /**
+ * Check for an empty or null buffers.
+ *
+ * @param buf the buffer to check
+ * @return true if the buffer is null or empty.
+ */
+ public static boolean isEmpty(ByteBuffer[] buf)
+ {
+ if (buf == null || buf.length == 0)
+ return true;
+ for (ByteBuffer b : buf)
+ {
+ if (b != null && b.hasRemaining())
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Get the remaining bytes in 0 or more buffers.
+ *
+ * @param buf the buffers to check
+ * @return number of bytes remaining in all buffers.
+ */
+ public static long remaining(ByteBuffer... buf)
+ {
+ long remaining = 0;
+ if (buf != null)
+ for (ByteBuffer b : buf)
+ {
+ if (b != null)
+ remaining += b.remaining();
+ }
+ return remaining;
+ }
+
+ /**
+ * Check for a non null and non empty buffer.
+ *
+ * @param buf the buffer to check
+ * @return true if the buffer is not null and not empty.
+ */
+ public static boolean hasContent(ByteBuffer buf)
+ {
+ return buf != null && buf.remaining() > 0;
+ }
+
+ /**
+ * Check for a non null and full buffer.
+ *
+ * @param buf the buffer to check
+ * @return true if the buffer is not null and the limit equals the capacity.
+ */
+ public static boolean isFull(ByteBuffer buf)
+ {
+ return buf != null && buf.limit() == buf.capacity();
+ }
+
+ /**
+ * Get remaining from null checked buffer
+ *
+ * @param buffer The buffer to get the remaining from, in flush mode.
+ * @return 0 if the buffer is null, else the bytes remaining in the buffer.
+ */
+ public static int length(ByteBuffer buffer)
+ {
+ return buffer == null ? 0 : buffer.remaining();
+ }
+
+ /**
+ * Get the space from the limit to the capacity
+ *
+ * @param buffer the buffer to get the space from
+ * @return space
+ */
+ public static int space(ByteBuffer buffer)
+ {
+ if (buffer == null)
+ return 0;
+ return buffer.capacity() - buffer.limit();
+ }
+
+ /**
+ * Compact the buffer
+ *
+ * @param buffer the buffer to compact
+ * @return true if the compact made a full buffer have space
+ */
+ public static boolean compact(ByteBuffer buffer)
+ {
+ if (buffer.position() == 0)
+ return false;
+ boolean full = buffer.limit() == buffer.capacity();
+ buffer.compact().flip();
+ return full && buffer.limit() < buffer.capacity();
+ }
+
+ /**
+ * Put data from one buffer into another, avoiding over/under flows
+ *
+ * @param from Buffer to take bytes from in flush mode
+ * @param to Buffer to put bytes to in fill mode.
+ * @return number of bytes moved
+ */
+ public static int put(ByteBuffer from, ByteBuffer to)
+ {
+ int put;
+ int remaining = from.remaining();
+ if (remaining > 0)
+ {
+ if (remaining <= to.remaining())
+ {
+ to.put(from);
+ put = remaining;
+ from.position(from.limit());
+ }
+ else if (from.hasArray())
+ {
+ put = to.remaining();
+ to.put(from.array(), from.arrayOffset() + from.position(), put);
+ from.position(from.position() + put);
+ }
+ else
+ {
+ put = to.remaining();
+ ByteBuffer slice = from.slice();
+ slice.limit(put);
+ to.put(slice);
+ from.position(from.position() + put);
+ }
+ }
+ else
+ put = 0;
+
+ return put;
+ }
+
+ /**
+ * Put data from one buffer into another, avoiding over/under flows
+ *
+ * @param from Buffer to take bytes from in flush mode
+ * @param to Buffer to put bytes to in flush mode. The buffer is flipToFill before the put and flipToFlush after.
+ * @return number of bytes moved
+ * @deprecated use {@link #append(ByteBuffer, ByteBuffer)}
+ */
+ public static int flipPutFlip(ByteBuffer from, ByteBuffer to)
+ {
+ return append(to, from);
+ }
+
+ /**
+ * Append bytes to a buffer.
+ *
+ * @param to Buffer is flush mode
+ * @param b bytes to append
+ * @param off offset into byte
+ * @param len length to append
+ * @throws BufferOverflowException if unable to append buffer due to space limits
+ */
+ public static void append(ByteBuffer to, byte[] b, int off, int len) throws BufferOverflowException
+ {
+ int pos = flipToFill(to);
+ try
+ {
+ to.put(b, off, len);
+ }
+ finally
+ {
+ flipToFlush(to, pos);
+ }
+ }
+
+ /**
+ * Appends a byte to a buffer
+ *
+ * @param to Buffer is flush mode
+ * @param b byte to append
+ * @throws BufferOverflowException if unable to append buffer due to space limits
+ */
+ public static void append(ByteBuffer to, byte b)
+ {
+ int pos = flipToFill(to);
+ try
+ {
+ to.put(b);
+ }
+ finally
+ {
+ flipToFlush(to, pos);
+ }
+ }
+
+ /**
+ * Appends a buffer to a buffer
+ *
+ * @param to Buffer is flush mode
+ * @param b buffer to append
+ * @return The position of the valid data before the flipped position.
+ */
+ public static int append(ByteBuffer to, ByteBuffer b)
+ {
+ int pos = flipToFill(to);
+ try
+ {
+ return put(b, to);
+ }
+ finally
+ {
+ flipToFlush(to, pos);
+ }
+ }
+
+ /**
+ * Like append, but does not throw {@link BufferOverflowException}
+ *
+ * @param to Buffer The buffer to fill to. The buffer will be flipped to fill mode and then flipped back to flush mode.
+ * @param b bytes The bytes to fill
+ * @param off offset into bytes
+ * @param len length to fill
+ * @return the number of bytes taken from the buffer.
+ */
+ public static int fill(ByteBuffer to, byte[] b, int off, int len)
+ {
+ int pos = flipToFill(to);
+ try
+ {
+ int remaining = to.remaining();
+ int take = remaining < len ? remaining : len;
+ to.put(b, off, take);
+ return take;
+ }
+ finally
+ {
+ flipToFlush(to, pos);
+ }
+ }
+
+ public static void readFrom(File file, ByteBuffer buffer) throws IOException
+ {
+ try (RandomAccessFile raf = new RandomAccessFile(file, "r"))
+ {
+ FileChannel channel = raf.getChannel();
+ long needed = raf.length();
+
+ while (needed > 0 && buffer.hasRemaining())
+ {
+ needed = needed - channel.read(buffer);
+ }
+ }
+ }
+
+ public static void readFrom(InputStream is, int needed, ByteBuffer buffer) throws IOException
+ {
+ ByteBuffer tmp = allocate(8192);
+
+ while (needed > 0 && buffer.hasRemaining())
+ {
+ int l = is.read(tmp.array(), 0, 8192);
+ if (l < 0)
+ break;
+ tmp.position(0);
+ tmp.limit(l);
+ buffer.put(tmp);
+ }
+ }
+
+ public static void writeTo(ByteBuffer buffer, OutputStream out) throws IOException
+ {
+ if (buffer.hasArray())
+ {
+ out.write(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
+ // update buffer position, in way similar to non-array version of writeTo
+ buffer.position(buffer.position() + buffer.remaining());
+ }
+ else
+ {
+ byte[] bytes = new byte[Math.min(buffer.remaining(), TEMP_BUFFER_SIZE)];
+ while (buffer.hasRemaining())
+ {
+ int byteCountToWrite = Math.min(buffer.remaining(), TEMP_BUFFER_SIZE);
+ buffer.get(bytes, 0, byteCountToWrite);
+ out.write(bytes, 0, byteCountToWrite);
+ }
+ }
+ }
+
+ /**
+ * Convert the buffer to an ISO-8859-1 String
+ *
+ * @param buffer The buffer to convert in flush mode. The buffer is unchanged
+ * @return The buffer as a string.
+ */
+ public static String toString(ByteBuffer buffer)
+ {
+ return toString(buffer, StandardCharsets.ISO_8859_1);
+ }
+
+ /**
+ * Convert the buffer to an UTF-8 String
+ *
+ * @param buffer The buffer to convert in flush mode. The buffer is unchanged
+ * @return The buffer as a string.
+ */
+ public static String toUTF8String(ByteBuffer buffer)
+ {
+ return toString(buffer, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Convert the buffer to an ISO-8859-1 String
+ *
+ * @param buffer The buffer to convert in flush mode. The buffer is unchanged
+ * @param charset The {@link Charset} to use to convert the bytes
+ * @return The buffer as a string.
+ */
+ public static String toString(ByteBuffer buffer, Charset charset)
+ {
+ if (buffer == null)
+ return null;
+ byte[] array = buffer.hasArray() ? buffer.array() : null;
+ if (array == null)
+ {
+ byte[] to = new byte[buffer.remaining()];
+ buffer.slice().get(to);
+ return new String(to, 0, to.length, charset);
+ }
+ return new String(array, buffer.arrayOffset() + buffer.position(), buffer.remaining(), charset);
+ }
+
+ /**
+ * Convert a partial buffer to a String.
+ *
+ * @param buffer the buffer to convert
+ * @param position The position in the buffer to start the string from
+ * @param length The length of the buffer
+ * @param charset The {@link Charset} to use to convert the bytes
+ * @return The buffer as a string.
+ */
+ public static String toString(ByteBuffer buffer, int position, int length, Charset charset)
+ {
+ if (buffer == null)
+ return null;
+ byte[] array = buffer.hasArray() ? buffer.array() : null;
+ if (array == null)
+ {
+ ByteBuffer ro = buffer.asReadOnlyBuffer();
+ ro.position(position);
+ ro.limit(position + length);
+ byte[] to = new byte[length];
+ ro.get(to);
+ return new String(to, 0, to.length, charset);
+ }
+ return new String(array, buffer.arrayOffset() + position, length, charset);
+ }
+
+ /**
+ * Convert buffer to an integer. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown
+ *
+ * @param buffer A buffer containing an integer in flush mode. The position is not changed.
+ * @return an int
+ */
+ public static int toInt(ByteBuffer buffer)
+ {
+ return toInt(buffer, buffer.position(), buffer.remaining());
+ }
+
+ /**
+ * Convert buffer to an integer. Parses up to the first non-numeric character. If no number is found an
+ * IllegalArgumentException is thrown
+ *
+ * @param buffer A buffer containing an integer in flush mode. The position is not changed.
+ * @param position the position in the buffer to start reading from
+ * @param length the length of the buffer to use for conversion
+ * @return an int of the buffer bytes
+ */
+ public static int toInt(ByteBuffer buffer, int position, int length)
+ {
+ int val = 0;
+ boolean started = false;
+ boolean minus = false;
+
+ int limit = position + length;
+
+ if (length <= 0)
+ throw new NumberFormatException(toString(buffer, position, length, StandardCharsets.UTF_8));
+
+ for (int i = position; i < limit; i++)
+ {
+ byte b = buffer.get(i);
+ if (b <= SPACE)
+ {
+ if (started)
+ break;
+ }
+ else if (b >= '0' && b <= '9')
+ {
+ val = val * 10 + (b - '0');
+ started = true;
+ }
+ else if (b == MINUS && !started)
+ {
+ minus = true;
+ }
+ else
+ break;
+ }
+
+ if (started)
+ return minus ? (-val) : val;
+ throw new NumberFormatException(toString(buffer));
+ }
+
+ /**
+ * Convert buffer to an integer. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown
+ *
+ * @param buffer A buffer containing an integer in flush mode. The position is updated.
+ * @return an int
+ */
+ public static int takeInt(ByteBuffer buffer)
+ {
+ int val = 0;
+ boolean started = false;
+ boolean minus = false;
+ int i;
+ for (i = buffer.position(); i < buffer.limit(); i++)
+ {
+ byte b = buffer.get(i);
+ if (b <= SPACE)
+ {
+ if (started)
+ break;
+ }
+ else if (b >= '0' && b <= '9')
+ {
+ val = val * 10 + (b - '0');
+ started = true;
+ }
+ else if (b == MINUS && !started)
+ {
+ minus = true;
+ }
+ else
+ break;
+ }
+
+ if (started)
+ {
+ buffer.position(i);
+ return minus ? (-val) : val;
+ }
+ throw new NumberFormatException(toString(buffer));
+ }
+
+ /**
+ * Convert buffer to an long. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown
+ *
+ * @param buffer A buffer containing an integer in flush mode. The position is not changed.
+ * @return an int
+ */
+ public static long toLong(ByteBuffer buffer)
+ {
+ long val = 0;
+ boolean started = false;
+ boolean minus = false;
+
+ for (int i = buffer.position(); i < buffer.limit(); i++)
+ {
+ byte b = buffer.get(i);
+ if (b <= SPACE)
+ {
+ if (started)
+ break;
+ }
+ else if (b >= '0' && b <= '9')
+ {
+ val = val * 10L + (b - '0');
+ started = true;
+ }
+ else if (b == MINUS && !started)
+ {
+ minus = true;
+ }
+ else
+ break;
+ }
+
+ if (started)
+ return minus ? (-val) : val;
+ throw new NumberFormatException(toString(buffer));
+ }
+
+ public static void putHexInt(ByteBuffer buffer, int n)
+ {
+ if (n < 0)
+ {
+ buffer.put((byte)'-');
+
+ if (n == Integer.MIN_VALUE)
+ {
+ buffer.put((byte)(0x7f & '8'));
+ buffer.put((byte)(0x7f & '0'));
+ buffer.put((byte)(0x7f & '0'));
+ buffer.put((byte)(0x7f & '0'));
+ buffer.put((byte)(0x7f & '0'));
+ buffer.put((byte)(0x7f & '0'));
+ buffer.put((byte)(0x7f & '0'));
+ buffer.put((byte)(0x7f & '0'));
+
+ return;
+ }
+ n = -n;
+ }
+
+ if (n < 0x10)
+ {
+ buffer.put(DIGIT[n]);
+ }
+ else
+ {
+ boolean started = false;
+ // This assumes constant time int arithmatic
+ for (int hexDivisor : hexDivisors)
+ {
+ if (n < hexDivisor)
+ {
+ if (started)
+ buffer.put((byte)'0');
+ continue;
+ }
+
+ started = true;
+ int d = n / hexDivisor;
+ buffer.put(DIGIT[d]);
+ n = n - d * hexDivisor;
+ }
+ }
+ }
+
+ public static void putDecInt(ByteBuffer buffer, int n)
+ {
+ if (n < 0)
+ {
+ buffer.put((byte)'-');
+
+ if (n == Integer.MIN_VALUE)
+ {
+ buffer.put((byte)'2');
+ n = 147483648;
+ }
+ else
+ n = -n;
+ }
+
+ if (n < 10)
+ {
+ buffer.put(DIGIT[n]);
+ }
+ else
+ {
+ boolean started = false;
+ // This assumes constant time int arithmatic
+ for (int decDivisor : decDivisors)
+ {
+ if (n < decDivisor)
+ {
+ if (started)
+ buffer.put((byte)'0');
+ continue;
+ }
+
+ started = true;
+ int d = n / decDivisor;
+ buffer.put(DIGIT[d]);
+ n = n - d * decDivisor;
+ }
+ }
+ }
+
+ public static void putDecLong(ByteBuffer buffer, long n)
+ {
+ if (n < 0)
+ {
+ buffer.put((byte)'-');
+
+ if (n == Long.MIN_VALUE)
+ {
+ buffer.put((byte)'9');
+ n = 223372036854775808L;
+ }
+ else
+ n = -n;
+ }
+
+ if (n < 10)
+ {
+ buffer.put(DIGIT[(int)n]);
+ }
+ else
+ {
+ boolean started = false;
+ // This assumes constant time int arithmatic
+ for (long aDecDivisorsL : decDivisorsL)
+ {
+ if (n < aDecDivisorsL)
+ {
+ if (started)
+ buffer.put((byte)'0');
+ continue;
+ }
+
+ started = true;
+ long d = n / aDecDivisorsL;
+ buffer.put(DIGIT[(int)d]);
+ n = n - d * aDecDivisorsL;
+ }
+ }
+ }
+
+ public static ByteBuffer toBuffer(int value)
+ {
+ ByteBuffer buf = ByteBuffer.allocate(32);
+ putDecInt(buf, value);
+ return buf;
+ }
+
+ public static ByteBuffer toBuffer(long value)
+ {
+ ByteBuffer buf = ByteBuffer.allocate(32);
+ putDecLong(buf, value);
+ return buf;
+ }
+
+ public static ByteBuffer toBuffer(String s)
+ {
+ return toBuffer(s, StandardCharsets.ISO_8859_1);
+ }
+
+ public static ByteBuffer toBuffer(String s, Charset charset)
+ {
+ if (s == null)
+ return EMPTY_BUFFER;
+ return toBuffer(s.getBytes(charset));
+ }
+
+ /**
+ * Create a new ByteBuffer using provided byte array.
+ *
+ * @param array the byte array to back buffer with.
+ * @return ByteBuffer with provided byte array, in flush mode
+ */
+ public static ByteBuffer toBuffer(byte[] array)
+ {
+ if (array == null)
+ return EMPTY_BUFFER;
+ return toBuffer(array, 0, array.length);
+ }
+
+ /**
+ * Create a new ByteBuffer using the provided byte array.
+ *
+ * @param array the byte array to use.
+ * @param offset the offset within the byte array to use from
+ * @param length the length in bytes of the array to use
+ * @return ByteBuffer with provided byte array, in flush mode
+ */
+ public static ByteBuffer toBuffer(byte[] array, int offset, int length)
+ {
+ if (array == null)
+ return EMPTY_BUFFER;
+ return ByteBuffer.wrap(array, offset, length);
+ }
+
+ public static ByteBuffer toDirectBuffer(String s)
+ {
+ return toDirectBuffer(s, StandardCharsets.ISO_8859_1);
+ }
+
+ public static ByteBuffer toDirectBuffer(String s, Charset charset)
+ {
+ if (s == null)
+ return EMPTY_BUFFER;
+ byte[] bytes = s.getBytes(charset);
+ ByteBuffer buf = ByteBuffer.allocateDirect(bytes.length);
+ buf.put(bytes);
+ buf.flip();
+ return buf;
+ }
+
+ public static ByteBuffer toMappedBuffer(File file) throws IOException
+ {
+ return toMappedBuffer(file.toPath(), 0, file.length());
+ }
+
+ public static ByteBuffer toMappedBuffer(Path filePath, long pos, long len) throws IOException
+ {
+ try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ))
+ {
+ return channel.map(MapMode.READ_ONLY, pos, len);
+ }
+ }
+
+ /**
+ * @param buffer the buffer to test
+ * @return {@code false}
+ * @deprecated don't use - there is no way to reliably tell if a ByteBuffer is mapped.
+ */
+ @Deprecated
+ public static boolean isMappedBuffer(ByteBuffer buffer)
+ {
+ return false;
+ }
+
+ public static ByteBuffer toBuffer(Resource resource, boolean direct) throws IOException
+ {
+ long len = resource.length();
+ if (len < 0)
+ throw new IllegalArgumentException("invalid resource: " + resource + " len=" + len);
+
+ if (len > Integer.MAX_VALUE)
+ {
+ // This method cannot handle resources of this size.
+ return null;
+ }
+
+ int ilen = (int)len;
+
+ ByteBuffer buffer = direct ? BufferUtil.allocateDirect(ilen) : BufferUtil.allocate(ilen);
+
+ int pos = BufferUtil.flipToFill(buffer);
+ if (resource.getFile() != null)
+ BufferUtil.readFrom(resource.getFile(), buffer);
+ else
+ {
+ try (InputStream is = resource.getInputStream())
+ {
+ BufferUtil.readFrom(is, ilen, buffer);
+ }
+ }
+ BufferUtil.flipToFlush(buffer, pos);
+
+ return buffer;
+ }
+
+ public static String toSummaryString(ByteBuffer buffer)
+ {
+ if (buffer == null)
+ return "null";
+ StringBuilder buf = new StringBuilder();
+ buf.append("[p=");
+ buf.append(buffer.position());
+ buf.append(",l=");
+ buf.append(buffer.limit());
+ buf.append(",c=");
+ buf.append(buffer.capacity());
+ buf.append(",r=");
+ buf.append(buffer.remaining());
+ buf.append("]");
+ return buf.toString();
+ }
+
+ public static String toDetailString(ByteBuffer[] buffer)
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append('[');
+ for (int i = 0; i < buffer.length; i++)
+ {
+ if (i > 0)
+ builder.append(',');
+ builder.append(toDetailString(buffer[i]));
+ }
+ builder.append(']');
+ return builder.toString();
+ }
+
+ /**
+ * Convert Buffer to string ID independent of content
+ */
+ private static void idString(ByteBuffer buffer, StringBuilder out)
+ {
+ out.append(buffer.getClass().getSimpleName());
+ out.append("@");
+ if (buffer.hasArray() && buffer.arrayOffset() == 4)
+ {
+ out.append('T');
+ byte[] array = buffer.array();
+ TypeUtil.toHex(array[0], out);
+ TypeUtil.toHex(array[1], out);
+ TypeUtil.toHex(array[2], out);
+ TypeUtil.toHex(array[3], out);
+ }
+ else
+ out.append(Integer.toHexString(System.identityHashCode(buffer)));
+ }
+
+ /**
+ * Convert Buffer to string ID independent of content
+ *
+ * @param buffer the buffet to generate a string ID from
+ * @return A string showing the buffer ID
+ */
+ public static String toIDString(ByteBuffer buffer)
+ {
+ StringBuilder buf = new StringBuilder();
+ idString(buffer, buf);
+ return buf.toString();
+ }
+
+ /**
+ * Convert Buffer to a detail debug string of pointers and content
+ *
+ * @param buffer the buffer to generate a detail string from
+ * @return A string showing the pointers and content of the buffer
+ */
+ public static String toDetailString(ByteBuffer buffer)
+ {
+ if (buffer == null)
+ return "null";
+
+ StringBuilder buf = new StringBuilder();
+ idString(buffer, buf);
+ buf.append("[p=");
+ buf.append(buffer.position());
+ buf.append(",l=");
+ buf.append(buffer.limit());
+ buf.append(",c=");
+ buf.append(buffer.capacity());
+ buf.append(",r=");
+ buf.append(buffer.remaining());
+ buf.append("]={");
+
+ appendDebugString(buf, buffer);
+
+ buf.append("}");
+
+ return buf.toString();
+ }
+
+ private static void appendDebugString(StringBuilder buf, ByteBuffer buffer)
+ {
+ // Take a readonly copy so we can adjust the limit
+ buffer = buffer.asReadOnlyBuffer();
+ try
+ {
+ for (int i = 0; i < buffer.position(); i++)
+ {
+ appendContentChar(buf, buffer.get(i));
+ if (i == 8 && buffer.position() > 16)
+ {
+ buf.append("...");
+ i = buffer.position() - 8;
+ }
+ }
+ buf.append("<<<");
+ for (int i = buffer.position(); i < buffer.limit(); i++)
+ {
+ appendContentChar(buf, buffer.get(i));
+ if (i == buffer.position() + 24 && buffer.limit() > buffer.position() + 48)
+ {
+ buf.append("...");
+ i = buffer.limit() - 24;
+ }
+ }
+ buf.append(">>>");
+ int limit = buffer.limit();
+ buffer.limit(buffer.capacity());
+ for (int i = limit; i < buffer.capacity(); i++)
+ {
+ appendContentChar(buf, buffer.get(i));
+ if (i == limit + 8 && buffer.capacity() > limit + 16)
+ {
+ buf.append("...");
+ i = buffer.capacity() - 8;
+ }
+ }
+ buffer.limit(limit);
+ }
+ catch (Throwable x)
+ {
+ Log.getRootLogger().ignore(x);
+ buf.append("!!concurrent mod!!");
+ }
+ }
+
+ private static void appendContentChar(StringBuilder buf, byte b)
+ {
+ if (b == '\\')
+ buf.append("\\\\");
+ else if ((b >= 0x20) && (b <= 0x7E)) // limit to 7-bit printable US-ASCII character space
+ buf.append((char)b);
+ else if (b == '\r')
+ buf.append("\\r");
+ else if (b == '\n')
+ buf.append("\\n");
+ else if (b == '\t')
+ buf.append("\\t");
+ else
+ buf.append("\\x").append(TypeUtil.toHexString(b));
+ }
+
+ /**
+ * Convert buffer to a Hex Summary String.
+ *
+ * @param buffer the buffer to generate a hex byte summary from
+ * @return A string showing a summary of the content in hex
+ */
+ public static String toHexSummary(ByteBuffer buffer)
+ {
+ if (buffer == null)
+ return "null";
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("b[").append(buffer.remaining()).append("]=");
+ for (int i = buffer.position(); i < buffer.limit(); i++)
+ {
+ TypeUtil.toHex(buffer.get(i), buf);
+ if (i == buffer.position() + 24 && buffer.limit() > buffer.position() + 32)
+ {
+ buf.append("...");
+ i = buffer.limit() - 8;
+ }
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Convert buffer to a Hex String.
+ *
+ * @param buffer the buffer to generate a hex byte summary from
+ * @return A hex string
+ */
+ public static String toHexString(ByteBuffer buffer)
+ {
+ if (buffer == null)
+ return "null";
+ return TypeUtil.toHexString(toArray(buffer));
+ }
+
+ private static final int[] decDivisors =
+ {1000000000, 100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10, 1};
+
+ private static final int[] hexDivisors =
+ {0x10000000, 0x1000000, 0x100000, 0x10000, 0x1000, 0x100, 0x10, 0x1};
+
+ private static final long[] decDivisorsL =
+ {
+ 1000000000000000000L, 100000000000000000L, 10000000000000000L, 1000000000000000L, 100000000000000L, 10000000000000L,
+ 1000000000000L, 100000000000L,
+ 10000000000L, 1000000000L, 100000000L, 10000000L, 1000000L, 100000L, 10000L, 1000L, 100L, 10L, 1L
+ };
+
+ public static void putCRLF(ByteBuffer buffer)
+ {
+ buffer.put((byte)13);
+ buffer.put((byte)10);
+ }
+
+ public static boolean isPrefix(ByteBuffer prefix, ByteBuffer buffer)
+ {
+ if (prefix.remaining() > buffer.remaining())
+ return false;
+ int bi = buffer.position();
+ for (int i = prefix.position(); i < prefix.limit(); i++)
+ {
+ if (prefix.get(i) != buffer.get(bi++))
+ return false;
+ }
+ return true;
+ }
+
+ public static ByteBuffer ensureCapacity(ByteBuffer buffer, int capacity)
+ {
+ if (buffer == null)
+ return allocate(capacity);
+
+ if (buffer.capacity() >= capacity)
+ return buffer;
+
+ if (buffer.hasArray())
+ return ByteBuffer.wrap(Arrays.copyOfRange(buffer.array(), buffer.arrayOffset(), buffer.arrayOffset() + capacity), buffer.position(), buffer.remaining());
+
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ByteArrayISO8859Writer.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ByteArrayISO8859Writer.java
new file mode 100644
index 0000000..11caea1
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ByteArrayISO8859Writer.java
@@ -0,0 +1,249 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+/**
+ * Byte Array ISO 8859 writer.
+ * This class combines the features of a OutputStreamWriter for
+ * ISO8859 encoding with that of a ByteArrayOutputStream. It avoids
+ * many inefficiencies associated with these standard library classes.
+ * It has been optimized for standard ASCII characters.
+ */
+public class ByteArrayISO8859Writer extends Writer
+{
+ private byte[] _buf;
+ private int _size;
+ private ByteArrayOutputStream2 _bout = null;
+ private OutputStreamWriter _writer = null;
+ private boolean _fixed = false;
+
+ /**
+ * Constructor.
+ */
+ public ByteArrayISO8859Writer()
+ {
+ _buf = new byte[2048];
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param capacity Buffer capacity
+ */
+ public ByteArrayISO8859Writer(int capacity)
+ {
+ _buf = new byte[capacity];
+ }
+
+ public ByteArrayISO8859Writer(byte[] buf)
+ {
+ _buf = buf;
+ _fixed = true;
+ }
+
+ public Object getLock()
+ {
+ return lock;
+ }
+
+ public int size()
+ {
+ return _size;
+ }
+
+ public int capacity()
+ {
+ return _buf.length;
+ }
+
+ public int spareCapacity()
+ {
+ return _buf.length - _size;
+ }
+
+ public void setLength(int l)
+ {
+ _size = l;
+ }
+
+ public byte[] getBuf()
+ {
+ return _buf;
+ }
+
+ public void writeTo(OutputStream out)
+ throws IOException
+ {
+ out.write(_buf, 0, _size);
+ }
+
+ public void write(char c)
+ throws IOException
+ {
+ ensureSpareCapacity(1);
+ if (c >= 0 && c <= 0x7f)
+ _buf[_size++] = (byte)c;
+ else
+ {
+ char[] ca = {c};
+ writeEncoded(ca, 0, 1);
+ }
+ }
+
+ @Override
+ public void write(char[] ca)
+ throws IOException
+ {
+ ensureSpareCapacity(ca.length);
+ for (int i = 0; i < ca.length; i++)
+ {
+ char c = ca[i];
+ if (c >= 0 && c <= 0x7f)
+ _buf[_size++] = (byte)c;
+ else
+ {
+ writeEncoded(ca, i, ca.length - i);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void write(char[] ca, int offset, int length)
+ throws IOException
+ {
+ ensureSpareCapacity(length);
+ for (int i = 0; i < length; i++)
+ {
+ char c = ca[offset + i];
+ if (c >= 0 && c <= 0x7f)
+ _buf[_size++] = (byte)c;
+ else
+ {
+ writeEncoded(ca, offset + i, length - i);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void write(String s)
+ throws IOException
+ {
+ if (s == null)
+ {
+ write("null", 0, 4);
+ return;
+ }
+
+ int length = s.length();
+ ensureSpareCapacity(length);
+ for (int i = 0; i < length; i++)
+ {
+ char c = s.charAt(i);
+ if (c >= 0x0 && c <= 0x7f)
+ _buf[_size++] = (byte)c;
+ else
+ {
+ writeEncoded(s.toCharArray(), i, length - i);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void write(String s, int offset, int length)
+ throws IOException
+ {
+ ensureSpareCapacity(length);
+ for (int i = 0; i < length; i++)
+ {
+ char c = s.charAt(offset + i);
+ if (c >= 0 && c <= 0x7f)
+ _buf[_size++] = (byte)c;
+ else
+ {
+ writeEncoded(s.toCharArray(), offset + i, length - i);
+ break;
+ }
+ }
+ }
+
+ private void writeEncoded(char[] ca, int offset, int length)
+ throws IOException
+ {
+ if (_bout == null)
+ {
+ _bout = new ByteArrayOutputStream2(2 * length);
+ _writer = new OutputStreamWriter(_bout, StandardCharsets.ISO_8859_1);
+ }
+ else
+ _bout.reset();
+ _writer.write(ca, offset, length);
+ _writer.flush();
+ ensureSpareCapacity(_bout.getCount());
+ System.arraycopy(_bout.getBuf(), 0, _buf, _size, _bout.getCount());
+ _size += _bout.getCount();
+ }
+
+ @Override
+ public void flush()
+ {
+ }
+
+ public void resetWriter()
+ {
+ _size = 0;
+ }
+
+ @Override
+ public void close()
+ {
+ }
+
+ public void destroy()
+ {
+ _buf = null;
+ }
+
+ public void ensureSpareCapacity(int n)
+ throws IOException
+ {
+ if (_size + n > _buf.length)
+ {
+ if (_fixed)
+ throw new IOException("Buffer overflow: " + _buf.length);
+ _buf = Arrays.copyOf(_buf, (_buf.length + n) * 4 / 3);
+ }
+ }
+
+ public byte[] getByteArray()
+ {
+ return Arrays.copyOf(_buf, _size);
+ }
+}
+
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ByteArrayOutputStream2.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ByteArrayOutputStream2.java
new file mode 100644
index 0000000..cb33e44
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ByteArrayOutputStream2.java
@@ -0,0 +1,72 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.Charset;
+
+/**
+ * ByteArrayOutputStream with public internals
+ */
+public class ByteArrayOutputStream2 extends ByteArrayOutputStream
+{
+ public ByteArrayOutputStream2()
+ {
+ super();
+ }
+
+ public ByteArrayOutputStream2(int size)
+ {
+ super(size);
+ }
+
+ public byte[] getBuf()
+ {
+ return buf;
+ }
+
+ public int getCount()
+ {
+ return count;
+ }
+
+ public void setCount(int count)
+ {
+ this.count = count;
+ }
+
+ public void reset(int minSize)
+ {
+ reset();
+ if (buf.length < minSize)
+ {
+ buf = new byte[minSize];
+ }
+ }
+
+ public void writeUnchecked(int b)
+ {
+ buf[count++] = (byte)b;
+ }
+
+ public String toString(Charset charset)
+ {
+ return new String(buf, 0, count, charset);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java
new file mode 100644
index 0000000..fd4f447
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java
@@ -0,0 +1,396 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+import org.eclipse.jetty.util.thread.Invocable;
+
+/**
+ * <p>A callback abstraction that handles completed/failed events of asynchronous operations.</p>
+ *
+ * <p>Semantically this is equivalent to an optimise Promise<Void>, but callback is a more meaningful
+ * name than EmptyPromise</p>
+ */
+public interface Callback extends Invocable
+{
+ /**
+ * Instance of Adapter that can be used when the callback methods need an empty
+ * implementation without incurring in the cost of allocating a new Adapter object.
+ */
+ Callback NOOP = new Callback()
+ {
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return InvocationType.NON_BLOCKING;
+ }
+ };
+
+ /**
+ * <p>Callback invoked when the operation completes.</p>
+ *
+ * @see #failed(Throwable)
+ */
+ default void succeeded()
+ {
+ }
+
+ /**
+ * <p>Callback invoked when the operation fails.</p>
+ *
+ * @param x the reason for the operation failure
+ */
+ default void failed(Throwable x)
+ {
+ }
+
+ /**
+ * <p>Creates a non-blocking callback from the given incomplete CompletableFuture.</p>
+ * <p>When the callback completes, either succeeding or failing, the
+ * CompletableFuture is also completed, respectively via
+ * {@link CompletableFuture#complete(Object)} or
+ * {@link CompletableFuture#completeExceptionally(Throwable)}.</p>
+ *
+ * @param completable the CompletableFuture to convert into a callback
+ * @return a callback that when completed, completes the given CompletableFuture
+ */
+ static Callback from(CompletableFuture<?> completable)
+ {
+ return from(completable, InvocationType.NON_BLOCKING);
+ }
+
+ /**
+ * <p>Creates a callback from the given incomplete CompletableFuture,
+ * with the given {@code blocking} characteristic.</p>
+ *
+ * @param completable the CompletableFuture to convert into a callback
+ * @param invocation whether the callback is blocking
+ * @return a callback that when completed, completes the given CompletableFuture
+ */
+ static Callback from(CompletableFuture<?> completable, InvocationType invocation)
+ {
+ if (completable instanceof Callback)
+ return (Callback)completable;
+
+ return new Callback()
+ {
+ @Override
+ public void succeeded()
+ {
+ completable.complete(null);
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ completable.completeExceptionally(x);
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return invocation;
+ }
+ };
+ }
+
+ /**
+ * Creates a callback from the given success and failure lambdas.
+ *
+ * @param success Called when the callback succeeds
+ * @param failure Called when the callback fails
+ * @return a new Callback
+ */
+ static Callback from(Runnable success, Consumer<Throwable> failure)
+ {
+ return from(InvocationType.BLOCKING, success, failure);
+ }
+
+ /**
+ * Creates a callback with the given InvocationType from the given success and failure lambdas.
+ *
+ * @param invocationType the Callback invocation type
+ * @param success Called when the callback succeeds
+ * @param failure Called when the callback fails
+ * @return a new Callback
+ */
+ static Callback from(InvocationType invocationType, Runnable success, Consumer<Throwable> failure)
+ {
+ return new Callback()
+ {
+ @Override
+ public void succeeded()
+ {
+ success.run();
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ failure.accept(x);
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return invocationType;
+ }
+ };
+ }
+
+ /**
+ * Creaste a callback that runs completed when it succeeds or fails
+ *
+ * @param completed The completion to run on success or failure
+ * @return a new callback
+ */
+ static Callback from(Runnable completed)
+ {
+ return new Completing()
+ {
+ public void completed()
+ {
+ completed.run();
+ }
+ };
+ }
+
+ /**
+ * Create a nested callback that runs completed after
+ * completing the nested callback.
+ *
+ * @param callback The nested callback
+ * @param completed The completion to run after the nested callback is completed
+ * @return a new callback.
+ */
+ static Callback from(Callback callback, Runnable completed)
+ {
+ return new Nested(callback)
+ {
+ public void completed()
+ {
+ completed.run();
+ }
+ };
+ }
+
+ /**
+ * Create a nested callback that runs completed before
+ * completing the nested callback.
+ *
+ * @param callback The nested callback
+ * @param completed The completion to run before the nested callback is completed. Any exceptions thrown
+ * from completed will result in a callback failure.
+ * @return a new callback.
+ */
+ static Callback from(Runnable completed, Callback callback)
+ {
+ return new Callback()
+ {
+ @Override
+ public void succeeded()
+ {
+ try
+ {
+ completed.run();
+ callback.succeeded();
+ }
+ catch (Throwable t)
+ {
+ callback.failed(t);
+ }
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ try
+ {
+ completed.run();
+ }
+ catch (Throwable t)
+ {
+ x.addSuppressed(t);
+ }
+ callback.failed(x);
+ }
+ };
+ }
+
+ class Completing implements Callback
+ {
+ @Override
+ public void succeeded()
+ {
+ completed();
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ completed();
+ }
+
+ public void completed()
+ {
+ }
+ }
+
+ /**
+ * Nested Completing Callback that completes after
+ * completing the nested callback
+ */
+ class Nested extends Completing
+ {
+ private final Callback callback;
+
+ public Nested(Callback callback)
+ {
+ this.callback = callback;
+ }
+
+ public Nested(Nested nested)
+ {
+ this.callback = nested.callback;
+ }
+
+ public Callback getCallback()
+ {
+ return callback;
+ }
+
+ @Override
+ public void succeeded()
+ {
+ try
+ {
+ callback.succeeded();
+ }
+ finally
+ {
+ completed();
+ }
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ try
+ {
+ callback.failed(x);
+ }
+ finally
+ {
+ completed();
+ }
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return callback.getInvocationType();
+ }
+ }
+
+ static Callback combine(Callback cb1, Callback cb2)
+ {
+ if (cb1 == null || cb1 == cb2)
+ return cb2;
+ if (cb2 == null)
+ return cb1;
+
+ return new Callback()
+ {
+ @Override
+ public void succeeded()
+ {
+ try
+ {
+ cb1.succeeded();
+ }
+ finally
+ {
+ cb2.succeeded();
+ }
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ try
+ {
+ cb1.failed(x);
+ }
+ catch (Throwable t)
+ {
+ if (x != t)
+ x.addSuppressed(t);
+ }
+ finally
+ {
+ cb2.failed(x);
+ }
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return Invocable.combine(Invocable.getInvocationType(cb1), Invocable.getInvocationType(cb2));
+ }
+ };
+ }
+
+ /**
+ * <p>A CompletableFuture that is also a Callback.</p>
+ */
+ class Completable extends CompletableFuture<Void> implements Callback
+ {
+ private final InvocationType invocation;
+
+ public Completable()
+ {
+ this(Invocable.InvocationType.NON_BLOCKING);
+ }
+
+ public Completable(InvocationType invocation)
+ {
+ this.invocation = invocation;
+ }
+
+ @Override
+ public void succeeded()
+ {
+ complete(null);
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ completeExceptionally(x);
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return invocation;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ClassLoadingObjectInputStream.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ClassLoadingObjectInputStream.java
new file mode 100644
index 0000000..9fc9545
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ClassLoadingObjectInputStream.java
@@ -0,0 +1,130 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Proxy;
+
+/**
+ * ClassLoadingObjectInputStream
+ *
+ * For re-inflating serialized objects, this class uses the thread context classloader
+ * rather than the jvm's default classloader selection.
+ */
+public class ClassLoadingObjectInputStream extends ObjectInputStream
+{
+
+ protected static class ClassLoaderThreadLocal extends ThreadLocal<ClassLoader>
+ {
+ protected static final ClassLoader UNSET = new ClassLoader() {};
+
+ @Override
+ protected ClassLoader initialValue()
+ {
+ return UNSET;
+ }
+ }
+
+ private ThreadLocal<ClassLoader> _classloader = new ClassLoaderThreadLocal();
+
+ public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException
+ {
+ super(in);
+ }
+
+ public ClassLoadingObjectInputStream() throws IOException
+ {
+ super();
+ }
+
+ public Object readObject(ClassLoader loader)
+ throws IOException, ClassNotFoundException
+ {
+ try
+ {
+ _classloader.set(loader);
+ return readObject();
+ }
+ finally
+ {
+ _classloader.set(ClassLoaderThreadLocal.UNSET);
+ }
+ }
+
+ @Override
+ public Class<?> resolveClass(java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException
+ {
+ try
+ {
+ ClassLoader loader = _classloader.get();
+ if (ClassLoaderThreadLocal.UNSET == loader)
+ loader = Thread.currentThread().getContextClassLoader();
+
+ return Class.forName(cl.getName(), false, loader);
+ }
+ catch (ClassNotFoundException e)
+ {
+ return super.resolveClass(cl);
+ }
+ }
+
+ @Override
+ protected Class<?> resolveProxyClass(String[] interfaces)
+ throws IOException, ClassNotFoundException
+ {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+
+ ClassLoader nonPublicLoader = null;
+ boolean hasNonPublicInterface = false;
+
+ // define proxy in class loader of non-public interface(s), if any
+ Class<?>[] classObjs = new Class[interfaces.length];
+ for (int i = 0; i < interfaces.length; i++)
+ {
+ Class<?> cl = Class.forName(interfaces[i], false, loader);
+ if ((cl.getModifiers() & Modifier.PUBLIC) == 0)
+ {
+ if (hasNonPublicInterface)
+ {
+ if (nonPublicLoader != cl.getClassLoader())
+ {
+ throw new IllegalAccessError(
+ "conflicting non-public interface class loaders");
+ }
+ }
+ else
+ {
+ nonPublicLoader = cl.getClassLoader();
+ hasNonPublicInterface = true;
+ }
+ }
+ classObjs[i] = cl;
+ }
+ try
+ {
+ return Proxy.getProxyClass(hasNonPublicInterface ? nonPublicLoader : loader, classObjs);
+ }
+ catch (IllegalArgumentException e)
+ {
+ throw new ClassNotFoundException(null, e);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ClassVisibilityChecker.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ClassVisibilityChecker.java
new file mode 100644
index 0000000..76d80f3
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ClassVisibilityChecker.java
@@ -0,0 +1,52 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+/**
+ * ClassVisibilityChecker
+ *
+ * Interface to be implemented by classes capable of checking class visibility
+ * for a context.
+ */
+public interface ClassVisibilityChecker
+{
+
+ /**
+ * Is the class a System Class.
+ * A System class is a class that is visible to a webapplication,
+ * but that cannot be overridden by the contents of WEB-INF/lib or
+ * WEB-INF/classes
+ *
+ * @param clazz The fully qualified name of the class.
+ * @return True if the class is a system class.
+ */
+ boolean isSystemClass(Class<?> clazz);
+
+ /**
+ * Is the class a Server Class.
+ * A Server class is a class that is part of the implementation of
+ * the server and is NIT visible to a webapplication. The web
+ * application may provide it's own implementation of the class,
+ * to be loaded from WEB-INF/lib or WEB-INF/classes
+ *
+ * @param clazz The fully qualified name of the class.
+ * @return True if the class is a server class.
+ */
+ boolean isServerClass(Class<?> clazz);
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/CompletableCallback.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/CompletableCallback.java
new file mode 100644
index 0000000..ede9f9f
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/CompletableCallback.java
@@ -0,0 +1,183 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * <p>
+ * A callback to be used by driver code that needs to know whether the callback has been
+ * succeeded or failed (that is, completed) just after the asynchronous operation or not,
+ * typically because further processing depends on the callback being completed.
+ * The driver code competes with the asynchronous operation to complete the callback.
+ * </p>
+ * <p>
+ * If the callback is already completed, the driver code continues the processing,
+ * otherwise it suspends it. If it is suspended, the callback will be completed some time
+ * later, and {@link #resume()} or {@link #abort(Throwable)} will be called to allow the
+ * application to resume the processing.
+ * </p>
+ * Typical usage:
+ * <pre>
+ * CompletableCallback callback = new CompletableCallback()
+ * {
+ * @Override
+ * public void resume()
+ * {
+ * // continue processing
+ * }
+ *
+ * @Override
+ * public void abort(Throwable failure)
+ * {
+ * // abort processing
+ * }
+ * }
+ * asyncOperation(callback);
+ * boolean completed = callback.tryComplete();
+ * if (completed)
+ * // suspend processing, async operation not done yet
+ * else
+ * // continue processing, async operation already done
+ * </pre>
+ *
+ * @deprecated not used anymore
+ */
+@Deprecated
+public abstract class CompletableCallback implements Callback
+{
+ private final AtomicReference<State> state = new AtomicReference<>(State.IDLE);
+
+ @Override
+ public void succeeded()
+ {
+ while (true)
+ {
+ State current = state.get();
+ switch (current)
+ {
+ case IDLE:
+ {
+ if (state.compareAndSet(current, State.SUCCEEDED))
+ return;
+ break;
+ }
+ case COMPLETED:
+ {
+ if (state.compareAndSet(current, State.SUCCEEDED))
+ {
+ resume();
+ return;
+ }
+ break;
+ }
+ case FAILED:
+ {
+ return;
+ }
+ default:
+ {
+ throw new IllegalStateException(current.toString());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ while (true)
+ {
+ State current = state.get();
+ switch (current)
+ {
+ case IDLE:
+ case COMPLETED:
+ {
+ if (state.compareAndSet(current, State.FAILED))
+ {
+ abort(x);
+ return;
+ }
+ break;
+ }
+ case FAILED:
+ {
+ return;
+ }
+ default:
+ {
+ throw new IllegalStateException(current.toString());
+ }
+ }
+ }
+ }
+
+ /**
+ * Callback method invoked when this callback is succeeded
+ * <em>after</em> a first call to {@link #tryComplete()}.
+ */
+ public abstract void resume();
+
+ /**
+ * Callback method invoked when this callback is failed.
+ *
+ * @param failure the throwable reprsenting the callback failure
+ */
+ public abstract void abort(Throwable failure);
+
+ /**
+ * Tries to complete this callback; driver code should call
+ * this method once <em>after</em> the asynchronous operation
+ * to detect whether the asynchronous operation has already
+ * completed or not.
+ *
+ * @return whether the attempt to complete was successful.
+ */
+ public boolean tryComplete()
+ {
+ while (true)
+ {
+ State current = state.get();
+ switch (current)
+ {
+ case IDLE:
+ {
+ if (state.compareAndSet(current, State.COMPLETED))
+ return true;
+ break;
+ }
+ case SUCCEEDED:
+ case FAILED:
+ {
+ return false;
+ }
+ default:
+ {
+ throw new IllegalStateException(current.toString());
+ }
+ }
+ }
+ }
+
+ private enum State
+ {
+ IDLE, SUCCEEDED, FAILED, COMPLETED
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ConcurrentHashSet.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ConcurrentHashSet.java
new file mode 100644
index 0000000..bbc84cb
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ConcurrentHashSet.java
@@ -0,0 +1,130 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @deprecated Use Java 8 method {@code ConcurrentHashMap.newKeySet()} instead.
+ */
+@Deprecated
+public class ConcurrentHashSet<E> extends AbstractSet<E> implements Set<E>
+{
+ private final Map<E, Boolean> _map = new ConcurrentHashMap<E, Boolean>();
+ private transient Set<E> _keys = _map.keySet();
+
+ public ConcurrentHashSet()
+ {
+ }
+
+ @Override
+ public boolean add(E e)
+ {
+ return _map.put(e, Boolean.TRUE) == null;
+ }
+
+ @Override
+ public void clear()
+ {
+ _map.clear();
+ }
+
+ @Override
+ public boolean contains(Object o)
+ {
+ return _map.containsKey(o);
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> c)
+ {
+ return _keys.containsAll(c);
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ return o == this || _keys.equals(o);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return _keys.hashCode();
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return _map.isEmpty();
+ }
+
+ @Override
+ public Iterator<E> iterator()
+ {
+ return _keys.iterator();
+ }
+
+ @Override
+ public boolean remove(Object o)
+ {
+ return _map.remove(o) != null;
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> c)
+ {
+ return _keys.removeAll(c);
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> c)
+ {
+ return _keys.retainAll(c);
+ }
+
+ @Override
+ public int size()
+ {
+ return _map.size();
+ }
+
+ @Override
+ public Object[] toArray()
+ {
+ return _keys.toArray();
+ }
+
+ @Override
+ public <T> T[] toArray(T[] a)
+ {
+ return _keys.toArray(a);
+ }
+
+ @Override
+ public String toString()
+ {
+ return _keys.toString();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ConstantThrowable.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ConstantThrowable.java
new file mode 100644
index 0000000..c34ac52
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ConstantThrowable.java
@@ -0,0 +1,42 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+/**
+ * A {@link Throwable} that may be used in static contexts. It uses Java 7
+ * constructor that prevents setting stackTrace inside exception object.
+ */
+public class ConstantThrowable extends Throwable
+{
+ public ConstantThrowable()
+ {
+ this(null);
+ }
+
+ public ConstantThrowable(String name)
+ {
+ super(name, null, false, false);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.valueOf(getMessage());
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/CountingCallback.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/CountingCallback.java
new file mode 100644
index 0000000..853bd0a
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/CountingCallback.java
@@ -0,0 +1,99 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * <p>A callback wrapper that succeeds the wrapped callback when the count is
+ * reached, or on first failure.</p>
+ * <p>This callback is particularly useful when an async operation is split
+ * into multiple parts, for example when an original byte buffer that needs
+ * to be written, along with a callback, is split into multiple byte buffers,
+ * since it allows the original callback to be wrapped and notified only when
+ * the last part has been processed.</p>
+ * <p>Example:</p>
+ * <pre>
+ * public void process(EndPoint endPoint, ByteBuffer buffer, Callback callback)
+ * {
+ * ByteBuffer[] buffers = split(buffer);
+ * CountCallback countCallback = new CountCallback(callback, buffers.length);
+ * endPoint.write(countCallback, buffers);
+ * }
+ * </pre>
+ */
+public class CountingCallback extends Callback.Nested
+{
+ private final AtomicInteger count;
+
+ public CountingCallback(Callback callback, int count)
+ {
+ super(callback);
+ if (count < 1)
+ throw new IllegalArgumentException();
+ this.count = new AtomicInteger(count);
+ }
+
+ @Override
+ public void succeeded()
+ {
+ // Forward success on the last success.
+ while (true)
+ {
+ int current = count.get();
+
+ // Already completed ?
+ if (current == 0)
+ return;
+
+ if (count.compareAndSet(current, current - 1))
+ {
+ if (current == 1)
+ super.succeeded();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void failed(Throwable failure)
+ {
+ // Forward failure on the first failure.
+ while (true)
+ {
+ int current = count.get();
+
+ // Already completed ?
+ if (current == 0)
+ return;
+
+ if (count.compareAndSet(current, 0))
+ {
+ super.failed(failure);
+ return;
+ }
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x", getClass().getSimpleName(), hashCode());
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java
new file mode 100644
index 0000000..07f0335
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java
@@ -0,0 +1,253 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Date Format Cache.
+ * Computes String representations of Dates and caches
+ * the results so that subsequent requests within the same second
+ * will be fast.
+ *
+ * Only format strings that contain either "ss". Sub second formatting is
+ * not handled.
+ *
+ * The timezone of the date may be included as an ID with the "zzz"
+ * format string or as an offset with the "ZZZ" format string.
+ *
+ * If consecutive calls are frequently very different, then this
+ * may be a little slower than a normal DateFormat.
+ */
+public class DateCache
+{
+ public static final String DEFAULT_FORMAT = "EEE MMM dd HH:mm:ss zzz yyyy";
+
+ private final String _formatString;
+ private final String _tzFormatString;
+ private final DateTimeFormatter _tzFormat;
+ private final Locale _locale;
+ private final ZoneId _zoneId;
+
+ private volatile Tick _tick;
+
+ public static class Tick
+ {
+ final long _seconds;
+ final String _string;
+
+ public Tick(long seconds, String string)
+ {
+ _seconds = seconds;
+ _string = string;
+ }
+ }
+
+ /**
+ * Constructor.
+ * Make a DateCache that will use a default format. The default format
+ * generates the same results as Date.toString().
+ */
+ public DateCache()
+ {
+ this(DEFAULT_FORMAT);
+ }
+
+ /**
+ * Constructor.
+ * Make a DateCache that will use the given format
+ *
+ * @param format the format to use
+ */
+ public DateCache(String format)
+ {
+ this(format, null, TimeZone.getDefault());
+ }
+
+ public DateCache(String format, Locale l)
+ {
+ this(format, l, TimeZone.getDefault());
+ }
+
+ public DateCache(String format, Locale l, String tz)
+ {
+ this(format, l, TimeZone.getTimeZone(tz));
+ }
+
+ public DateCache(String format, Locale l, TimeZone tz)
+ {
+ _formatString = format;
+ _locale = l;
+
+ int zIndex = _formatString.indexOf("ZZZ");
+ if (zIndex >= 0)
+ {
+ String ss1 = _formatString.substring(0, zIndex);
+ String ss2 = _formatString.substring(zIndex + 3);
+ int tzOffset = tz.getRawOffset();
+
+ StringBuilder sb = new StringBuilder(_formatString.length() + 10);
+ sb.append(ss1);
+ sb.append("'");
+ if (tzOffset >= 0)
+ sb.append('+');
+ else
+ {
+ tzOffset = -tzOffset;
+ sb.append('-');
+ }
+
+ int raw = tzOffset / (1000 * 60); // Convert to seconds
+ int hr = raw / 60;
+ int min = raw % 60;
+
+ if (hr < 10)
+ sb.append('0');
+ sb.append(hr);
+ if (min < 10)
+ sb.append('0');
+ sb.append(min);
+ sb.append('\'');
+
+ sb.append(ss2);
+ _tzFormatString = sb.toString();
+ }
+ else
+ _tzFormatString = _formatString;
+
+ if (_locale != null)
+ {
+ _tzFormat = DateTimeFormatter.ofPattern(_tzFormatString, _locale);
+ }
+ else
+ {
+ _tzFormat = DateTimeFormatter.ofPattern(_tzFormatString);
+ }
+ _zoneId = tz.toZoneId();
+ _tzFormat.withZone(_zoneId);
+ _tick = null;
+ }
+
+ public TimeZone getTimeZone()
+ {
+ return TimeZone.getTimeZone(_zoneId);
+ }
+
+ /**
+ * Format a date according to our stored formatter.
+ *
+ * @param inDate the Date
+ * @return Formatted date
+ */
+ public String format(Date inDate)
+ {
+ long seconds = inDate.getTime() / 1000;
+
+ Tick tick = _tick;
+
+ // Is this the cached time
+ if (tick == null || seconds != tick._seconds)
+ {
+ return ZonedDateTime.ofInstant(inDate.toInstant(), _zoneId).format(_tzFormat);
+ }
+
+ return tick._string;
+ }
+
+ /**
+ * Format a date according to our stored formatter.
+ * If it happens to be in the same second as the last formatNow
+ * call, then the format is reused.
+ *
+ * @param inDate the date in milliseconds since unix epoch
+ * @return Formatted date
+ */
+ public String format(long inDate)
+ {
+ long seconds = inDate / 1000;
+
+ Tick tick = _tick;
+
+ // Is this the cached time
+ if (tick == null || seconds != tick._seconds)
+ {
+ // It's a cache miss
+ return ZonedDateTime.ofInstant(Instant.ofEpochMilli(inDate), _zoneId).format(_tzFormat);
+ }
+
+ return tick._string;
+ }
+
+ /**
+ * Format a date according to our stored formatter.
+ * The passed time is expected to be close to the current time, so it is
+ * compared to the last value passed and if it is within the same second,
+ * the format is reused. Otherwise a new cached format is created.
+ *
+ * @param now the milliseconds since unix epoch
+ * @return Formatted date
+ */
+ public String formatNow(long now)
+ {
+ long seconds = now / 1000;
+
+ Tick tick = _tick;
+
+ // Is this the cached time
+ if (tick != null && tick._seconds == seconds)
+ return tick._string;
+ return formatTick(now)._string;
+ }
+
+ public String now()
+ {
+ return formatNow(System.currentTimeMillis());
+ }
+
+ public Tick tick()
+ {
+ return formatTick(System.currentTimeMillis());
+ }
+
+ protected Tick formatTick(long now)
+ {
+ long seconds = now / 1000;
+
+ Tick tick = _tick;
+ // recheck the tick, to save multiple formats
+ if (tick == null || tick._seconds != seconds)
+ {
+ String s = ZonedDateTime.ofInstant(Instant.ofEpochMilli(now), _zoneId).format(_tzFormat);
+ _tick = new Tick(seconds, s);
+ tick = _tick;
+ }
+ return tick;
+ }
+
+ public String getFormatString()
+ {
+ return _formatString;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/DecoratedObjectFactory.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/DecoratedObjectFactory.java
new file mode 100644
index 0000000..d6aa843
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/DecoratedObjectFactory.java
@@ -0,0 +1,127 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * An ObjectFactory enhanced by {@link Decorator} instances.
+ * <p>
+ * Consistent single location for all Decorator behavior, with equal behavior in a ServletContext and also for a stand
+ * alone client.
+ * <p>
+ * Used by ServletContextHandler, WebAppContext, WebSocketServerFactory, and WebSocketClient.
+ * <p>
+ * Can be found in the ServletContext Attributes at the {@link #ATTR DecoratedObjectFactory.ATTR} key.
+ */
+public class DecoratedObjectFactory implements Iterable<Decorator>
+{
+ private static final Logger LOG = Log.getLogger(DecoratedObjectFactory.class);
+
+ /**
+ * ServletContext attribute for the active DecoratedObjectFactory
+ */
+ public static final String ATTR = DecoratedObjectFactory.class.getName();
+
+ private List<Decorator> decorators = new ArrayList<>();
+
+ public void addDecorator(Decorator decorator)
+ {
+ LOG.debug("Adding Decorator: {}", decorator);
+ decorators.add(decorator);
+ }
+
+ public boolean removeDecorator(Decorator decorator)
+ {
+ LOG.debug("Remove Decorator: {}", decorator);
+ return decorators.remove(decorator);
+ }
+
+ public void clear()
+ {
+ this.decorators.clear();
+ }
+
+ public <T> T createInstance(Class<T> clazz) throws InstantiationException, IllegalAccessException,
+ NoSuchMethodException, InvocationTargetException
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Creating Instance: " + clazz);
+ }
+ T o = clazz.getDeclaredConstructor().newInstance();
+ return decorate(o);
+ }
+
+ public <T> T decorate(T obj)
+ {
+ T f = obj;
+ // Decorate is always backwards
+ for (int i = decorators.size() - 1; i >= 0; i--)
+ {
+ f = decorators.get(i).decorate(f);
+ }
+ return f;
+ }
+
+ public void destroy(Object obj)
+ {
+ for (Decorator decorator : this.decorators)
+ {
+ decorator.destroy(obj);
+ }
+ }
+
+ public List<Decorator> getDecorators()
+ {
+ return Collections.unmodifiableList(decorators);
+ }
+
+ @Override
+ public Iterator<Decorator> iterator()
+ {
+ return this.decorators.iterator();
+ }
+
+ public void setDecorators(List<? extends Decorator> decorators)
+ {
+ this.decorators.clear();
+ if (decorators != null)
+ {
+ this.decorators.addAll(decorators);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder str = new StringBuilder();
+ str.append(this.getClass().getName()).append("[decorators=");
+ str.append(decorators.size());
+ str.append("]");
+ return str.toString();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Decorator.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Decorator.java
new file mode 100644
index 0000000..13f9115
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Decorator.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+/**
+ * Interface for 3rd party libraries to decorate recently created objects in Jetty.
+ * <p>
+ * Most common use is weld/CDI.
+ * <p>
+ * This was moved from org.eclipse.jetty.servlet.ServletContextHandler to allow
+ * client applications to also use Weld/CDI to decorate objects.
+ * Such as websocket client (which has no servlet api requirement)
+ */
+public interface Decorator
+{
+ <T> T decorate(T o);
+
+ void destroy(Object o);
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/DeprecationWarning.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/DeprecationWarning.java
new file mode 100644
index 0000000..253cad7
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/DeprecationWarning.java
@@ -0,0 +1,86 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class DeprecationWarning implements Decorator
+{
+ private static final Logger LOG = Log.getLogger(DeprecationWarning.class);
+
+ @Override
+ public <T> T decorate(T o)
+ {
+ if (o == null)
+ {
+ return null;
+ }
+
+ Class<?> clazz = o.getClass();
+
+ try
+ {
+ Deprecated depr = clazz.getAnnotation(Deprecated.class);
+ if (depr != null)
+ {
+ LOG.warn("Using @Deprecated Class {}", clazz.getName());
+ }
+ }
+ catch (Throwable t)
+ {
+ LOG.ignore(t);
+ }
+
+ verifyIndirectTypes(clazz.getSuperclass(), clazz, "Class");
+ for (Class<?> ifaceClazz : clazz.getInterfaces())
+ {
+ verifyIndirectTypes(ifaceClazz, clazz, "Interface");
+ }
+
+ return o;
+ }
+
+ private void verifyIndirectTypes(Class<?> superClazz, Class<?> clazz, String typeName)
+ {
+ try
+ {
+ // Report on super class deprecation too
+ while (superClazz != null && superClazz != Object.class)
+ {
+ Deprecated supDepr = superClazz.getAnnotation(Deprecated.class);
+ if (supDepr != null)
+ {
+ LOG.warn("Using indirect @Deprecated {} {} - (seen from {})", typeName, superClazz.getName(), clazz);
+ }
+
+ superClazz = superClazz.getSuperclass();
+ }
+ }
+ catch (Throwable t)
+ {
+ LOG.ignore(t);
+ }
+ }
+
+ @Override
+ public void destroy(Object o)
+ {
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Fields.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Fields.java
new file mode 100644
index 0000000..02d159b
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Fields.java
@@ -0,0 +1,342 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * <p>A container for name/value pairs, known as fields.</p>
+ * <p>A {@link Field} is composed of a name string that can be case-sensitive
+ * or case-insensitive (by specifying the option at the constructor) and
+ * of a case-sensitive set of value strings.</p>
+ * <p>The implementation of this class is not thread safe.</p>
+ */
+public class Fields implements Iterable<Fields.Field>
+{
+ private final boolean caseSensitive;
+ private final Map<String, Field> fields;
+
+ /**
+ * <p>Creates an empty, modifiable, case insensitive {@link Fields} instance.</p>
+ *
+ * @see #Fields(Fields, boolean)
+ */
+ public Fields()
+ {
+ this(false);
+ }
+
+ /**
+ * <p>Creates an empty, modifiable, case insensitive {@link Fields} instance.</p>
+ *
+ * @param caseSensitive whether this {@link Fields} instance must be case sensitive
+ * @see #Fields(Fields, boolean)
+ */
+ public Fields(boolean caseSensitive)
+ {
+ this.caseSensitive = caseSensitive;
+ fields = new LinkedHashMap<>();
+ }
+
+ /**
+ * <p>Creates a {@link Fields} instance by copying the fields from the given
+ * {@link Fields} and making it (im)mutable depending on the given {@code immutable} parameter</p>
+ *
+ * @param original the {@link Fields} to copy fields from
+ * @param immutable whether this instance is immutable
+ */
+ public Fields(Fields original, boolean immutable)
+ {
+ this.caseSensitive = original.caseSensitive;
+ Map<String, Field> copy = new LinkedHashMap<>();
+ copy.putAll(original.fields);
+ fields = immutable ? Collections.unmodifiableMap(copy) : copy;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ return true;
+ if (obj == null || getClass() != obj.getClass())
+ return false;
+ Fields that = (Fields)obj;
+ if (getSize() != that.getSize())
+ return false;
+ if (caseSensitive != that.caseSensitive)
+ return false;
+ for (Map.Entry<String, Field> entry : fields.entrySet())
+ {
+ String name = entry.getKey();
+ Field value = entry.getValue();
+ if (!value.equals(that.get(name), caseSensitive))
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return fields.hashCode();
+ }
+
+ /**
+ * @return a set of field names
+ */
+ public Set<String> getNames()
+ {
+ Set<String> result = new LinkedHashSet<>();
+ for (Field field : fields.values())
+ {
+ result.add(field.getName());
+ }
+ return result;
+ }
+
+ private String normalizeName(String name)
+ {
+ return caseSensitive ? name : name.toLowerCase(Locale.ENGLISH);
+ }
+
+ /**
+ * @param name the field name
+ * @return the {@link Field} with the given name, or null if no such field exists
+ */
+ public Field get(String name)
+ {
+ return fields.get(normalizeName(name));
+ }
+
+ /**
+ * <p>Inserts or replaces the given name/value pair as a single-valued {@link Field}.</p>
+ *
+ * @param name the field name
+ * @param value the field value
+ */
+ public void put(String name, String value)
+ {
+ // Preserve the case for the field name
+ Field field = new Field(name, value);
+ fields.put(normalizeName(name), field);
+ }
+
+ /**
+ * <p>Inserts or replaces the given {@link Field}, mapped to the {@link Field#getName() field's name}</p>
+ *
+ * @param field the field to put
+ */
+ public void put(Field field)
+ {
+ if (field != null)
+ fields.put(normalizeName(field.getName()), field);
+ }
+
+ /**
+ * <p>Adds the given value to a field with the given name,
+ * creating a {@link Field} is none exists for the given name.</p>
+ *
+ * @param name the field name
+ * @param value the field value to add
+ */
+ public void add(String name, String value)
+ {
+ String key = normalizeName(name);
+ Field field = fields.get(key);
+ if (field == null)
+ {
+ // Preserve the case for the field name
+ field = new Field(name, value);
+ fields.put(key, field);
+ }
+ else
+ {
+ field = new Field(field.getName(), field.getValues(), value);
+ fields.put(key, field);
+ }
+ }
+
+ /**
+ * <p>Removes the {@link Field} with the given name</p>
+ *
+ * @param name the name of the field to remove
+ * @return the removed field, or null if no such field existed
+ */
+ public Field remove(String name)
+ {
+ return fields.remove(normalizeName(name));
+ }
+
+ /**
+ * <p>Empties this {@link Fields} instance from all fields</p>
+ *
+ * @see #isEmpty()
+ */
+ public void clear()
+ {
+ fields.clear();
+ }
+
+ /**
+ * @return whether this {@link Fields} instance is empty
+ */
+ public boolean isEmpty()
+ {
+ return fields.isEmpty();
+ }
+
+ /**
+ * @return the number of fields
+ */
+ public int getSize()
+ {
+ return fields.size();
+ }
+
+ /**
+ * @return an iterator over the {@link Field}s present in this instance
+ */
+ @Override
+ public Iterator<Field> iterator()
+ {
+ return fields.values().iterator();
+ }
+
+ @Override
+ public String toString()
+ {
+ return fields.toString();
+ }
+
+ /**
+ * <p>A named list of string values.</p>
+ * <p>The name is case-sensitive and there must be at least one value.</p>
+ */
+ public static class Field
+ {
+ private final String name;
+ private final List<String> values;
+
+ public Field(String name, String value)
+ {
+ this(name, Collections.singletonList(value));
+ }
+
+ private Field(String name, List<String> values, String... moreValues)
+ {
+ this.name = name;
+ List<String> list = new ArrayList<>(values.size() + moreValues.length);
+ list.addAll(values);
+ list.addAll(Arrays.asList(moreValues));
+ this.values = Collections.unmodifiableList(list);
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ public boolean equals(Field that, boolean caseSensitive)
+ {
+ if (this == that)
+ return true;
+ if (that == null)
+ return false;
+ if (caseSensitive)
+ return equals(that);
+ return name.equalsIgnoreCase(that.name) && values.equals(that.values);
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ return true;
+ if (obj == null || getClass() != obj.getClass())
+ return false;
+ Field that = (Field)obj;
+ return name.equals(that.name) && values.equals(that.values);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ int result = name.hashCode();
+ result = 31 * result + values.hashCode();
+ return result;
+ }
+
+ /**
+ * @return the field's name
+ */
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * @return the first field's value
+ */
+ public String getValue()
+ {
+ return values.get(0);
+ }
+
+ /**
+ * <p>Attempts to convert the result of {@link #getValue()} to an integer,
+ * returning it if the conversion is successful; returns null if the
+ * result of {@link #getValue()} is null.</p>
+ *
+ * @return the result of {@link #getValue()} converted to an integer, or null
+ * @throws NumberFormatException if the conversion fails
+ */
+ public Integer getValueAsInt()
+ {
+ final String value = getValue();
+ return value == null ? null : Integer.valueOf(value);
+ }
+
+ /**
+ * @return the field's values
+ */
+ public List<String> getValues()
+ {
+ return values;
+ }
+
+ /**
+ * @return whether the field has multiple values
+ */
+ public boolean hasMultipleValues()
+ {
+ return values.size() > 1;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s=%s", name, values);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/FutureCallback.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/FutureCallback.java
new file mode 100644
index 0000000..6cf5b9d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/FutureCallback.java
@@ -0,0 +1,158 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class FutureCallback implements Future<Void>, Callback
+{
+ private static final Throwable COMPLETED = new ConstantThrowable();
+ public static final FutureCallback SUCCEEDED = new FutureCallback(true);
+ private final AtomicBoolean _done = new AtomicBoolean(false);
+ private final CountDownLatch _latch = new CountDownLatch(1);
+ private Throwable _cause;
+
+ public FutureCallback()
+ {
+ }
+
+ public FutureCallback(boolean completed)
+ {
+ if (completed)
+ {
+ _cause = COMPLETED;
+ _done.set(true);
+ _latch.countDown();
+ }
+ }
+
+ public FutureCallback(Throwable failed)
+ {
+ _cause = failed;
+ _done.set(true);
+ _latch.countDown();
+ }
+
+ @Override
+ public void succeeded()
+ {
+ if (_done.compareAndSet(false, true))
+ {
+ _cause = COMPLETED;
+ _latch.countDown();
+ }
+ }
+
+ @Override
+ public void failed(Throwable cause)
+ {
+ if (_done.compareAndSet(false, true))
+ {
+ _cause = cause;
+ _latch.countDown();
+ }
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning)
+ {
+ if (_done.compareAndSet(false, true))
+ {
+ _cause = new CancellationException();
+ _latch.countDown();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isCancelled()
+ {
+ if (_done.get())
+ {
+ try
+ {
+ _latch.await();
+ }
+ catch (InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ return _cause instanceof CancellationException;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isDone()
+ {
+ return _done.get() && _latch.getCount() == 0;
+ }
+
+ @Override
+ public Void get() throws InterruptedException, ExecutionException
+ {
+ _latch.await();
+ if (_cause == COMPLETED)
+ return null;
+ if (_cause instanceof CancellationException)
+ throw (CancellationException)new CancellationException().initCause(_cause);
+ throw new ExecutionException(_cause);
+ }
+
+ @Override
+ public Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
+ {
+ if (!_latch.await(timeout, unit))
+ throw new TimeoutException();
+
+ if (_cause == COMPLETED)
+ return null;
+ if (_cause instanceof TimeoutException)
+ throw (TimeoutException)_cause;
+ if (_cause instanceof CancellationException)
+ throw (CancellationException)new CancellationException().initCause(_cause);
+ throw new ExecutionException(_cause);
+ }
+
+ public static void rethrow(ExecutionException e) throws IOException
+ {
+ Throwable cause = e.getCause();
+ if (cause instanceof IOException)
+ throw (IOException)cause;
+ if (cause instanceof Error)
+ throw (Error)cause;
+ if (cause instanceof RuntimeException)
+ throw (RuntimeException)cause;
+ throw new RuntimeException(cause);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("FutureCallback@%x{%b,%b}", hashCode(), _done.get(), _cause == COMPLETED);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/FuturePromise.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/FuturePromise.java
new file mode 100644
index 0000000..886ee03
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/FuturePromise.java
@@ -0,0 +1,159 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class FuturePromise<C> implements Future<C>, Promise<C>
+{
+ private static Throwable COMPLETED = new ConstantThrowable();
+ private final AtomicBoolean _done = new AtomicBoolean(false);
+ private final CountDownLatch _latch = new CountDownLatch(1);
+ private Throwable _cause;
+ private C _result;
+
+ public FuturePromise()
+ {
+ }
+
+ public FuturePromise(C result)
+ {
+ _cause = COMPLETED;
+ _result = result;
+ _done.set(true);
+ _latch.countDown();
+ }
+
+ public FuturePromise(C ctx, Throwable failed)
+ {
+ _result = ctx;
+ _cause = failed;
+ _done.set(true);
+ _latch.countDown();
+ }
+
+ @Override
+ public void succeeded(C result)
+ {
+ if (_done.compareAndSet(false, true))
+ {
+ _result = result;
+ _cause = COMPLETED;
+ _latch.countDown();
+ }
+ }
+
+ @Override
+ public void failed(Throwable cause)
+ {
+ if (_done.compareAndSet(false, true))
+ {
+ _cause = cause;
+ _latch.countDown();
+ }
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning)
+ {
+ if (_done.compareAndSet(false, true))
+ {
+ _result = null;
+ _cause = new CancellationException();
+ _latch.countDown();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isCancelled()
+ {
+ if (_done.get())
+ {
+ try
+ {
+ _latch.await();
+ }
+ catch (InterruptedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ return _cause instanceof CancellationException;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isDone()
+ {
+ return _done.get() && _latch.getCount() == 0;
+ }
+
+ @Override
+ public C get() throws InterruptedException, ExecutionException
+ {
+ _latch.await();
+ if (_cause == COMPLETED)
+ return _result;
+ if (_cause instanceof CancellationException)
+ throw (CancellationException)new CancellationException().initCause(_cause);
+ throw new ExecutionException(_cause);
+ }
+
+ @Override
+ public C get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
+ {
+ if (!_latch.await(timeout, unit))
+ throw new TimeoutException();
+
+ if (_cause == COMPLETED)
+ return _result;
+ if (_cause instanceof TimeoutException)
+ throw (TimeoutException)_cause;
+ if (_cause instanceof CancellationException)
+ throw (CancellationException)new CancellationException().initCause(_cause);
+ throw new ExecutionException(_cause);
+ }
+
+ public static void rethrow(ExecutionException e) throws IOException
+ {
+ Throwable cause = e.getCause();
+ if (cause instanceof IOException)
+ throw (IOException)cause;
+ if (cause instanceof Error)
+ throw (Error)cause;
+ if (cause instanceof RuntimeException)
+ throw (RuntimeException)cause;
+ throw new RuntimeException(cause);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("FutureCallback@%x{%b,%b,%s}", hashCode(), _done.get(), _cause == COMPLETED, _result);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/HostMap.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/HostMap.java
new file mode 100644
index 0000000..d9225de
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/HostMap.java
@@ -0,0 +1,105 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+/**
+ * @param <TYPE> the element type
+ */
+@SuppressWarnings("serial")
+public class HostMap<TYPE> extends HashMap<String, TYPE>
+{
+
+ /**
+ * Construct empty HostMap.
+ */
+ public HostMap()
+ {
+ super(11);
+ }
+
+ /**
+ * Construct empty HostMap.
+ *
+ * @param capacity initial capacity
+ */
+ public HostMap(int capacity)
+ {
+ super(capacity);
+ }
+
+ /**
+ * @see java.util.HashMap#put(java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public TYPE put(String host, TYPE object)
+ throws IllegalArgumentException
+ {
+ return super.put(host, object);
+ }
+
+ /**
+ * @see java.util.HashMap#get(java.lang.Object)
+ */
+ @Override
+ public TYPE get(Object key)
+ {
+ return super.get(key);
+ }
+
+ /**
+ * Retrieve a lazy list of map entries associated with specified
+ * hostname by taking into account the domain suffix matches.
+ *
+ * @param host hostname
+ * @return lazy list of map entries
+ */
+ public Object getLazyMatches(String host)
+ {
+ if (host == null)
+ return LazyList.getList(super.entrySet());
+
+ int idx = 0;
+ String domain = host.trim();
+ HashSet<String> domains = new HashSet<String>();
+ do
+ {
+ domains.add(domain);
+ if ((idx = domain.indexOf('.')) > 0)
+ {
+ domain = domain.substring(idx + 1);
+ }
+ }
+ while (idx > 0);
+
+ Object entries = null;
+ for (Map.Entry<String, TYPE> entry : super.entrySet())
+ {
+ if (domains.contains(entry.getKey()))
+ {
+ entries = LazyList.add(entries, entry);
+ }
+ }
+
+ return entries;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java
new file mode 100644
index 0000000..9888e2e
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java
@@ -0,0 +1,177 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+/**
+ * <p>Parse an authority string (in the form {@code host:port}) into
+ * {@code host} and {@code port}, handling IPv4 and IPv6 host formats
+ * as defined in https://www.ietf.org/rfc/rfc2732.txt</p>
+ */
+public class HostPort
+{
+ private final String _host;
+ private final int _port;
+
+ public HostPort(String host, int port)
+ {
+ _host = normalizeHost(host);
+ _port = port;
+ }
+
+ public HostPort(String authority) throws IllegalArgumentException
+ {
+ if (authority == null)
+ throw new IllegalArgumentException("No Authority");
+ try
+ {
+ if (authority.isEmpty())
+ {
+ _host = authority;
+ _port = 0;
+ }
+ else if (authority.charAt(0) == '[')
+ {
+ // ipv6reference
+ int close = authority.lastIndexOf(']');
+ if (close < 0)
+ throw new IllegalArgumentException("Bad IPv6 host");
+ _host = authority.substring(0, close + 1);
+
+ if (authority.length() > close + 1)
+ {
+ if (authority.charAt(close + 1) != ':')
+ throw new IllegalArgumentException("Bad IPv6 port");
+ _port = parsePort(authority.substring(close + 2));
+ }
+ else
+ {
+ _port = 0;
+ }
+ }
+ else
+ {
+ // ipv6address or ipv4address or hostname
+ int c = authority.lastIndexOf(':');
+ if (c >= 0)
+ {
+ // ipv6address
+ if (c != authority.indexOf(':'))
+ {
+ _host = "[" + authority + "]";
+ _port = 0;
+ }
+ else
+ {
+ _host = authority.substring(0, c);
+ _port = parsePort(authority.substring(c + 1));
+ }
+ }
+ else
+ {
+ _host = authority;
+ _port = 0;
+ }
+ }
+ }
+ catch (IllegalArgumentException iae)
+ {
+ throw iae;
+ }
+ catch (Exception ex)
+ {
+ throw new IllegalArgumentException("Bad HostPort", ex);
+ }
+ }
+
+ /**
+ * Get the host.
+ *
+ * @return the host
+ */
+ public String getHost()
+ {
+ return _host;
+ }
+
+ /**
+ * Get the port.
+ *
+ * @return the port
+ */
+ public int getPort()
+ {
+ return _port;
+ }
+
+ /**
+ * Get the port or the given default port.
+ *
+ * @param defaultPort, the default port to return if a port is not specified
+ * @return the port
+ */
+ public int getPort(int defaultPort)
+ {
+ return _port > 0 ? _port : defaultPort;
+ }
+
+ @Override
+ public String toString()
+ {
+ if (_port > 0)
+ return _host + ":" + _port;
+ return _host;
+ }
+
+ /**
+ * Normalizes IPv6 address as per https://tools.ietf.org/html/rfc2732
+ * and https://tools.ietf.org/html/rfc6874,
+ * surrounding with square brackets if they are absent.
+ *
+ * @param host a host name, IPv4 address, IPv6 address or IPv6 literal
+ * @return a host name or an IPv4 address or an IPv6 literal (not an IPv6 address)
+ */
+ public static String normalizeHost(String host)
+ {
+ // if it is normalized IPv6 or could not be IPv6, return
+ if (host == null || host.isEmpty() || host.charAt(0) == '[' || host.indexOf(':') < 0)
+ return host;
+
+ // normalize with [ ]
+ return "[" + host + "]";
+ }
+
+ /**
+ * Parse a string representing a port validating it is a valid port value.
+ *
+ * @param rawPort the port string.
+ * @return the integer value for the port.
+ * @throws IllegalArgumentException if the port is invalid
+ */
+ public static int parsePort(String rawPort) throws IllegalArgumentException
+ {
+ if (StringUtil.isEmpty(rawPort))
+ throw new IllegalArgumentException("Bad port");
+
+ int port = Integer.parseInt(rawPort);
+ if (port <= 0 || port > 65535)
+ throw new IllegalArgumentException("Bad port");
+
+ return port;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/HttpCookieStore.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/HttpCookieStore.java
new file mode 100644
index 0000000..998bbac
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/HttpCookieStore.java
@@ -0,0 +1,147 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.net.CookieManager;
+import java.net.CookieStore;
+import java.net.HttpCookie;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Implementation of {@link CookieStore} that delegates to an instance created by {@link CookieManager}
+ * via {@link CookieManager#getCookieStore()}.
+ */
+public class HttpCookieStore implements CookieStore
+{
+ private final CookieStore delegate;
+
+ public HttpCookieStore()
+ {
+ delegate = new CookieManager().getCookieStore();
+ }
+
+ @Override
+ public void add(URI uri, HttpCookie cookie)
+ {
+ delegate.add(uri, cookie);
+ }
+
+ @Override
+ public List<HttpCookie> get(URI uri)
+ {
+ return delegate.get(uri);
+ }
+
+ @Override
+ public List<HttpCookie> getCookies()
+ {
+ return delegate.getCookies();
+ }
+
+ @Override
+ public List<URI> getURIs()
+ {
+ return delegate.getURIs();
+ }
+
+ @Override
+ public boolean remove(URI uri, HttpCookie cookie)
+ {
+ return delegate.remove(uri, cookie);
+ }
+
+ @Override
+ public boolean removeAll()
+ {
+ return delegate.removeAll();
+ }
+
+ public static List<HttpCookie> matchPath(URI uri, List<HttpCookie> cookies)
+ {
+ if (cookies == null || cookies.isEmpty())
+ return Collections.emptyList();
+ List<HttpCookie> result = new ArrayList<>(4);
+ String path = uri.getPath();
+ if (path == null || path.trim().isEmpty())
+ path = "/";
+ for (HttpCookie cookie : cookies)
+ {
+ String cookiePath = cookie.getPath();
+ if (cookiePath == null)
+ {
+ result.add(cookie);
+ }
+ else
+ {
+ // RFC 6265, section 5.1.4, path matching algorithm.
+ if (path.equals(cookiePath))
+ {
+ result.add(cookie);
+ }
+ else if (path.startsWith(cookiePath))
+ {
+ if (cookiePath.endsWith("/") || path.charAt(cookiePath.length()) == '/')
+ result.add(cookie);
+ }
+ }
+ }
+ return result;
+ }
+
+ public static class Empty implements CookieStore
+ {
+ @Override
+ public void add(URI uri, HttpCookie cookie)
+ {
+ }
+
+ @Override
+ public List<HttpCookie> get(URI uri)
+ {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<HttpCookie> getCookies()
+ {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<URI> getURIs()
+ {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean remove(URI uri, HttpCookie cookie)
+ {
+ return false;
+ }
+
+ @Override
+ public boolean removeAll()
+ {
+ return false;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IO.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IO.java
new file mode 100644
index 0000000..872cca7
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IO.java
@@ -0,0 +1,640 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.nio.channels.GatheringByteChannel;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * IO Utilities.
+ * Provides stream handling utilities in
+ * singleton Threadpool implementation accessed by static members.
+ */
+public class IO
+{
+ private static final Logger LOG = Log.getLogger(IO.class);
+
+ public static final String
+ CRLF = "\r\n";
+
+ public static final byte[]
+ CRLF_BYTES = {(byte)'\r', (byte)'\n'};
+
+ public static final int bufferSize = 64 * 1024;
+
+ static class Job implements Runnable
+ {
+ InputStream in;
+ OutputStream out;
+ Reader read;
+ Writer write;
+
+ Job(InputStream in, OutputStream out)
+ {
+ this.in = in;
+ this.out = out;
+ this.read = null;
+ this.write = null;
+ }
+
+ Job(Reader read, Writer write)
+ {
+ this.in = null;
+ this.out = null;
+ this.read = read;
+ this.write = write;
+ }
+
+ /*
+ * @see java.lang.Runnable#run()
+ */
+ @Override
+ public void run()
+ {
+ try
+ {
+ if (in != null)
+ copy(in, out, -1);
+ else
+ copy(read, write, -1);
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ try
+ {
+ if (out != null)
+ out.close();
+ if (write != null)
+ write.close();
+ }
+ catch (IOException ex2)
+ {
+ LOG.ignore(ex2);
+ }
+ }
+ }
+ }
+
+ /**
+ * Copy Stream in to Stream out until EOF or exception.
+ *
+ * @param in the input stream to read from (until EOF)
+ * @param out the output stream to write to
+ * @throws IOException if unable to copy streams
+ */
+ public static void copy(InputStream in, OutputStream out)
+ throws IOException
+ {
+ copy(in, out, -1);
+ }
+
+ /**
+ * Copy Reader to Writer out until EOF or exception.
+ *
+ * @param in the read to read from (until EOF)
+ * @param out the writer to write to
+ * @throws IOException if unable to copy the streams
+ */
+ public static void copy(Reader in, Writer out)
+ throws IOException
+ {
+ copy(in, out, -1);
+ }
+
+ /**
+ * Copy Stream in to Stream for byteCount bytes or until EOF or exception.
+ *
+ * @param in the stream to read from
+ * @param out the stream to write to
+ * @param byteCount the number of bytes to copy
+ * @throws IOException if unable to copy the streams
+ */
+ public static void copy(InputStream in,
+ OutputStream out,
+ long byteCount)
+ throws IOException
+ {
+ byte[] buffer = new byte[bufferSize];
+ int len = bufferSize;
+
+ if (byteCount >= 0)
+ {
+ while (byteCount > 0)
+ {
+ int max = byteCount < bufferSize ? (int)byteCount : bufferSize;
+ len = in.read(buffer, 0, max);
+
+ if (len == -1)
+ break;
+
+ byteCount -= len;
+ out.write(buffer, 0, len);
+ }
+ }
+ else
+ {
+ while (true)
+ {
+ len = in.read(buffer, 0, bufferSize);
+ if (len < 0)
+ break;
+ out.write(buffer, 0, len);
+ }
+ }
+ }
+
+ /**
+ * Copy Reader to Writer for byteCount bytes or until EOF or exception.
+ *
+ * @param in the Reader to read from
+ * @param out the Writer to write to
+ * @param byteCount the number of bytes to copy
+ * @throws IOException if unable to copy streams
+ */
+ public static void copy(Reader in,
+ Writer out,
+ long byteCount)
+ throws IOException
+ {
+ char[] buffer = new char[bufferSize];
+ int len = bufferSize;
+
+ if (byteCount >= 0)
+ {
+ while (byteCount > 0)
+ {
+ if (byteCount < bufferSize)
+ len = in.read(buffer, 0, (int)byteCount);
+ else
+ len = in.read(buffer, 0, bufferSize);
+
+ if (len == -1)
+ break;
+
+ byteCount -= len;
+ out.write(buffer, 0, len);
+ }
+ }
+ else if (out instanceof PrintWriter)
+ {
+ PrintWriter pout = (PrintWriter)out;
+ while (!pout.checkError())
+ {
+ len = in.read(buffer, 0, bufferSize);
+ if (len == -1)
+ break;
+ out.write(buffer, 0, len);
+ }
+ }
+ else
+ {
+ while (true)
+ {
+ len = in.read(buffer, 0, bufferSize);
+ if (len == -1)
+ break;
+ out.write(buffer, 0, len);
+ }
+ }
+ }
+
+ /**
+ * Copy files or directories
+ *
+ * @param from the file to copy
+ * @param to the destination to copy to
+ * @throws IOException if unable to copy
+ */
+ public static void copy(File from, File to) throws IOException
+ {
+ if (from.isDirectory())
+ copyDir(from, to);
+ else
+ copyFile(from, to);
+ }
+
+ public static void copyDir(File from, File to) throws IOException
+ {
+ if (to.exists())
+ {
+ if (!to.isDirectory())
+ throw new IllegalArgumentException(to.toString());
+ }
+ else
+ to.mkdirs();
+
+ File[] files = from.listFiles();
+ if (files != null)
+ {
+ for (int i = 0; i < files.length; i++)
+ {
+ String name = files[i].getName();
+ if (".".equals(name) || "..".equals(name))
+ continue;
+ copy(files[i], new File(to, name));
+ }
+ }
+ }
+
+ public static void copyFile(File from, File to) throws IOException
+ {
+ try (InputStream in = new FileInputStream(from);
+ OutputStream out = new FileOutputStream(to))
+ {
+ copy(in, out);
+ }
+ }
+
+ /**
+ * Read Path to string.
+ *
+ * @param path the path to read from (until EOF)
+ * @param charset the charset to read with
+ * @return the String parsed from path (default Charset)
+ * @throws IOException if unable to read the path (or handle the charset)
+ */
+ public static String toString(Path path, Charset charset)
+ throws IOException
+ {
+ byte[] buf = Files.readAllBytes(path);
+ return new String(buf, charset);
+ }
+
+ /**
+ * Read input stream to string.
+ *
+ * @param in the stream to read from (until EOF)
+ * @return the String parsed from stream (default Charset)
+ * @throws IOException if unable to read the stream (or handle the charset)
+ */
+ public static String toString(InputStream in)
+ throws IOException
+ {
+ return toString(in, (Charset)null);
+ }
+
+ /**
+ * Read input stream to string.
+ *
+ * @param in the stream to read from (until EOF)
+ * @param encoding the encoding to use (can be null to use default Charset)
+ * @return the String parsed from the stream
+ * @throws IOException if unable to read the stream (or handle the charset)
+ */
+ public static String toString(InputStream in, String encoding)
+ throws IOException
+ {
+ return toString(in, encoding == null ? null : Charset.forName(encoding));
+ }
+
+ /**
+ * Read input stream to string.
+ *
+ * @param in the stream to read from (until EOF)
+ * @param encoding the Charset to use (can be null to use default Charset)
+ * @return the String parsed from the stream
+ * @throws IOException if unable to read the stream (or handle the charset)
+ */
+ public static String toString(InputStream in, Charset encoding)
+ throws IOException
+ {
+ StringWriter writer = new StringWriter();
+ InputStreamReader reader = encoding == null ? new InputStreamReader(in) : new InputStreamReader(in, encoding);
+
+ copy(reader, writer);
+ return writer.toString();
+ }
+
+ /**
+ * Read input stream to string.
+ *
+ * @param in the reader to read from (until EOF)
+ * @return the String parsed from the reader
+ * @throws IOException if unable to read the stream (or handle the charset)
+ */
+ public static String toString(Reader in)
+ throws IOException
+ {
+ StringWriter writer = new StringWriter();
+ copy(in, writer);
+ return writer.toString();
+ }
+
+ /**
+ * Delete File.
+ * This delete will recursively delete directories - BE CAREFUL
+ *
+ * @param file The file (or directory) to be deleted.
+ * @return true if file was deleted, or directory referenced was deleted.
+ * false if file doesn't exist, or was null.
+ */
+ public static boolean delete(File file)
+ {
+ if (file == null)
+ return false;
+ if (!file.exists())
+ return false;
+ if (file.isDirectory())
+ {
+ File[] files = file.listFiles();
+ for (int i = 0; files != null && i < files.length; i++)
+ {
+ delete(files[i]);
+ }
+ }
+ return file.delete();
+ }
+
+ /**
+ * Test if directory is empty.
+ *
+ * @param dir the directory
+ * @return true if directory is null, doesn't exist, or has no content.
+ * false if not a directory, or has contents
+ */
+ public static boolean isEmptyDir(File dir)
+ {
+ if (dir == null)
+ return true;
+ if (!dir.exists())
+ return true;
+ if (!dir.isDirectory())
+ return false;
+ String[] list = dir.list();
+ if (list == null)
+ return true;
+ return list.length <= 0;
+ }
+
+ /**
+ * Closes an arbitrary closable, and logs exceptions at ignore level
+ *
+ * @param closeable the closeable to close
+ */
+ public static void close(Closeable closeable)
+ {
+ try
+ {
+ if (closeable != null)
+ closeable.close();
+ }
+ catch (IOException ignore)
+ {
+ LOG.ignore(ignore);
+ }
+ }
+
+ /**
+ * closes an input stream, and logs exceptions
+ *
+ * @param is the input stream to close
+ */
+ public static void close(InputStream is)
+ {
+ close((Closeable)is);
+ }
+
+ /**
+ * closes an output stream, and logs exceptions
+ *
+ * @param os the output stream to close
+ */
+ public static void close(OutputStream os)
+ {
+ close((Closeable)os);
+ }
+
+ /**
+ * closes a reader, and logs exceptions
+ *
+ * @param reader the reader to close
+ */
+ public static void close(Reader reader)
+ {
+ close((Closeable)reader);
+ }
+
+ /**
+ * closes a writer, and logs exceptions
+ *
+ * @param writer the writer to close
+ */
+ public static void close(Writer writer)
+ {
+ close((Closeable)writer);
+ }
+
+ public static byte[] readBytes(InputStream in)
+ throws IOException
+ {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ copy(in, bout);
+ return bout.toByteArray();
+ }
+
+ /**
+ * A gathering write utility wrapper.
+ * <p>
+ * This method wraps a gather write with a loop that handles the limitations of some operating systems that have a
+ * limit on the number of buffers written. The method loops on the write until either all the content is written or
+ * no progress is made.
+ *
+ * @param out The GatheringByteChannel to write to
+ * @param buffers The buffers to write
+ * @param offset The offset into the buffers array
+ * @param length The length in buffers to write
+ * @return The total bytes written
+ * @throws IOException if unable write to the GatheringByteChannel
+ */
+ public static long write(GatheringByteChannel out, ByteBuffer[] buffers, int offset, int length) throws IOException
+ {
+ long total = 0;
+ write:
+ while (length > 0)
+ {
+ // Write as much as we can
+ long wrote = out.write(buffers, offset, length);
+
+ // If we can't write any more, give up
+ if (wrote == 0)
+ break;
+
+ // count the total
+ total += wrote;
+
+ // Look for unwritten content
+ for (int i = offset; i < buffers.length; i++)
+ {
+ if (buffers[i].hasRemaining())
+ {
+ // loop with new offset and length;
+ length = length - (i - offset);
+ offset = i;
+ continue write;
+ }
+ }
+ length = 0;
+ }
+
+ return total;
+ }
+
+ /**
+ * @return An outputstream to nowhere
+ */
+ public static OutputStream getNullStream()
+ {
+ return __nullStream;
+ }
+
+ /**
+ * @return An outputstream to nowhere
+ */
+ public static InputStream getClosedStream()
+ {
+ return __closedStream;
+ }
+
+ private static class NullOS extends OutputStream
+ {
+ @Override
+ public void close()
+ {
+ }
+
+ @Override
+ public void flush()
+ {
+ }
+
+ @Override
+ public void write(byte[] b)
+ {
+ }
+
+ @Override
+ public void write(byte[] b, int i, int l)
+ {
+ }
+
+ @Override
+ public void write(int b)
+ {
+ }
+ }
+
+ private static NullOS __nullStream = new NullOS();
+
+ private static class ClosedIS extends InputStream
+ {
+ @Override
+ public int read() throws IOException
+ {
+ return -1;
+ }
+ }
+
+ private static ClosedIS __closedStream = new ClosedIS();
+
+ /**
+ * @return An writer to nowhere
+ */
+ public static Writer getNullWriter()
+ {
+ return __nullWriter;
+ }
+
+ /**
+ * @return An writer to nowhere
+ */
+ public static PrintWriter getNullPrintWriter()
+ {
+ return __nullPrintWriter;
+ }
+
+ private static class NullWrite extends Writer
+ {
+ @Override
+ public void close()
+ {
+ }
+
+ @Override
+ public void flush()
+ {
+ }
+
+ @Override
+ public void write(char[] b)
+ {
+ }
+
+ @Override
+ public void write(char[] b, int o, int l)
+ {
+ }
+
+ @Override
+ public void write(int b)
+ {
+ }
+
+ @Override
+ public void write(String s)
+ {
+ }
+
+ @Override
+ public void write(String s, int o, int l)
+ {
+ }
+ }
+
+ private static NullWrite __nullWriter = new NullWrite();
+ private static PrintWriter __nullPrintWriter = new PrintWriter(__nullWriter);
+}
+
+
+
+
+
+
+
+
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IPAddressMap.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IPAddressMap.java
new file mode 100644
index 0000000..d4c9598
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IPAddressMap.java
@@ -0,0 +1,354 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+/**
+ * Internet address map to object
+ * <p>
+ * Internet addresses may be specified as absolute address or as a combination of
+ * four octet wildcard specifications (a.b.c.d) that are defined as follows.
+ * </p>
+ * <pre>
+ * nnn - an absolute value (0-255)
+ * mmm-nnn - an inclusive range of absolute values,
+ * with following shorthand notations:
+ * nnn- => nnn-255
+ * -nnn => 0-nnn
+ * - => 0-255
+ * a,b,... - a list of wildcard specifications
+ * </pre>
+ *
+ * @param <TYPE> the Map Entry value type
+ * @deprecated
+ */
+@SuppressWarnings("serial")
+public class IPAddressMap<TYPE> extends HashMap<String, TYPE>
+{
+ private final HashMap<String, IPAddrPattern> _patterns = new HashMap<String, IPAddrPattern>();
+
+ /**
+ * Construct empty IPAddressMap.
+ */
+ public IPAddressMap()
+ {
+ super(11);
+ }
+
+ /**
+ * Construct empty IPAddressMap.
+ *
+ * @param capacity initial capacity
+ */
+ public IPAddressMap(int capacity)
+ {
+ super(capacity);
+ }
+
+ /**
+ * Insert a new internet address into map
+ *
+ * @see java.util.HashMap#put(java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public TYPE put(String addrSpec, TYPE object)
+ throws IllegalArgumentException
+ {
+ if (addrSpec == null || addrSpec.trim().length() == 0)
+ throw new IllegalArgumentException("Invalid IP address pattern: " + addrSpec);
+
+ String spec = addrSpec.trim();
+ if (_patterns.get(spec) == null)
+ _patterns.put(spec, new IPAddrPattern(spec));
+
+ return super.put(spec, object);
+ }
+
+ /**
+ * Retrieve the object mapped to the specified internet address literal
+ *
+ * @see java.util.HashMap#get(java.lang.Object)
+ */
+ @Override
+ public TYPE get(Object key)
+ {
+ return super.get(key);
+ }
+
+ /**
+ * Retrieve the first object that is associated with the specified
+ * internet address by taking into account the wildcard specifications.
+ *
+ * @param addr internet address
+ * @return associated object
+ */
+ public TYPE match(String addr)
+ {
+ Map.Entry<String, TYPE> entry = getMatch(addr);
+ return entry == null ? null : entry.getValue();
+ }
+
+ /**
+ * Retrieve the first map entry that is associated with the specified
+ * internet address by taking into account the wildcard specifications.
+ *
+ * @param addr internet address
+ * @return map entry associated
+ */
+ public Map.Entry<String, TYPE> getMatch(String addr)
+ {
+ if (addr != null)
+ {
+ for (Map.Entry<String, TYPE> entry : super.entrySet())
+ {
+ if (_patterns.get(entry.getKey()).match(addr))
+ {
+ return entry;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve a lazy list of map entries associated with specified
+ * internet address by taking into account the wildcard specifications.
+ *
+ * @param addr internet address
+ * @return lazy list of map entries
+ */
+ public Object getLazyMatches(String addr)
+ {
+ if (addr == null)
+ return LazyList.getList(super.entrySet());
+
+ Object entries = null;
+ for (Map.Entry<String, TYPE> entry : super.entrySet())
+ {
+ if (_patterns.get(entry.getKey()).match(addr))
+ {
+ entries = LazyList.add(entries, entry);
+ }
+ }
+ return entries;
+ }
+
+ /**
+ * IPAddrPattern
+ *
+ * Represents internet address wildcard.
+ * Matches the wildcard to provided internet address.
+ */
+ private static class IPAddrPattern
+ {
+ private final OctetPattern[] _octets = new OctetPattern[4];
+
+ /**
+ * Create new IPAddrPattern
+ *
+ * @param value internet address wildcard specification
+ * @throws IllegalArgumentException if wildcard specification is invalid
+ */
+ public IPAddrPattern(String value)
+ throws IllegalArgumentException
+ {
+ if (value == null || value.trim().length() == 0)
+ throw new IllegalArgumentException("Invalid IP address pattern: " + value);
+
+ try
+ {
+ StringTokenizer parts = new StringTokenizer(value, ".");
+
+ String part;
+ for (int idx = 0; idx < 4; idx++)
+ {
+ part = parts.hasMoreTokens() ? parts.nextToken().trim() : "0-255";
+
+ int len = part.length();
+ if (len == 0 && parts.hasMoreTokens())
+ throw new IllegalArgumentException("Invalid IP address pattern: " + value);
+
+ _octets[idx] = new OctetPattern(len == 0 ? "0-255" : part);
+ }
+ }
+ catch (IllegalArgumentException ex)
+ {
+ throw new IllegalArgumentException("Invalid IP address pattern: " + value, ex);
+ }
+ }
+
+ /**
+ * Match the specified internet address against the wildcard
+ *
+ * @param value internet address
+ * @return true if specified internet address matches wildcard specification
+ * @throws IllegalArgumentException if specified internet address is invalid
+ */
+ public boolean match(String value)
+ throws IllegalArgumentException
+ {
+ if (value == null || value.trim().length() == 0)
+ throw new IllegalArgumentException("Invalid IP address: " + value);
+
+ try
+ {
+ StringTokenizer parts = new StringTokenizer(value, ".");
+
+ boolean result = true;
+ for (int idx = 0; idx < 4; idx++)
+ {
+ if (!parts.hasMoreTokens())
+ throw new IllegalArgumentException("Invalid IP address: " + value);
+
+ if (!(result &= _octets[idx].match(parts.nextToken())))
+ break;
+ }
+ return result;
+ }
+ catch (IllegalArgumentException ex)
+ {
+ throw new IllegalArgumentException("Invalid IP address: " + value, ex);
+ }
+ }
+ }
+
+ /**
+ * OctetPattern
+ *
+ * Represents a single octet wildcard.
+ * Matches the wildcard to the specified octet value.
+ */
+ private static class OctetPattern extends BitSet
+ {
+ private final BitSet _mask = new BitSet(256);
+
+ /**
+ * Create new OctetPattern
+ *
+ * @param octetSpec octet wildcard specification
+ * @throws IllegalArgumentException if wildcard specification is invalid
+ */
+ public OctetPattern(String octetSpec)
+ throws IllegalArgumentException
+ {
+ try
+ {
+ if (octetSpec != null)
+ {
+ String spec = octetSpec.trim();
+ if (spec.length() == 0)
+ {
+ _mask.set(0, 255);
+ }
+ else
+ {
+ StringTokenizer parts = new StringTokenizer(spec, ",");
+ while (parts.hasMoreTokens())
+ {
+ String part = parts.nextToken().trim();
+ if (part.length() > 0)
+ {
+ if (part.indexOf('-') < 0)
+ {
+ int value = Integer.parseInt(part);
+ _mask.set(value);
+ }
+ else
+ {
+ int low = 0;
+ int high = 255;
+
+ String[] bounds = part.split("-", -2);
+ if (bounds.length != 2)
+ {
+ throw new IllegalArgumentException("Invalid octet spec: " + octetSpec);
+ }
+
+ if (bounds[0].length() > 0)
+ {
+ low = Integer.parseInt(bounds[0]);
+ }
+ if (bounds[1].length() > 0)
+ {
+ high = Integer.parseInt(bounds[1]);
+ }
+
+ if (low > high)
+ {
+ throw new IllegalArgumentException("Invalid octet spec: " + octetSpec);
+ }
+
+ _mask.set(low, high + 1);
+ }
+ }
+ }
+ }
+ }
+ }
+ catch (NumberFormatException ex)
+ {
+ throw new IllegalArgumentException("Invalid octet spec: " + octetSpec, ex);
+ }
+ }
+
+ /**
+ * Match specified octet value against the wildcard
+ *
+ * @param value octet value
+ * @return true if specified octet value matches the wildcard
+ * @throws IllegalArgumentException if specified octet value is invalid
+ */
+ public boolean match(String value)
+ throws IllegalArgumentException
+ {
+ if (value == null || value.trim().length() == 0)
+ throw new IllegalArgumentException("Invalid octet: " + value);
+
+ try
+ {
+ int number = Integer.parseInt(value);
+ return match(number);
+ }
+ catch (NumberFormatException ex)
+ {
+ throw new IllegalArgumentException("Invalid octet: " + value);
+ }
+ }
+
+ /**
+ * Match specified octet value against the wildcard
+ *
+ * @param number octet value
+ * @return true if specified octet value matches the wildcard
+ * @throws IllegalArgumentException if specified octet value is invalid
+ */
+ public boolean match(int number)
+ throws IllegalArgumentException
+ {
+ if (number < 0 || number > 255)
+ throw new IllegalArgumentException("Invalid octet: " + number);
+
+ return _mask.get(number);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java
new file mode 100644
index 0000000..9f13861
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java
@@ -0,0 +1,50 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * Utility class to maintain a set of inclusions and exclusions.
+ * <p>This extension of the {@link IncludeExcludeSet} class is used
+ * when the type of the set elements is the same as the type of
+ * the predicate test.
+ * <p>
+ *
+ * @param <ITEM> The type of element
+ */
+public class IncludeExclude<ITEM> extends IncludeExcludeSet<ITEM, ITEM>
+{
+ public IncludeExclude()
+ {
+ super();
+ }
+
+ public <SET extends Set<ITEM>> IncludeExclude(Class<SET> setClass)
+ {
+ super(setClass);
+ }
+
+ public <SET extends Set<ITEM>> IncludeExclude(Set<ITEM> includeSet, Predicate<ITEM> includePredicate, Set<ITEM> excludeSet,
+ Predicate<ITEM> excludePredicate)
+ {
+ super(includeSet, includePredicate, excludeSet, excludePredicate);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExcludeSet.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExcludeSet.java
new file mode 100644
index 0000000..c54d086
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExcludeSet.java
@@ -0,0 +1,272 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * Utility class to maintain a set of inclusions and exclusions.
+ * <p>Maintains a set of included and excluded elements. The method {@link #test(Object)}
+ * will return true IFF the passed object is not in the excluded set AND ( either the
+ * included set is empty OR the object is in the included set)
+ * <p>The type of the underlying {@link Set} used may be passed into the
+ * constructor, so special sets like Servlet PathMap may be used.
+ * <p>
+ *
+ * @param <T> The type of element of the set (often a pattern)
+ * @param <P> The type of the instance passed to the predicate
+ */
+public class IncludeExcludeSet<T, P> implements Predicate<P>
+{
+ private final Set<T> _includes;
+ private final Predicate<P> _includePredicate;
+ private final Set<T> _excludes;
+ private final Predicate<P> _excludePredicate;
+
+ private static class SetContainsPredicate<T> implements Predicate<T>
+ {
+ private final Set<T> set;
+
+ public SetContainsPredicate(Set<T> set)
+ {
+ this.set = set;
+ }
+
+ @Override
+ public boolean test(T item)
+ {
+ return set.contains(item);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "CONTAINS";
+ }
+ }
+
+ /**
+ * Default constructor over {@link HashSet}
+ */
+ public IncludeExcludeSet()
+ {
+ this(HashSet.class);
+ }
+
+ /**
+ * Construct an IncludeExclude.
+ *
+ * @param setClass The type of {@link Set} to using internally to hold patterns. Two instances will be created.
+ * one for include patterns and one for exclude patters. If the class is also a {@link Predicate},
+ * then it is also used as the item test for the set, otherwise a {@link SetContainsPredicate} instance
+ * is created.
+ * @param <SET> The type of a set to use as the backing store
+ */
+ public <SET extends Set<T>> IncludeExcludeSet(Class<SET> setClass)
+ {
+ try
+ {
+ _includes = setClass.getDeclaredConstructor().newInstance();
+ _excludes = setClass.getDeclaredConstructor().newInstance();
+
+ if (_includes instanceof Predicate)
+ {
+ _includePredicate = (Predicate<P>)_includes;
+ }
+ else
+ {
+ _includePredicate = new SetContainsPredicate(_includes);
+ }
+
+ if (_excludes instanceof Predicate)
+ {
+ _excludePredicate = (Predicate<P>)_excludes;
+ }
+ else
+ {
+ _excludePredicate = new SetContainsPredicate(_excludes);
+ }
+ }
+ catch (RuntimeException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Construct an IncludeExclude
+ *
+ * @param includeSet the Set of items that represent the included space
+ * @param includePredicate the Predicate for included item testing (null for simple {@link Set#contains(Object)} test)
+ * @param excludeSet the Set of items that represent the excluded space
+ * @param excludePredicate the Predicate for excluded item testing (null for simple {@link Set#contains(Object)} test)
+ * @param <SET> The type of a set to use as the backing store
+ */
+ public <SET extends Set<T>> IncludeExcludeSet(Set<T> includeSet, Predicate<P> includePredicate, Set<T> excludeSet, Predicate<P> excludePredicate)
+ {
+ Objects.requireNonNull(includeSet, "Include Set");
+ Objects.requireNonNull(includePredicate, "Include Predicate");
+ Objects.requireNonNull(excludeSet, "Exclude Set");
+ Objects.requireNonNull(excludePredicate, "Exclude Predicate");
+
+ _includes = includeSet;
+ _includePredicate = includePredicate;
+ _excludes = excludeSet;
+ _excludePredicate = excludePredicate;
+ }
+
+ public void include(T element)
+ {
+ _includes.add(element);
+ }
+
+ public void include(T... element)
+ {
+ for (T e : element)
+ {
+ _includes.add(e);
+ }
+ }
+
+ public void exclude(T element)
+ {
+ _excludes.add(element);
+ }
+
+ public void exclude(T... element)
+ {
+ for (T e : element)
+ {
+ _excludes.add(e);
+ }
+ }
+
+ @Deprecated
+ public boolean matches(P t)
+ {
+ return test(t);
+ }
+
+ @Override
+ public boolean test(P t)
+ {
+ if (!_includes.isEmpty() && !_includePredicate.test(t))
+ return false;
+ return !_excludePredicate.test(t);
+ }
+
+ /**
+ * Test Included and not Excluded
+ *
+ * @param item The item to test
+ * @return Boolean.TRUE if item is included, Boolean.FALSE if item is excluded or null if neither
+ */
+ public Boolean isIncludedAndNotExcluded(P item)
+ {
+ if (_excludePredicate.test(item))
+ return Boolean.FALSE;
+ if (_includePredicate.test(item))
+ return Boolean.TRUE;
+
+ return null;
+ }
+
+ public boolean hasIncludes()
+ {
+ return !_includes.isEmpty();
+ }
+
+ public boolean hasExcludes()
+ {
+ return !_excludes.isEmpty();
+ }
+
+ public int size()
+ {
+ return _includes.size() + _excludes.size();
+ }
+
+ public Set<T> getIncluded()
+ {
+ return _includes;
+ }
+
+ public Set<T> getExcluded()
+ {
+ return _excludes;
+ }
+
+ public void clear()
+ {
+ _includes.clear();
+ _excludes.clear();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{i=%s,ip=%s,e=%s,ep=%s}", this.getClass().getSimpleName(), hashCode(),
+ _includes,
+ _includePredicate == _includes ? "SELF" : _includePredicate,
+ _excludes,
+ _excludePredicate == _excludes ? "SELF" : _excludePredicate);
+ }
+
+ public boolean isEmpty()
+ {
+ return _includes.isEmpty() && _excludes.isEmpty();
+ }
+
+ /**
+ * Match items in combined IncludeExcludeSets.
+ * @param item1 The item to match against set1
+ * @param set1 A IncludeExcludeSet to match item1 against
+ * @param item2 The item to match against set2
+ * @param set2 A IncludeExcludeSet to match item2 against
+ * @param <T1> The type of item1
+ * @param <T2> The type of item2
+ * @return True IFF <ul>
+ * <li>Neither item is excluded from their respective sets</li>
+ * <li>Both sets have no includes OR at least one of the items is included in its respective set</li>
+ * </ul>
+ */
+ public static <T1, T2> boolean matchCombined(T1 item1, IncludeExcludeSet<?, T1> set1, T2 item2, IncludeExcludeSet<?, T2> set2)
+ {
+ Boolean match1 = set1.isIncludedAndNotExcluded(item1);
+ Boolean match2 = set2.isIncludedAndNotExcluded(item2);
+
+ // if we are excluded from either set, then we do not match
+ if (match1 == Boolean.FALSE || match2 == Boolean.FALSE)
+ return false;
+
+ // If either set has any includes, then we must be included by one of them
+ if (set1.hasIncludes() || set2.hasIncludes())
+ return match1 == Boolean.TRUE || match2 == Boolean.TRUE;
+
+ // If not excluded and no includes, then we match
+ return true;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/InetAddressSet.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/InetAddressSet.java
new file mode 100644
index 0000000..98540ca
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/InetAddressSet.java
@@ -0,0 +1,329 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.net.InetAddress;
+import java.util.AbstractSet;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * A set of InetAddress patterns.
+ * <p>This is a {@link Set} of String patterns that are used to match
+ * a {@link Predicate} over InetAddress for containment semantics.
+ * The patterns that may be set are:
+ * </p>
+ * <dl>
+ * <dt>InetAddress</dt><dd>A single InetAddress either in hostname or address format.
+ * All formats supported by {@link InetAddress} are accepted. Not ethat using hostname
+ * matches may force domain lookups. eg. "[::1]", "1.2.3.4", "::ffff:127.0.0.1"</dd>
+ * <dt>InetAddress/CIDR</dt><dd>An InetAddress with a integer number of bits to indicate
+ * the significant prefix. eg. "192.168.0.0/16" will match from "192.168.0.0" to
+ * "192.168.255.255" </dd>
+ * <dt>InetAddress-InetAddress</dt><dd>An inclusive range of InetAddresses.
+ * eg. "[a000::1]-[afff::]", "192.168.128.0-192.168.128.255"</dd>
+ * <dt>Legacy format</dt><dd>The legacy format used by {@link IPAddressMap} for IPv4 only.
+ * eg. "10.10.10-14.0-128"</dd>
+ * </dl>
+ * <p>This class is designed to work with {@link IncludeExcludeSet}</p>
+ *
+ * @see IncludeExcludeSet
+ */
+public class InetAddressSet extends AbstractSet<String> implements Set<String>, Predicate<InetAddress>
+{
+ private Map<String, InetPattern> _patterns = new HashMap<>();
+
+ @Override
+ public boolean add(String pattern)
+ {
+ return _patterns.put(pattern, newInetRange(pattern)) == null;
+ }
+
+ private InetPattern newInetRange(String pattern)
+ {
+ if (pattern == null)
+ return null;
+
+ int slash = pattern.lastIndexOf('/');
+ int dash = pattern.lastIndexOf('-');
+ try
+ {
+ if (slash >= 0)
+ return new CidrInetRange(pattern, InetAddress.getByName(pattern.substring(0, slash).trim()), StringUtil.toInt(pattern, slash + 1));
+
+ if (dash >= 0)
+ return new MinMaxInetRange(pattern, InetAddress.getByName(pattern.substring(0, dash).trim()), InetAddress.getByName(pattern.substring(dash + 1).trim()));
+
+ return new SingletonInetRange(pattern, InetAddress.getByName(pattern));
+ }
+ catch (Exception e)
+ {
+ try
+ {
+ if (slash < 0 && dash > 0)
+ return new LegacyInetRange(pattern);
+ }
+ catch (Exception ex2)
+ {
+ e.addSuppressed(ex2);
+ }
+ throw new IllegalArgumentException("Bad pattern: " + pattern, e);
+ }
+ }
+
+ @Override
+ public boolean remove(Object pattern)
+ {
+ return _patterns.remove(pattern) != null;
+ }
+
+ @Override
+ public Iterator<String> iterator()
+ {
+ return _patterns.keySet().iterator();
+ }
+
+ @Override
+ public int size()
+ {
+ return _patterns.size();
+ }
+
+ @Override
+ public boolean test(InetAddress address)
+ {
+ if (address == null)
+ return false;
+ byte[] raw = address.getAddress();
+ for (InetPattern pattern : _patterns.values())
+ {
+ if (pattern.test(address, raw))
+ return true;
+ }
+ return false;
+ }
+
+ abstract static class InetPattern
+ {
+ final String _pattern;
+
+ InetPattern(String pattern)
+ {
+ _pattern = pattern;
+ }
+
+ abstract boolean test(InetAddress address, byte[] raw);
+
+ @Override
+ public String toString()
+ {
+ return _pattern;
+ }
+ }
+
+ static class SingletonInetRange extends InetPattern
+ {
+ final InetAddress _address;
+
+ public SingletonInetRange(String pattern, InetAddress address)
+ {
+ super(pattern);
+ _address = address;
+ }
+
+ @Override
+ public boolean test(InetAddress address, byte[] raw)
+ {
+ return _address.equals(address);
+ }
+ }
+
+ static class MinMaxInetRange extends InetPattern
+ {
+ final int[] _min;
+ final int[] _max;
+
+ public MinMaxInetRange(String pattern, InetAddress min, InetAddress max)
+ {
+ super(pattern);
+
+ byte[] rawMin = min.getAddress();
+ byte[] rawMax = max.getAddress();
+ if (rawMin.length != rawMax.length)
+ throw new IllegalArgumentException("Cannot mix IPv4 and IPv6: " + pattern);
+
+ if (rawMin.length == 4)
+ {
+ // there must be 6 '.' or this is likely to be a legacy pattern
+ int count = 0;
+ for (char c : pattern.toCharArray())
+ {
+ if (c == '.')
+ count++;
+ }
+ if (count != 6)
+ throw new IllegalArgumentException("Legacy pattern: " + pattern);
+ }
+
+ _min = new int[rawMin.length];
+ _max = new int[rawMin.length];
+
+ for (int i = 0; i < _min.length; i++)
+ {
+ _min[i] = 0xff & rawMin[i];
+ _max[i] = 0xff & rawMax[i];
+ }
+
+ for (int i = 0; i < _min.length; i++)
+ {
+ if (_min[i] > _max[i])
+ throw new IllegalArgumentException("min is greater than max: " + pattern);
+ if (_min[i] < _max[i])
+ break;
+ }
+ }
+
+ @Override
+ public boolean test(InetAddress item, byte[] raw)
+ {
+ if (raw.length != _min.length)
+ return false;
+
+ boolean minOk = false;
+ boolean maxOk = false;
+
+ for (int i = 0; i < _min.length; i++)
+ {
+ int r = 0xff & raw[i];
+ if (!minOk)
+ {
+ if (r < _min[i])
+ return false;
+ if (r > _min[i])
+ minOk = true;
+ }
+ if (!maxOk)
+ {
+ if (r > _max[i])
+ return false;
+ if (r < _max[i])
+ maxOk = true;
+ }
+
+ if (minOk && maxOk)
+ break;
+ }
+
+ return true;
+ }
+ }
+
+ static class CidrInetRange extends InetPattern
+ {
+ final byte[] _raw;
+ final int _octets;
+ final int _mask;
+ final int _masked;
+
+ public CidrInetRange(String pattern, InetAddress address, int cidr)
+ {
+ super(pattern);
+ _raw = address.getAddress();
+ _octets = cidr / 8;
+ _mask = 0xff & (0xff << (8 - cidr % 8));
+ _masked = _mask == 0 ? 0 : _raw[_octets] & _mask;
+
+ if (cidr > (_raw.length * 8))
+ throw new IllegalArgumentException("CIDR too large: " + pattern);
+
+ if (_mask != 0 && (0xff & _raw[_octets]) != _masked)
+ throw new IllegalArgumentException("CIDR bits non zero: " + pattern);
+
+ for (int o = _octets + (_mask == 0 ? 0 : 1); o < _raw.length; o++)
+ {
+ if (_raw[o] != 0)
+ throw new IllegalArgumentException("CIDR bits non zero: " + pattern);
+ }
+ }
+
+ @Override
+ public boolean test(InetAddress item, byte[] raw)
+ {
+ if (raw.length != _raw.length)
+ return false;
+
+ for (int o = 0; o < _octets; o++)
+ {
+ if (_raw[o] != raw[o])
+ return false;
+ }
+
+ return _mask == 0 || (raw[_octets] & _mask) == _masked;
+ }
+ }
+
+ static class LegacyInetRange extends InetPattern
+ {
+ int[] _min = new int[4];
+ int[] _max = new int[4];
+
+ public LegacyInetRange(String pattern)
+ {
+ super(pattern);
+
+ String[] parts = pattern.split("\\.");
+ if (parts.length != 4)
+ throw new IllegalArgumentException("Bad legacy pattern: " + pattern);
+
+ for (int i = 0; i < 4; i++)
+ {
+ String part = parts[i].trim();
+ int dash = part.indexOf('-');
+ if (dash < 0)
+ _min[i] = _max[i] = Integer.parseInt(part);
+ else
+ {
+ _min[i] = (dash == 0) ? 0 : StringUtil.toInt(part, 0);
+ _max[i] = (dash == part.length() - 1) ? 255 : StringUtil.toInt(part, dash + 1);
+ }
+
+ if (_min[i] < 0 || _min[i] > _max[i] || _max[i] > 255)
+ throw new IllegalArgumentException("Bad legacy pattern: " + pattern);
+ }
+ }
+
+ @Override
+ public boolean test(InetAddress item, byte[] raw)
+ {
+ if (raw.length != 4)
+ return false;
+
+ for (int i = 0; i < 4; i++)
+ {
+ if ((0xff & raw[i]) < _min[i] || (0xff & raw[i]) > _max[i])
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IntrospectionUtil.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IntrospectionUtil.java
new file mode 100644
index 0000000..5d2f357
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IntrospectionUtil.java
@@ -0,0 +1,263 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * IntrospectionUtil
+ */
+public class IntrospectionUtil
+{
+
+ public static boolean isJavaBeanCompliantSetter(Method method)
+ {
+ if (method == null)
+ return false;
+
+ if (method.getReturnType() != Void.TYPE)
+ return false;
+
+ if (!method.getName().startsWith("set"))
+ return false;
+
+ return method.getParameterCount() == 1;
+ }
+
+ public static Method findMethod(Class<?> clazz, String methodName, Class<?>[] args, boolean checkInheritance, boolean strictArgs)
+ throws NoSuchMethodException
+ {
+ if (clazz == null)
+ throw new NoSuchMethodException("No class");
+ if (methodName == null || methodName.trim().equals(""))
+ throw new NoSuchMethodException("No method name");
+
+ Method method = null;
+ Method[] methods = clazz.getDeclaredMethods();
+ for (int i = 0; i < methods.length && method == null; i++)
+ {
+ if (methods[i].getName().equals(methodName) && checkParams(methods[i].getParameterTypes(), (args == null ? new Class[]{} : args), strictArgs))
+ {
+ method = methods[i];
+ }
+ }
+ if (method != null)
+ {
+ return method;
+ }
+ else if (checkInheritance)
+ return findInheritedMethod(clazz.getPackage(), clazz.getSuperclass(), methodName, args, strictArgs);
+ else
+ throw new NoSuchMethodException("No such method " + methodName + " on class " + clazz.getName());
+ }
+
+ public static Field findField(Class<?> clazz, String targetName, Class<?> targetType, boolean checkInheritance, boolean strictType)
+ throws NoSuchFieldException
+ {
+ if (clazz == null)
+ throw new NoSuchFieldException("No class");
+ if (targetName == null)
+ throw new NoSuchFieldException("No field name");
+
+ try
+ {
+ Field field = clazz.getDeclaredField(targetName);
+ if (strictType)
+ {
+ if (field.getType().equals(targetType))
+ return field;
+ }
+ else
+ {
+ if (field.getType().isAssignableFrom(targetType))
+ return field;
+ }
+ if (checkInheritance)
+ {
+ return findInheritedField(clazz.getPackage(), clazz.getSuperclass(), targetName, targetType, strictType);
+ }
+ else
+ throw new NoSuchFieldException("No field with name " + targetName + " in class " + clazz.getName() + " of type " + targetType);
+ }
+ catch (NoSuchFieldException e)
+ {
+ return findInheritedField(clazz.getPackage(), clazz.getSuperclass(), targetName, targetType, strictType);
+ }
+ }
+
+ public static boolean isInheritable(Package pack, Member member)
+ {
+ if (pack == null)
+ return false;
+ if (member == null)
+ return false;
+
+ int modifiers = member.getModifiers();
+ if (Modifier.isPublic(modifiers))
+ return true;
+ if (Modifier.isProtected(modifiers))
+ return true;
+ return !Modifier.isPrivate(modifiers) && pack.equals(member.getDeclaringClass().getPackage());
+ }
+
+ public static boolean checkParams(Class<?>[] formalParams, Class<?>[] actualParams, boolean strict)
+ {
+ if (formalParams == null)
+ return actualParams == null;
+ if (actualParams == null)
+ return false;
+
+ if (formalParams.length != actualParams.length)
+ return false;
+
+ if (formalParams.length == 0)
+ return true;
+
+ int j = 0;
+ if (strict)
+ {
+ while (j < formalParams.length && formalParams[j].equals(actualParams[j]))
+ {
+ j++;
+ }
+ }
+ else
+ {
+ while ((j < formalParams.length) && (formalParams[j].isAssignableFrom(actualParams[j])))
+ {
+ j++;
+ }
+ }
+
+ return j == formalParams.length;
+ }
+
+ public static boolean isSameSignature(Method methodA, Method methodB)
+ {
+ if (methodA == null)
+ return false;
+ if (methodB == null)
+ return false;
+
+ List<Class<?>> parameterTypesA = Arrays.asList(methodA.getParameterTypes());
+ List<Class<?>> parameterTypesB = Arrays.asList(methodB.getParameterTypes());
+
+ return methodA.getName().equals(methodB.getName()) && parameterTypesA.containsAll(parameterTypesB);
+ }
+
+ public static boolean isTypeCompatible(Class<?> formalType, Class<?> actualType, boolean strict)
+ {
+ if (formalType == null)
+ return actualType == null;
+ if (actualType == null)
+ return false;
+
+ if (strict)
+ return formalType.equals(actualType);
+ else
+ return formalType.isAssignableFrom(actualType);
+ }
+
+ public static boolean containsSameMethodSignature(Method method, Class<?> c, boolean checkPackage)
+ {
+ if (checkPackage)
+ {
+ if (!c.getPackage().equals(method.getDeclaringClass().getPackage()))
+ return false;
+ }
+
+ boolean samesig = false;
+ Method[] methods = c.getDeclaredMethods();
+ for (int i = 0; i < methods.length && !samesig; i++)
+ {
+ if (IntrospectionUtil.isSameSignature(method, methods[i]))
+ samesig = true;
+ }
+ return samesig;
+ }
+
+ public static boolean containsSameFieldName(Field field, Class<?> c, boolean checkPackage)
+ {
+ if (checkPackage)
+ {
+ if (!c.getPackage().equals(field.getDeclaringClass().getPackage()))
+ return false;
+ }
+
+ boolean sameName = false;
+ Field[] fields = c.getDeclaredFields();
+ for (int i = 0; i < fields.length && !sameName; i++)
+ {
+ if (fields[i].getName().equals(field.getName()))
+ sameName = true;
+ }
+ return sameName;
+ }
+
+ protected static Method findInheritedMethod(Package pack, Class<?> clazz, String methodName, Class<?>[] args, boolean strictArgs)
+ throws NoSuchMethodException
+ {
+ if (clazz == null)
+ throw new NoSuchMethodException("No class");
+ if (methodName == null)
+ throw new NoSuchMethodException("No method name");
+
+ Method method = null;
+ Method[] methods = clazz.getDeclaredMethods();
+ for (int i = 0; i < methods.length && method == null; i++)
+ {
+ if (methods[i].getName().equals(methodName) &&
+ isInheritable(pack, methods[i]) &&
+ checkParams(methods[i].getParameterTypes(), args, strictArgs))
+ method = methods[i];
+ }
+ if (method != null)
+ {
+ return method;
+ }
+ else
+ return findInheritedMethod(clazz.getPackage(), clazz.getSuperclass(), methodName, args, strictArgs);
+ }
+
+ protected static Field findInheritedField(Package pack, Class<?> clazz, String fieldName, Class<?> fieldType, boolean strictType)
+ throws NoSuchFieldException
+ {
+ if (clazz == null)
+ throw new NoSuchFieldException("No class");
+ if (fieldName == null)
+ throw new NoSuchFieldException("No field name");
+ try
+ {
+ Field field = clazz.getDeclaredField(fieldName);
+ if (isInheritable(pack, field) && isTypeCompatible(fieldType, field.getType(), strictType))
+ return field;
+ else
+ return findInheritedField(clazz.getPackage(), clazz.getSuperclass(), fieldName, fieldType, strictType);
+ }
+ catch (NoSuchFieldException e)
+ {
+ return findInheritedField(clazz.getPackage(), clazz.getSuperclass(), fieldName, fieldType, strictType);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java
new file mode 100644
index 0000000..d8ce740
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java
@@ -0,0 +1,508 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+
+import org.eclipse.jetty.util.thread.Locker;
+
+/**
+ * This specialized callback implements a pattern that allows
+ * a large job to be broken into smaller tasks using iteration
+ * rather than recursion.
+ * <p>
+ * A typical example is the write of a large content to a socket,
+ * divided in chunks. Chunk C1 is written by thread T1, which
+ * also invokes the callback, which writes chunk C2, which invokes
+ * the callback again, which writes chunk C3, and so forth.
+ * </p>
+ * <p>
+ * The problem with the example is that if the callback thread
+ * is the same that performs the I/O operation, then the process
+ * is recursive and may result in a stack overflow.
+ * To avoid the stack overflow, a thread dispatch must be performed,
+ * causing context switching and cache misses, affecting performance.
+ * </p>
+ * <p>
+ * To avoid this issue, this callback uses an AtomicReference to
+ * record whether success callback has been called during the processing
+ * of a sub task, and if so then the processing iterates rather than
+ * recurring.
+ * </p>
+ * <p>
+ * Subclasses must implement method {@link #process()} where the sub
+ * task is executed and a suitable {@link IteratingCallback.Action} is
+ * returned to this callback to indicate the overall progress of the job.
+ * This callback is passed to the asynchronous execution of each sub
+ * task and a call the {@link #succeeded()} on this callback represents
+ * the completion of the sub task.
+ * </p>
+ */
+public abstract class IteratingCallback implements Callback
+{
+ /**
+ * The internal states of this callback
+ */
+ private enum State
+ {
+ /**
+ * This callback is IDLE, ready to iterate.
+ */
+ IDLE,
+
+ /**
+ * This callback is iterating calls to {@link #process()} and is dealing with
+ * the returns. To get into processing state, it much of held the lock state
+ * and set iterating to true.
+ */
+ PROCESSING,
+
+ /**
+ * Waiting for a schedule callback
+ */
+ PENDING,
+
+ /**
+ * Called by a schedule callback
+ */
+ CALLED,
+
+ /**
+ * The overall job has succeeded as indicated by a {@link Action#SUCCEEDED} return
+ * from {@link IteratingCallback#process()}
+ */
+ SUCCEEDED,
+
+ /**
+ * The overall job has failed as indicated by a call to {@link IteratingCallback#failed(Throwable)}
+ */
+ FAILED,
+
+ /**
+ * This callback has been closed and cannot be reset.
+ */
+ CLOSED
+ }
+
+ /**
+ * The indication of the overall progress of the overall job that
+ * implementations of {@link #process()} must return.
+ */
+ protected enum Action
+ {
+ /**
+ * Indicates that {@link #process()} has no more work to do,
+ * but the overall job is not completed yet, probably waiting
+ * for additional events to trigger more work.
+ */
+ IDLE,
+ /**
+ * Indicates that {@link #process()} is executing asynchronously
+ * a sub task, where the execution has started but the callback
+ * may have not yet been invoked.
+ */
+ SCHEDULED,
+
+ /**
+ * Indicates that {@link #process()} has completed the overall job.
+ */
+ SUCCEEDED
+ }
+
+ private Locker _locker = new Locker();
+ private State _state;
+ private boolean _iterate;
+
+ protected IteratingCallback()
+ {
+ _state = State.IDLE;
+ }
+
+ protected IteratingCallback(boolean needReset)
+ {
+ _state = needReset ? State.SUCCEEDED : State.IDLE;
+ }
+
+ /**
+ * Method called by {@link #iterate()} to process the sub task.
+ * <p>
+ * Implementations must start the asynchronous execution of the sub task
+ * (if any) and return an appropriate action:
+ * </p>
+ * <ul>
+ * <li>{@link Action#IDLE} when no sub tasks are available for execution
+ * but the overall job is not completed yet</li>
+ * <li>{@link Action#SCHEDULED} when the sub task asynchronous execution
+ * has been started</li>
+ * <li>{@link Action#SUCCEEDED} when the overall job is completed</li>
+ * </ul>
+ *
+ * @return the appropriate Action
+ * @throws Throwable if the sub task processing throws
+ */
+ protected abstract Action process() throws Throwable;
+
+ /**
+ * Invoked when the overall task has completed successfully.
+ *
+ * @see #onCompleteFailure(Throwable)
+ */
+ protected void onCompleteSuccess()
+ {
+ }
+
+ /**
+ * Invoked when the overall task has completed with a failure.
+ *
+ * @param cause the throwable to indicate cause of failure
+ * @see #onCompleteSuccess()
+ */
+ protected void onCompleteFailure(Throwable cause)
+ {
+ }
+
+ /**
+ * This method must be invoked by applications to start the processing
+ * of sub tasks. It can be called at any time by any thread, and it's
+ * contract is that when called, then the {@link #process()} method will
+ * be called during or soon after, either by the calling thread or by
+ * another thread.
+ */
+ public void iterate()
+ {
+ boolean process = false;
+
+ loop:
+ while (true)
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ switch (_state)
+ {
+ case PENDING:
+ case CALLED:
+ // process will be called when callback is handled
+ break loop;
+
+ case IDLE:
+ _state = State.PROCESSING;
+ process = true;
+ break loop;
+
+ case PROCESSING:
+ _iterate = true;
+ break loop;
+
+ case FAILED:
+ case SUCCEEDED:
+ break loop;
+
+ case CLOSED:
+ default:
+ throw new IllegalStateException(toString());
+ }
+ }
+ }
+ if (process)
+ processing();
+ }
+
+ private void processing()
+ {
+ // This should only ever be called when in processing state, however a failed or close call
+ // may happen concurrently, so state is not assumed.
+
+ boolean onCompleteSuccess = false;
+
+ // While we are processing
+ processing:
+ while (true)
+ {
+ // Call process to get the action that we have to take.
+ Action action;
+ try
+ {
+ action = process();
+ }
+ catch (Throwable x)
+ {
+ failed(x);
+ break processing;
+ }
+
+ // acted on the action we have just received
+ try (Locker.Lock lock = _locker.lock())
+ {
+ switch (_state)
+ {
+ case PROCESSING:
+ {
+ switch (action)
+ {
+ case IDLE:
+ {
+ // Has iterate been called while we were processing?
+ if (_iterate)
+ {
+ // yes, so skip idle and keep processing
+ _iterate = false;
+ _state = State.PROCESSING;
+ continue processing;
+ }
+
+ // No, so we can go idle
+ _state = State.IDLE;
+ break processing;
+ }
+
+ case SCHEDULED:
+ {
+ // we won the race against the callback, so the callback has to process and we can break processing
+ _state = State.PENDING;
+ break processing;
+ }
+
+ case SUCCEEDED:
+ {
+ // we lost the race against the callback,
+ _iterate = false;
+ _state = State.SUCCEEDED;
+ onCompleteSuccess = true;
+ break processing;
+ }
+
+ default:
+ break;
+ }
+ throw new IllegalStateException(String.format("%s[action=%s]", this, action));
+ }
+
+ case CALLED:
+ {
+ switch (action)
+ {
+ case SCHEDULED:
+ {
+ // we lost the race, so we have to keep processing
+ _state = State.PROCESSING;
+ continue processing;
+ }
+
+ default:
+ throw new IllegalStateException(String.format("%s[action=%s]", this, action));
+ }
+ }
+
+ case SUCCEEDED:
+ case FAILED:
+ case CLOSED:
+ break processing;
+
+ case IDLE:
+ case PENDING:
+ default:
+ throw new IllegalStateException(String.format("%s[action=%s]", this, action));
+ }
+ }
+ }
+
+ if (onCompleteSuccess)
+ onCompleteSuccess();
+ }
+
+ /**
+ * Invoked when the sub task succeeds.
+ * Subclasses that override this method must always remember to call
+ * {@code super.succeeded()}.
+ */
+ @Override
+ public void succeeded()
+ {
+ boolean process = false;
+ try (Locker.Lock lock = _locker.lock())
+ {
+ switch (_state)
+ {
+ case PROCESSING:
+ {
+ _state = State.CALLED;
+ break;
+ }
+ case PENDING:
+ {
+ _state = State.PROCESSING;
+ process = true;
+ break;
+ }
+ case CLOSED:
+ case FAILED:
+ {
+ // Too late!
+ break;
+ }
+ default:
+ {
+ throw new IllegalStateException(toString());
+ }
+ }
+ }
+ if (process)
+ processing();
+ }
+
+ /**
+ * Invoked when the sub task fails.
+ * Subclasses that override this method must always remember to call
+ * {@code super.failed(Throwable)}.
+ */
+ @Override
+ public void failed(Throwable x)
+ {
+ boolean failure = false;
+ try (Locker.Lock lock = _locker.lock())
+ {
+ switch (_state)
+ {
+ case SUCCEEDED:
+ case FAILED:
+ case IDLE:
+ case CLOSED:
+ case CALLED:
+ // too late!.
+ break;
+
+ case PENDING:
+ case PROCESSING:
+ {
+ _state = State.FAILED;
+ failure = true;
+ break;
+ }
+ default:
+ throw new IllegalStateException(toString());
+ }
+ }
+ if (failure)
+ onCompleteFailure(x);
+ }
+
+ public void close()
+ {
+ String failure = null;
+ try (Locker.Lock lock = _locker.lock())
+ {
+ switch (_state)
+ {
+ case IDLE:
+ case SUCCEEDED:
+ case FAILED:
+ _state = State.CLOSED;
+ break;
+
+ case CLOSED:
+ break;
+
+ default:
+ failure = String.format("Close %s in state %s", this, _state);
+ _state = State.CLOSED;
+ }
+ }
+
+ if (failure != null)
+ onCompleteFailure(new IOException(failure));
+ }
+
+ /*
+ * only for testing
+ * @return whether this callback is idle and {@link #iterate()} needs to be called
+ */
+ boolean isIdle()
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ return _state == State.IDLE;
+ }
+ }
+
+ public boolean isClosed()
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ return _state == State.CLOSED;
+ }
+ }
+
+ /**
+ * @return whether this callback has failed
+ */
+ public boolean isFailed()
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ return _state == State.FAILED;
+ }
+ }
+
+ /**
+ * @return whether this callback has succeeded
+ */
+ public boolean isSucceeded()
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ return _state == State.SUCCEEDED;
+ }
+ }
+
+ /**
+ * Resets this callback.
+ * <p>
+ * A callback can only be reset to IDLE from the
+ * SUCCEEDED or FAILED states or if it is already IDLE.
+ * </p>
+ *
+ * @return true if the reset was successful
+ */
+ public boolean reset()
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ switch (_state)
+ {
+ case IDLE:
+ return true;
+
+ case SUCCEEDED:
+ case FAILED:
+ _iterate = false;
+ _state = State.IDLE;
+ return true;
+
+ default:
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), _state);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingNestedCallback.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingNestedCallback.java
new file mode 100644
index 0000000..d34c5e7
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingNestedCallback.java
@@ -0,0 +1,71 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+/**
+ * Iterating Nested Callback.
+ * <p>This specialized callback is used when breaking up an
+ * asynchronous task into smaller asynchronous tasks. A typical pattern
+ * is that a successful callback is used to schedule the next sub task, but
+ * if that task completes quickly and uses the calling thread to callback
+ * the success notification, this can result in a growing stack depth.
+ * </p>
+ * <p>To avoid this issue, this callback uses an AtomicBoolean to note
+ * if the success callback has been called during the processing of a
+ * sub task, and if so then the processing iterates rather than recurses.
+ * </p>
+ * <p>This callback is passed to the asynchronous handling of each sub
+ * task and a call the {@link #succeeded()} on this call back represents
+ * completion of the subtask. Only once all the subtasks are completed is
+ * the {@link Callback#succeeded()} method called on the {@link Callback} instance
+ * passed the the {@link #IteratingNestedCallback(Callback)} constructor.</p>
+ */
+public abstract class IteratingNestedCallback extends IteratingCallback
+{
+ final Callback _callback;
+
+ public IteratingNestedCallback(Callback callback)
+ {
+ _callback = callback;
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return _callback.getInvocationType();
+ }
+
+ @Override
+ protected void onCompleteSuccess()
+ {
+ _callback.succeeded();
+ }
+
+ @Override
+ protected void onCompleteFailure(Throwable x)
+ {
+ _callback.failed(x);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x", getClass().getSimpleName(), hashCode());
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/JavaVersion.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/JavaVersion.java
new file mode 100644
index 0000000..8e7fca8
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/JavaVersion.java
@@ -0,0 +1,153 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+/**
+ * Java Version Utility class.
+ * <p>Parses java versions to extract a consistent set of version parts</p>
+ */
+public class JavaVersion
+{
+ /**
+ * Context attribute that can be set to target a different version of the jvm than the current runtime.
+ * Acceptable values should correspond to those returned by JavaVersion.getPlatform().
+ */
+ public static final String JAVA_TARGET_PLATFORM = "org.eclipse.jetty.javaTargetPlatform";
+
+ public static final JavaVersion VERSION = parse(System.getProperty("java.version"));
+
+ public static JavaVersion parse(String v)
+ {
+ // $VNUM is a dot-separated list of integers of arbitrary length
+ String[] split = v.split("[^0-9]");
+ int len = Math.min(split.length, 3);
+ int[] version = new int[len];
+ for (int i = 0; i < len; i++)
+ {
+ try
+ {
+ version[i] = Integer.parseInt(split[i]);
+ }
+ catch (Throwable e)
+ {
+ len = i - 1;
+ break;
+ }
+ }
+
+ return new JavaVersion(
+ v,
+ (version[0] >= 9 || len == 1) ? version[0] : version[1],
+ version[0],
+ len > 1 ? version[1] : 0,
+ len > 2 ? version[2] : 0);
+ }
+
+ private final String version;
+ private final int platform;
+ private final int major;
+ private final int minor;
+ private final int micro;
+
+ private JavaVersion(String version, int platform, int major, int minor, int micro)
+ {
+ this.version = version;
+ this.platform = platform;
+ this.major = major;
+ this.minor = minor;
+ this.micro = micro;
+ }
+
+ /**
+ * @return the string from which this JavaVersion was created
+ */
+ public String getVersion()
+ {
+ return version;
+ }
+
+ /**
+ * <p>Returns the Java Platform version, such as {@code 8} for JDK 1.8.0_92 and {@code 9} for JDK 9.2.4.</p>
+ *
+ * @return the Java Platform version
+ */
+ public int getPlatform()
+ {
+ return platform;
+ }
+
+ /**
+ * <p>Returns the major number version, such as {@code 1} for JDK 1.8.0_92 and {@code 9} for JDK 9.2.4.</p>
+ *
+ * @return the major number version
+ */
+ public int getMajor()
+ {
+ return major;
+ }
+
+ /**
+ * <p>Returns the minor number version, such as {@code 8} for JDK 1.8.0_92 and {@code 2} for JDK 9.2.4.</p>
+ *
+ * @return the minor number version
+ */
+ public int getMinor()
+ {
+ return minor;
+ }
+
+ /**
+ * <p>Returns the micro number version (aka security number), such as {@code 0} for JDK 1.8.0_92 and {@code 4} for JDK 9.2.4.</p>
+ *
+ * @return the micro number version
+ */
+ public int getMicro()
+ {
+ return micro;
+ }
+
+ /**
+ * <p>Returns the update number version, such as {@code 92} for JDK 1.8.0_92 and {@code 0} for JDK 9.2.4.</p>
+ *
+ * @return the update number version
+ */
+ @Deprecated
+ public int getUpdate()
+ {
+ return 0;
+ }
+
+ /**
+ * <p>Returns the remaining string after the version numbers, such as {@code -internal} for
+ * JDK 1.8.0_92-internal and {@code -ea} for JDK 9-ea, or {@code +13} for JDK 9.2.4+13.</p>
+ *
+ * @return the remaining string after the version numbers
+ */
+ @Deprecated
+ public String getSuffix()
+ {
+ return null;
+ }
+
+ @Override
+ public String toString()
+ {
+ return version;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java
new file mode 100644
index 0000000..bcfe71f
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java
@@ -0,0 +1,98 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.InputStream;
+import java.time.Instant;
+import java.util.Properties;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class Jetty
+{
+ private static final Logger LOG = Log.getLogger(Jetty.class);
+
+ public static final String VERSION;
+ public static final String POWERED_BY;
+ public static final boolean STABLE;
+ public static final String GIT_HASH;
+
+ /**
+ * a formatted build timestamp with pattern yyyy-MM-dd'T'HH:mm:ssXXX
+ */
+ public static final String BUILD_TIMESTAMP;
+ private static final Properties __buildProperties = new Properties();
+
+ static
+ {
+ try
+ {
+ try (InputStream inputStream = //
+ Jetty.class.getResourceAsStream("/org/eclipse/jetty/version/build.properties"))
+ {
+ __buildProperties.load(inputStream);
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+
+ String gitHash = __buildProperties.getProperty("buildNumber", "unknown");
+ if (gitHash.startsWith("${"))
+ gitHash = "unknown";
+ GIT_HASH = gitHash;
+ System.setProperty("jetty.git.hash", GIT_HASH);
+ BUILD_TIMESTAMP = formatTimestamp(__buildProperties.getProperty("timestamp", "unknown"));
+
+ // using __buildProperties.getProperty("version") will contain version from the pom
+
+ Package pkg = Jetty.class.getPackage();
+ if (pkg != null &&
+ "Eclipse Jetty Project".equals(pkg.getImplementationVendor()) &&
+ pkg.getImplementationVersion() != null)
+ VERSION = pkg.getImplementationVersion();
+ else
+ VERSION = System.getProperty("jetty.version", __buildProperties.getProperty("version", "9.4.z-SNAPSHOT"));
+
+ POWERED_BY = "<a href=\"https://eclipse.org/jetty\">Powered by Jetty:// " + VERSION + "</a>";
+
+ // Show warning when RC# or M# is in version string
+ STABLE = !VERSION.matches("^.*\\.(RC|M)[0-9]+$");
+ }
+
+ private Jetty()
+ {
+ }
+
+ private static String formatTimestamp(String timestamp)
+ {
+ try
+ {
+ long epochMillis = Long.parseLong(timestamp);
+ return Instant.ofEpochMilli(epochMillis).toString();
+ }
+ catch (NumberFormatException e)
+ {
+ LOG.ignore(e);
+ return "unknown";
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/LazyList.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/LazyList.java
new file mode 100644
index 0000000..8b0ba5d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/LazyList.java
@@ -0,0 +1,455 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Lazy List creation.
+ * <p>
+ * A List helper class that attempts to avoid unnecessary List
+ * creation. If a method needs to create a List to return, but it is
+ * expected that this will either be empty or frequently contain a
+ * single item, then using LazyList will avoid additional object
+ * creations by using {@link Collections#EMPTY_LIST} or
+ * {@link Collections#singletonList(Object)} where possible.
+ * </p>
+ * <p>
+ * LazyList works by passing an opaque representation of the list in
+ * and out of all the LazyList methods. This opaque object is either
+ * null for an empty list, an Object for a list with a single entry
+ * or an {@link ArrayList} for a list of items.
+ * </p>
+ * <strong>Usage</strong>
+ * <pre>
+ * Object lazylist =null;
+ * while(loopCondition)
+ * {
+ * Object item = getItem();
+ * if (item.isToBeAdded())
+ * lazylist = LazyList.add(lazylist,item);
+ * }
+ * return LazyList.getList(lazylist);
+ * </pre>
+ *
+ * An ArrayList of default size is used as the initial LazyList.
+ *
+ * @see java.util.List
+ */
+@SuppressWarnings("serial")
+public class LazyList
+ implements Cloneable, Serializable
+{
+ private static final String[] __EMPTY_STRING_ARRAY = new String[0];
+
+ private LazyList()
+ {
+ }
+
+ /**
+ * Add an item to a LazyList
+ *
+ * @param list The list to add to or null if none yet created.
+ * @param item The item to add.
+ * @return The lazylist created or added to.
+ */
+ @SuppressWarnings("unchecked")
+ public static Object add(Object list, Object item)
+ {
+ if (list == null)
+ {
+ if (item instanceof List || item == null)
+ {
+ List<Object> l = new ArrayList<Object>();
+ l.add(item);
+ return l;
+ }
+
+ return item;
+ }
+
+ if (list instanceof List)
+ {
+ ((List<Object>)list).add(item);
+ return list;
+ }
+
+ List<Object> l = new ArrayList<Object>();
+ l.add(list);
+ l.add(item);
+ return l;
+ }
+
+ /**
+ * Add an item to a LazyList
+ *
+ * @param list The list to add to or null if none yet created.
+ * @param index The index to add the item at.
+ * @param item The item to add.
+ * @return The lazylist created or added to.
+ */
+ @SuppressWarnings("unchecked")
+ public static Object add(Object list, int index, Object item)
+ {
+ if (list == null)
+ {
+ if (index > 0 || item instanceof List || item == null)
+ {
+ List<Object> l = new ArrayList<Object>();
+ l.add(index, item);
+ return l;
+ }
+ return item;
+ }
+
+ if (list instanceof List)
+ {
+ ((List<Object>)list).add(index, item);
+ return list;
+ }
+
+ List<Object> l = new ArrayList<Object>();
+ l.add(list);
+ l.add(index, item);
+ return l;
+ }
+
+ /**
+ * Add the contents of a Collection to a LazyList
+ *
+ * @param list The list to add to or null if none yet created.
+ * @param collection The Collection whose contents should be added.
+ * @return The lazylist created or added to.
+ */
+ public static Object addCollection(Object list, Collection<?> collection)
+ {
+ Iterator<?> i = collection.iterator();
+ while (i.hasNext())
+ {
+ list = LazyList.add(list, i.next());
+ }
+ return list;
+ }
+
+ /**
+ * Add the contents of an array to a LazyList
+ *
+ * @param list The list to add to or null if none yet created.
+ * @param array The array whose contents should be added.
+ * @return The lazylist created or added to.
+ */
+ public static Object addArray(Object list, Object[] array)
+ {
+ for (int i = 0; array != null && i < array.length; i++)
+ {
+ list = LazyList.add(list, array[i]);
+ }
+ return list;
+ }
+
+ /**
+ * Ensure the capacity of the underlying list.
+ *
+ * @param list the list to grow
+ * @param initialSize the size to grow to
+ * @return the new List with new size
+ */
+ public static Object ensureSize(Object list, int initialSize)
+ {
+ if (list == null)
+ return new ArrayList<Object>(initialSize);
+ if (list instanceof ArrayList)
+ {
+ ArrayList<?> ol = (ArrayList<?>)list;
+ if (ol.size() > initialSize)
+ return ol;
+ ArrayList<Object> nl = new ArrayList<Object>(initialSize);
+ nl.addAll(ol);
+ return nl;
+ }
+ List<Object> l = new ArrayList<Object>(initialSize);
+ l.add(list);
+ return l;
+ }
+
+ public static Object remove(Object list, Object o)
+ {
+ if (list == null)
+ return null;
+
+ if (list instanceof List)
+ {
+ List<?> l = (List<?>)list;
+ l.remove(o);
+ if (l.size() == 0)
+ return null;
+ return list;
+ }
+
+ if (list.equals(o))
+ return null;
+ return list;
+ }
+
+ public static Object remove(Object list, int i)
+ {
+ if (list == null)
+ return null;
+
+ if (list instanceof List)
+ {
+ List<?> l = (List<?>)list;
+ l.remove(i);
+ if (l.size() == 0)
+ return null;
+ return list;
+ }
+
+ if (i == 0)
+ return null;
+ return list;
+ }
+
+ /**
+ * Get the real List from a LazyList.
+ *
+ * @param list A LazyList returned from LazyList.add(Object)
+ * @param <E> the list entry type
+ * @return The List of added items, which may be an EMPTY_LIST
+ * or a SingletonList.
+ */
+ public static <E> List<E> getList(Object list)
+ {
+ return getList(list, false);
+ }
+
+ /**
+ * Get the real List from a LazyList.
+ *
+ * @param list A LazyList returned from LazyList.add(Object) or null
+ * @param nullForEmpty If true, null is returned instead of an
+ * empty list.
+ * @param <E> the list entry type
+ * @return The List of added items, which may be null, an EMPTY_LIST
+ * or a SingletonList.
+ */
+ @SuppressWarnings("unchecked")
+ public static <E> List<E> getList(Object list, boolean nullForEmpty)
+ {
+ if (list == null)
+ {
+ if (nullForEmpty)
+ return null;
+ return Collections.emptyList();
+ }
+ if (list instanceof List)
+ return (List<E>)list;
+
+ return (List<E>)Collections.singletonList(list);
+ }
+
+ /**
+ * Simple utility method to test if List has at least 1 entry.
+ *
+ * @param list a LazyList, {@link List} or {@link Object}
+ * @return true if not-null and is not empty
+ */
+ public static boolean hasEntry(Object list)
+ {
+ if (list == null)
+ return false;
+ if (list instanceof List)
+ return !((List<?>)list).isEmpty();
+ return true;
+ }
+
+ /**
+ * Simple utility method to test if List is empty
+ *
+ * @param list a LazyList, {@link List} or {@link Object}
+ * @return true if null or is empty
+ */
+ public static boolean isEmpty(Object list)
+ {
+ if (list == null)
+ return true;
+ if (list instanceof List)
+ return ((List<?>)list).isEmpty();
+ return false;
+ }
+
+ public static String[] toStringArray(Object list)
+ {
+ if (list == null)
+ return __EMPTY_STRING_ARRAY;
+
+ if (list instanceof List)
+ {
+ List<?> l = (List<?>)list;
+ String[] a = new String[l.size()];
+ for (int i = l.size(); i-- > 0; )
+ {
+ Object o = l.get(i);
+ if (o != null)
+ a[i] = o.toString();
+ }
+ return a;
+ }
+
+ return new String[]{list.toString()};
+ }
+
+ /**
+ * Convert a lazylist to an array
+ *
+ * @param list The list to convert
+ * @param clazz The class of the array, which may be a primitive type
+ * @return array of the lazylist entries passed in
+ */
+ public static Object toArray(Object list, Class<?> clazz)
+ {
+ if (list == null)
+ return Array.newInstance(clazz, 0);
+
+ if (list instanceof List)
+ {
+ List<?> l = (List<?>)list;
+ if (clazz.isPrimitive())
+ {
+ Object a = Array.newInstance(clazz, l.size());
+ for (int i = 0; i < l.size(); i++)
+ {
+ Array.set(a, i, l.get(i));
+ }
+ return a;
+ }
+ return l.toArray((Object[])Array.newInstance(clazz, l.size()));
+ }
+
+ Object a = Array.newInstance(clazz, 1);
+ Array.set(a, 0, list);
+ return a;
+ }
+
+ /**
+ * The size of a lazy List
+ *
+ * @param list A LazyList returned from LazyList.add(Object) or null
+ * @return the size of the list.
+ */
+ public static int size(Object list)
+ {
+ if (list == null)
+ return 0;
+ if (list instanceof List)
+ return ((List<?>)list).size();
+ return 1;
+ }
+
+ /**
+ * Get item from the list
+ *
+ * @param list A LazyList returned from LazyList.add(Object) or null
+ * @param i int index
+ * @param <E> the list entry type
+ * @return the item from the list.
+ */
+ @SuppressWarnings("unchecked")
+ public static <E> E get(Object list, int i)
+ {
+ if (list == null)
+ throw new IndexOutOfBoundsException();
+
+ if (list instanceof List)
+ return (E)((List<?>)list).get(i);
+
+ if (i == 0)
+ return (E)list;
+
+ throw new IndexOutOfBoundsException();
+ }
+
+ public static boolean contains(Object list, Object item)
+ {
+ if (list == null)
+ return false;
+
+ if (list instanceof List)
+ return ((List<?>)list).contains(item);
+
+ return list.equals(item);
+ }
+
+ public static Object clone(Object list)
+ {
+ if (list == null)
+ return null;
+ if (list instanceof List)
+ return new ArrayList<Object>((List<?>)list);
+ return list;
+ }
+
+ public static String toString(Object list)
+ {
+ if (list == null)
+ return "[]";
+ if (list instanceof List)
+ return list.toString();
+ return "[" + list + "]";
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <E> Iterator<E> iterator(Object list)
+ {
+ if (list == null)
+ {
+ List<E> empty = Collections.emptyList();
+ return empty.iterator();
+ }
+ if (list instanceof List)
+ {
+ return ((List<E>)list).iterator();
+ }
+ List<E> l = getList(list);
+ return l.iterator();
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <E> ListIterator<E> listIterator(Object list)
+ {
+ if (list == null)
+ {
+ List<E> empty = Collections.emptyList();
+ return empty.listIterator();
+ }
+ if (list instanceof List)
+ return ((List<E>)list).listIterator();
+
+ List<E> l = getList(list);
+ return l.listIterator();
+ }
+}
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/LeakDetector.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/LeakDetector.java
new file mode 100644
index 0000000..96d12cd
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/LeakDetector.java
@@ -0,0 +1,199 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * A facility to detect improper usage of resource pools.
+ * <p>
+ * Resource pools usually have a method to acquire a pooled resource and a method to released it back to the pool.
+ * <p>
+ * To detect if client code acquires a resource but never releases it, the resource pool can be modified to use a
+ * {@link LeakDetector}. The modified resource pool should call {@link #acquired(Object)} every time the method to
+ * acquire a resource is called, and {@link #released(Object)} every time the method to release the resource is called.
+ * {@link LeakDetector} keeps track of these resources and invokes method
+ * {@link #leaked(org.eclipse.jetty.util.LeakDetector.LeakInfo)} when it detects that a resource has been leaked (that
+ * is, acquired but never released).
+ * <p>
+ * To detect whether client code releases a resource without having acquired it, the resource pool can be modified to
+ * check the return value of {@link #released(Object)}: if false, it means that the resource was not acquired.
+ * <p>
+ * IMPLEMENTATION NOTES
+ * <p>
+ * This class relies on {@link System#identityHashCode(Object)} to create a unique id for each resource passed to
+ * {@link #acquired(Object)} and {@link #released(Object)}. {@link System#identityHashCode(Object)} does not guarantee
+ * that it will not generate the same number for different objects, but in practice the chance of collision is rare.
+ * <p>
+ * {@link LeakDetector} uses {@link PhantomReference}s to detect leaks. {@link PhantomReference}s are enqueued in their
+ * {@link ReferenceQueue} <em>after</em> they have been garbage collected (differently from {@link WeakReference}s that
+ * are enqueued <em>before</em>). Since the resource is now garbage collected, {@link LeakDetector} checks whether it
+ * has been released and if not, it reports a leak. Using {@link PhantomReference}s is better than overriding
+ * {@link #finalize()} and works also in those cases where {@link #finalize()} is not overridable.
+ *
+ * @param <T> the resource type.
+ */
+public class LeakDetector<T> extends AbstractLifeCycle implements Runnable
+{
+ private static final Logger LOG = Log.getLogger(LeakDetector.class);
+
+ private final ReferenceQueue<T> queue = new ReferenceQueue<>();
+ private final ConcurrentMap<String, LeakInfo> resources = new ConcurrentHashMap<>();
+ private Thread thread;
+
+ /**
+ * Tracks the resource as been acquired.
+ *
+ * @param resource the resource that has been acquired
+ * @return true whether the resource has been acquired normally, false if the resource has detected a leak (meaning
+ * that another acquire occurred before a release of the same resource)
+ * @see #released(Object)
+ */
+ public boolean acquired(T resource)
+ {
+ String id = id(resource);
+ LeakInfo info = resources.putIfAbsent(id, new LeakInfo(resource, id));
+ // Leak detected, prior acquire exists (not released) or id clash.
+ return info == null; // Normal behavior.
+ }
+
+ /**
+ * Tracks the resource as been released.
+ *
+ * @param resource the resource that has been released
+ * @return true whether the resource has been released normally (based on a previous acquire). false if the resource
+ * has been released without a prior acquire (such as a double release scenario)
+ * @see #acquired(Object)
+ */
+ public boolean released(T resource)
+ {
+ String id = id(resource);
+ LeakInfo info = resources.remove(id);
+ // Normal behavior.
+ return info != null;
+
+ // Leak detected (released without acquire).
+ }
+
+ /**
+ * Generates a unique ID for the given resource.
+ *
+ * @param resource the resource to generate the unique ID for
+ * @return the unique ID of the given resource
+ */
+ public String id(T resource)
+ {
+ return String.valueOf(System.identityHashCode(resource));
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+ thread = new Thread(this, getClass().getSimpleName());
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ super.doStop();
+ thread.interrupt();
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ while (isRunning())
+ {
+ @SuppressWarnings("unchecked")
+ LeakInfo leakInfo = (LeakInfo)queue.remove();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Resource GC'ed: {}", leakInfo);
+ if (resources.remove(leakInfo.id) != null)
+ leaked(leakInfo);
+ }
+ }
+ catch (InterruptedException x)
+ {
+ // Exit
+ }
+ }
+
+ /**
+ * Callback method invoked by {@link LeakDetector} when it detects that a resource has been leaked.
+ *
+ * @param leakInfo the information about the leak
+ */
+ protected void leaked(LeakInfo leakInfo)
+ {
+ LOG.warn("Resource leaked: " + leakInfo.description, leakInfo.stackFrames);
+ }
+
+ /**
+ * Information about the leak of a resource.
+ */
+ public class LeakInfo extends PhantomReference<T>
+ {
+ private final String id;
+ private final String description;
+ private final Throwable stackFrames;
+
+ private LeakInfo(T referent, String id)
+ {
+ super(referent, queue);
+ this.id = id;
+ this.description = referent.toString();
+ this.stackFrames = new Throwable();
+ }
+
+ /**
+ * @return the resource description as provided by the resource's {@link Object#toString()} method.
+ */
+ public String getResourceDescription()
+ {
+ return description;
+ }
+
+ /**
+ * @return a Throwable instance that contains the stack frames at the time of resource acquisition.
+ */
+ public Throwable getStackFrames()
+ {
+ return stackFrames;
+ }
+
+ @Override
+ public String toString()
+ {
+ return description;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Loader.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Loader.java
new file mode 100644
index 0000000..c1a5f41
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Loader.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.net.URL;
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+/**
+ * ClassLoader Helper.
+ * This helper class allows classes to be loaded either from the
+ * Thread's ContextClassLoader, the classloader of the derived class
+ * or the system ClassLoader.
+ *
+ * <B>Usage:</B><PRE>
+ * public class MyClass {
+ * void myMethod() {
+ * ...
+ * Class c=Loader.loadClass(this.getClass(),classname);
+ * ...
+ * }
+ * </PRE>
+ */
+public class Loader
+{
+
+ public static URL getResource(String name)
+ {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ return loader == null ? ClassLoader.getSystemResource(name) : loader.getResource(name);
+ }
+
+ /**
+ * Load a class.
+ * <p>Load a class either from the thread context classloader or if none, the system
+ * loader</p>
+ *
+ * @param name the name of the new class to load
+ * @return Class
+ * @throws ClassNotFoundException if not able to find the class
+ */
+ @SuppressWarnings("rawtypes")
+ public static Class loadClass(String name)
+ throws ClassNotFoundException
+ {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ return (loader == null) ? Class.forName(name) : loader.loadClass(name);
+ }
+
+ /**
+ * Load a class.
+ * Load a class from the same classloader as the passed <code>loadClass</code>, or if none
+ * then use {@link #loadClass(String)}
+ *
+ * @param loaderClass a similar class, belong in the same classloader of the desired class to load
+ * @param name the name of the new class to load
+ * @return Class
+ * @throws ClassNotFoundException if not able to find the class
+ */
+ @SuppressWarnings("rawtypes")
+ public static Class loadClass(Class loaderClass, String name)
+ throws ClassNotFoundException
+ {
+ if (loaderClass != null && loaderClass.getClassLoader() != null)
+ return loaderClass.getClassLoader().loadClass(name);
+ return loadClass(name);
+ }
+
+ public static ResourceBundle getResourceBundle(String name, boolean checkParents, Locale locale)
+ throws MissingResourceException
+ {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ return loader == null ? ResourceBundle.getBundle(name, locale) : ResourceBundle.getBundle(name, locale, loader);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ManifestUtils.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ManifestUtils.java
new file mode 100644
index 0000000..593382d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ManifestUtils.java
@@ -0,0 +1,85 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.File;
+import java.net.URL;
+import java.security.CodeSource;
+import java.util.Optional;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+public class ManifestUtils
+{
+ private ManifestUtils()
+ {
+ }
+
+ public static Optional<Manifest> getManifest(Class<?> klass)
+ {
+ try
+ {
+ CodeSource codeSource = klass.getProtectionDomain().getCodeSource();
+ if (codeSource != null)
+ {
+ URL location = codeSource.getLocation();
+ if (location != null)
+ {
+ try (JarFile jarFile = new JarFile(new File(location.toURI())))
+ {
+ return Optional.of(jarFile.getManifest());
+ }
+ }
+ }
+ return Optional.empty();
+ }
+ catch (Throwable x)
+ {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * <p>Attempts to return the version of the jar/module for the given class.</p>
+ * <p>First, retrieves the {@code Implementation-Version} main attribute of the manifest;
+ * if that is missing, retrieves the JPMS module version (via reflection);
+ * if that is missing, returns an empty Optional.</p>
+ *
+ * @param klass the class of the jar/module to retrieve the version
+ * @return the jar/module version, or an empty Optional
+ */
+ public static Optional<String> getVersion(Class<?> klass)
+ {
+ Optional<String> version = getManifest(klass).map(Manifest::getMainAttributes)
+ .map(attributes -> attributes.getValue("Implementation-Version"));
+ if (version.isPresent())
+ return version;
+
+ try
+ {
+ Object module = klass.getClass().getMethod("getModule").invoke(klass);
+ Object descriptor = module.getClass().getMethod("getDescriptor").invoke(module);
+ return (Optional<String>)descriptor.getClass().getMethod("rawVersion").invoke(descriptor);
+ }
+ catch (Throwable x)
+ {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MathUtils.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MathUtils.java
new file mode 100644
index 0000000..62034fd
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MathUtils.java
@@ -0,0 +1,65 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+public class MathUtils
+{
+ private MathUtils()
+ {
+ }
+
+ /**
+ * Returns whether the sum of the arguments overflows an {@code int}.
+ *
+ * @param a the first value
+ * @param b the second value
+ * @return whether the sum of the arguments overflows an {@code int}
+ */
+ public static boolean sumOverflows(int a, int b)
+ {
+ try
+ {
+ Math.addExact(a, b);
+ return false;
+ }
+ catch (ArithmeticException x)
+ {
+ return true;
+ }
+ }
+
+ /**
+ * Returns the sum of its arguments, capping to {@link Long#MAX_VALUE} if they overflow.
+ *
+ * @param a the first value
+ * @param b the second value
+ * @return the sum of the values, capped to {@link Long#MAX_VALUE}
+ */
+ public static long cappedAdd(long a, long b)
+ {
+ try
+ {
+ return Math.addExact(a, b);
+ }
+ catch (ArithmeticException x)
+ {
+ return Long.MAX_VALUE;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MemoryUtils.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MemoryUtils.java
new file mode 100644
index 0000000..3f73ddc
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MemoryUtils.java
@@ -0,0 +1,70 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+
+/**
+ * MemoryUtils provides an abstraction over memory properties and operations.
+ */
+public class MemoryUtils
+{
+ private static final int cacheLineBytes;
+
+ static
+ {
+ final int defaultValue = 64;
+ int value = defaultValue;
+ try
+ {
+ value = Integer.parseInt(AccessController.doPrivileged(new PrivilegedAction<String>()
+ {
+ @Override
+ public String run()
+ {
+ return System.getProperty("org.eclipse.jetty.util.cacheLineBytes", String.valueOf(defaultValue));
+ }
+ }));
+ }
+ catch (Exception ignored)
+ {
+ }
+ cacheLineBytes = value;
+ }
+
+ private MemoryUtils()
+ {
+ }
+
+ public static int getCacheLineBytes()
+ {
+ return cacheLineBytes;
+ }
+
+ public static int getIntegersPerCacheLine()
+ {
+ return getCacheLineBytes() >> 2;
+ }
+
+ public static int getLongsPerCacheLine()
+ {
+ return getCacheLineBytes() >> 3;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ModuleLocation.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ModuleLocation.java
new file mode 100644
index 0000000..b5eb4eb
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ModuleLocation.java
@@ -0,0 +1,166 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.util.Optional;
+import java.util.function.Function;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+import static java.lang.invoke.MethodType.methodType;
+
+/**
+ * Equivalent of ...
+ *
+ * <pre>
+ * Module module = clazz.getModule();
+ * if (module != null)
+ * {
+ * Configuration configuration = module.getLayer().configuration();
+ * Optional<ResolvedModule> resolvedModule = configuration.findModule(module.getName());
+ * if (resolvedModule.isPresent())
+ * {
+ * ModuleReference moduleReference = resolvedModule.get().reference();
+ * Optional<URI> location = moduleReference.location();
+ * if (location.isPresent())
+ * {
+ * return location.get();
+ * }
+ * }
+ * }
+ * return null;
+ * </pre>
+ *
+ * In Jetty 10, this entire class can be moved to direct calls to java.lang.Module in TypeUtil.getModuleLocation()
+ */
+class ModuleLocation implements Function<Class<?>, URI>
+{
+ private static final Logger LOG = Log.getLogger(ModuleLocation.class);
+
+ private final Class<?> classModule;
+ private final MethodHandle handleGetModule;
+ private final MethodHandle handleGetLayer;
+ private final MethodHandle handleConfiguration;
+ private final MethodHandle handleGetName;
+ private final MethodHandle handleOptionalResolvedModule;
+ private final MethodHandle handleReference;
+ private final MethodHandle handleLocation;
+
+ public ModuleLocation()
+ {
+ MethodHandles.Lookup lookup = MethodHandles.lookup();
+ ClassLoader loader = ClassLoader.getSystemClassLoader();
+
+ try
+ {
+ classModule = loader.loadClass("java.lang.Module");
+ handleGetModule = lookup.findVirtual(Class.class, "getModule", methodType(classModule));
+
+ Class<?> classLayer = loader.loadClass("java.lang.ModuleLayer");
+ handleGetLayer = lookup.findVirtual(classModule, "getLayer", methodType(classLayer));
+
+ Class<?> classConfiguration = loader.loadClass("java.lang.module.Configuration");
+ handleConfiguration = lookup.findVirtual(classLayer, "configuration", methodType(classConfiguration));
+
+ handleGetName = lookup.findVirtual(classModule, "getName", methodType(String.class));
+
+ Method findModuleMethod = classConfiguration.getMethod("findModule", String.class);
+ handleOptionalResolvedModule = lookup.findVirtual(classConfiguration, "findModule", methodType(findModuleMethod.getReturnType(), String.class));
+
+ Class<?> classResolvedModule = loader.loadClass("java.lang.module.ResolvedModule");
+ Class<?> classReference = loader.loadClass("java.lang.module.ModuleReference");
+ handleReference = lookup.findVirtual(classResolvedModule, "reference", methodType(classReference));
+
+ Method locationMethod = classReference.getMethod("location");
+ handleLocation = lookup.findVirtual(classReference, "location", methodType(locationMethod.getReturnType()));
+ }
+ catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e)
+ {
+ throw new UnsupportedOperationException("Not supported on this runtime", e);
+ }
+ }
+
+ @Override
+ public URI apply(Class<?> clazz)
+ {
+ try
+ {
+ // Module module = clazz.getModule();
+ Object module = handleGetModule.invoke(clazz);
+ if (module == null)
+ {
+ return null;
+ }
+
+ // ModuleLayer layer = module.getLayer();
+ Object layer = handleGetLayer.invoke(module);
+ if (layer == null)
+ {
+ return null;
+ }
+
+ // Configuration configuration = layer.configuration();
+ Object configuration = handleConfiguration.invoke(layer);
+ if (configuration == null)
+ {
+ return null;
+ }
+
+ // String moduleName = module.getName();
+ String moduleName = (String)handleGetName.invoke(module);
+ if (moduleName == null)
+ {
+ return null;
+ }
+
+ // Optional<ResolvedModule> optionalResolvedModule = configuration.findModule(moduleName);
+ Optional<?> optionalResolvedModule = (Optional<?>)handleOptionalResolvedModule.invoke(configuration, moduleName);
+ if (!optionalResolvedModule.isPresent())
+ {
+ return null;
+ }
+
+ // ResolveModule resolved = optionalResolvedModule.get();
+ Object resolved = optionalResolvedModule.get();
+
+ // ModuleReference moduleReference = resolved.reference();
+ Object moduleReference = handleReference.invoke(resolved);
+
+ // Optional<URI> location = moduleReference.location();
+ Optional<URI> location = (Optional<URI>)handleLocation.invoke(moduleReference);
+ if (location != null || location.isPresent())
+ {
+ return location.get();
+ }
+ }
+ catch (Throwable ignored)
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.ignore(ignored);
+ }
+ }
+ return null;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiException.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiException.java
new file mode 100644
index 0000000..faf5af9
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiException.java
@@ -0,0 +1,223 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Wraps multiple exceptions.
+ *
+ * Allows multiple exceptions to be thrown as a single exception.
+ *
+ * The MultiException itself should not be thrown instead one of the
+ * ifExceptionThrow* methods should be called instead.
+ */
+@SuppressWarnings("serial")
+public class MultiException extends Exception
+{
+ private static final String DEFAULT_MESSAGE = "Multiple exceptions";
+
+ private List<Throwable> nested;
+
+ public MultiException()
+ {
+ // Avoid filling in stack trace information.
+ super(DEFAULT_MESSAGE, null, false, false);
+ this.nested = new ArrayList<>();
+ }
+
+ /**
+ * Create a MultiException which may be thrown.
+ *
+ * @param nested The nested exceptions which will be suppressed by this
+ * exception.
+ */
+ private MultiException(List<Throwable> nested)
+ {
+ super(DEFAULT_MESSAGE);
+ this.nested = new ArrayList<>(nested);
+
+ if (nested.size() > 0)
+ initCause(nested.get(0));
+
+ for (Throwable t : nested)
+ {
+ if (t != this)
+ addSuppressed(t);
+ }
+ }
+
+ public void add(Throwable e)
+ {
+ if (e instanceof MultiException)
+ {
+ MultiException me = (MultiException)e;
+ nested.addAll(me.nested);
+ }
+ else
+ nested.add(e);
+ }
+
+ public int size()
+ {
+ return (nested == null) ? 0 : nested.size();
+ }
+
+ public List<Throwable> getThrowables()
+ {
+ if (nested == null)
+ return Collections.emptyList();
+ return nested;
+ }
+
+ public Throwable getThrowable(int i)
+ {
+ return nested.get(i);
+ }
+
+ /**
+ * Throw a multiexception.
+ * If this multi exception is empty then no action is taken. If it
+ * contains a single exception that is thrown, otherwise the this
+ * multi exception is thrown.
+ *
+ * @throws Exception the Error or Exception if nested is 1, or the MultiException itself if nested is more than 1.
+ */
+ public void ifExceptionThrow()
+ throws Exception
+ {
+ if (nested == null)
+ return;
+
+ switch (nested.size())
+ {
+ case 0:
+ break;
+ case 1:
+ Throwable th = nested.get(0);
+ if (th instanceof Error)
+ throw (Error)th;
+ if (th instanceof Exception)
+ throw (Exception)th;
+ throw new MultiException(nested);
+ default:
+ throw new MultiException(nested);
+ }
+ }
+
+ /**
+ * Throw a Runtime exception.
+ * If this multi exception is empty then no action is taken. If it
+ * contains a single error or runtime exception that is thrown, otherwise the this
+ * multi exception is thrown, wrapped in a runtime exception.
+ *
+ * @throws Error If this exception contains exactly 1 {@link Error}
+ * @throws RuntimeException If this exception contains 1 {@link Throwable} but it is not an error,
+ * or it contains more than 1 {@link Throwable} of any type.
+ */
+ public void ifExceptionThrowRuntime()
+ throws Error
+ {
+ if (nested == null)
+ return;
+
+ switch (nested.size())
+ {
+ case 0:
+ break;
+ case 1:
+ Throwable th = nested.get(0);
+ if (th instanceof Error)
+ throw (Error)th;
+ else if (th instanceof RuntimeException)
+ throw (RuntimeException)th;
+ else
+ throw new RuntimeException(th);
+ default:
+ throw new RuntimeException(new MultiException(nested));
+ }
+ }
+
+ /**
+ * Throw a multiexception.
+ * If this multi exception is empty then no action is taken. If it
+ * contains a any exceptions then this
+ * multi exception is thrown.
+ *
+ * @throws MultiException the multiexception if there are nested exception
+ */
+ public void ifExceptionThrowMulti()
+ throws MultiException
+ {
+ if (nested == null)
+ return;
+
+ if (nested.size() > 0)
+ {
+ throw new MultiException(nested);
+ }
+ }
+
+ /**
+ * Throw an Exception, potentially with suppress.
+ * If this multi exception is empty then no action is taken. If the first
+ * exception added is an Error or Exception, then it is throw with
+ * any additional exceptions added as suppressed. Otherwise a MultiException
+ * is thrown, with all exceptions added as suppressed.
+ *
+ * @throws Exception the Error or Exception if at least one is added.
+ */
+ public void ifExceptionThrowSuppressed()
+ throws Exception
+ {
+ if (nested == null || nested.size() == 0)
+ return;
+
+ Throwable th = nested.get(0);
+ if (!(th instanceof Error) && !(th instanceof Exception))
+ th = new MultiException(Collections.emptyList());
+
+ for (Throwable s : nested)
+ {
+ if (s != th)
+ th.addSuppressed(s);
+ }
+ if (th instanceof Error)
+ throw (Error)th;
+ throw (Exception)th;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder str = new StringBuilder();
+ str.append(MultiException.class.getSimpleName());
+ if ((nested == null) || (nested.size() <= 0))
+ {
+ str.append("[]");
+ }
+ else
+ {
+ str.append(nested);
+ }
+ return str.toString();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiMap.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiMap.java
new file mode 100644
index 0000000..ae77531
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiMap.java
@@ -0,0 +1,391 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A multi valued Map.
+ *
+ * @param <V> the entry type for multimap values
+ */
+@SuppressWarnings("serial")
+public class MultiMap<V> extends HashMap<String, List<V>>
+{
+ public MultiMap()
+ {
+ super();
+ }
+
+ public MultiMap(Map<String, List<V>> map)
+ {
+ super(map);
+ }
+
+ public MultiMap(MultiMap<V> map)
+ {
+ super(map);
+ }
+
+ /**
+ * Get multiple values.
+ * Single valued entries are converted to singleton lists.
+ *
+ * @param name The entry key.
+ * @return Unmodifieable List of values.
+ */
+ public List<V> getValues(String name)
+ {
+ List<V> vals = super.get(name);
+ if ((vals == null) || vals.isEmpty())
+ {
+ return null;
+ }
+ return vals;
+ }
+
+ /**
+ * Get a value from a multiple value.
+ * If the value is not a multivalue, then index 0 retrieves the
+ * value or null.
+ *
+ * @param name The entry key.
+ * @param i Index of element to get.
+ * @return Unmodifieable List of values.
+ */
+ public V getValue(String name, int i)
+ {
+ List<V> vals = getValues(name);
+ if (vals == null)
+ {
+ return null;
+ }
+ if (i == 0 && vals.isEmpty())
+ {
+ return null;
+ }
+ return vals.get(i);
+ }
+
+ /**
+ * Get value as String.
+ * Single valued items are converted to a String with the toString()
+ * Object method. Multi valued entries are converted to a comma separated
+ * List. No quoting of commas within values is performed.
+ *
+ * @param name The entry key.
+ * @return String value.
+ */
+ public String getString(String name)
+ {
+ List<V> vals = get(name);
+ if ((vals == null) || (vals.isEmpty()))
+ {
+ return null;
+ }
+
+ if (vals.size() == 1)
+ {
+ // simple form.
+ return vals.get(0).toString();
+ }
+
+ // delimited form
+ StringBuilder values = new StringBuilder(128);
+ for (V e : vals)
+ {
+ if (e != null)
+ {
+ if (values.length() > 0)
+ values.append(',');
+ values.append(e.toString());
+ }
+ }
+ return values.toString();
+ }
+
+ /**
+ * Put multi valued entry.
+ *
+ * @param name The entry key.
+ * @param value The simple value
+ * @return The previous value or null.
+ */
+ public List<V> put(String name, V value)
+ {
+ if (value == null)
+ {
+ return super.put(name, null);
+ }
+ List<V> vals = new ArrayList<>();
+ vals.add(value);
+ return put(name, vals);
+ }
+
+ /**
+ * Shorthand version of putAll
+ *
+ * @param input the input map
+ */
+ public void putAllValues(Map<String, V> input)
+ {
+ for (Map.Entry<String, V> entry : input.entrySet())
+ {
+ put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Put multi valued entry.
+ *
+ * @param name The entry key.
+ * @param values The List of multiple values.
+ * @return The previous value or null.
+ */
+ public List<V> putValues(String name, List<V> values)
+ {
+ return super.put(name, values);
+ }
+
+ /**
+ * Put multi valued entry.
+ *
+ * @param name The entry key.
+ * @param values The array of multiple values.
+ * @return The previous value or null.
+ */
+ @SafeVarargs
+ public final List<V> putValues(String name, V... values)
+ {
+ List<V> list = new ArrayList<>();
+ list.addAll(Arrays.asList(values));
+ return super.put(name, list);
+ }
+
+ /**
+ * Add value to multi valued entry.
+ * If the entry is single valued, it is converted to the first
+ * value of a multi valued entry.
+ *
+ * @param name The entry key.
+ * @param value The entry value.
+ */
+ public void add(String name, V value)
+ {
+ List<V> lo = get(name);
+ if (lo == null)
+ {
+ lo = new ArrayList<>();
+ }
+ lo.add(value);
+ super.put(name, lo);
+ }
+
+ /**
+ * Add values to multi valued entry.
+ * If the entry is single valued, it is converted to the first
+ * value of a multi valued entry.
+ *
+ * @param name The entry key.
+ * @param values The List of multiple values.
+ */
+ public void addValues(String name, List<V> values)
+ {
+ List<V> lo = get(name);
+ if (lo == null)
+ {
+ lo = new ArrayList<>();
+ }
+ lo.addAll(values);
+ put(name, lo);
+ }
+
+ /**
+ * Add values to multi valued entry.
+ * If the entry is single valued, it is converted to the first
+ * value of a multi valued entry.
+ *
+ * @param name The entry key.
+ * @param values The String array of multiple values.
+ */
+ public void addValues(String name, V[] values)
+ {
+ List<V> lo = get(name);
+ if (lo == null)
+ {
+ lo = new ArrayList<>();
+ }
+ lo.addAll(Arrays.asList(values));
+ put(name, lo);
+ }
+
+ /**
+ * Merge values.
+ *
+ * @param map the map to overlay on top of this one, merging together values if needed.
+ * @return true if an existing key was merged with potentially new values, false if either no change was made, or there were only new keys.
+ */
+ public boolean addAllValues(MultiMap<V> map)
+ {
+ boolean merged = false;
+
+ if ((map == null) || (map.isEmpty()))
+ {
+ // done
+ return merged;
+ }
+
+ for (Map.Entry<String, List<V>> entry : map.entrySet())
+ {
+ String name = entry.getKey();
+ List<V> values = entry.getValue();
+
+ if (this.containsKey(name))
+ {
+ merged = true;
+ }
+
+ this.addValues(name, values);
+ }
+
+ return merged;
+ }
+
+ /**
+ * Remove value.
+ *
+ * @param name The entry key.
+ * @param value The entry value.
+ * @return true if it was removed.
+ */
+ public boolean removeValue(String name, V value)
+ {
+ List<V> lo = get(name);
+ if ((lo == null) || (lo.isEmpty()))
+ {
+ return false;
+ }
+ boolean ret = lo.remove(value);
+ if (lo.isEmpty())
+ {
+ remove(name);
+ }
+ else
+ {
+ put(name, lo);
+ }
+ return ret;
+ }
+
+ /**
+ * Test for a specific single value in the map.
+ * <p>
+ * NOTE: This is a SLOW operation, and is actively discouraged.
+ *
+ * @param value the value to search for
+ * @return true if contains simple value
+ */
+ public boolean containsSimpleValue(V value)
+ {
+ for (List<V> vals : values())
+ {
+ if ((vals.size() == 1) && vals.contains(value))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString()
+ {
+ Iterator<Entry<String, List<V>>> iter = entrySet().iterator();
+ StringBuilder sb = new StringBuilder();
+ sb.append('{');
+ boolean delim = false;
+ while (iter.hasNext())
+ {
+ Entry<String, List<V>> e = iter.next();
+ if (delim)
+ {
+ sb.append(", ");
+ }
+ String key = e.getKey();
+ List<V> vals = e.getValue();
+ sb.append(key);
+ sb.append('=');
+ if (vals.size() == 1)
+ {
+ sb.append(vals.get(0));
+ }
+ else
+ {
+ sb.append(vals);
+ }
+ delim = true;
+ }
+ sb.append('}');
+ return sb.toString();
+ }
+
+ /**
+ * @return Map of String arrays
+ */
+ public Map<String, String[]> toStringArrayMap()
+ {
+ HashMap<String, String[]> map = new HashMap<String, String[]>(size() * 3 / 2)
+ {
+ @Override
+ public String toString()
+ {
+ StringBuilder b = new StringBuilder();
+ b.append('{');
+ for (String k : super.keySet())
+ {
+ if (b.length() > 1)
+ b.append(',');
+ b.append(k);
+ b.append('=');
+ b.append(Arrays.asList(super.get(k)));
+ }
+
+ b.append('}');
+ return b.toString();
+ }
+ };
+
+ for (Map.Entry<String, List<V>> entry : entrySet())
+ {
+ String[] a = null;
+ if (entry.getValue() != null)
+ {
+ a = new String[entry.getValue().size()];
+ a = entry.getValue().toArray(a);
+ }
+ map.put(entry.getKey(), a);
+ }
+ return map;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java
new file mode 100644
index 0000000..cb4017d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java
@@ -0,0 +1,966 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Locale;
+import javax.servlet.MultipartConfigElement;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.Part;
+
+import org.eclipse.jetty.util.ReadLineInputStream.Termination;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * MultiPartInputStream
+ *
+ * Handle a MultiPart Mime input stream, breaking it up on the boundary into files and strings.
+ *
+ * Non Compliance warnings are documented by the method {@link #getNonComplianceWarnings()}
+ *
+ * @deprecated Replaced by org.eclipse.jetty.http.MultiPartFormInputStream
+ * The code for MultiPartInputStream is slower than its replacement MultiPartFormInputStream. However
+ * this class accepts formats non compliant the RFC that the new MultiPartFormInputStream does not accept.
+ */
+@Deprecated
+public class MultiPartInputStreamParser
+{
+ private static final Logger LOG = Log.getLogger(MultiPartInputStreamParser.class);
+ public static final MultipartConfigElement __DEFAULT_MULTIPART_CONFIG = new MultipartConfigElement(System.getProperty("java.io.tmpdir"));
+ public static final MultiMap<Part> EMPTY_MAP = new MultiMap(Collections.emptyMap());
+ protected InputStream _in;
+ protected MultipartConfigElement _config;
+ protected String _contentType;
+ protected MultiMap<Part> _parts;
+ protected Exception _err;
+ protected File _tmpDir;
+ protected File _contextTmpDir;
+ protected boolean _writeFilesWithFilenames;
+ protected boolean _parsed;
+
+ private EnumSet<NonCompliance> nonComplianceWarnings = EnumSet.noneOf(NonCompliance.class);
+
+ public enum NonCompliance
+ {
+ CR_LINE_TERMINATION("https://tools.ietf.org/html/rfc2046#section-4.1.1"),
+ LF_LINE_TERMINATION("https://tools.ietf.org/html/rfc2046#section-4.1.1"),
+ NO_CRLF_AFTER_PREAMBLE("https://tools.ietf.org/html/rfc2046#section-5.1.1"),
+ BASE64_TRANSFER_ENCODING("https://tools.ietf.org/html/rfc7578#section-4.7"),
+ QUOTED_PRINTABLE_TRANSFER_ENCODING("https://tools.ietf.org/html/rfc7578#section-4.7");
+
+ final String _rfcRef;
+
+ NonCompliance(String rfcRef)
+ {
+ _rfcRef = rfcRef;
+ }
+
+ public String getURL()
+ {
+ return _rfcRef;
+ }
+ }
+
+ /**
+ * @return an EnumSet of non compliances with the RFC that were accepted by this parser
+ */
+ public EnumSet<NonCompliance> getNonComplianceWarnings()
+ {
+ return nonComplianceWarnings;
+ }
+
+ public class MultiPart implements Part
+ {
+ protected String _name;
+ protected String _filename;
+ protected File _file;
+ protected OutputStream _out;
+ protected ByteArrayOutputStream2 _bout;
+ protected String _contentType;
+ protected MultiMap<String> _headers;
+ protected long _size = 0;
+ protected boolean _temporary = true;
+
+ public MultiPart(String name, String filename)
+ throws IOException
+ {
+ _name = name;
+ _filename = filename;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("Part{n=%s,fn=%s,ct=%s,s=%d,t=%b,f=%s}", _name, _filename, _contentType, _size, _temporary, _file);
+ }
+
+ protected void setContentType(String contentType)
+ {
+ _contentType = contentType;
+ }
+
+ protected void open()
+ throws IOException
+ {
+ //We will either be writing to a file, if it has a filename on the content-disposition
+ //and otherwise a byte-array-input-stream, OR if we exceed the getFileSizeThreshold, we
+ //will need to change to write to a file.
+ if (isWriteFilesWithFilenames() && _filename != null && _filename.trim().length() > 0)
+ {
+ createFile();
+ }
+ else
+ {
+ //Write to a buffer in memory until we discover we've exceed the
+ //MultipartConfig fileSizeThreshold
+ _out = _bout = new ByteArrayOutputStream2();
+ }
+ }
+
+ protected void close()
+ throws IOException
+ {
+ _out.close();
+ }
+
+ protected void write(int b)
+ throws IOException
+ {
+ if (MultiPartInputStreamParser.this._config.getMaxFileSize() > 0 && _size + 1 > MultiPartInputStreamParser.this._config.getMaxFileSize())
+ throw new IllegalStateException("Multipart Mime part " + _name + " exceeds max filesize");
+
+ if (MultiPartInputStreamParser.this._config.getFileSizeThreshold() > 0 && _size + 1 > MultiPartInputStreamParser.this._config.getFileSizeThreshold() && _file == null)
+ createFile();
+
+ _out.write(b);
+ _size++;
+ }
+
+ protected void write(byte[] bytes, int offset, int length)
+ throws IOException
+ {
+ if (MultiPartInputStreamParser.this._config.getMaxFileSize() > 0 && _size + length > MultiPartInputStreamParser.this._config.getMaxFileSize())
+ throw new IllegalStateException("Multipart Mime part " + _name + " exceeds max filesize");
+
+ if (MultiPartInputStreamParser.this._config.getFileSizeThreshold() > 0 && _size + length > MultiPartInputStreamParser.this._config.getFileSizeThreshold() && _file == null)
+ createFile();
+
+ _out.write(bytes, offset, length);
+ _size += length;
+ }
+
+ protected void createFile()
+ throws IOException
+ {
+ Path parent = MultiPartInputStreamParser.this._tmpDir.toPath();
+ Path tempFile = Files.createTempFile(parent, "MultiPart", "");
+ _file = tempFile.toFile();
+
+ OutputStream fos = Files.newOutputStream(tempFile, StandardOpenOption.WRITE);
+ BufferedOutputStream bos = new BufferedOutputStream(fos);
+
+ if (_size > 0 && _out != null)
+ {
+ //already written some bytes, so need to copy them into the file
+ _out.flush();
+ _bout.writeTo(bos);
+ _out.close();
+ }
+ _bout = null;
+ _out = bos;
+ }
+
+ protected void setHeaders(MultiMap<String> headers)
+ {
+ _headers = headers;
+ }
+
+ /**
+ * @see javax.servlet.http.Part#getContentType()
+ */
+ @Override
+ public String getContentType()
+ {
+ return _contentType;
+ }
+
+ /**
+ * @see javax.servlet.http.Part#getHeader(java.lang.String)
+ */
+ @Override
+ public String getHeader(String name)
+ {
+ if (name == null)
+ return null;
+ return _headers.getValue(name.toLowerCase(Locale.ENGLISH), 0);
+ }
+
+ /**
+ * @see javax.servlet.http.Part#getHeaderNames()
+ */
+ @Override
+ public Collection<String> getHeaderNames()
+ {
+ return _headers.keySet();
+ }
+
+ /**
+ * @see javax.servlet.http.Part#getHeaders(java.lang.String)
+ */
+ @Override
+ public Collection<String> getHeaders(String name)
+ {
+ return _headers.getValues(name);
+ }
+
+ /**
+ * @see javax.servlet.http.Part#getInputStream()
+ */
+ @Override
+ public InputStream getInputStream() throws IOException
+ {
+ if (_file != null)
+ {
+ //written to a file, whether temporary or not
+ return new BufferedInputStream(new FileInputStream(_file));
+ }
+ else
+ {
+ //part content is in memory
+ return new ByteArrayInputStream(_bout.getBuf(), 0, _bout.size());
+ }
+ }
+
+ /**
+ * @see javax.servlet.http.Part#getSubmittedFileName()
+ */
+ @Override
+ public String getSubmittedFileName()
+ {
+ return getContentDispositionFilename();
+ }
+
+ public byte[] getBytes()
+ {
+ if (_bout != null)
+ return _bout.toByteArray();
+ return null;
+ }
+
+ /**
+ * @see javax.servlet.http.Part#getName()
+ */
+ @Override
+ public String getName()
+ {
+ return _name;
+ }
+
+ /**
+ * @see javax.servlet.http.Part#getSize()
+ */
+ @Override
+ public long getSize()
+ {
+ return _size;
+ }
+
+ /**
+ * @see javax.servlet.http.Part#write(java.lang.String)
+ */
+ @Override
+ public void write(String fileName) throws IOException
+ {
+ if (_file == null)
+ {
+ _temporary = false;
+
+ //part data is only in the ByteArrayOutputStream and never been written to disk
+ _file = new File(_tmpDir, fileName);
+
+ BufferedOutputStream bos = null;
+ try
+ {
+ bos = new BufferedOutputStream(new FileOutputStream(_file));
+ _bout.writeTo(bos);
+ bos.flush();
+ }
+ finally
+ {
+ if (bos != null)
+ bos.close();
+ _bout = null;
+ }
+ }
+ else
+ {
+ //the part data is already written to a temporary file, just rename it
+ _temporary = false;
+
+ Path src = _file.toPath();
+ Path target = src.resolveSibling(fileName);
+ Files.move(src, target, StandardCopyOption.REPLACE_EXISTING);
+ _file = target.toFile();
+ }
+ }
+
+ /**
+ * Remove the file, whether or not Part.write() was called on it
+ * (ie no longer temporary)
+ *
+ * @see javax.servlet.http.Part#delete()
+ */
+ @Override
+ public void delete() throws IOException
+ {
+ if (_file != null && _file.exists())
+ _file.delete();
+ }
+
+ /**
+ * Only remove tmp files.
+ *
+ * @throws IOException if unable to delete the file
+ */
+ public void cleanUp() throws IOException
+ {
+ if (_temporary && _file != null && _file.exists())
+ _file.delete();
+ }
+
+ /**
+ * Get the file
+ *
+ * @return the file, if any, the data has been written to.
+ */
+ public File getFile()
+ {
+ return _file;
+ }
+
+ /**
+ * Get the filename from the content-disposition.
+ *
+ * @return null or the filename
+ */
+ public String getContentDispositionFilename()
+ {
+ return _filename;
+ }
+ }
+
+ /**
+ * @param in Request input stream
+ * @param contentType Content-Type header
+ * @param config MultipartConfigElement
+ * @param contextTmpDir javax.servlet.context.tempdir
+ */
+ public MultiPartInputStreamParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
+ {
+ _contentType = contentType;
+ _config = config;
+ _contextTmpDir = contextTmpDir;
+ if (_contextTmpDir == null)
+ _contextTmpDir = new File(System.getProperty("java.io.tmpdir"));
+
+ if (_config == null)
+ _config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
+
+ if (in instanceof ServletInputStream)
+ {
+ if (((ServletInputStream)in).isFinished())
+ {
+ _parts = EMPTY_MAP;
+ _parsed = true;
+ return;
+ }
+ }
+ _in = new ReadLineInputStream(in);
+ }
+
+ /**
+ * Get the already parsed parts.
+ *
+ * @return the parts that were parsed
+ */
+ public Collection<Part> getParsedParts()
+ {
+ if (_parts == null)
+ return Collections.emptyList();
+
+ Collection<List<Part>> values = _parts.values();
+ List<Part> parts = new ArrayList<>();
+ for (List<Part> o : values)
+ {
+ List<Part> asList = LazyList.getList(o, false);
+ parts.addAll(asList);
+ }
+ return parts;
+ }
+
+ /**
+ * Delete any tmp storage for parts, and clear out the parts list.
+ */
+ public void deleteParts()
+ {
+ if (!_parsed)
+ return;
+
+ Collection<Part> parts = getParsedParts();
+ MultiException err = new MultiException();
+ for (Part p : parts)
+ {
+ try
+ {
+ ((MultiPartInputStreamParser.MultiPart)p).cleanUp();
+ }
+ catch (Exception e)
+ {
+ err.add(e);
+ }
+ }
+ _parts.clear();
+
+ err.ifExceptionThrowRuntime();
+ }
+
+ /**
+ * Parse, if necessary, the multipart data and return the list of Parts.
+ *
+ * @return the parts
+ * @throws IOException if unable to get the parts
+ */
+ public Collection<Part> getParts()
+ throws IOException
+ {
+ if (!_parsed)
+ parse();
+ throwIfError();
+
+ Collection<List<Part>> values = _parts.values();
+ List<Part> parts = new ArrayList<>();
+ for (List<Part> o : values)
+ {
+ List<Part> asList = LazyList.getList(o, false);
+ parts.addAll(asList);
+ }
+ return parts;
+ }
+
+ /**
+ * Get the named Part.
+ *
+ * @param name the part name
+ * @return the parts
+ * @throws IOException if unable to get the part
+ */
+ public Part getPart(String name)
+ throws IOException
+ {
+ if (!_parsed)
+ parse();
+ throwIfError();
+ return _parts.getValue(name, 0);
+ }
+
+ /**
+ * Throws an exception if one has been latched.
+ *
+ * @throws IOException the exception (if present)
+ */
+ protected void throwIfError()
+ throws IOException
+ {
+ if (_err != null)
+ {
+ if (_err instanceof IOException)
+ throw (IOException)_err;
+ if (_err instanceof IllegalStateException)
+ throw (IllegalStateException)_err;
+ throw new IllegalStateException(_err);
+ }
+ }
+
+ /**
+ * Parse, if necessary, the multipart stream.
+ */
+ protected void parse()
+ {
+ //have we already parsed the input?
+ if (_parsed)
+ return;
+ _parsed = true;
+
+ //initialize
+ long total = 0; //keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize
+ _parts = new MultiMap<>();
+
+ //if its not a multipart request, don't parse it
+ if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
+ return;
+
+ try
+ {
+ //sort out the location to which to write the files
+
+ if (_config.getLocation() == null)
+ _tmpDir = _contextTmpDir;
+ else if ("".equals(_config.getLocation()))
+ _tmpDir = _contextTmpDir;
+ else
+ {
+ File f = new File(_config.getLocation());
+ if (f.isAbsolute())
+ _tmpDir = f;
+ else
+ _tmpDir = new File(_contextTmpDir, _config.getLocation());
+ }
+
+ if (!_tmpDir.exists())
+ _tmpDir.mkdirs();
+
+ String contentTypeBoundary = "";
+ int bstart = _contentType.indexOf("boundary=");
+ if (bstart >= 0)
+ {
+ int bend = _contentType.indexOf(";", bstart);
+ bend = (bend < 0 ? _contentType.length() : bend);
+ contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart, bend)).trim());
+ }
+
+ String boundary = "--" + contentTypeBoundary;
+ String lastBoundary = boundary + "--";
+ byte[] byteBoundary = lastBoundary.getBytes(StandardCharsets.ISO_8859_1);
+
+ // Get first boundary
+ String line = null;
+ try
+ {
+ line = ((ReadLineInputStream)_in).readLine();
+ }
+ catch (IOException e)
+ {
+ LOG.warn("Badly formatted multipart request");
+ throw e;
+ }
+
+ if (line == null)
+ throw new IOException("Missing content for multipart request");
+
+ boolean badFormatLogged = false;
+
+ String untrimmed = line;
+ line = line.trim();
+ while (line != null && !line.equals(boundary) && !line.equals(lastBoundary))
+ {
+ if (!badFormatLogged)
+ {
+ LOG.warn("Badly formatted multipart request");
+ badFormatLogged = true;
+ }
+ line = ((ReadLineInputStream)_in).readLine();
+ untrimmed = line;
+ if (line != null)
+ line = line.trim();
+ }
+
+ if (line == null || line.length() == 0)
+ throw new IOException("Missing initial multi part boundary");
+
+ // Empty multipart.
+ if (line.equals(lastBoundary))
+ return;
+
+ // check compliance of preamble
+ if (Character.isWhitespace(untrimmed.charAt(0)))
+ nonComplianceWarnings.add(NonCompliance.NO_CRLF_AFTER_PREAMBLE);
+
+ // Read each part
+ boolean lastPart = false;
+
+ outer:
+ while (!lastPart)
+ {
+ String contentDisposition = null;
+ String contentType = null;
+ String contentTransferEncoding = null;
+
+ MultiMap<String> headers = new MultiMap<>();
+ while (true)
+ {
+ line = ((ReadLineInputStream)_in).readLine();
+
+ //No more input
+ if (line == null)
+ break outer;
+
+ //end of headers:
+ if ("".equals(line))
+ break;
+
+ total += line.length();
+ if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
+ throw new IllegalStateException("Request exceeds maxRequestSize (" + _config.getMaxRequestSize() + ")");
+
+ //get content-disposition and content-type
+ int c = line.indexOf(':');
+ if (c > 0)
+ {
+ String key = line.substring(0, c).trim().toLowerCase(Locale.ENGLISH);
+ String value = line.substring(c + 1).trim();
+ headers.put(key, value);
+ if (key.equalsIgnoreCase("content-disposition"))
+ contentDisposition = value;
+ if (key.equalsIgnoreCase("content-type"))
+ contentType = value;
+ if (key.equals("content-transfer-encoding"))
+ contentTransferEncoding = value;
+ }
+ }
+
+ // Extract content-disposition
+ boolean formData = false;
+ if (contentDisposition == null)
+ {
+ throw new IOException("Missing content-disposition");
+ }
+
+ QuotedStringTokenizer tok = new QuotedStringTokenizer(contentDisposition, ";", false, true);
+ String name = null;
+ String filename = null;
+ while (tok.hasMoreTokens())
+ {
+ String t = tok.nextToken().trim();
+ String tl = t.toLowerCase(Locale.ENGLISH);
+ if (tl.startsWith("form-data"))
+ formData = true;
+ else if (tl.startsWith("name="))
+ name = value(t);
+ else if (tl.startsWith("filename="))
+ filename = filenameValue(t);
+ }
+
+ // Check disposition
+ if (!formData)
+ {
+ continue;
+ }
+ //It is valid for reset and submit buttons to have an empty name.
+ //If no name is supplied, the browser skips sending the info for that field.
+ //However, if you supply the empty string as the name, the browser sends the
+ //field, with name as the empty string. So, only continue this loop if we
+ //have not yet seen a name field.
+ if (name == null)
+ {
+ continue;
+ }
+
+ //Have a new Part
+ MultiPart part = new MultiPart(name, filename);
+ part.setHeaders(headers);
+ part.setContentType(contentType);
+ _parts.add(name, part);
+ part.open();
+
+ InputStream partInput = null;
+ if ("base64".equalsIgnoreCase(contentTransferEncoding))
+ {
+ nonComplianceWarnings.add(NonCompliance.BASE64_TRANSFER_ENCODING);
+ partInput = new Base64InputStream((ReadLineInputStream)_in);
+ }
+ else if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding))
+ {
+ nonComplianceWarnings.add(NonCompliance.QUOTED_PRINTABLE_TRANSFER_ENCODING);
+ partInput = new FilterInputStream(_in)
+ {
+ @Override
+ public int read() throws IOException
+ {
+ int c = in.read();
+ if (c >= 0 && c == '=')
+ {
+ int hi = in.read();
+ int lo = in.read();
+ if (hi < 0 || lo < 0)
+ {
+ throw new IOException("Unexpected end to quoted-printable byte");
+ }
+ char[] chars = new char[]{(char)hi, (char)lo};
+ c = Integer.parseInt(new String(chars), 16);
+ }
+ return c;
+ }
+ };
+ }
+ else
+ partInput = _in;
+
+ try
+ {
+ int state = -2;
+ int c;
+ boolean cr = false;
+ boolean lf = false;
+
+ // loop for all lines
+ while (true)
+ {
+ int b = 0;
+ while ((c = (state != -2) ? state : partInput.read()) != -1)
+ {
+ total++;
+ if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
+ throw new IllegalStateException("Request exceeds maxRequestSize (" + _config.getMaxRequestSize() + ")");
+
+ state = -2;
+
+ // look for CR and/or LF
+ if (c == 13 || c == 10)
+ {
+ if (c == 13)
+ {
+ partInput.mark(1);
+ int tmp = partInput.read();
+ if (tmp != 10)
+ partInput.reset();
+ else
+ state = tmp;
+ }
+ break;
+ }
+
+ // Look for boundary
+ if (b >= 0 && b < byteBoundary.length && c == byteBoundary[b])
+ {
+ b++;
+ }
+ else
+ {
+ // Got a character not part of the boundary, so we don't have the boundary marker.
+ // Write out as many chars as we matched, then the char we're looking at.
+ if (cr)
+ part.write(13);
+
+ if (lf)
+ part.write(10);
+
+ cr = lf = false;
+ if (b > 0)
+ part.write(byteBoundary, 0, b);
+
+ b = -1;
+ part.write(c);
+ }
+ }
+
+ // Check for incomplete boundary match, writing out the chars we matched along the way
+ if ((b > 0 && b < byteBoundary.length - 2) || (b == byteBoundary.length - 1))
+ {
+ if (cr)
+ part.write(13);
+
+ if (lf)
+ part.write(10);
+
+ cr = lf = false;
+ part.write(byteBoundary, 0, b);
+ b = -1;
+ }
+
+ // Boundary match. If we've run out of input or we matched the entire final boundary marker, then this is the last part.
+ if (b > 0 || c == -1)
+ {
+
+ if (b == byteBoundary.length)
+ lastPart = true;
+ if (state == 10)
+ state = -2;
+ break;
+ }
+
+ // handle CR LF
+ if (cr)
+ part.write(13);
+
+ if (lf)
+ part.write(10);
+
+ cr = (c == 13);
+ lf = (c == 10 || state == 10);
+ if (state == 10)
+ state = -2;
+ }
+ }
+ finally
+ {
+ part.close();
+ }
+ }
+ if (lastPart)
+ {
+ while (line != null)
+ {
+ line = ((ReadLineInputStream)_in).readLine();
+ }
+
+ EnumSet<Termination> term = ((ReadLineInputStream)_in).getLineTerminations();
+
+ if (term.contains(Termination.CR))
+ nonComplianceWarnings.add(NonCompliance.CR_LINE_TERMINATION);
+ if (term.contains(Termination.LF))
+ nonComplianceWarnings.add(NonCompliance.LF_LINE_TERMINATION);
+ }
+ else
+ throw new IOException("Incomplete parts");
+ }
+ catch (Exception e)
+ {
+ _err = e;
+ }
+ }
+
+ /**
+ * @deprecated no replacement offered.
+ */
+ @Deprecated
+ public void setDeleteOnExit(boolean deleteOnExit)
+ {
+ // does nothing
+ }
+
+ public void setWriteFilesWithFilenames(boolean writeFilesWithFilenames)
+ {
+ _writeFilesWithFilenames = writeFilesWithFilenames;
+ }
+
+ public boolean isWriteFilesWithFilenames()
+ {
+ return _writeFilesWithFilenames;
+ }
+
+ /**
+ * @deprecated no replacement offered.
+ */
+ @Deprecated
+ public boolean isDeleteOnExit()
+ {
+ return false;
+ }
+
+ private String value(String nameEqualsValue)
+ {
+ int idx = nameEqualsValue.indexOf('=');
+ String value = nameEqualsValue.substring(idx + 1).trim();
+ return QuotedStringTokenizer.unquoteOnly(value);
+ }
+
+ private String filenameValue(String nameEqualsValue)
+ {
+ int idx = nameEqualsValue.indexOf('=');
+ String value = nameEqualsValue.substring(idx + 1).trim();
+
+ if (value.matches(".??[a-z,A-Z]\\:\\\\[^\\\\].*"))
+ {
+ //incorrectly escaped IE filenames that have the whole path
+ //we just strip any leading & trailing quotes and leave it as is
+ char first = value.charAt(0);
+ if (first == '"' || first == '\'')
+ value = value.substring(1);
+ char last = value.charAt(value.length() - 1);
+ if (last == '"' || last == '\'')
+ value = value.substring(0, value.length() - 1);
+
+ return value;
+ }
+ else
+ //unquote the string, but allow any backslashes that don't
+ //form a valid escape sequence to remain as many browsers
+ //even on *nix systems will not escape a filename containing
+ //backslashes
+ return QuotedStringTokenizer.unquoteOnly(value, true);
+ }
+
+ // TODO: considers switching to Base64.getMimeDecoder().wrap(InputStream)
+ private static class Base64InputStream extends InputStream
+ {
+ ReadLineInputStream _in;
+ String _line;
+ byte[] _buffer;
+ int _pos;
+ Base64.Decoder base64Decoder = Base64.getDecoder();
+
+ public Base64InputStream(ReadLineInputStream rlis)
+ {
+ _in = rlis;
+ }
+
+ @Override
+ public int read() throws IOException
+ {
+ if (_buffer == null || _pos >= _buffer.length)
+ {
+ //Any CR and LF will be consumed by the readLine() call.
+ //We need to put them back into the bytes returned from this
+ //method because the parsing of the multipart content uses them
+ //as markers to determine when we've reached the end of a part.
+ _line = _in.readLine();
+ if (_line == null)
+ return -1; //nothing left
+ if (_line.startsWith("--"))
+ _buffer = (_line + "\r\n").getBytes(); //boundary marking end of part
+ else if (_line.length() == 0)
+ _buffer = "\r\n".getBytes(); //blank line
+ else
+ {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream((4 * _line.length() / 3) + 2);
+ baos.write(base64Decoder.decode(_line));
+ baos.write(13);
+ baos.write(10);
+ _buffer = baos.toByteArray();
+ }
+
+ _pos = 0;
+ }
+
+ return _buffer[_pos++];
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartOutputStream.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartOutputStream.java
new file mode 100644
index 0000000..8f9313a
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartOutputStream.java
@@ -0,0 +1,156 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Handle a multipart MIME response.
+ */
+public class MultiPartOutputStream extends FilterOutputStream
+{
+
+ private static final byte[] CRLF = {'\r', '\n'};
+ private static final byte[] DASHDASH = {'-', '-'};
+
+ public static final String MULTIPART_MIXED = "multipart/mixed";
+ public static final String MULTIPART_X_MIXED_REPLACE = "multipart/x-mixed-replace";
+
+ private final String boundary;
+ private final byte[] boundaryBytes;
+
+ private boolean inPart = false;
+
+ public MultiPartOutputStream(OutputStream out)
+ throws IOException
+ {
+ super(out);
+
+ boundary = "jetty" + System.identityHashCode(this) +
+ Long.toString(System.currentTimeMillis(), 36);
+ boundaryBytes = boundary.getBytes(StandardCharsets.ISO_8859_1);
+ }
+
+ public MultiPartOutputStream(OutputStream out, String boundary)
+ throws IOException
+ {
+ super(out);
+
+ this.boundary = boundary;
+ boundaryBytes = boundary.getBytes(StandardCharsets.ISO_8859_1);
+ }
+
+ /**
+ * End the current part.
+ *
+ * @throws IOException IOException
+ */
+ @Override
+ public void close()
+ throws IOException
+ {
+ try
+ {
+ if (inPart)
+ out.write(CRLF);
+ out.write(DASHDASH);
+ out.write(boundaryBytes);
+ out.write(DASHDASH);
+ out.write(CRLF);
+ inPart = false;
+ }
+ finally
+ {
+ super.close();
+ }
+ }
+
+ public String getBoundary()
+ {
+ return boundary;
+ }
+
+ public OutputStream getOut()
+ {
+ return out;
+ }
+
+ /**
+ * Start creation of the next Content.
+ *
+ * @param contentType the content type of the part
+ * @throws IOException if unable to write the part
+ */
+ public void startPart(String contentType)
+ throws IOException
+ {
+ if (inPart)
+ {
+ out.write(CRLF);
+ }
+ inPart = true;
+ out.write(DASHDASH);
+ out.write(boundaryBytes);
+ out.write(CRLF);
+ if (contentType != null)
+ {
+ out.write(("Content-Type: " + contentType).getBytes(StandardCharsets.ISO_8859_1));
+ out.write(CRLF);
+ }
+ out.write(CRLF);
+ }
+
+ /**
+ * Start creation of the next Content.
+ *
+ * @param contentType the content type of the part
+ * @param headers the part headers
+ * @throws IOException if unable to write the part
+ */
+ public void startPart(String contentType, String[] headers)
+ throws IOException
+ {
+ if (inPart)
+ out.write(CRLF);
+ inPart = true;
+ out.write(DASHDASH);
+ out.write(boundaryBytes);
+ out.write(CRLF);
+ if (contentType != null)
+ {
+ out.write(("Content-Type: " + contentType).getBytes(StandardCharsets.ISO_8859_1));
+ out.write(CRLF);
+ }
+ for (int i = 0; headers != null && i < headers.length; i++)
+ {
+ out.write(headers[i].getBytes(StandardCharsets.ISO_8859_1));
+ out.write(CRLF);
+ }
+ out.write(CRLF);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException
+ {
+ out.write(b, off, len);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartWriter.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartWriter.java
new file mode 100644
index 0000000..be70fcc
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartWriter.java
@@ -0,0 +1,145 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.FilterWriter;
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Handle a multipart MIME response.
+ */
+public class MultiPartWriter extends FilterWriter
+{
+
+ private static final String CRLF = IO.CRLF;
+ private static final String DASHDASH = "--";
+
+ public static final String MULTIPART_MIXED = MultiPartOutputStream.MULTIPART_MIXED;
+ public static final String MULTIPART_X_MIXED_REPLACE = MultiPartOutputStream.MULTIPART_X_MIXED_REPLACE;
+
+ private String boundary;
+
+ private boolean inPart = false;
+
+ public MultiPartWriter(Writer out)
+ throws IOException
+ {
+ super(out);
+ boundary = "jetty" + System.identityHashCode(this) +
+ Long.toString(System.currentTimeMillis(), 36);
+
+ inPart = false;
+ }
+
+ /**
+ * End the current part.
+ *
+ * @throws IOException IOException
+ */
+ @Override
+ public void close()
+ throws IOException
+ {
+ try
+ {
+ if (inPart)
+ out.write(CRLF);
+ out.write(DASHDASH);
+ out.write(boundary);
+ out.write(DASHDASH);
+ out.write(CRLF);
+ inPart = false;
+ }
+ finally
+ {
+ super.close();
+ }
+ }
+
+ public String getBoundary()
+ {
+ return boundary;
+ }
+
+ /**
+ * Start creation of the next Content.
+ *
+ * @param contentType the content type
+ * @throws IOException if unable to write the part
+ */
+ public void startPart(String contentType)
+ throws IOException
+ {
+ if (inPart)
+ out.write(CRLF);
+ out.write(DASHDASH);
+ out.write(boundary);
+ out.write(CRLF);
+ out.write("Content-Type: ");
+ out.write(contentType);
+ out.write(CRLF);
+ out.write(CRLF);
+ inPart = true;
+ }
+
+ /**
+ * end creation of the next Content.
+ *
+ * @throws IOException if unable to write the part
+ */
+ public void endPart()
+ throws IOException
+ {
+ if (inPart)
+ out.write(CRLF);
+ inPart = false;
+ }
+
+ /**
+ * Start creation of the next Content.
+ *
+ * @param contentType the content type of the part
+ * @param headers the part headers
+ * @throws IOException if unable to write the part
+ */
+ public void startPart(String contentType, String[] headers)
+ throws IOException
+ {
+ if (inPart)
+ out.write(CRLF);
+ out.write(DASHDASH);
+ out.write(boundary);
+ out.write(CRLF);
+ out.write("Content-Type: ");
+ out.write(contentType);
+ out.write(CRLF);
+ for (int i = 0; headers != null && i < headers.length; i++)
+ {
+ out.write(headers[i]);
+ out.write(CRLF);
+ }
+ out.write(CRLF);
+ inPart = true;
+ }
+}
+
+
+
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiReleaseJarFile.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiReleaseJarFile.java
new file mode 100644
index 0000000..ef8d85a
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/MultiReleaseJarFile.java
@@ -0,0 +1,260 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.stream.Stream;
+
+/**
+ * <p>Utility class to handle a Multi Release Jar file</p>
+ */
+public class MultiReleaseJarFile implements Closeable
+{
+ private static final String META_INF_VERSIONS = "META-INF/versions/";
+
+ private final JarFile jarFile;
+ private final int platform;
+ private final boolean multiRelease;
+
+ /* Map to hold unversioned name to VersionedJarEntry */
+ private final Map<String, VersionedJarEntry> entries;
+
+ /**
+ * Construct a multi release jar file for the current JVM version, ignoring directories.
+ *
+ * @param file The file to open
+ * @throws IOException if the jar file cannot be read
+ */
+ public MultiReleaseJarFile(File file) throws IOException
+ {
+ this(file, JavaVersion.VERSION.getPlatform(), false);
+ }
+
+ /**
+ * Construct a multi release jar file
+ *
+ * @param file The file to open
+ * @param javaPlatform The JVM platform to apply when selecting a version.
+ * @param includeDirectories true if any directory entries should not be ignored
+ * @throws IOException if the jar file cannot be read
+ */
+ public MultiReleaseJarFile(File file, int javaPlatform, boolean includeDirectories) throws IOException
+ {
+ if (file == null || !file.exists() || !file.canRead() || file.isDirectory())
+ throw new IllegalArgumentException("bad jar file: " + file);
+
+ jarFile = new JarFile(file, true, JarFile.OPEN_READ);
+ this.platform = javaPlatform;
+
+ Manifest manifest = jarFile.getManifest();
+ if (manifest == null)
+ multiRelease = false;
+ else
+ multiRelease = Boolean.parseBoolean(String.valueOf(manifest.getMainAttributes().getValue("Multi-Release")));
+
+ Map<String, VersionedJarEntry> map = new TreeMap<>();
+ jarFile.stream()
+ .map(VersionedJarEntry::new)
+ .filter(e -> (includeDirectories || !e.isDirectory()) && e.isApplicable())
+ .forEach(e -> map.compute(e.name, (k, v) -> v == null || v.isReplacedBy(e) ? e : v));
+
+ for (Iterator<Map.Entry<String, VersionedJarEntry>> i = map.entrySet().iterator(); i.hasNext(); )
+ {
+ Map.Entry<String, VersionedJarEntry> e = i.next();
+ VersionedJarEntry entry = e.getValue();
+ if (entry.inner)
+ {
+ VersionedJarEntry outer = entry.outer == null ? null : map.get(entry.outer);
+ if (outer == null || outer.version != entry.version)
+ i.remove();
+ }
+ }
+
+ entries = Collections.unmodifiableMap(map);
+ }
+
+ /**
+ * @return true IFF the jar is a multi release jar
+ */
+ public boolean isMultiRelease()
+ {
+ return multiRelease;
+ }
+
+ /**
+ * @return The major version applied to this jar for the purposes of selecting entries
+ */
+ public int getVersion()
+ {
+ return platform;
+ }
+
+ /**
+ * @return A stream of versioned entries from the jar, excluded any that are not applicable
+ */
+ public Stream<VersionedJarEntry> stream()
+ {
+ return entries.values().stream();
+ }
+
+ /**
+ * Get a versioned resource entry by name
+ *
+ * @param name The unversioned name of the resource
+ * @return The versioned entry of the resource
+ */
+ public VersionedJarEntry getEntry(String name)
+ {
+ return entries.get(name);
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ if (jarFile != null)
+ jarFile.close();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[%b,%d]", jarFile.getName(), isMultiRelease(), getVersion());
+ }
+
+ /**
+ * A versioned Jar entry
+ */
+ public class VersionedJarEntry
+ {
+ final JarEntry entry;
+ final String name;
+ final int version;
+ final boolean inner;
+ final String outer;
+
+ VersionedJarEntry(JarEntry entry)
+ {
+ int v = 0;
+ String name = entry.getName();
+ if (name.startsWith(META_INF_VERSIONS))
+ {
+ v = -1;
+ int index = name.indexOf('/', META_INF_VERSIONS.length());
+ if (index > META_INF_VERSIONS.length() && index < name.length())
+ {
+ try
+ {
+ v = TypeUtil.parseInt(name, META_INF_VERSIONS.length(), index - META_INF_VERSIONS.length(), 10);
+ name = name.substring(index + 1);
+ }
+ catch (NumberFormatException x)
+ {
+ throw new RuntimeException("illegal version in " + jarFile, x);
+ }
+ }
+ }
+
+ this.entry = entry;
+ this.name = name;
+ this.version = v;
+ this.inner = name.contains("$") && name.toLowerCase(Locale.ENGLISH).endsWith(".class");
+ this.outer = inner ? name.substring(0, name.indexOf('$')) + ".class" : null;
+ }
+
+ /**
+ * @return the unversioned name of the resource
+ */
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * @return The name of the resource within the jar, which could be versioned
+ */
+ public String getNameInJar()
+ {
+ return entry.getName();
+ }
+
+ /**
+ * @return The version of the resource or 0 for a base version
+ */
+ public int getVersion()
+ {
+ return version;
+ }
+
+ /**
+ * @return True iff the entry is not from the base version
+ */
+ public boolean isVersioned()
+ {
+ return version > 0;
+ }
+
+ /**
+ * @return True iff the entry is a directory
+ */
+ public boolean isDirectory()
+ {
+ return entry.isDirectory();
+ }
+
+ /**
+ * @return An input stream of the content of the versioned entry.
+ * @throws IOException if something goes wrong!
+ */
+ public InputStream getInputStream() throws IOException
+ {
+ return jarFile.getInputStream(entry);
+ }
+
+ boolean isApplicable()
+ {
+ if (multiRelease)
+ return (this.version == 0 || this.version == platform) && name.length() > 0;
+ return this.version == 0;
+ }
+
+ boolean isReplacedBy(VersionedJarEntry entry)
+ {
+ if (isDirectory())
+ return entry.version == 0;
+ return this.name.equals(entry.name) && entry.version > version;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s->%s[%d]", name, entry.getName(), version);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/PathWatcher.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/PathWatcher.java
new file mode 100644
index 0000000..13abdde
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/PathWatcher.java
@@ -0,0 +1,1403 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.ClosedWatchServiceException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EventListener;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+
+/**
+ * Watch a Path (and sub directories) for Path changes.
+ * <p>
+ * Suitable replacement for the old {@link Scanner} implementation.
+ * <p>
+ * Allows for configured Excludes and Includes using {@link FileSystem#getPathMatcher(String)} syntax.
+ * <p>
+ * Reports activity via registered {@link Listener}s
+ */
+public class PathWatcher extends AbstractLifeCycle implements Runnable
+{
+ public static class Config implements Predicate<Path>
+ {
+ public static final int UNLIMITED_DEPTH = -9999;
+
+ private static final String PATTERN_SEP;
+
+ static
+ {
+ String sep = File.separator;
+ if (File.separatorChar == '\\')
+ {
+ sep = "\\\\";
+ }
+ PATTERN_SEP = sep;
+ }
+
+ protected final Config parent;
+ protected final Path path;
+ protected final IncludeExcludeSet<PathMatcher, Path> includeExclude;
+ protected int recurseDepth = 0; // 0 means no sub-directories are scanned
+ protected boolean excludeHidden = false;
+ protected long pauseUntil;
+
+ public Config(Path path)
+ {
+ this(path, null);
+ }
+
+ public Config(Path path, Config parent)
+ {
+ this.parent = parent;
+ this.includeExclude = parent == null ? new IncludeExcludeSet<>(PathMatcherSet.class) : parent.includeExclude;
+
+ Path dir = path;
+ if (!Files.exists(path))
+ throw new IllegalStateException("Path does not exist: " + path);
+
+ if (!Files.isDirectory(path))
+ {
+ dir = path.getParent();
+ includeExclude.include(new ExactPathMatcher(path));
+ setRecurseDepth(0);
+ }
+
+ this.path = dir;
+ }
+
+ public Config getParent()
+ {
+ return parent;
+ }
+
+ public void setPauseUntil(long time)
+ {
+ if (time > pauseUntil)
+ pauseUntil = time;
+ }
+
+ public boolean isPaused(long now)
+ {
+ if (pauseUntil == 0)
+ return false;
+ if (pauseUntil > now)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("PAUSED {}", this);
+ return true;
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("unpaused {}", this);
+ pauseUntil = 0;
+ return false;
+ }
+
+ /**
+ * Add an exclude PathMatcher
+ *
+ * @param matcher the path matcher for this exclude
+ */
+ public void addExclude(PathMatcher matcher)
+ {
+ includeExclude.exclude(matcher);
+ }
+
+ /**
+ * Add an exclude PathMatcher.
+ * <p>
+ * Note: this pattern is FileSystem specific (so use "/" for Linux and OSX, and "\\" for Windows)
+ *
+ * @param syntaxAndPattern the PathMatcher syntax and pattern to use
+ * @see FileSystem#getPathMatcher(String) for detail on syntax and pattern
+ */
+ public void addExclude(final String syntaxAndPattern)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Adding exclude: [{}]", syntaxAndPattern);
+ addExclude(path.getFileSystem().getPathMatcher(syntaxAndPattern));
+ }
+
+ /**
+ * Add a <code>glob:</code> syntax pattern exclude reference in a directory relative, os neutral, pattern.
+ *
+ * <pre>
+ * On Linux:
+ * Config config = new Config(Path("/home/user/example"));
+ * config.addExcludeGlobRelative("*.war") => "glob:/home/user/example/*.war"
+ *
+ * On Windows
+ * Config config = new Config(Path("D:/code/examples"));
+ * config.addExcludeGlobRelative("*.war") => "glob:D:\\code\\examples\\*.war"
+ *
+ * </pre>
+ *
+ * @param pattern the pattern, in unixy format, relative to config.dir
+ */
+ public void addExcludeGlobRelative(String pattern)
+ {
+ addExclude(toGlobPattern(path, pattern));
+ }
+
+ /**
+ * Exclude hidden files and hidden directories
+ */
+ public void addExcludeHidden()
+ {
+ if (!excludeHidden)
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Adding hidden files and directories to exclusions");
+ }
+ excludeHidden = true;
+ }
+ }
+
+ /**
+ * Add multiple exclude PathMatchers
+ *
+ * @param syntaxAndPatterns the list of PathMatcher syntax and patterns to use
+ * @see FileSystem#getPathMatcher(String) for detail on syntax and pattern
+ */
+ public void addExcludes(List<String> syntaxAndPatterns)
+ {
+ for (String syntaxAndPattern : syntaxAndPatterns)
+ {
+ addExclude(syntaxAndPattern);
+ }
+ }
+
+ /**
+ * Add an include PathMatcher
+ *
+ * @param matcher the path matcher for this include
+ */
+ public void addInclude(PathMatcher matcher)
+ {
+ includeExclude.include(matcher);
+ }
+
+ /**
+ * Add an include PathMatcher
+ *
+ * @param syntaxAndPattern the PathMatcher syntax and pattern to use
+ * @see FileSystem#getPathMatcher(String) for detail on syntax and pattern
+ */
+ public void addInclude(String syntaxAndPattern)
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Adding include: [{}]", syntaxAndPattern);
+ }
+ addInclude(path.getFileSystem().getPathMatcher(syntaxAndPattern));
+ }
+
+ /**
+ * Add a <code>glob:</code> syntax pattern reference in a directory relative, os neutral, pattern.
+ *
+ * <pre>
+ * On Linux:
+ * Config config = new Config(Path("/home/user/example"));
+ * config.addIncludeGlobRelative("*.war") => "glob:/home/user/example/*.war"
+ *
+ * On Windows
+ * Config config = new Config(Path("D:/code/examples"));
+ * config.addIncludeGlobRelative("*.war") => "glob:D:\\code\\examples\\*.war"
+ *
+ * </pre>
+ *
+ * @param pattern the pattern, in unixy format, relative to config.dir
+ */
+ public void addIncludeGlobRelative(String pattern)
+ {
+ addInclude(toGlobPattern(path, pattern));
+ }
+
+ /**
+ * Add multiple include PathMatchers
+ *
+ * @param syntaxAndPatterns the list of PathMatcher syntax and patterns to use
+ * @see FileSystem#getPathMatcher(String) for detail on syntax and pattern
+ */
+ public void addIncludes(List<String> syntaxAndPatterns)
+ {
+ for (String syntaxAndPattern : syntaxAndPatterns)
+ {
+ addInclude(syntaxAndPattern);
+ }
+ }
+
+ /**
+ * Build a new config from a this configuration.
+ * <p>
+ * Useful for working with sub-directories that also need to be watched.
+ *
+ * @param dir the directory to build new Config from (using this config as source of includes/excludes)
+ * @return the new Config
+ */
+ public Config asSubConfig(Path dir)
+ {
+ Config subconfig = new Config(dir, this);
+ if (dir == this.path)
+ throw new IllegalStateException("sub " + dir.toString() + " of " + this);
+
+ if (this.recurseDepth == UNLIMITED_DEPTH)
+ subconfig.recurseDepth = UNLIMITED_DEPTH;
+ else
+ subconfig.recurseDepth = this.recurseDepth - (dir.getNameCount() - this.path.getNameCount());
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("subconfig {} of {}", subconfig, path);
+ return subconfig;
+ }
+
+ public int getRecurseDepth()
+ {
+ return recurseDepth;
+ }
+
+ public boolean isRecurseDepthUnlimited()
+ {
+ return this.recurseDepth == UNLIMITED_DEPTH;
+ }
+
+ public Path getPath()
+ {
+ return this.path;
+ }
+
+ public Path resolve(Path path)
+ {
+ if (Files.isDirectory(this.path))
+ return this.path.resolve(path);
+ if (Files.exists(this.path))
+ return this.path;
+ return path;
+ }
+
+ @Override
+ public boolean test(Path path)
+ {
+ if (excludeHidden && isHidden(path))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("test({}) -> [Hidden]", toShortPath(path));
+ return false;
+ }
+
+ if (!path.startsWith(this.path))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("test({}) -> [!child {}]", toShortPath(path), this.path);
+ return false;
+ }
+
+ if (recurseDepth != UNLIMITED_DEPTH)
+ {
+ int depth = path.getNameCount() - this.path.getNameCount() - 1;
+
+ if (depth > recurseDepth)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("test({}) -> [depth {}>{}]", toShortPath(path), depth, recurseDepth);
+ return false;
+ }
+ }
+
+ boolean matched = includeExclude.test(path);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("test({}) -> {}", toShortPath(path), matched);
+
+ return matched;
+ }
+
+ /**
+ * Set the recurse depth for the directory scanning.
+ * <p>
+ * -999 indicates arbitrarily deep recursion, 0 indicates no recursion, 1 is only one directory deep, and so on.
+ *
+ * @param depth the number of directories deep to recurse
+ */
+ public void setRecurseDepth(int depth)
+ {
+ this.recurseDepth = depth;
+ }
+
+ private String toGlobPattern(Path path, String subPattern)
+ {
+ StringBuilder s = new StringBuilder();
+ s.append("glob:");
+
+ boolean needDelim = false;
+
+ // Add root (aka "C:\" for Windows)
+ Path root = path.getRoot();
+ if (root != null)
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Path: {} -> Root: {}", path, root);
+ }
+ for (char c : root.toString().toCharArray())
+ {
+ if (c == '\\')
+ {
+ s.append(PATTERN_SEP);
+ }
+ else
+ {
+ s.append(c);
+ }
+ }
+ }
+ else
+ {
+ needDelim = true;
+ }
+
+ // Add the individual path segments
+ for (Path segment : path)
+ {
+ if (needDelim)
+ {
+ s.append(PATTERN_SEP);
+ }
+ s.append(segment);
+ needDelim = true;
+ }
+
+ // Add the sub pattern (if specified)
+ if ((subPattern != null) && (subPattern.length() > 0))
+ {
+ if (needDelim)
+ {
+ s.append(PATTERN_SEP);
+ }
+ for (char c : subPattern.toCharArray())
+ {
+ if (c == '/')
+ {
+ s.append(PATTERN_SEP);
+ }
+ else
+ {
+ s.append(c);
+ }
+ }
+ }
+
+ return s.toString();
+ }
+
+ DirAction handleDir(Path path)
+ {
+ try
+ {
+ if (!Files.isDirectory(path))
+ return DirAction.IGNORE;
+ if (excludeHidden && isHidden(path))
+ return DirAction.IGNORE;
+ if (getRecurseDepth() == 0)
+ return DirAction.WATCH;
+ return DirAction.ENTER;
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ return DirAction.IGNORE;
+ }
+ }
+
+ public boolean isHidden(Path path)
+ {
+ try
+ {
+ if (!path.startsWith(this.path))
+ return true;
+ for (int i = this.path.getNameCount(); i < path.getNameCount(); i++)
+ {
+ if (path.getName(i).toString().startsWith("."))
+ {
+ return true;
+ }
+ }
+ return Files.exists(path) && Files.isHidden(path);
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ return false;
+ }
+ }
+
+ public String toShortPath(Path path)
+ {
+ if (!path.startsWith(this.path))
+ return path.toString();
+ return this.path.relativize(path).toString();
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder s = new StringBuilder();
+ s.append(path).append(" [depth=");
+ if (recurseDepth == UNLIMITED_DEPTH)
+ s.append("UNLIMITED");
+ else
+ s.append(recurseDepth);
+ s.append(']');
+ return s.toString();
+ }
+ }
+
+ public enum DirAction
+ {
+ IGNORE, WATCH, ENTER
+ }
+
+ /**
+ * Listener for path change events
+ */
+ public interface Listener extends EventListener
+ {
+ void onPathWatchEvent(PathWatchEvent event);
+ }
+
+ /**
+ * EventListListener
+ *
+ * Listener that reports accumulated events in one shot
+ */
+ public interface EventListListener extends EventListener
+ {
+ void onPathWatchEvents(List<PathWatchEvent> events);
+ }
+
+ /**
+ * PathWatchEvent
+ *
+ * Represents a file event. Reported to registered listeners.
+ */
+ public class PathWatchEvent
+ {
+ private final Path path;
+ private final PathWatchEventType type;
+ private final Config config;
+ long checked;
+ long modified;
+ long length;
+
+ public PathWatchEvent(Path path, PathWatchEventType type, Config config)
+ {
+ this.path = path;
+ this.type = type;
+ this.config = config;
+ checked = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ check();
+ }
+
+ public Config getConfig()
+ {
+ return config;
+ }
+
+ public PathWatchEvent(Path path, WatchEvent<Path> event, Config config)
+ {
+ this.path = path;
+ if (event.kind() == ENTRY_CREATE)
+ {
+ this.type = PathWatchEventType.ADDED;
+ }
+ else if (event.kind() == ENTRY_DELETE)
+ {
+ this.type = PathWatchEventType.DELETED;
+ }
+ else if (event.kind() == ENTRY_MODIFY)
+ {
+ this.type = PathWatchEventType.MODIFIED;
+ }
+ else
+ {
+ this.type = PathWatchEventType.UNKNOWN;
+ }
+ this.config = config;
+ checked = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ check();
+ }
+
+ private void check()
+ {
+ if (Files.exists(path))
+ {
+ try
+ {
+ modified = Files.getLastModifiedTime(path).toMillis();
+ length = Files.size(path);
+ }
+ catch (IOException e)
+ {
+ modified = -1;
+ length = -1;
+ }
+ }
+ else
+ {
+ modified = -1;
+ length = -1;
+ }
+ }
+
+ public boolean isQuiet(long now, long quietTime)
+ {
+ long lastModified = modified;
+ long lastLength = length;
+
+ check();
+
+ if (lastModified == modified && lastLength == length)
+ return (now - checked) >= quietTime;
+
+ checked = now;
+ return false;
+ }
+
+ public long toQuietCheck(long now, long quietTime)
+ {
+ long check = quietTime - (now - checked);
+ if (check <= 0)
+ return quietTime;
+ return check;
+ }
+
+ public void modified()
+ {
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ checked = now;
+ check();
+ config.setPauseUntil(now + getUpdateQuietTimeMillis());
+ }
+
+ /**
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ {
+ return true;
+ }
+ if (obj == null)
+ {
+ return false;
+ }
+ if (getClass() != obj.getClass())
+ {
+ return false;
+ }
+ PathWatchEvent other = (PathWatchEvent)obj;
+ if (path == null)
+ {
+ if (other.path != null)
+ {
+ return false;
+ }
+ }
+ else if (!path.equals(other.path))
+ {
+ return false;
+ }
+ return type == other.type;
+ }
+
+ public Path getPath()
+ {
+ return path;
+ }
+
+ public PathWatchEventType getType()
+ {
+ return type;
+ }
+
+ @Deprecated
+ public int getCount()
+ {
+ return 1;
+ }
+
+ /**
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = (prime * result) + ((path == null) ? 0 : path.hashCode());
+ result = (prime * result) + ((type == null) ? 0 : type.hashCode());
+ return result;
+ }
+
+ /**
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString()
+ {
+ return String.format("PathWatchEvent[%8s|%s]", type, path);
+ }
+ }
+
+ /**
+ * PathWatchEventType
+ *
+ * Type of an event
+ */
+ public enum PathWatchEventType
+ {
+ ADDED, DELETED, MODIFIED, UNKNOWN
+ }
+
+ private static final boolean IS_WINDOWS;
+
+ static
+ {
+ String os = System.getProperty("os.name");
+ if (os == null)
+ {
+ IS_WINDOWS = false;
+ }
+ else
+ {
+ String osl = os.toLowerCase(Locale.ENGLISH);
+ IS_WINDOWS = osl.contains("windows");
+ }
+ }
+
+ static final Logger LOG = Log.getLogger(PathWatcher.class);
+
+ @SuppressWarnings("unchecked")
+ protected static <T> WatchEvent<T> cast(WatchEvent<?> event)
+ {
+ return (WatchEvent<T>)event;
+ }
+
+ private static final WatchEvent.Kind<?>[] WATCH_EVENT_KINDS = {ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY};
+ private static final WatchEvent.Kind<?>[] WATCH_DIR_KINDS = {ENTRY_CREATE, ENTRY_DELETE};
+
+ private WatchService watchService;
+
+ private final List<Config> configs = new ArrayList<>();
+ private final Map<WatchKey, Config> keys = new ConcurrentHashMap<>();
+ private final List<EventListener> listeners = new CopyOnWriteArrayList<>(); //a listener may modify the listener list directly or by stopping the PathWatcher
+
+ private final Map<Path, PathWatchEvent> pending = new LinkedHashMap<>(32, (float)0.75, false);
+ private final List<PathWatchEvent> events = new ArrayList<>();
+
+ /**
+ * Update Quiet Time - set to 1000 ms as default (a lower value in Windows is not supported)
+ */
+ private long updateQuietTimeDuration = 1000;
+ private TimeUnit updateQuietTimeUnit = TimeUnit.MILLISECONDS;
+ private Thread thread;
+ private boolean _notifyExistingOnStart = true;
+
+ /**
+ * Construct new PathWatcher
+ */
+ public PathWatcher()
+ {
+ }
+
+ public Collection<Config> getConfigs()
+ {
+ return configs;
+ }
+
+ /**
+ * Request watch on a the given path (either file or dir)
+ * using all Config defaults. In the case of a dir,
+ * the default is not to recurse into subdirs for watching.
+ *
+ * @param file the path to watch
+ */
+ public void watch(final Path file)
+ {
+ //Make a config for the dir above it and
+ //include a match only for the given path
+ //using all defaults for the configuration
+ Path abs = file;
+ if (!abs.isAbsolute())
+ {
+ abs = file.toAbsolutePath();
+ }
+
+ //Check we don't already have a config for the parent directory.
+ //If we do, add in this filename.
+ Config config = null;
+ Path parent = abs.getParent();
+ for (Config c : configs)
+ {
+ if (c.getPath().equals(parent))
+ {
+ config = c;
+ break;
+ }
+ }
+
+ //Make a new config
+ if (config == null)
+ {
+ config = new Config(abs.getParent());
+ // the include for the directory itself
+ config.addIncludeGlobRelative("");
+ //add the include for the file
+ config.addIncludeGlobRelative(file.getFileName().toString());
+ watch(config);
+ }
+ else
+ //add the include for the file
+ config.addIncludeGlobRelative(file.getFileName().toString());
+ }
+
+ /**
+ * Request watch on a path with custom Config
+ * provided.
+ *
+ * @param config the configuration to watch
+ */
+ public void watch(final Config config)
+ {
+ //Add a custom config
+ configs.add(config);
+ }
+
+ /**
+ * Add a listener for changes the watcher notices.
+ *
+ * @param listener change listener
+ */
+ public void addListener(EventListener listener)
+ {
+ listeners.add(listener);
+ }
+
+ /**
+ * Append some info on the paths that we are watching.
+ */
+ private void appendConfigId(StringBuilder s)
+ {
+ List<Path> dirs = new ArrayList<>();
+
+ for (Config config : keys.values())
+ {
+ dirs.add(config.path);
+ }
+
+ Collections.sort(dirs);
+
+ s.append("[");
+ if (dirs.size() > 0)
+ {
+ s.append(dirs.get(0));
+ if (dirs.size() > 1)
+ {
+ s.append(" (+").append(dirs.size() - 1).append(")");
+ }
+ }
+ else
+ {
+ s.append("<null>");
+ }
+ s.append("]");
+ }
+
+ /**
+ * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ //create a new watchservice
+ this.watchService = FileSystems.getDefault().newWatchService();
+
+ //ensure setting of quiet time is appropriate now we have a watcher
+ setUpdateQuietTime(getUpdateQuietTimeMillis(), TimeUnit.MILLISECONDS);
+
+ // Register all watched paths, walking dir hierarchies as needed, possibly generating
+ // fake add events if notifyExistingOnStart is true
+ for (Config c : configs)
+ {
+ registerTree(c.getPath(), c, isNotifyExistingOnStart());
+ }
+
+ // Start Thread for watcher take/pollKeys loop
+ StringBuilder threadId = new StringBuilder();
+ threadId.append("PathWatcher@");
+ threadId.append(Integer.toHexString(hashCode()));
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} -> {}", this, threadId);
+
+ thread = new Thread(this, threadId.toString());
+ thread.setDaemon(true);
+ thread.start();
+ super.doStart();
+ }
+
+ /**
+ * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
+ */
+ @Override
+ protected void doStop() throws Exception
+ {
+ if (watchService != null)
+ watchService.close(); //will invalidate registered watch keys, interrupt thread in take or poll
+ watchService = null;
+ thread = null;
+ keys.clear();
+ pending.clear();
+ events.clear();
+ super.doStop();
+ }
+
+ /**
+ * Remove all current configs and listeners.
+ */
+ public void reset()
+ {
+ if (!isStopped())
+ throw new IllegalStateException("PathWatcher must be stopped before reset.");
+
+ configs.clear();
+ listeners.clear();
+ }
+
+ /**
+ * Check to see if the watcher is in a state where it should generate
+ * watch events to the listeners. Used to determine if watcher should generate
+ * events for existing files and dirs on startup.
+ *
+ * @return true if the watcher should generate events to the listeners.
+ */
+ protected boolean isNotifiable()
+ {
+ return (isStarted() || (!isStarted() && isNotifyExistingOnStart()));
+ }
+
+ /**
+ * Get an iterator over the listeners.
+ *
+ * @return iterator over the listeners.
+ */
+ public Iterator<EventListener> getListeners()
+ {
+ return listeners.iterator();
+ }
+
+ /**
+ * Change the quiet time.
+ *
+ * @return the quiet time in millis
+ */
+ public long getUpdateQuietTimeMillis()
+ {
+ return TimeUnit.MILLISECONDS.convert(updateQuietTimeDuration, updateQuietTimeUnit);
+ }
+
+ private void registerTree(Path dir, Config config, boolean notify) throws IOException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("registerTree {} {} {}", dir, config, notify);
+
+ if (!Files.isDirectory(dir))
+ throw new IllegalArgumentException(dir.toString());
+
+ register(dir, config);
+
+ final MultiException me = new MultiException();
+ try (Stream<Path> stream = Files.list(dir))
+ {
+ stream.forEach(p ->
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("registerTree? {}", p);
+
+ try
+ {
+ if (notify && config.test(p))
+ pending.put(p, new PathWatchEvent(p, PathWatchEventType.ADDED, config));
+
+ switch (config.handleDir(p))
+ {
+ case ENTER:
+ registerTree(p, config.asSubConfig(p), notify);
+ break;
+ case WATCH:
+ registerDir(p, config);
+ break;
+ case IGNORE:
+ default:
+ break;
+ }
+ }
+ catch (IOException e)
+ {
+ me.add(e);
+ }
+ });
+ }
+ try
+ {
+ me.ifExceptionThrow();
+ }
+ catch (IOException e)
+ {
+ throw e;
+ }
+ catch (Throwable ex)
+ {
+ throw new IOException(ex);
+ }
+ }
+
+ private void registerDir(Path path, Config config) throws IOException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("registerDir {} {}", path, config);
+
+ if (!Files.isDirectory(path))
+ throw new IllegalArgumentException(path.toString());
+
+ register(path, config.asSubConfig(path), WATCH_DIR_KINDS);
+ }
+
+ protected void register(Path path, Config config) throws IOException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Registering watch on {}", path);
+
+ register(path, config, WATCH_EVENT_KINDS);
+ }
+
+ private void register(Path path, Config config, WatchEvent.Kind<?>[] kinds) throws IOException
+ {
+ // Native Watcher
+ WatchKey key = path.register(watchService, kinds);
+ keys.put(key, config);
+ }
+
+ /**
+ * Delete a listener
+ *
+ * @param listener the listener to remove
+ * @return true if the listener existed and was removed
+ */
+ public boolean removeListener(Listener listener)
+ {
+ return listeners.remove(listener);
+ }
+
+ /**
+ * Forever loop.
+ *
+ * Wait for the WatchService to report some filesystem events for the
+ * watched paths.
+ *
+ * When an event for a path first occurs, it is subjected to a quiet time.
+ * Subsequent events that arrive for the same path during this quiet time are
+ * accumulated and the timer reset. Only when the quiet time has expired are
+ * the accumulated events sent. MODIFY events are handled slightly differently -
+ * multiple MODIFY events arriving within a quiet time are coalesced into a
+ * single MODIFY event. Both the accumulation of events and coalescing of MODIFY
+ * events reduce the number and frequency of event reporting for "noisy" files (ie
+ * those that are undergoing rapid change).
+ *
+ * @see java.lang.Runnable#run()
+ */
+ @Override
+ public void run()
+ {
+ // Start the java.nio watching
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Starting java.nio file watching with {}", watchService);
+ }
+
+ long waitTime = getUpdateQuietTimeMillis();
+
+ WatchService watch = watchService;
+
+ while (isRunning() && thread == Thread.currentThread())
+ {
+
+ WatchKey key;
+
+ try
+ {
+ // Reset all keys before watching
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ for (Map.Entry<WatchKey, Config> e : keys.entrySet())
+ {
+ WatchKey k = e.getKey();
+ Config c = e.getValue();
+
+ if (!c.isPaused(now) && !k.reset())
+ {
+ keys.remove(k);
+ if (keys.isEmpty())
+ {
+ return; // all done, no longer monitoring anything
+ }
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Waiting for poll({})", waitTime);
+ key = waitTime < 0 ? watch.take() : waitTime > 0 ? watch.poll(waitTime, updateQuietTimeUnit) : watch.poll();
+
+ // handle all active keys
+ while (key != null)
+ {
+ handleKey(key);
+ key = watch.poll();
+ }
+
+ waitTime = processPending();
+
+ notifyEvents();
+ }
+ catch (ClosedWatchServiceException e)
+ {
+ // Normal shutdown of watcher
+ return;
+ }
+ catch (InterruptedException e)
+ {
+ if (isRunning())
+ {
+ LOG.warn(e);
+ }
+ else
+ {
+ LOG.ignore(e);
+ }
+ }
+ }
+ }
+
+ private void handleKey(WatchKey key)
+ {
+ Config config = keys.get(key);
+ if (config == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("WatchKey not recognized: {}", key);
+ return;
+ }
+
+ for (WatchEvent<?> event : key.pollEvents())
+ {
+ WatchEvent<Path> ev = cast(event);
+ Path name = ev.context();
+ Path path = config.resolve(name);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("handleKey? {} {} {}", ev.kind(), config.toShortPath(path), config);
+
+ // Ignore modified events on directories. These are handled as create/delete events of their contents
+ if (ev.kind() == ENTRY_MODIFY && Files.exists(path) && Files.isDirectory(path))
+ continue;
+
+ if (config.test(path))
+ handleWatchEvent(path, new PathWatchEvent(path, ev, config));
+ else if (config.getRecurseDepth() == -1)
+ {
+ // Convert a watched directory into a modify event on its parent
+ Path parent = path.getParent();
+ Config parentConfig = config.getParent();
+ handleWatchEvent(parent, new PathWatchEvent(parent, PathWatchEventType.MODIFIED, parentConfig));
+ continue;
+ }
+
+ if (ev.kind() == ENTRY_CREATE)
+ {
+ try
+ {
+ switch (config.handleDir(path))
+ {
+ case ENTER:
+ registerTree(path, config.asSubConfig(path), true);
+ break;
+ case WATCH:
+ registerDir(path, config);
+ break;
+ case IGNORE:
+ default:
+ break;
+ }
+ }
+ catch (IOException e)
+ {
+ LOG.warn(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Add an event reported by the WatchService to list of pending events
+ * that will be sent after their quiet time has expired.
+ *
+ * @param path the path to add to the pending list
+ * @param event the pending event
+ */
+ public void handleWatchEvent(Path path, PathWatchEvent event)
+ {
+ PathWatchEvent existing = pending.get(path);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("handleWatchEvent {} {} <= {}", path, event, existing);
+
+ switch (event.getType())
+ {
+ case ADDED:
+ if (existing != null && existing.getType() == PathWatchEventType.MODIFIED)
+ events.add(new PathWatchEvent(path, PathWatchEventType.DELETED, existing.getConfig()));
+ pending.put(path, event);
+ break;
+
+ case MODIFIED:
+ if (existing == null)
+ pending.put(path, event);
+ else
+ existing.modified();
+ break;
+
+ case DELETED:
+ case UNKNOWN:
+ if (existing != null)
+ pending.remove(path);
+ events.add(event);
+ break;
+ }
+ }
+
+ private long processPending()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("processPending> {}", pending.values());
+
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ long wait = Long.MAX_VALUE;
+
+ // pending map is maintained in LRU order
+ for (PathWatchEvent event : new ArrayList<>(pending.values()))
+ {
+ Path path = event.getPath();
+ // for directories, wait until parent is quiet
+ if (pending.containsKey(path.getParent()))
+ continue;
+
+ // if the path is quiet move to events
+ if (event.isQuiet(now, getUpdateQuietTimeMillis()))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("isQuiet {}", event);
+ pending.remove(path);
+ events.add(event);
+ }
+ else
+ {
+ long msToCheck = event.toQuietCheck(now, getUpdateQuietTimeMillis());
+ if (LOG.isDebugEnabled())
+ LOG.debug("pending {} {}", event, msToCheck);
+ if (msToCheck < wait)
+ wait = msToCheck;
+ }
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("processPending< {}", pending.values());
+ return wait == Long.MAX_VALUE ? -1 : wait;
+ }
+
+ private void notifyEvents()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("notifyEvents {}", events.size());
+
+ if (events.isEmpty())
+ return;
+
+ boolean eventListeners = false;
+ for (EventListener listener : listeners)
+ {
+ if (listener instanceof EventListListener)
+ {
+ try
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("notifyEvents {} {}", listener, events);
+ ((EventListListener)listener).onPathWatchEvents(events);
+ }
+ catch (Throwable t)
+ {
+ LOG.warn(t);
+ }
+ }
+ else
+ eventListeners = true;
+ }
+
+ if (eventListeners)
+ {
+ for (PathWatchEvent event : events)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("notifyEvent {} {}", event, listeners);
+ for (EventListener listener : listeners)
+ {
+ if (listener instanceof Listener)
+ {
+ try
+ {
+ ((Listener)listener).onPathWatchEvent(event);
+ }
+ catch (Throwable t)
+ {
+ LOG.warn(t);
+ }
+ }
+ }
+ }
+ }
+
+ events.clear();
+ }
+
+ /**
+ * Whether or not to issue notifications for directories and files that
+ * already exist when the watcher starts.
+ *
+ * @param notify true if existing paths should be notified or not
+ */
+ public void setNotifyExistingOnStart(boolean notify)
+ {
+ _notifyExistingOnStart = notify;
+ }
+
+ public boolean isNotifyExistingOnStart()
+ {
+ return _notifyExistingOnStart;
+ }
+
+ /**
+ * Set the quiet time.
+ *
+ * @param duration the quiet time duration
+ * @param unit the quite time unit
+ */
+ public void setUpdateQuietTime(long duration, TimeUnit unit)
+ {
+ long desiredMillis = unit.toMillis(duration);
+
+ if (IS_WINDOWS && (desiredMillis < 1000))
+ {
+ LOG.warn("Quiet Time is too low for Microsoft Windows: {} < 1000 ms (defaulting to 1000 ms)", desiredMillis);
+ this.updateQuietTimeDuration = 1000;
+ this.updateQuietTimeUnit = TimeUnit.MILLISECONDS;
+ return;
+ }
+
+ // All other OS and watch service combinations can use desired setting
+ this.updateQuietTimeDuration = duration;
+ this.updateQuietTimeUnit = unit;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder s = new StringBuilder(this.getClass().getName());
+ appendConfigId(s);
+ return s.toString();
+ }
+
+ private static class ExactPathMatcher implements PathMatcher
+ {
+ private final Path path;
+
+ ExactPathMatcher(Path path)
+ {
+ this.path = path;
+ }
+
+ @Override
+ public boolean matches(Path path)
+ {
+ return this.path.equals(path);
+ }
+ }
+
+ public static class PathMatcherSet extends HashSet<PathMatcher> implements Predicate<Path>
+ {
+ @Override
+ public boolean test(Path path)
+ {
+ for (PathMatcher pm : this)
+ {
+ if (pm.matches(path))
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/PatternMatcher.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/PatternMatcher.java
new file mode 100644
index 0000000..51bb2ec
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/PatternMatcher.java
@@ -0,0 +1,101 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public abstract class PatternMatcher
+{
+ public abstract void matched(URI uri) throws Exception;
+
+ /**
+ * Find jar names from the provided list matching a pattern.
+ *
+ * If the pattern is null and isNullInclusive is true, then
+ * all jar names will match.
+ *
+ * A pattern is a set of acceptable jar names. Each acceptable
+ * jar name is a regex. Each regex can be separated by either a
+ * "," or a "|". If you use a "|" this or's together the jar
+ * name patterns. This means that ordering of the matches is
+ * unimportant to you. If instead, you want to match particular
+ * jar names, and you want to match them in order, you should
+ * separate the regexs with "," instead.
+ *
+ * Eg "aaa-.*\\.jar|bbb-.*\\.jar"
+ * Will iterate over the jar names and match
+ * in any order.
+ *
+ * Eg "aaa-*\\.jar,bbb-.*\\.jar"
+ * Will iterate over the jar names, matching
+ * all those starting with "aaa-" first, then "bbb-".
+ *
+ * @param pattern the pattern
+ * @param uris the uris to test the pattern against
+ * @param isNullInclusive if true, an empty pattern means all names match, if false, none match
+ * @throws Exception if fundamental error in pattern matching
+ */
+ public void match(Pattern pattern, URI[] uris, boolean isNullInclusive)
+ throws Exception
+ {
+ if (uris != null)
+ {
+ String[] patterns = (pattern == null ? null : pattern.pattern().split(","));
+
+ List<Pattern> subPatterns = new ArrayList<Pattern>();
+ for (int i = 0; patterns != null && i < patterns.length; i++)
+ {
+ subPatterns.add(Pattern.compile(patterns[i]));
+ }
+ if (subPatterns.isEmpty())
+ subPatterns.add(pattern);
+
+ if (subPatterns.isEmpty())
+ {
+ matchPatterns(null, uris, isNullInclusive);
+ }
+ else
+ {
+ //for each subpattern, iterate over all the urls, processing those that match
+ for (Pattern p : subPatterns)
+ {
+ matchPatterns(p, uris, isNullInclusive);
+ }
+ }
+ }
+ }
+
+ public void matchPatterns(Pattern pattern, URI[] uris, boolean isNullInclusive)
+ throws Exception
+ {
+ for (int i = 0; i < uris.length; i++)
+ {
+ URI uri = uris[i];
+ String s = uri.toString();
+ if ((pattern == null && isNullInclusive) ||
+ (pattern != null && pattern.matcher(s).matches()))
+ {
+ matched(uris[i]);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Pool.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Pool.java
new file mode 100644
index 0000000..921d6af
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Pool.java
@@ -0,0 +1,1043 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.component.DumpableCollection;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Locker;
+
+/**
+ * <p>A pool of objects, with optional support for multiplexing,
+ * max usage count and several optimized strategies plus
+ * an optional {@link ThreadLocal} cache of the last release entry.</p>
+ * <p>When the method {@link #close()} is called, all {@link Closeable}s
+ * object pooled by the pool are also closed.</p>
+ *
+ * @param <T> the type of the pooled objects
+ */
+@ManagedObject
+public class Pool<T> implements AutoCloseable, Dumpable
+{
+ private static final Logger LOGGER = Log.getLogger(Pool.class);
+
+ private final List<Entry> entries = new CopyOnWriteArrayList<>();
+ private final int maxEntries;
+ private final StrategyType strategyType;
+ /*
+ * The cache is used to avoid hammering on the first index of the entry list.
+ * Caches can become poisoned (i.e.: containing entries that are in use) when
+ * the release isn't done by the acquiring thread or when the entry pool is
+ * undersized compared to the load applied on it.
+ * When an entry can't be found in the cache, the global list is iterated
+ * with the configured strategy so the cache has no visible effect besides performance.
+ */
+ private final Locker locker = new Locker();
+ private final ThreadLocal<Entry> cache;
+ private final AtomicInteger nextIndex;
+ private volatile boolean closed;
+ @Deprecated
+ private volatile int maxUsage = -1;
+ @Deprecated
+ private volatile int maxMultiplex = -1;
+
+ /**
+ * The type of the strategy to use for the pool.
+ * The strategy primarily determines where iteration over the pool entries begins.
+ */
+ public enum StrategyType
+ {
+ /**
+ * A strategy that looks for an entry always starting from the first entry.
+ * It will favour the early entries in the pool, but may contend on them more.
+ */
+ FIRST,
+
+ /**
+ * A strategy that looks for an entry by iterating from a random starting
+ * index. No entries are favoured and contention is reduced.
+ */
+ RANDOM,
+
+ /**
+ * A strategy that uses the {@link Thread#getId()} of the current thread
+ * to select a starting point for an entry search. Whilst not as performant as
+ * using the {@link ThreadLocal} cache, it may be suitable when the pool is substantially smaller
+ * than the number of available threads.
+ * No entries are favoured and contention is reduced.
+ */
+ THREAD_ID,
+
+ /**
+ * A strategy that looks for an entry by iterating from a starting point
+ * that is incremented on every search. This gives similar results to the
+ * random strategy but with more predictable behaviour.
+ * No entries are favoured and contention is reduced.
+ */
+ ROUND_ROBIN
+ }
+
+ /**
+ * Construct a Pool with a specified lookup strategy and no
+ * {@link ThreadLocal} cache.
+ *
+ * @param strategyType The strategy to used for looking up entries.
+ * @param maxEntries the maximum amount of entries that the pool will accept.
+ */
+ public Pool(StrategyType strategyType, int maxEntries)
+ {
+ this(strategyType, maxEntries, false);
+ }
+
+ /**
+ * Construct a Pool with the specified thread-local cache size and
+ * an optional {@link ThreadLocal} cache.
+ *
+ * @param strategyType The strategy to used for looking up entries.
+ * @param maxEntries the maximum amount of entries that the pool will accept.
+ * @param cache True if a {@link ThreadLocal} cache should be used to try the most recently released entry.
+ */
+ public Pool(StrategyType strategyType, int maxEntries, boolean cache)
+ {
+ this.maxEntries = maxEntries;
+ this.strategyType = strategyType;
+ this.cache = cache ? new ThreadLocal<>() : null;
+ this.nextIndex = strategyType == StrategyType.ROUND_ROBIN ? new AtomicInteger() : null;
+ }
+
+ /**
+ * @return the number of reserved entries
+ */
+ @ManagedAttribute("The number of reserved entries")
+ public int getReservedCount()
+ {
+ return (int)entries.stream().filter(Entry::isReserved).count();
+ }
+
+ /**
+ * @return the number of idle entries
+ */
+ @ManagedAttribute("The number of idle entries")
+ public int getIdleCount()
+ {
+ return (int)entries.stream().filter(Entry::isIdle).count();
+ }
+
+ /**
+ * @return the number of in-use entries
+ */
+ @ManagedAttribute("The number of in-use entries")
+ public int getInUseCount()
+ {
+ return (int)entries.stream().filter(Entry::isInUse).count();
+ }
+
+ /**
+ * @return the number of closed entries
+ */
+ @ManagedAttribute("The number of closed entries")
+ public int getClosedCount()
+ {
+ return (int)entries.stream().filter(Entry::isClosed).count();
+ }
+
+ /**
+ * @return the maximum number of entries
+ */
+ @ManagedAttribute("The maximum number of entries")
+ public int getMaxEntries()
+ {
+ return maxEntries;
+ }
+
+ /**
+ * @return the default maximum multiplex count of entries
+ * @deprecated Multiplex functionalities will be removed
+ */
+ @ManagedAttribute("The default maximum multiplex count of entries")
+ @Deprecated
+ public int getMaxMultiplex()
+ {
+ return maxMultiplex == -1 ? 1 : maxMultiplex;
+ }
+
+ /**
+ * <p>Retrieves the max multiplex count for the given pooled object.</p>
+ *
+ * @param pooled the pooled object
+ * @return the max multiplex count for the given pooled object
+ * @deprecated Multiplex functionalities will be removed
+ */
+ @Deprecated
+ protected int getMaxMultiplex(T pooled)
+ {
+ return getMaxMultiplex();
+ }
+
+ /**
+ * <p>Sets the default maximum multiplex count for the Pool's entries.</p>
+ *
+ * @param maxMultiplex the default maximum multiplex count of entries
+ * @deprecated Multiplex functionalities will be removed
+ */
+ @Deprecated
+ public final void setMaxMultiplex(int maxMultiplex)
+ {
+ if (maxMultiplex < 1)
+ throw new IllegalArgumentException("Max multiplex must be >= 1");
+ try (Locker.Lock l = locker.lock())
+ {
+ if (closed)
+ return;
+
+ if (entries.stream().anyMatch(MonoEntry.class::isInstance))
+ throw new IllegalStateException("Pool entries do not support multiplexing");
+
+ this.maxMultiplex = maxMultiplex;
+ }
+ }
+
+ /**
+ * <p>Returns the maximum number of times the entries of the pool
+ * can be acquired.</p>
+ *
+ * @return the default maximum usage count of entries
+ * @deprecated MaxUsage functionalities will be removed
+ */
+ @ManagedAttribute("The default maximum usage count of entries")
+ @Deprecated
+ public int getMaxUsageCount()
+ {
+ return maxUsage;
+ }
+
+ /**
+ * <p>Retrieves the max usage count for the given pooled object.</p>
+ *
+ * @param pooled the pooled object
+ * @return the max usage count for the given pooled object
+ * @deprecated MaxUsage functionalities will be removed
+ */
+ @Deprecated
+ protected int getMaxUsageCount(T pooled)
+ {
+ return getMaxUsageCount();
+ }
+
+ /**
+ * <p>Sets the maximum usage count for the Pool's entries.</p>
+ * <p>All existing idle entries that have a usage count larger
+ * than this new value are removed from the Pool and closed.</p>
+ *
+ * @param maxUsageCount the default maximum usage count of entries
+ * @deprecated MaxUsage functionalities will be removed
+ */
+ @Deprecated
+ public final void setMaxUsageCount(int maxUsageCount)
+ {
+ if (maxUsageCount == 0)
+ throw new IllegalArgumentException("Max usage count must be != 0");
+
+ // Iterate the entries, remove overused ones and collect a list of the closeable removed ones.
+ List<Closeable> copy;
+ try (Locker.Lock l = locker.lock())
+ {
+ if (closed)
+ return;
+
+ if (entries.stream().anyMatch(MonoEntry.class::isInstance))
+ throw new IllegalStateException("Pool entries do not support max usage");
+
+ this.maxUsage = maxUsageCount;
+
+ copy = entries.stream()
+ .filter(entry -> entry.isIdleAndOverUsed() && remove(entry) && entry.pooled instanceof Closeable)
+ .map(entry -> (Closeable)entry.pooled)
+ .collect(Collectors.toList());
+ }
+
+ // Iterate the copy and close the collected entries.
+ copy.forEach(IO::close);
+ }
+
+ /**
+ * <p>Creates a new disabled slot into the pool.</p>
+ * <p>The returned entry must ultimately have the {@link Entry#enable(Object, boolean)}
+ * method called or be removed via {@link Pool.Entry#remove()} or
+ * {@link Pool#remove(Pool.Entry)}.</p>
+ *
+ * @param allotment the desired allotment, where each entry handles an allotment of maxMultiplex,
+ * or a negative number to always trigger the reservation of a new entry.
+ * @return a disabled entry that is contained in the pool,
+ * or null if the pool is closed or if the pool already contains
+ * {@link #getMaxEntries()} entries, or the allotment has already been reserved
+ * @deprecated Use {@link #reserve()} instead
+ */
+ @Deprecated
+ public Entry reserve(int allotment)
+ {
+ try (Locker.Lock l = locker.lock())
+ {
+ if (closed)
+ return null;
+
+ int space = maxEntries - entries.size();
+ if (space <= 0)
+ return null;
+
+ if (allotment >= 0 && (getReservedCount() * getMaxMultiplex()) >= allotment)
+ return null;
+
+ Entry entry = newEntry();
+ entries.add(entry);
+ return entry;
+ }
+ }
+
+ /**
+ * <p>Creates a new disabled slot into the pool.</p>
+ * <p>The returned entry must ultimately have the {@link Entry#enable(Object, boolean)}
+ * method called or be removed via {@link Pool.Entry#remove()} or
+ * {@link Pool#remove(Pool.Entry)}.</p>
+ *
+ * @return a disabled entry that is contained in the pool,
+ * or null if the pool is closed or if the pool already contains
+ * {@link #getMaxEntries()} entries
+ */
+ public Entry reserve()
+ {
+ try (Locker.Lock l = locker.lock())
+ {
+ if (closed)
+ return null;
+
+ // If we have no space
+ if (entries.size() >= maxEntries)
+ return null;
+
+ Entry entry = newEntry();
+ entries.add(entry);
+ return entry;
+ }
+ }
+
+ private Entry newEntry()
+ {
+ // Do not allow more than 2 implementations of Entry, otherwise call sites in Pool
+ // referencing Entry methods will become mega-morphic and kill the performance.
+ if (maxMultiplex >= 0 || maxUsage >= 0)
+ return new MultiEntry();
+ return new MonoEntry();
+ }
+
+ /**
+ * <p>Acquires the entry from the pool at the specified index.</p>
+ * <p>This method bypasses the thread-local cache mechanism.</p>
+ *
+ * @param idx the index of the entry to acquire.
+ * @return the specified entry or null if there is none at the specified index or if it is not available.
+ * @deprecated No longer supported. Instead use a {@link StrategyType} to configure the pool.
+ */
+ @Deprecated
+ public Entry acquireAt(int idx)
+ {
+ if (closed)
+ return null;
+
+ try
+ {
+ Entry entry = entries.get(idx);
+ if (entry.tryAcquire())
+ return entry;
+ }
+ catch (IndexOutOfBoundsException e)
+ {
+ // no entry at that index
+ }
+ return null;
+ }
+
+ /**
+ * <p>Acquires an entry from the pool.</p>
+ * <p>Only enabled entries will be returned from this method
+ * and their {@link Entry#enable(Object, boolean)}
+ * method must not be called.</p>
+ *
+ * @return an entry from the pool or null if none is available.
+ */
+ public Entry acquire()
+ {
+ if (closed)
+ return null;
+
+ int size = entries.size();
+ if (size == 0)
+ return null;
+
+ if (cache != null)
+ {
+ Pool<T>.Entry entry = cache.get();
+ if (entry != null && entry.tryAcquire())
+ return entry;
+ }
+
+ int index = startIndex(size);
+
+ for (int tries = size; tries-- > 0;)
+ {
+ try
+ {
+ Pool<T>.Entry entry = entries.get(index);
+ if (entry != null && entry.tryAcquire())
+ return entry;
+ }
+ catch (IndexOutOfBoundsException e)
+ {
+ LOGGER.ignore(e);
+ size = entries.size();
+ // Size can be 0 when the pool is in the middle of
+ // acquiring a connection while another thread
+ // removes the last one from the pool.
+ if (size == 0)
+ break;
+ }
+ index = (index + 1) % size;
+ }
+ return null;
+ }
+
+ private int startIndex(int size)
+ {
+ switch (strategyType)
+ {
+ case FIRST:
+ return 0;
+ case RANDOM:
+ return ThreadLocalRandom.current().nextInt(size);
+ case ROUND_ROBIN:
+ return nextIndex.getAndUpdate(c -> Math.max(0, c + 1)) % size;
+ case THREAD_ID:
+ return (int)(Thread.currentThread().getId() % size);
+ default:
+ throw new IllegalArgumentException("Unknown strategy type: " + strategyType);
+ }
+ }
+
+ /**
+ * <p>Acquires an entry from the pool,
+ * reserving and creating a new entry if necessary.</p>
+ *
+ * @param creator a function to create the pooled value for a reserved entry.
+ * @return an entry from the pool or null if none is available.
+ */
+ public Entry acquire(Function<Pool<T>.Entry, T> creator)
+ {
+ Entry entry = acquire();
+ if (entry != null)
+ return entry;
+
+ entry = reserve();
+ if (entry == null)
+ return null;
+
+ T value;
+ try
+ {
+ value = creator.apply(entry);
+ }
+ catch (Throwable th)
+ {
+ remove(entry);
+ throw th;
+ }
+
+ if (value == null)
+ {
+ remove(entry);
+ return null;
+ }
+
+ return entry.enable(value, true) ? entry : null;
+ }
+
+ /**
+ * <p>Releases an {@link #acquire() acquired} entry to the pool.</p>
+ * <p>Entries that are acquired from the pool but never released
+ * will result in a memory leak.</p>
+ *
+ * @param entry the value to return to the pool
+ * @return true if the entry was released and could be acquired again,
+ * false if the entry should be removed by calling {@link #remove(Pool.Entry)}
+ * and the object contained by the entry should be disposed.
+ */
+ public boolean release(Entry entry)
+ {
+ if (closed)
+ return false;
+
+ boolean released = entry.tryRelease();
+ if (released && cache != null)
+ cache.set(entry);
+ return released;
+ }
+
+ /**
+ * <p>Removes an entry from the pool.</p>
+ *
+ * @param entry the value to remove
+ * @return true if the entry was removed, false otherwise
+ */
+ public boolean remove(Entry entry)
+ {
+ if (closed)
+ return false;
+
+ if (!entry.tryRemove())
+ {
+ if (LOGGER.isDebugEnabled())
+ LOGGER.debug("Attempt to remove an object from the pool that is still in use: {}", entry);
+ return false;
+ }
+
+ boolean removed = entries.remove(entry);
+ if (!removed && LOGGER.isDebugEnabled())
+ LOGGER.debug("Attempt to remove an object from the pool that does not exist: {}", entry);
+
+ return removed;
+ }
+
+ public boolean isClosed()
+ {
+ return closed;
+ }
+
+ @Override
+ public void close()
+ {
+ List<Entry> copy;
+ try (Locker.Lock l = locker.lock())
+ {
+ closed = true;
+ copy = new ArrayList<>(entries);
+ entries.clear();
+ }
+
+ // iterate the copy and close its entries
+ for (Entry entry : copy)
+ {
+ boolean removed = entry.tryRemove();
+ if (removed)
+ {
+ if (entry.pooled instanceof Closeable)
+ IO.close((Closeable)entry.pooled);
+ }
+ else
+ {
+ if (LOGGER.isDebugEnabled())
+ LOGGER.debug("Pooled object still in use: {}", entry);
+ }
+ }
+ }
+
+ public int size()
+ {
+ return entries.size();
+ }
+
+ public Collection<Entry> values()
+ {
+ return Collections.unmodifiableCollection(entries);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ Dumpable.dumpObjects(out, indent, this,
+ new DumpableCollection("entries", entries));
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x[inUse=%d,size=%d,capacity=%d,closed=%b]",
+ getClass().getSimpleName(),
+ hashCode(),
+ getInUseCount(),
+ size(),
+ getMaxEntries(),
+ isClosed());
+ }
+
+ /**
+ * <p>A Pool entry that holds metadata and a pooled object.</p>
+ */
+ public abstract class Entry
+ {
+ // The pooled object. This is not volatile as it is set once and then never changed.
+ // Other threads accessing must check the state field above first, so a good before/after
+ // relationship exists to make a memory barrier.
+ private T pooled;
+
+ /**
+ * <p>Enables this, previously {@link #reserve() reserved}, Entry.</p>
+ * <p>An entry returned from the {@link #reserve()} method must be enabled with this method,
+ * once and only once, before it is usable by the pool.</p>
+ * <p>The entry may be enabled and not acquired, in which case it is immediately available to be
+ * acquired, potentially by another thread; or it can be enabled and acquired atomically so that
+ * no other thread can acquire it, although the acquire may still fail if the pool has been closed.</p>
+ *
+ * @param pooled the pooled object for this Entry
+ * @param acquire whether this Entry should be atomically enabled and acquired
+ * @return whether this Entry was enabled
+ * @throws IllegalStateException if this Entry was already enabled
+ */
+ public boolean enable(T pooled, boolean acquire)
+ {
+ Objects.requireNonNull(pooled);
+
+ if (!isReserved())
+ {
+ if (isClosed())
+ return false; // Pool has been closed
+ throw new IllegalStateException("Entry already enabled: " + this);
+ }
+ this.pooled = pooled;
+
+ if (tryEnable(acquire))
+ return true;
+
+ this.pooled = null;
+ if (isClosed())
+ return false; // Pool has been closed
+ throw new IllegalStateException("Entry already enabled: " + this);
+ }
+
+ /**
+ * @return the pooled object
+ */
+ public T getPooled()
+ {
+ return pooled;
+ }
+
+ /**
+ * <p>Releases this Entry.</p>
+ * <p>This is equivalent to calling {@link Pool#release(Pool.Entry)} passing this entry.</p>
+ *
+ * @return whether this Entry was released
+ */
+ public boolean release()
+ {
+ return Pool.this.release(this);
+ }
+
+ /**
+ * <p>Removes this Entry from the Pool.</p>
+ * <p>This is equivalent to calling {@link Pool#remove(Pool.Entry)} passing this entry.</p>
+ *
+ * @return whether this Entry was removed
+ */
+ public boolean remove()
+ {
+ return Pool.this.remove(this);
+ }
+
+ /**
+ * <p>Tries to enable, and possible also acquire, this Entry.</p>
+ *
+ * @param acquire whether to also acquire this Entry
+ * @return whether this Entry was enabled
+ */
+ abstract boolean tryEnable(boolean acquire);
+
+ /**
+ * <p>Tries to acquire this Entry.</p>
+ *
+ * @return whether this Entry was acquired
+ */
+ abstract boolean tryAcquire();
+
+ /**
+ * <p>Tries to release this Entry.</p>
+ *
+ * @return true if this Entry was released,
+ * false if {@link #tryRemove()} should be called.
+ */
+ abstract boolean tryRelease();
+
+ /**
+ * <p>Tries to remove the entry by marking it as closed.</p>
+ *
+ * @return whether the entry can be removed from the containing pool
+ */
+ abstract boolean tryRemove();
+
+ /**
+ * @return whether this Entry is closed
+ */
+ public abstract boolean isClosed();
+
+ /**
+ * @return whether this Entry is reserved
+ */
+ public abstract boolean isReserved();
+
+ /**
+ * @return whether this Entry is idle
+ */
+ public abstract boolean isIdle();
+
+ /**
+ * @return whether this entry is in use.
+ */
+ public abstract boolean isInUse();
+
+ /**
+ * @return whether this entry has been used beyond {@link #getMaxUsageCount()}
+ * @deprecated MaxUsage functionalities will be removed
+ */
+ @Deprecated
+ public boolean isOverUsed()
+ {
+ return false;
+ }
+
+ boolean isIdleAndOverUsed()
+ {
+ return false;
+ }
+
+ // Only for testing.
+ int getUsageCount()
+ {
+ return 0;
+ }
+
+ // Only for testing.
+ void setUsageCount(int usageCount)
+ {
+ }
+ }
+
+ /**
+ * <p>A Pool entry that holds metadata and a pooled object,
+ * that can only be acquired concurrently at most once, and
+ * can be acquired/released multiple times.</p>
+ */
+ private class MonoEntry extends Entry
+ {
+ // MIN_VALUE => pending; -1 => closed; 0 => idle; 1 => active;
+ private final AtomicInteger state = new AtomicInteger(Integer.MIN_VALUE);
+
+ @Override
+ protected boolean tryEnable(boolean acquire)
+ {
+ return state.compareAndSet(Integer.MIN_VALUE, acquire ? 1 : 0);
+ }
+
+ @Override
+ boolean tryAcquire()
+ {
+ while (true)
+ {
+ int s = state.get();
+ if (s != 0)
+ return false;
+ if (state.compareAndSet(s, 1))
+ return true;
+ }
+ }
+
+ @Override
+ boolean tryRelease()
+ {
+ while (true)
+ {
+ int s = state.get();
+ if (s < 0)
+ return false;
+ if (s == 0)
+ throw new IllegalStateException("Cannot release an already released entry");
+ if (state.compareAndSet(s, 0))
+ return true;
+ }
+ }
+
+ @Override
+ boolean tryRemove()
+ {
+ state.set(-1);
+ return true;
+ }
+
+ @Override
+ public boolean isClosed()
+ {
+ return state.get() < 0;
+ }
+
+ @Override
+ public boolean isReserved()
+ {
+ return state.get() == Integer.MIN_VALUE;
+ }
+
+ @Override
+ public boolean isIdle()
+ {
+ return state.get() == 0;
+ }
+
+ @Override
+ public boolean isInUse()
+ {
+ return state.get() == 1;
+ }
+
+ @Override
+ public String toString()
+ {
+ String s;
+ switch (state.get())
+ {
+ case Integer.MIN_VALUE:
+ s = "PENDING";
+ break;
+ case -1:
+ s = "CLOSED";
+ break;
+ case 0:
+ s = "IDLE";
+ break;
+ default:
+ s = "ACTIVE";
+ }
+ return String.format("%s@%x{%s,pooled=%s}",
+ getClass().getSimpleName(),
+ hashCode(),
+ s,
+ getPooled());
+ }
+ }
+
+ /**
+ * <p>A Pool entry that holds metadata and a pooled object,
+ * that can be acquired concurrently multiple times, and
+ * can be acquired/released multiple times.</p>
+ */
+ class MultiEntry extends Entry
+ {
+ // hi: MIN_VALUE => pending; -1 => closed; 0+ => usage counter;
+ // lo: 0 => idle; positive => multiplex counter
+ private final AtomicBiInteger state;
+
+ MultiEntry()
+ {
+ this.state = new AtomicBiInteger(Integer.MIN_VALUE, 0);
+ }
+
+ @Override
+ void setUsageCount(int usageCount)
+ {
+ this.state.getAndSetHi(usageCount);
+ }
+
+ @Override
+ protected boolean tryEnable(boolean acquire)
+ {
+ int usage = acquire ? 1 : 0;
+ return state.compareAndSet(Integer.MIN_VALUE, usage, 0, usage);
+ }
+
+ /**
+ * <p>Tries to acquire the entry if possible by incrementing both the usage
+ * count and the multiplex count.</p>
+ *
+ * @return true if the usage count is less than {@link #getMaxUsageCount()} and
+ * the multiplex count is less than {@link #getMaxMultiplex(Object)} and
+ * the entry is not closed, false otherwise.
+ */
+ @Override
+ boolean tryAcquire()
+ {
+ while (true)
+ {
+ long encoded = state.get();
+ int usageCount = AtomicBiInteger.getHi(encoded);
+ int multiplexCount = AtomicBiInteger.getLo(encoded);
+ boolean closed = usageCount < 0;
+ if (closed)
+ return false;
+ T pooled = getPooled();
+ int maxUsageCount = getMaxUsageCount(pooled);
+ int maxMultiplexed = getMaxMultiplex(pooled);
+ if (maxMultiplexed > 0 && multiplexCount >= maxMultiplexed)
+ return false;
+ if (maxUsageCount > 0 && usageCount >= maxUsageCount)
+ return false;
+
+ // Prevent overflowing the usage counter by capping it at Integer.MAX_VALUE.
+ int newUsageCount = usageCount == Integer.MAX_VALUE ? Integer.MAX_VALUE : usageCount + 1;
+ if (state.compareAndSet(encoded, newUsageCount, multiplexCount + 1))
+ return true;
+ }
+ }
+
+ /**
+ * <p>Tries to release the entry if possible by decrementing the multiplex
+ * count unless the entity is closed.</p>
+ *
+ * @return true if the entry was released,
+ * false if {@link #tryRemove()} should be called.
+ */
+ @Override
+ boolean tryRelease()
+ {
+ int newMultiplexCount;
+ int usageCount;
+ while (true)
+ {
+ long encoded = state.get();
+ usageCount = AtomicBiInteger.getHi(encoded);
+ boolean closed = usageCount < 0;
+ if (closed)
+ return false;
+
+ newMultiplexCount = AtomicBiInteger.getLo(encoded) - 1;
+ if (newMultiplexCount < 0)
+ throw new IllegalStateException("Cannot release an already released entry");
+
+ if (state.compareAndSet(encoded, usageCount, newMultiplexCount))
+ break;
+ }
+
+ int currentMaxUsageCount = maxUsage;
+ boolean overUsed = currentMaxUsageCount > 0 && usageCount >= currentMaxUsageCount;
+ return !(overUsed && newMultiplexCount == 0);
+ }
+
+ /**
+ * <p>Tries to remove the entry by marking it as closed and decrementing the multiplex counter.</p>
+ * <p>The multiplex counter will never go below zero and if it reaches zero, the entry is considered removed.</p>
+ *
+ * @return true if the entry can be removed from the containing pool, false otherwise.
+ */
+ @Override
+ boolean tryRemove()
+ {
+ while (true)
+ {
+ long encoded = state.get();
+ int usageCount = AtomicBiInteger.getHi(encoded);
+ int multiplexCount = AtomicBiInteger.getLo(encoded);
+ int newMultiplexCount = Math.max(multiplexCount - 1, 0);
+
+ boolean removed = state.compareAndSet(usageCount, -1, multiplexCount, newMultiplexCount);
+ if (removed)
+ return newMultiplexCount == 0;
+ }
+ }
+
+ @Override
+ public boolean isClosed()
+ {
+ return state.getHi() < 0;
+ }
+
+ @Override
+ public boolean isReserved()
+ {
+ return state.getHi() == Integer.MIN_VALUE;
+ }
+
+ @Override
+ public boolean isIdle()
+ {
+ long encoded = state.get();
+ return AtomicBiInteger.getHi(encoded) >= 0 && AtomicBiInteger.getLo(encoded) == 0;
+ }
+
+ @Override
+ public boolean isInUse()
+ {
+ long encoded = state.get();
+ return AtomicBiInteger.getHi(encoded) >= 0 && AtomicBiInteger.getLo(encoded) > 0;
+ }
+
+ @Override
+ public boolean isOverUsed()
+ {
+ int maxUsageCount = getMaxUsageCount();
+ int usageCount = state.getHi();
+ return maxUsageCount > 0 && usageCount >= maxUsageCount;
+ }
+
+ @Override
+ boolean isIdleAndOverUsed()
+ {
+ int maxUsageCount = getMaxUsageCount();
+ long encoded = state.get();
+ int usageCount = AtomicBiInteger.getHi(encoded);
+ int multiplexCount = AtomicBiInteger.getLo(encoded);
+ return maxUsageCount > 0 && usageCount >= maxUsageCount && multiplexCount == 0;
+ }
+
+ @Override
+ int getUsageCount()
+ {
+ return Math.max(state.getHi(), 0);
+ }
+
+ @Override
+ public String toString()
+ {
+ long encoded = state.get();
+ int usageCount = AtomicBiInteger.getHi(encoded);
+ int multiplexCount = AtomicBiInteger.getLo(encoded);
+
+ String state = usageCount < 0
+ ? (usageCount == Integer.MIN_VALUE ? "PENDING" : "CLOSED")
+ : (multiplexCount == 0 ? "IDLE" : "ACTIVE");
+
+ return String.format("%s@%x{%s,usage=%d,multiplex=%d,pooled=%s}",
+ getClass().getSimpleName(),
+ hashCode(),
+ state,
+ Math.max(usageCount, 0),
+ Math.max(multiplexCount, 0),
+ getPooled());
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ProcessorUtils.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ProcessorUtils.java
new file mode 100644
index 0000000..cda45db
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ProcessorUtils.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+/**
+ * <p>ProcessorUtils provides access to runtime info about processors, that may be
+ * overridden by system properties or environment variables.</p>
+ * <p>This can be useful in virtualized environments where the runtime may miss
+ * report the available resources.</p>
+ */
+public class ProcessorUtils
+{
+ public static final String AVAILABLE_PROCESSORS = "JETTY_AVAILABLE_PROCESSORS";
+ private static int __availableProcessors = init();
+
+ static int init()
+ {
+ String processors = System.getProperty(AVAILABLE_PROCESSORS, System.getenv(AVAILABLE_PROCESSORS));
+ if (processors != null)
+ {
+ try
+ {
+ return Integer.parseInt(processors);
+ }
+ catch (NumberFormatException ignored)
+ {
+ }
+ }
+ return Runtime.getRuntime().availableProcessors();
+ }
+
+ /**
+ * Returns the number of available processors, from System Property "JETTY_AVAILABLE_PROCESSORS",
+ * or if not set then from environment variable "JETTY_AVAILABLE_PROCESSORS" or if not set then
+ * from {@link Runtime#availableProcessors()}.
+ *
+ * @return the number of processors
+ */
+ public static int availableProcessors()
+ {
+ return __availableProcessors;
+ }
+
+ public static void setAvailableProcessors(int processors)
+ {
+ if (processors < 1)
+ throw new IllegalArgumentException("Invalid number of processors: " + processors);
+ __availableProcessors = processors;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Promise.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Promise.java
new file mode 100644
index 0000000..9f61d91
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Promise.java
@@ -0,0 +1,157 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.jetty.util.log.Log;
+
+/**
+ * <p>A callback abstraction that handles completed/failed events of asynchronous operations.</p>
+ *
+ * @param <C> the type of the context object
+ */
+public interface Promise<C>
+{
+ /**
+ * <p>Callback invoked when the operation completes.</p>
+ *
+ * @param result the context
+ * @see #failed(Throwable)
+ */
+ default void succeeded(C result)
+ {
+ }
+
+ /**
+ * <p>Callback invoked when the operation fails.</p>
+ *
+ * @param x the reason for the operation failure
+ */
+ default void failed(Throwable x)
+ {
+ }
+
+ /**
+ * <p>Empty implementation of {@link Promise}.</p>
+ *
+ * @param <U> the type of the result
+ */
+ class Adapter<U> implements Promise<U>
+ {
+ @Override
+ public void failed(Throwable x)
+ {
+ Log.getLogger(this.getClass()).warn(x);
+ }
+ }
+
+ /**
+ * <p>Creates a promise from the given incomplete CompletableFuture.</p>
+ * <p>When the promise completes, either succeeding or failing, the
+ * CompletableFuture is also completed, respectively via
+ * {@link CompletableFuture#complete(Object)} or
+ * {@link CompletableFuture#completeExceptionally(Throwable)}.</p>
+ *
+ * @param completable the CompletableFuture to convert into a promise
+ * @param <T> the type of the result
+ * @return a promise that when completed, completes the given CompletableFuture
+ */
+ static <T> Promise<T> from(CompletableFuture<? super T> completable)
+ {
+ if (completable instanceof Promise)
+ return (Promise<T>)completable;
+
+ return new Promise<T>()
+ {
+ @Override
+ public void succeeded(T result)
+ {
+ completable.complete(result);
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ completable.completeExceptionally(x);
+ }
+ };
+ }
+
+ /**
+ * <p>A CompletableFuture that is also a Promise.</p>
+ *
+ * @param <S> the type of the result
+ */
+ class Completable<S> extends CompletableFuture<S> implements Promise<S>
+ {
+ @Override
+ public void succeeded(S result)
+ {
+ complete(result);
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ completeExceptionally(x);
+ }
+ }
+
+ class Wrapper<W> implements Promise<W>
+ {
+ private final Promise<W> promise;
+
+ public Wrapper(Promise<W> promise)
+ {
+ this.promise = Objects.requireNonNull(promise);
+ }
+
+ @Override
+ public void succeeded(W result)
+ {
+ promise.succeeded(result);
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ promise.failed(x);
+ }
+
+ public Promise<W> getPromise()
+ {
+ return promise;
+ }
+
+ public Promise<W> unwrap()
+ {
+ Promise<W> result = promise;
+ while (true)
+ {
+ if (result instanceof Wrapper)
+ result = ((Wrapper<W>)result).unwrap();
+ else
+ break;
+ }
+ return result;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/QuotedStringTokenizer.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/QuotedStringTokenizer.java
new file mode 100644
index 0000000..f1a1d7a
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/QuotedStringTokenizer.java
@@ -0,0 +1,592 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+import java.util.StringTokenizer;
+
+/**
+ * StringTokenizer with Quoting support.
+ *
+ * This class is a copy of the java.util.StringTokenizer API and
+ * the behaviour is the same, except that single and double quoted
+ * string values are recognised.
+ * Delimiters within quotes are not considered delimiters.
+ * Quotes can be escaped with '\'.
+ *
+ * @see java.util.StringTokenizer
+ */
+public class QuotedStringTokenizer
+ extends StringTokenizer
+{
+ private static final String __delim = "\t\n\r";
+ private String _string;
+ private String _delim = __delim;
+ private boolean _returnQuotes = false;
+ private boolean _returnDelimiters = false;
+ private StringBuffer _token;
+ private boolean _hasToken = false;
+ private int _i = 0;
+ private int _lastStart = 0;
+ private boolean _double = true;
+ private boolean _single = true;
+
+ public QuotedStringTokenizer(String str,
+ String delim,
+ boolean returnDelimiters,
+ boolean returnQuotes)
+ {
+ super("");
+ _string = str;
+ if (delim != null)
+ _delim = delim;
+ _returnDelimiters = returnDelimiters;
+ _returnQuotes = returnQuotes;
+
+ if (_delim.indexOf('\'') >= 0 ||
+ _delim.indexOf('"') >= 0)
+ throw new Error("Can't use quotes as delimiters: " + _delim);
+
+ _token = new StringBuffer(_string.length() > 1024 ? 512 : _string.length() / 2);
+ }
+
+ public QuotedStringTokenizer(String str,
+ String delim,
+ boolean returnDelimiters)
+ {
+ this(str, delim, returnDelimiters, false);
+ }
+
+ public QuotedStringTokenizer(String str,
+ String delim)
+ {
+ this(str, delim, false, false);
+ }
+
+ public QuotedStringTokenizer(String str)
+ {
+ this(str, null, false, false);
+ }
+
+ @Override
+ public boolean hasMoreTokens()
+ {
+ // Already found a token
+ if (_hasToken)
+ return true;
+
+ _lastStart = _i;
+
+ int state = 0;
+ boolean escape = false;
+ while (_i < _string.length())
+ {
+ char c = _string.charAt(_i++);
+
+ switch (state)
+ {
+ case 0: // Start
+ if (_delim.indexOf(c) >= 0)
+ {
+ if (_returnDelimiters)
+ {
+ _token.append(c);
+ return _hasToken = true;
+ }
+ }
+ else if (c == '\'' && _single)
+ {
+ if (_returnQuotes)
+ _token.append(c);
+ state = 2;
+ }
+ else if (c == '\"' && _double)
+ {
+ if (_returnQuotes)
+ _token.append(c);
+ state = 3;
+ }
+ else
+ {
+ _token.append(c);
+ _hasToken = true;
+ state = 1;
+ }
+ break;
+
+ case 1: // Token
+ _hasToken = true;
+ if (_delim.indexOf(c) >= 0)
+ {
+ if (_returnDelimiters)
+ _i--;
+ return _hasToken;
+ }
+ else if (c == '\'' && _single)
+ {
+ if (_returnQuotes)
+ _token.append(c);
+ state = 2;
+ }
+ else if (c == '\"' && _double)
+ {
+ if (_returnQuotes)
+ _token.append(c);
+ state = 3;
+ }
+ else
+ {
+ _token.append(c);
+ }
+ break;
+
+ case 2: // Single Quote
+ _hasToken = true;
+ if (escape)
+ {
+ escape = false;
+ _token.append(c);
+ }
+ else if (c == '\'')
+ {
+ if (_returnQuotes)
+ _token.append(c);
+ state = 1;
+ }
+ else if (c == '\\')
+ {
+ if (_returnQuotes)
+ _token.append(c);
+ escape = true;
+ }
+ else
+ {
+ _token.append(c);
+ }
+ break;
+
+ case 3: // Double Quote
+ _hasToken = true;
+ if (escape)
+ {
+ escape = false;
+ _token.append(c);
+ }
+ else if (c == '\"')
+ {
+ if (_returnQuotes)
+ _token.append(c);
+ state = 1;
+ }
+ else if (c == '\\')
+ {
+ if (_returnQuotes)
+ _token.append(c);
+ escape = true;
+ }
+ else
+ {
+ _token.append(c);
+ }
+ break;
+ }
+ }
+
+ return _hasToken;
+ }
+
+ @Override
+ public String nextToken()
+ throws NoSuchElementException
+ {
+ if (!hasMoreTokens() || _token == null)
+ throw new NoSuchElementException();
+ String t = _token.toString();
+ _token.setLength(0);
+ _hasToken = false;
+ return t;
+ }
+
+ @Override
+ public String nextToken(String delim)
+ throws NoSuchElementException
+ {
+ _delim = delim;
+ _i = _lastStart;
+ _token.setLength(0);
+ _hasToken = false;
+ return nextToken();
+ }
+
+ @Override
+ public boolean hasMoreElements()
+ {
+ return hasMoreTokens();
+ }
+
+ @Override
+ public Object nextElement()
+ throws NoSuchElementException
+ {
+ return nextToken();
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public int countTokens()
+ {
+ return -1;
+ }
+
+ /**
+ * Quote a string.
+ * The string is quoted only if quoting is required due to
+ * embedded delimiters, quote characters or the
+ * empty string.
+ *
+ * @param s The string to quote.
+ * @param delim the delimiter to use to quote the string
+ * @return quoted string
+ */
+ public static String quoteIfNeeded(String s, String delim)
+ {
+ if (s == null)
+ return null;
+ if (s.length() == 0)
+ return "\"\"";
+
+ for (int i = 0; i < s.length(); i++)
+ {
+ char c = s.charAt(i);
+ if (c == '\\' || c == '"' || c == '\'' || Character.isWhitespace(c) || delim.indexOf(c) >= 0)
+ {
+ StringBuffer b = new StringBuffer(s.length() + 8);
+ quote(b, s);
+ return b.toString();
+ }
+ }
+
+ return s;
+ }
+
+ /**
+ * Quote a string.
+ * The string is quoted only if quoting is required due to
+ * embedded delimiters, quote characters or the
+ * empty string.
+ *
+ * @param s The string to quote.
+ * @return quoted string
+ */
+ public static String quote(String s)
+ {
+ if (s == null)
+ return null;
+ if (s.length() == 0)
+ return "\"\"";
+
+ StringBuffer b = new StringBuffer(s.length() + 8);
+ quote(b, s);
+ return b.toString();
+ }
+
+ private static final char[] escapes = new char[32];
+
+ static
+ {
+ Arrays.fill(escapes, (char)0xFFFF);
+ escapes['\b'] = 'b';
+ escapes['\t'] = 't';
+ escapes['\n'] = 'n';
+ escapes['\f'] = 'f';
+ escapes['\r'] = 'r';
+ }
+
+ /**
+ * Quote a string into an Appendable.
+ * Only quotes and backslash are escaped.
+ *
+ * @param buffer The Appendable
+ * @param input The String to quote.
+ */
+ public static void quoteOnly(Appendable buffer, String input)
+ {
+ if (input == null)
+ return;
+
+ try
+ {
+ buffer.append('"');
+ for (int i = 0; i < input.length(); ++i)
+ {
+ char c = input.charAt(i);
+ if (c == '"' || c == '\\')
+ buffer.append('\\');
+ buffer.append(c);
+ }
+ buffer.append('"');
+ }
+ catch (IOException x)
+ {
+ throw new RuntimeException(x);
+ }
+ }
+
+ /**
+ * Quote a string into an Appendable.
+ * The characters ", \, \n, \r, \t, \f and \b are escaped
+ *
+ * @param buffer The Appendable
+ * @param input The String to quote.
+ */
+ public static void quote(Appendable buffer, String input)
+ {
+ if (input == null)
+ return;
+
+ try
+ {
+ buffer.append('"');
+ for (int i = 0; i < input.length(); ++i)
+ {
+ char c = input.charAt(i);
+ if (c >= 32)
+ {
+ if (c == '"' || c == '\\')
+ buffer.append('\\');
+ buffer.append(c);
+ }
+ else
+ {
+ char escape = escapes[c];
+ if (escape == 0xFFFF)
+ {
+ // Unicode escape
+ buffer.append('\\').append('u').append('0').append('0');
+ if (c < 0x10)
+ buffer.append('0');
+ buffer.append(Integer.toString(c, 16));
+ }
+ else
+ {
+ buffer.append('\\').append(escape);
+ }
+ }
+ }
+ buffer.append('"');
+ }
+ catch (IOException x)
+ {
+ throw new RuntimeException(x);
+ }
+ }
+
+ public static String unquoteOnly(String s)
+ {
+ return unquoteOnly(s, false);
+ }
+
+ /**
+ * Unquote a string, NOT converting unicode sequences
+ *
+ * @param s The string to unquote.
+ * @param lenient if true, will leave in backslashes that aren't valid escapes
+ * @return quoted string
+ */
+ public static String unquoteOnly(String s, boolean lenient)
+ {
+ if (s == null)
+ return null;
+ if (s.length() < 2)
+ return s;
+
+ char first = s.charAt(0);
+ char last = s.charAt(s.length() - 1);
+ if (first != last || (first != '"' && first != '\''))
+ return s;
+
+ StringBuilder b = new StringBuilder(s.length() - 2);
+ boolean escape = false;
+ for (int i = 1; i < s.length() - 1; i++)
+ {
+ char c = s.charAt(i);
+
+ if (escape)
+ {
+ escape = false;
+ if (lenient && !isValidEscaping(c))
+ {
+ b.append('\\');
+ }
+ b.append(c);
+ }
+ else if (c == '\\')
+ {
+ escape = true;
+ }
+ else
+ {
+ b.append(c);
+ }
+ }
+
+ return b.toString();
+ }
+
+ public static String unquote(String s)
+ {
+ return unquote(s, false);
+ }
+
+ /**
+ * Unquote a string.
+ *
+ * @param s The string to unquote.
+ * @param lenient true if unquoting should be lenient to escaped content, leaving some alone, false if string unescaping
+ * @return quoted string
+ */
+ public static String unquote(String s, boolean lenient)
+ {
+ if (s == null)
+ return null;
+ if (s.length() < 2)
+ return s;
+
+ char first = s.charAt(0);
+ char last = s.charAt(s.length() - 1);
+ if (first != last || (first != '"' && first != '\''))
+ return s;
+
+ StringBuilder b = new StringBuilder(s.length() - 2);
+ boolean escape = false;
+ for (int i = 1; i < s.length() - 1; i++)
+ {
+ char c = s.charAt(i);
+
+ if (escape)
+ {
+ escape = false;
+ switch (c)
+ {
+ case 'n':
+ b.append('\n');
+ break;
+ case 'r':
+ b.append('\r');
+ break;
+ case 't':
+ b.append('\t');
+ break;
+ case 'f':
+ b.append('\f');
+ break;
+ case 'b':
+ b.append('\b');
+ break;
+ case '\\':
+ b.append('\\');
+ break;
+ case '/':
+ b.append('/');
+ break;
+ case '"':
+ b.append('"');
+ break;
+ case 'u':
+ b.append((char)(
+ (TypeUtil.convertHexDigit((byte)s.charAt(i++)) << 24) +
+ (TypeUtil.convertHexDigit((byte)s.charAt(i++)) << 16) +
+ (TypeUtil.convertHexDigit((byte)s.charAt(i++)) << 8) +
+ (TypeUtil.convertHexDigit((byte)s.charAt(i++)))
+ )
+ );
+ break;
+ default:
+ if (lenient && !isValidEscaping(c))
+ {
+ b.append('\\');
+ }
+ b.append(c);
+ }
+ }
+ else if (c == '\\')
+ {
+ escape = true;
+ }
+ else
+ {
+ b.append(c);
+ }
+ }
+
+ return b.toString();
+ }
+
+ /**
+ * Check that char c (which is preceded by a backslash) is a valid
+ * escape sequence.
+ */
+ private static boolean isValidEscaping(char c)
+ {
+ return ((c == 'n') || (c == 'r') || (c == 't') ||
+ (c == 'f') || (c == 'b') || (c == '\\') ||
+ (c == '/') || (c == '"') || (c == 'u'));
+ }
+
+ public static boolean isQuoted(String s)
+ {
+ return s != null && s.length() > 0 && s.charAt(0) == '"' && s.charAt(s.length() - 1) == '"';
+ }
+
+ /**
+ * @return handle double quotes if true
+ */
+ public boolean getDouble()
+ {
+ return _double;
+ }
+
+ /**
+ * @param d handle double quotes if true
+ */
+ public void setDouble(boolean d)
+ {
+ _double = d;
+ }
+
+ /**
+ * @return handle single quotes if true
+ */
+ public boolean getSingle()
+ {
+ return _single;
+ }
+
+ /**
+ * @param single handle single quotes if true
+ */
+ public void setSingle(boolean single)
+ {
+ _single = single;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java
new file mode 100644
index 0000000..933c0e9
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java
@@ -0,0 +1,166 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.EnumSet;
+
+/**
+ * ReadLineInputStream
+ *
+ * Read from an input stream, accepting CR/LF, LF or just CR.
+ */
+@Deprecated
+public class ReadLineInputStream extends BufferedInputStream
+{
+ boolean _seenCRLF;
+ boolean _skipLF;
+ private EnumSet<Termination> _lineTerminations = EnumSet.noneOf(Termination.class);
+
+ public EnumSet<Termination> getLineTerminations()
+ {
+ return _lineTerminations;
+ }
+
+ public enum Termination
+ {
+ CRLF,
+ LF,
+ CR,
+ EOF
+ }
+
+ public ReadLineInputStream(InputStream in)
+ {
+ super(in);
+ }
+
+ public ReadLineInputStream(InputStream in, int size)
+ {
+ super(in, size);
+ }
+
+ public String readLine() throws IOException
+ {
+ mark(buf.length);
+
+ while (true)
+ {
+ int b = super.read();
+
+ if (markpos < 0)
+ throw new IOException("Buffer size exceeded: no line terminator");
+
+ if (_skipLF && b != '\n')
+ _lineTerminations.add(Termination.CR);
+
+ if (b == -1)
+ {
+ int m = markpos;
+ markpos = -1;
+ if (pos > m)
+ {
+ _lineTerminations.add(Termination.EOF);
+ return new String(buf, m, pos - m, StandardCharsets.UTF_8);
+ }
+ return null;
+ }
+
+ if (b == '\r')
+ {
+ int p = pos;
+
+ // if we have seen CRLF before, hungrily consume LF
+ if (_seenCRLF && pos < count)
+ {
+ if (buf[pos] == '\n')
+ {
+ _lineTerminations.add(Termination.CRLF);
+ pos += 1;
+ }
+ else
+ {
+ _lineTerminations.add(Termination.CR);
+ }
+ }
+ else
+ _skipLF = true;
+
+ int m = markpos;
+ markpos = -1;
+ return new String(buf, m, p - m - 1, StandardCharsets.UTF_8);
+ }
+
+ if (b == '\n')
+ {
+ if (_skipLF)
+ {
+ _skipLF = false;
+ _seenCRLF = true;
+ markpos++;
+ _lineTerminations.add(Termination.CRLF);
+ continue;
+ }
+ int m = markpos;
+ markpos = -1;
+ _lineTerminations.add(Termination.LF);
+ return new String(buf, m, pos - m - 1, StandardCharsets.UTF_8);
+ }
+ }
+ }
+
+ @Override
+ public synchronized int read() throws IOException
+ {
+ int b = super.read();
+ if (_skipLF)
+ {
+ _skipLF = false;
+ if (_seenCRLF && b == '\n')
+ b = super.read();
+ }
+ return b;
+ }
+
+ @Override
+ public synchronized int read(byte[] buf, int off, int len) throws IOException
+ {
+ if (_skipLF && len > 0)
+ {
+ _skipLF = false;
+ if (_seenCRLF)
+ {
+ int b = super.read();
+ if (b == -1)
+ return -1;
+
+ if (b != '\n')
+ {
+ buf[off] = (byte)(0xff & b);
+ return 1 + super.read(buf, off + 1, len - 1);
+ }
+ }
+ }
+
+ return super.read(buf, off, len);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/RegexSet.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/RegexSet.java
new file mode 100644
index 0000000..2b4bbb5
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/RegexSet.java
@@ -0,0 +1,110 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.AbstractSet;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+
+/**
+ * A Set of Regular expressions strings.
+ * <p>
+ * Provides the efficient {@link #matches(String)} method to check for a match against all the combined Regex's
+ */
+public class RegexSet extends AbstractSet<String> implements Predicate<String>
+{
+ private final Set<String> _patterns = new HashSet<String>();
+ private final Set<String> _unmodifiable = Collections.unmodifiableSet(_patterns);
+ private Pattern _pattern;
+
+ @Override
+ public Iterator<String> iterator()
+ {
+ return _unmodifiable.iterator();
+ }
+
+ @Override
+ public int size()
+ {
+ return _patterns.size();
+ }
+
+ @Override
+ public boolean add(String pattern)
+ {
+ boolean added = _patterns.add(pattern);
+ if (added)
+ updatePattern();
+ return added;
+ }
+
+ @Override
+ public boolean remove(Object pattern)
+ {
+ boolean removed = _patterns.remove(pattern);
+
+ if (removed)
+ updatePattern();
+ return removed;
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return _patterns.isEmpty();
+ }
+
+ @Override
+ public void clear()
+ {
+ _patterns.clear();
+ _pattern = null;
+ }
+
+ private void updatePattern()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append("^(");
+ for (String pattern : _patterns)
+ {
+ if (builder.length() > 2)
+ builder.append('|');
+ builder.append('(');
+ builder.append(pattern);
+ builder.append(')');
+ }
+ builder.append(")$");
+ _pattern = Pattern.compile(builder.toString());
+ }
+
+ @Override
+ public boolean test(String s)
+ {
+ return _pattern != null && _pattern.matcher(s).matches();
+ }
+
+ public boolean matches(String s)
+ {
+ return _pattern != null && _pattern.matcher(s).matches();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Retainable.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Retainable.java
new file mode 100644
index 0000000..c1cabbf
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Retainable.java
@@ -0,0 +1,24 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+public interface Retainable
+{
+ void retain();
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/RolloverFileOutputStream.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/RolloverFileOutputStream.java
new file mode 100644
index 0000000..fdb05f9
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/RolloverFileOutputStream.java
@@ -0,0 +1,433 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.text.SimpleDateFormat;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * RolloverFileOutputStream.
+ *
+ * <p>
+ * This output stream puts content in a file that is rolled over every 24 hours.
+ * The filename must include the string "yyyy_mm_dd", which is replaced with the
+ * actual date when creating and rolling over the file.
+ * </p>
+ * <p>
+ * Old files are retained for a number of days before being deleted.
+ * </p>
+ */
+public class RolloverFileOutputStream extends OutputStream
+{
+ private static Timer __rollover;
+
+ static final String YYYY_MM_DD = "yyyy_mm_dd";
+ static final String ROLLOVER_FILE_DATE_FORMAT = "yyyy_MM_dd";
+ static final String ROLLOVER_FILE_BACKUP_FORMAT = "HHmmssSSS";
+ static final int ROLLOVER_FILE_RETAIN_DAYS = 31;
+
+ private OutputStream _out;
+ private RollTask _rollTask;
+ private SimpleDateFormat _fileBackupFormat;
+ private SimpleDateFormat _fileDateFormat;
+
+ private String _filename;
+ private File _file;
+ private boolean _append;
+ private int _retainDays;
+
+ /**
+ * @param filename The filename must include the string "yyyy_mm_dd",
+ * which is replaced with the actual date when creating and rolling over the file.
+ * @throws IOException if unable to create output
+ */
+ public RolloverFileOutputStream(String filename)
+ throws IOException
+ {
+ this(filename, true, ROLLOVER_FILE_RETAIN_DAYS);
+ }
+
+ /**
+ * @param filename The filename must include the string "yyyy_mm_dd",
+ * which is replaced with the actual date when creating and rolling over the file.
+ * @param append If true, existing files will be appended to.
+ * @throws IOException if unable to create output
+ */
+ public RolloverFileOutputStream(String filename, boolean append)
+ throws IOException
+ {
+ this(filename, append, ROLLOVER_FILE_RETAIN_DAYS);
+ }
+
+ /**
+ * @param filename The filename must include the string "yyyy_mm_dd",
+ * which is replaced with the actual date when creating and rolling over the file.
+ * @param append If true, existing files will be appended to.
+ * @param retainDays The number of days to retain files before deleting them. 0 to retain forever.
+ * @throws IOException if unable to create output
+ */
+ public RolloverFileOutputStream(String filename,
+ boolean append,
+ int retainDays)
+ throws IOException
+ {
+ this(filename, append, retainDays, TimeZone.getDefault());
+ }
+
+ /**
+ * @param filename The filename must include the string "yyyy_mm_dd",
+ * which is replaced with the actual date when creating and rolling over the file.
+ * @param append If true, existing files will be appended to.
+ * @param retainDays The number of days to retain files before deleting them. 0 to retain forever.
+ * @param zone the timezone for the output
+ * @throws IOException if unable to create output
+ */
+ public RolloverFileOutputStream(String filename,
+ boolean append,
+ int retainDays,
+ TimeZone zone)
+ throws IOException
+ {
+ this(filename, append, retainDays, zone, null, null, ZonedDateTime.now(zone.toZoneId()));
+ }
+
+ /**
+ * @param filename The filename must include the string "yyyy_mm_dd",
+ * which is replaced with the actual date when creating and rolling over the file.
+ * @param append If true, existing files will be appended to.
+ * @param retainDays The number of days to retain files before deleting them. 0 to retain forever.
+ * @param zone the timezone for the output
+ * @param dateFormat The format for the date file substitution. The default is "yyyy_MM_dd". If set to the
+ * empty string, the file is rolledover to the same filename, with the current file being renamed to the backup filename.
+ * @param backupFormat The format for the file extension of backup files. The default is "HHmmssSSS".
+ * @throws IOException if unable to create output
+ */
+ public RolloverFileOutputStream(String filename,
+ boolean append,
+ int retainDays,
+ TimeZone zone,
+ String dateFormat,
+ String backupFormat)
+ throws IOException
+ {
+ this(filename, append, retainDays, zone, dateFormat, backupFormat, ZonedDateTime.now(zone.toZoneId()));
+ }
+
+ RolloverFileOutputStream(String filename,
+ boolean append,
+ int retainDays,
+ TimeZone zone,
+ String dateFormat,
+ String backupFormat,
+ ZonedDateTime now)
+ throws IOException
+ {
+ if (dateFormat == null)
+ dateFormat = ROLLOVER_FILE_DATE_FORMAT;
+ _fileDateFormat = new SimpleDateFormat(dateFormat);
+
+ if (backupFormat == null)
+ backupFormat = ROLLOVER_FILE_BACKUP_FORMAT;
+ _fileBackupFormat = new SimpleDateFormat(backupFormat);
+
+ _fileBackupFormat.setTimeZone(zone);
+ _fileDateFormat.setTimeZone(zone);
+
+ if (filename != null)
+ {
+ filename = filename.trim();
+ if (filename.length() == 0)
+ filename = null;
+ }
+ if (filename == null)
+ throw new IllegalArgumentException("Invalid filename");
+
+ _filename = filename;
+ _append = append;
+ _retainDays = retainDays;
+
+ // Calculate Today's Midnight, based on Configured TimeZone (will be in past, even if by a few milliseconds)
+ setFile(now);
+
+ synchronized (RolloverFileOutputStream.class)
+ {
+ if (__rollover == null)
+ __rollover = new Timer(RolloverFileOutputStream.class.getName(), true);
+ }
+
+ // This will schedule the rollover event to the next midnight
+ scheduleNextRollover(now);
+ }
+
+ /**
+ * Get the "start of day" for the provided DateTime at the zone specified.
+ *
+ * @param now the date time to calculate from
+ * @return start of the day of the date provided
+ */
+ public static ZonedDateTime toMidnight(ZonedDateTime now)
+ {
+ return now.toLocalDate().atStartOfDay(now.getZone()).plus(1, ChronoUnit.DAYS);
+ }
+
+ private void scheduleNextRollover(ZonedDateTime now)
+ {
+ _rollTask = new RollTask();
+ // Get tomorrow's midnight based on Configured TimeZone
+ ZonedDateTime midnight = toMidnight(now);
+
+ // Schedule next rollover event to occur, based on local machine's Unix Epoch milliseconds
+ long delay = midnight.toInstant().toEpochMilli() - now.toInstant().toEpochMilli();
+ synchronized (RolloverFileOutputStream.class)
+ {
+ __rollover.schedule(_rollTask, delay);
+ }
+ }
+
+ public String getFilename()
+ {
+ return _filename;
+ }
+
+ public String getDatedFilename()
+ {
+ if (_file == null)
+ return null;
+ return _file.toString();
+ }
+
+ public int getRetainDays()
+ {
+ return _retainDays;
+ }
+
+ void setFile(ZonedDateTime now)
+ throws IOException
+ {
+ File oldFile = null;
+ File newFile = null;
+ File backupFile = null;
+ synchronized (this)
+ {
+ // Check directory
+ File file = new File(_filename);
+ _filename = file.getCanonicalPath();
+ file = new File(_filename);
+ File dir = new File(file.getParent());
+ if (!dir.isDirectory() || !dir.canWrite())
+ throw new IOException("Cannot write log directory " + dir);
+
+ // Is this a rollover file?
+ String filename = file.getName();
+ int datePattern = filename.toLowerCase(Locale.ENGLISH).indexOf(YYYY_MM_DD);
+ if (datePattern >= 0)
+ {
+ file = new File(dir,
+ filename.substring(0, datePattern) +
+ _fileDateFormat.format(new Date(now.toInstant().toEpochMilli())) +
+ filename.substring(datePattern + YYYY_MM_DD.length()));
+ }
+
+ if (file.exists() && !file.canWrite())
+ throw new IOException("Cannot write log file " + file);
+
+ // Do we need to change the output stream?
+ if (_out == null || datePattern >= 0)
+ {
+ // Yep
+ oldFile = _file;
+ _file = file;
+ newFile = _file;
+
+ OutputStream oldOut = _out;
+ if (oldOut != null)
+ oldOut.close();
+
+ if (!_append && file.exists())
+ {
+ backupFile = new File(file.toString() + "." + _fileBackupFormat.format(new Date(now.toInstant().toEpochMilli())));
+ renameFile(file, backupFile);
+ }
+ _out = new FileOutputStream(file.toString(), _append);
+ //if(log.isDebugEnabled())log.debug("Opened "+_file);
+ }
+ }
+
+ if (newFile != null)
+ rollover(oldFile, backupFile, newFile);
+ }
+
+ private void renameFile(File src, File dest) throws IOException
+ {
+ // Try old school rename
+ if (!src.renameTo(dest))
+ {
+ try
+ {
+ // Try new move
+ Files.move(src.toPath(), dest.toPath());
+ }
+ catch (IOException e)
+ {
+ // Copy
+ Files.copy(src.toPath(), dest.toPath());
+ // Delete
+ Files.deleteIfExists(src.toPath());
+ }
+ }
+ }
+
+ /**
+ * This method is called whenever a log file is rolled over
+ *
+ * @param oldFile The original filename or null if this is the first creation
+ * @param backupFile The backup filename or null if the filename is dated.
+ * @param newFile The new filename that is now being used for logging
+ */
+ protected void rollover(File oldFile, File backupFile, File newFile)
+ {
+ }
+
+ void removeOldFiles(ZonedDateTime now)
+ {
+ if (_retainDays > 0)
+ {
+ // Establish expiration time, based on configured TimeZone
+ long expired = now.minus(_retainDays, ChronoUnit.DAYS).toInstant().toEpochMilli();
+
+ File file = new File(_filename);
+ File dir = new File(file.getParent());
+ String fn = file.getName();
+ int s = fn.toLowerCase(Locale.ENGLISH).indexOf(YYYY_MM_DD);
+ if (s < 0)
+ return;
+ String prefix = fn.substring(0, s);
+ String suffix = fn.substring(s + YYYY_MM_DD.length());
+
+ String[] logList = dir.list();
+ for (int i = 0; i < logList.length; i++)
+ {
+ fn = logList[i];
+ if (fn.startsWith(prefix) && fn.indexOf(suffix, prefix.length()) >= 0)
+ {
+ File f = new File(dir, fn);
+ if (f.lastModified() < expired)
+ {
+ f.delete();
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void write(int b) throws IOException
+ {
+ synchronized (this)
+ {
+ _out.write(b);
+ }
+ }
+
+ @Override
+ public void write(byte[] buf)
+ throws IOException
+ {
+ synchronized (this)
+ {
+ _out.write(buf);
+ }
+ }
+
+ @Override
+ public void write(byte[] buf, int off, int len)
+ throws IOException
+ {
+ synchronized (this)
+ {
+ _out.write(buf, off, len);
+ }
+ }
+
+ @Override
+ public void flush() throws IOException
+ {
+ synchronized (this)
+ {
+ _out.flush();
+ }
+ }
+
+ @Override
+ public void close()
+ throws IOException
+ {
+ synchronized (this)
+ {
+ try
+ {
+ _out.close();
+ }
+ finally
+ {
+ _out = null;
+ _file = null;
+ }
+ }
+
+ synchronized (RolloverFileOutputStream.class)
+ {
+ if (_rollTask != null)
+ {
+ _rollTask.cancel();
+ }
+ }
+ }
+
+ private class RollTask extends TimerTask
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ ZonedDateTime now = ZonedDateTime.now(_fileDateFormat.getTimeZone().toZoneId());
+ RolloverFileOutputStream.this.setFile(now);
+ RolloverFileOutputStream.this.removeOldFiles(now);
+ RolloverFileOutputStream.this.scheduleNextRollover(now);
+ }
+ catch (Throwable t)
+ {
+ // Cannot log this exception to a LOG, as RolloverFOS can be used by logging
+ t.printStackTrace(System.err);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java
new file mode 100644
index 0000000..264be49
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java
@@ -0,0 +1,898 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.function.Predicate;
+
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Scanner
+ *
+ * Utility for scanning a directory for added, removed and changed
+ * files and reporting these events via registered Listeners.
+ */
+public class Scanner extends AbstractLifeCycle
+{
+ /**
+ * When walking a directory, a depth of 1 ensures that
+ * the directory's descendants are visited, not just the
+ * directory itself (as a file).
+ *
+ * @see Visitor#preVisitDirectory
+ */
+ public static final int DEFAULT_SCAN_DEPTH = 1;
+ public static final int MAX_SCAN_DEPTH = Integer.MAX_VALUE;
+
+ private static final Logger LOG = Log.getLogger(Scanner.class);
+ private static int __scannerId = 0;
+ private int _scanInterval;
+ private int _scanCount = 0;
+ private final List<Listener> _listeners = new ArrayList<>();
+ private final Map<String, TimeNSize> _prevScan = new HashMap<>();
+ private final Map<String, TimeNSize> _currentScan = new HashMap<>();
+ private FilenameFilter _filter;
+ private final Map<Path, IncludeExcludeSet<PathMatcher, Path>> _scannables = new HashMap<>();
+ private volatile boolean _running = false;
+ private boolean _reportExisting = true;
+ private boolean _reportDirs = true;
+ private Timer _timer;
+ private TimerTask _task;
+ private int _scanDepth = DEFAULT_SCAN_DEPTH;
+
+ public enum Notification
+ {
+ ADDED, CHANGED, REMOVED
+ }
+
+ private final Map<String, Notification> _notifications = new HashMap<>();
+
+ /**
+ * PathMatcherSet
+ *
+ * A set of PathMatchers for testing Paths against path matching patterns via
+ * @see IncludeExcludeSet
+ */
+ static class PathMatcherSet extends HashSet<PathMatcher> implements Predicate<Path>
+ {
+ @Override
+ public boolean test(Path p)
+ {
+ for (PathMatcher pm : this)
+ {
+ if (pm.matches(p))
+ return true;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * TimeNSize
+ *
+ * Metadata about a file: Last modified time and file size.
+ */
+ static class TimeNSize
+ {
+ final long _lastModified;
+ final long _size;
+
+ public TimeNSize(long lastModified, long size)
+ {
+ _lastModified = lastModified;
+ _size = size;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return (int)_lastModified ^ (int)_size;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (o instanceof TimeNSize)
+ {
+ TimeNSize tns = (TimeNSize)o;
+ return tns._lastModified == _lastModified && tns._size == _size;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "[lm=" + _lastModified + ",s=" + _size + "]";
+ }
+ }
+
+ /**
+ * Visitor
+ *
+ * A FileVisitor for walking a subtree of paths. The Scanner uses
+ * this to examine the dirs and files it has been asked to scan.
+ */
+ class Visitor implements FileVisitor<Path>
+ {
+ Map<String, TimeNSize> scanInfoMap;
+ IncludeExcludeSet<PathMatcher, Path> rootIncludesExcludes;
+ Path root;
+
+ public Visitor(Path root, IncludeExcludeSet<PathMatcher, Path> rootIncludesExcludes, Map<String, TimeNSize> scanInfoMap)
+ {
+ this.root = root;
+ this.rootIncludesExcludes = rootIncludesExcludes;
+ this.scanInfoMap = scanInfoMap;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
+ {
+ if (!Files.exists(dir))
+ return FileVisitResult.SKIP_SUBTREE;
+
+ File f = dir.toFile();
+
+ //if we want to report directories and we haven't already seen it
+ if (_reportDirs && !scanInfoMap.containsKey(f.getCanonicalPath()))
+ {
+ boolean accepted = false;
+ if (rootIncludesExcludes != null && !rootIncludesExcludes.isEmpty())
+ {
+ //accepted if not explicitly excluded and either is explicitly included or there are no explicit inclusions
+ boolean result = rootIncludesExcludes.test(dir);
+ if (result)
+ accepted = true;
+ }
+ else
+ {
+ if (_filter == null || _filter.accept(f.getParentFile(), f.getName()))
+ accepted = true;
+ }
+
+ if (accepted)
+ {
+ scanInfoMap.put(f.getCanonicalPath(), new TimeNSize(f.lastModified(), f.isDirectory() ? 0 : f.length()));
+ if (LOG.isDebugEnabled()) LOG.debug("scan accepted dir {} mod={}", f, f.lastModified());
+ }
+ }
+
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
+ {
+ if (!Files.exists(file))
+ return FileVisitResult.CONTINUE;
+
+ File f = file.toFile();
+ boolean accepted = false;
+
+ if (f.isFile() || (f.isDirectory() && _reportDirs && !scanInfoMap.containsKey(f.getCanonicalPath())))
+ {
+ if (rootIncludesExcludes != null && !rootIncludesExcludes.isEmpty())
+ {
+ //accepted if not explicitly excluded and either is explicitly included or there are no explicit inclusions
+ boolean result = rootIncludesExcludes.test(file);
+ if (result)
+ accepted = true;
+ }
+ else if (_filter == null || _filter.accept(f.getParentFile(), f.getName()))
+ accepted = true;
+ }
+
+ if (accepted)
+ {
+ scanInfoMap.put(f.getCanonicalPath(), new TimeNSize(f.lastModified(), f.isDirectory() ? 0 : f.length()));
+ if (LOG.isDebugEnabled()) LOG.debug("scan accepted {} mod={}", f, f.lastModified());
+ }
+
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException
+ {
+ LOG.warn(exc);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException
+ {
+ return FileVisitResult.CONTINUE;
+ }
+ }
+
+ /**
+ * Listener
+ *
+ * Marker for notifications re file changes.
+ */
+ public interface Listener
+ {
+ }
+
+ public interface ScanListener extends Listener
+ {
+ void scan();
+ }
+
+ public interface DiscreteListener extends Listener
+ {
+ void fileChanged(String filename) throws Exception;
+
+ void fileAdded(String filename) throws Exception;
+
+ void fileRemoved(String filename) throws Exception;
+ }
+
+ public interface BulkListener extends Listener
+ {
+ void filesChanged(List<String> filenames) throws Exception;
+ }
+
+ /**
+ * Listener that notifies when a scan has started and when it has ended.
+ */
+ public interface ScanCycleListener extends Listener
+ {
+ void scanStarted(int cycle) throws Exception;
+
+ void scanEnded(int cycle) throws Exception;
+ }
+
+ /**
+ *
+ */
+ public Scanner()
+ {
+ }
+
+ /**
+ * Get the scan interval
+ *
+ * @return interval between scans in seconds
+ */
+ public synchronized int getScanInterval()
+ {
+ return _scanInterval;
+ }
+
+ /**
+ * Set the scan interval
+ *
+ * @param scanInterval pause between scans in seconds, or 0 for no scan after the initial scan.
+ */
+ public synchronized void setScanInterval(int scanInterval)
+ {
+ _scanInterval = scanInterval;
+ schedule();
+ }
+
+ public void setScanDirs(List<File> dirs)
+ {
+ _scannables.clear();
+ if (dirs == null)
+ return;
+
+ for (File f:dirs)
+ {
+ addScanDir(f);
+ }
+ }
+
+ @Deprecated
+ public synchronized void addScanDir(File dir)
+ {
+ if (dir == null)
+ return;
+ try
+ {
+ if (dir.isDirectory())
+ addDirectory(dir.toPath());
+ else
+ addFile(dir.toPath());
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ /**
+ * Add a file to be scanned. The file must not be null, and must exist.
+ *
+ * @param p the Path of the file to scan.
+ * @throws IOException
+ */
+ public synchronized void addFile(Path p) throws IOException
+ {
+ if (p == null)
+ throw new IllegalStateException("Null path");
+
+ File f = p.toFile();
+ if (!f.exists() || f.isDirectory())
+ throw new IllegalStateException("Not file or doesn't exist: " + f.getCanonicalPath());
+ _scannables.put(p, null);
+ }
+
+ /**
+ * Add a directory to be scanned. The directory must not be null and must exist.
+ *
+ * @param p the directory to scan.
+ * @return an IncludeExcludeSet to which the caller can add PathMatcher patterns to match
+ * @throws IOException
+ */
+ public synchronized IncludeExcludeSet<PathMatcher, Path> addDirectory(Path p)
+ throws IOException
+ {
+ if (p == null)
+ throw new IllegalStateException("Null path");
+
+ File f = p.toFile();
+ if (!f.exists() || !f.isDirectory())
+ throw new IllegalStateException("Not directory or doesn't exist: " + f.getCanonicalPath());
+
+ IncludeExcludeSet<PathMatcher, Path> includesExcludes = _scannables.get(p);
+ if (includesExcludes == null)
+ {
+ includesExcludes = new IncludeExcludeSet<>(PathMatcherSet.class);
+ _scannables.put(p.toRealPath(), includesExcludes);
+ }
+
+ return includesExcludes;
+ }
+
+ @Deprecated
+ public List<File> getScanDirs()
+ {
+ ArrayList<File> files = new ArrayList<>();
+ for (Path p : _scannables.keySet())
+ files.add(p.toFile());
+ return Collections.unmodifiableList(files);
+ }
+
+ public Set<Path> getScannables()
+ {
+ return _scannables.keySet();
+ }
+
+ /**
+ * @param recursive True if scanning is recursive
+ * @see #setScanDepth(int)
+ */
+ @Deprecated
+ public void setRecursive(boolean recursive)
+ {
+ _scanDepth = recursive ? Integer.MAX_VALUE : 1;
+ }
+
+ /**
+ * @return True if scanning is recursive
+ * @see #getScanDepth()
+ */
+ @Deprecated
+ public boolean getRecursive()
+ {
+ return _scanDepth > 1;
+ }
+
+ /**
+ * Get the scanDepth.
+ *
+ * @return the scanDepth
+ */
+ public int getScanDepth()
+ {
+ return _scanDepth;
+ }
+
+ /**
+ * Set the scanDepth.
+ *
+ * @param scanDepth the scanDepth to set
+ */
+ public void setScanDepth(int scanDepth)
+ {
+ _scanDepth = scanDepth;
+ }
+
+ /**
+ * Apply a filter to files found in the scan directory.
+ * Only files matching the filter will be reported as added/changed/removed.
+ *
+ * @param filter the filename filter to use
+ */
+ @Deprecated
+ public void setFilenameFilter(FilenameFilter filter)
+ {
+ _filter = filter;
+ }
+
+ /**
+ * Get any filter applied to files in the scan dir.
+ *
+ * @return the filename filter
+ */
+ @Deprecated
+ public FilenameFilter getFilenameFilter()
+ {
+ return _filter;
+ }
+
+ /**
+ * Whether or not an initial scan will report all files as being
+ * added.
+ *
+ * @param reportExisting if true, all files found on initial scan will be
+ * reported as being added, otherwise not
+ */
+ public void setReportExistingFilesOnStartup(boolean reportExisting)
+ {
+ _reportExisting = reportExisting;
+ }
+
+ public boolean getReportExistingFilesOnStartup()
+ {
+ return _reportExisting;
+ }
+
+ /**
+ * Set if found directories should be reported.
+ *
+ * @param dirs true to report directory changes as well
+ */
+ public void setReportDirs(boolean dirs)
+ {
+ _reportDirs = dirs;
+ }
+
+ public boolean getReportDirs()
+ {
+ return _reportDirs;
+ }
+
+ /**
+ * Add an added/removed/changed listener
+ *
+ * @param listener the listener to add
+ */
+ public synchronized void addListener(Listener listener)
+ {
+ if (listener == null)
+ return;
+ _listeners.add(listener);
+ }
+
+ /**
+ * Remove a registered listener
+ *
+ * @param listener the Listener to be removed
+ */
+ public synchronized void removeListener(Listener listener)
+ {
+ if (listener == null)
+ return;
+ _listeners.remove(listener);
+ }
+
+ /**
+ * Start the scanning action.
+ */
+ @Override
+ public synchronized void doStart()
+ {
+ if (_running)
+ return;
+
+ _running = true;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Scanner start: rprtExists={}, depth={}, rprtDirs={}, interval={}, filter={}, scannables={}",
+ _reportExisting, _scanDepth, _reportDirs, _scanInterval, _filter, _scannables);
+
+ if (_reportExisting)
+ {
+ // if files exist at startup, report them
+ scan();
+ scan(); // scan twice so files reported as stable
+ }
+ else
+ {
+ //just register the list of existing files and only report changes
+ scanFiles();
+ _prevScan.putAll(_currentScan);
+ }
+ schedule();
+ }
+
+ public TimerTask newTimerTask()
+ {
+ return new TimerTask()
+ {
+ @Override
+ public void run()
+ {
+ scan();
+ }
+ };
+ }
+
+ public Timer newTimer()
+ {
+ return new Timer("Scanner-" + __scannerId++, true);
+ }
+
+ public void schedule()
+ {
+ if (_running)
+ {
+ if (_timer != null)
+ _timer.cancel();
+ if (_task != null)
+ _task.cancel();
+ if (getScanInterval() > 0)
+ {
+ _timer = newTimer();
+ _task = newTimerTask();
+ _timer.schedule(_task, 1010L * getScanInterval(), 1010L * getScanInterval());
+ }
+ }
+ }
+
+ /**
+ * Stop the scanning.
+ */
+ @Override
+ public synchronized void doStop()
+ {
+ if (_running)
+ {
+ _running = false;
+ if (_timer != null)
+ _timer.cancel();
+ if (_task != null)
+ _task.cancel();
+ _task = null;
+ _timer = null;
+ }
+ }
+
+ /**
+ * Clear the list of scannables. The scanner must first
+ * be in the stopped state.
+ */
+ public void reset()
+ {
+ if (!isStopped())
+ throw new IllegalStateException("Not stopped");
+
+ //clear the scannables
+ _scannables.clear();
+
+ //clear the previous scans
+ _currentScan.clear();
+ _prevScan.clear();
+ }
+
+ /**
+ * @param path tests if the path exists
+ * @return true if the path exists in one of the scandirs
+ */
+ public boolean exists(String path)
+ {
+ for (Path p : _scannables.keySet())
+ {
+ if (p.resolve(path).toFile().exists())
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Perform a pass of the scanner and report changes
+ */
+ public synchronized void scan()
+ {
+ reportScanStart(++_scanCount);
+ scanFiles();
+ reportDifferences(_currentScan, _prevScan);
+ _prevScan.clear();
+ _prevScan.putAll(_currentScan);
+ reportScanEnd(_scanCount);
+
+ for (Listener l : _listeners)
+ {
+ try
+ {
+ if (l instanceof ScanListener)
+ ((ScanListener)l).scan();
+ }
+ catch (Throwable e)
+ {
+ LOG.warn(e);
+ }
+ }
+ }
+
+ /**
+ * Scan all of the given paths.
+ */
+ public synchronized void scanFiles()
+ {
+ _currentScan.clear();
+ for (Entry<Path, IncludeExcludeSet<PathMatcher, Path>> entry : _scannables.entrySet())
+ {
+ Path p = entry.getKey();
+ try
+ {
+ Files.walkFileTree(p, EnumSet.allOf(FileVisitOption.class), _scanDepth, new Visitor(p, entry.getValue(), _currentScan));
+ }
+ catch (IOException e)
+ {
+ LOG.warn("Error scanning files.", e);
+ }
+ }
+ }
+
+ /**
+ * Report the adds/changes/removes to the registered listeners
+ *
+ * @param currentScan the info from the most recent pass
+ * @param oldScan info from the previous pass
+ */
+ private synchronized void reportDifferences(Map<String, TimeNSize> currentScan, Map<String, TimeNSize> oldScan)
+ {
+ // scan the differences and add what was found to the map of notifications:
+ Set<String> oldScanKeys = new HashSet<>(oldScan.keySet());
+
+ // Look for new and changed files
+ for (Map.Entry<String, TimeNSize> entry : currentScan.entrySet())
+ {
+ String file = entry.getKey();
+ if (!oldScanKeys.contains(file))
+ {
+ Notification old = _notifications.put(file, Notification.ADDED);
+ if (old != null)
+ {
+ switch (old)
+ {
+ case REMOVED:
+ case CHANGED:
+ _notifications.put(file, Notification.CHANGED);
+ }
+ }
+ }
+ else if (!oldScan.get(file).equals(currentScan.get(file)))
+ {
+ Notification old = _notifications.put(file, Notification.CHANGED);
+ if (old == Notification.ADDED)
+ _notifications.put(file, Notification.ADDED);
+ }
+ }
+
+ // Look for deleted files
+ for (String file : oldScan.keySet())
+ {
+ if (!currentScan.containsKey(file))
+ {
+ Notification old = _notifications.put(file, Notification.REMOVED);
+ if (old == Notification.ADDED)
+ _notifications.remove(file);
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("scanned " + _scannables.keySet() + ": " + _notifications);
+
+ // Process notifications
+ // Only process notifications that are for stable files (ie same in old and current scan).
+ List<String> bulkChanges = new ArrayList<>();
+ for (Iterator<Entry<String, Notification>> iter = _notifications.entrySet().iterator(); iter.hasNext(); )
+ {
+
+ Entry<String, Notification> entry = iter.next();
+ String file = entry.getKey();
+ // Is the file stable?
+ if (oldScan.containsKey(file))
+ {
+ if (!oldScan.get(file).equals(currentScan.get(file)))
+ continue;
+ }
+ else if (currentScan.containsKey(file))
+ continue;
+
+ // File is stable so notify
+ Notification notification = entry.getValue();
+ iter.remove();
+ bulkChanges.add(file);
+ switch (notification)
+ {
+ case ADDED:
+ reportAddition(file);
+ break;
+ case CHANGED:
+ reportChange(file);
+ break;
+ case REMOVED:
+ reportRemoval(file);
+ break;
+ }
+ }
+ if (!bulkChanges.isEmpty())
+ reportBulkChanges(bulkChanges);
+ }
+
+ private void warn(Object listener, String filename, Throwable th)
+ {
+ LOG.warn(listener + " failed on '" + filename, th);
+ }
+
+ /**
+ * Report a file addition to the registered FileAddedListeners
+ *
+ * @param filename the filename
+ */
+ private void reportAddition(String filename)
+ {
+ for (Listener l : _listeners)
+ {
+ try
+ {
+ if (l instanceof DiscreteListener)
+ ((DiscreteListener)l).fileAdded(filename);
+ }
+ catch (Throwable e)
+ {
+ warn(l, filename, e);
+ }
+ }
+ }
+
+ /**
+ * Report a file removal to the FileRemovedListeners
+ *
+ * @param filename the filename
+ */
+ private void reportRemoval(String filename)
+ {
+ for (Object l : _listeners)
+ {
+ try
+ {
+ if (l instanceof DiscreteListener)
+ ((DiscreteListener)l).fileRemoved(filename);
+ }
+ catch (Throwable e)
+ {
+ warn(l, filename, e);
+ }
+ }
+ }
+
+ /**
+ * Report a file change to the FileChangedListeners
+ *
+ * @param filename the filename
+ */
+ private void reportChange(String filename)
+ {
+ for (Listener l : _listeners)
+ {
+ try
+ {
+ if (l instanceof DiscreteListener)
+ ((DiscreteListener)l).fileChanged(filename);
+ }
+ catch (Throwable e)
+ {
+ warn(l, filename, e);
+ }
+ }
+ }
+
+ /**
+ * Report the list of filenames for which changes were detected.
+ *
+ * @param filenames names of all files added/changed/removed
+ */
+ private void reportBulkChanges(List<String> filenames)
+ {
+ for (Listener l : _listeners)
+ {
+ try
+ {
+ if (l instanceof BulkListener)
+ ((BulkListener)l).filesChanged(filenames);
+ }
+ catch (Throwable e)
+ {
+ warn(l, filenames.toString(), e);
+ }
+ }
+ }
+
+ /**
+ * Call ScanCycleListeners with start of scan
+ */
+ private void reportScanStart(int cycle)
+ {
+ for (Listener listener : _listeners)
+ {
+ try
+ {
+ if (listener instanceof ScanCycleListener)
+ {
+ ((ScanCycleListener)listener).scanStarted(cycle);
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn(listener + " failed on scan start for cycle " + cycle, e);
+ }
+ }
+ }
+
+ /**
+ * Call ScanCycleListeners with end of scan.
+ */
+ private void reportScanEnd(int cycle)
+ {
+ for (Listener listener : _listeners)
+ {
+ try
+ {
+ if (listener instanceof ScanCycleListener)
+ {
+ ((ScanCycleListener)listener).scanEnded(cycle);
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn(listener + " failed on scan end for cycle " + cycle, e);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/SearchPattern.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/SearchPattern.java
new file mode 100644
index 0000000..f0bd684
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/SearchPattern.java
@@ -0,0 +1,192 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+/**
+ * SearchPattern
+ *
+ * Fast search for patterns within strings and arrays of bytes.
+ * Uses an implementation of the Boyer–Moore–Horspool algorithm
+ * with a 256 character alphabet.
+ *
+ * The algorithm has an average-case complexity of O(n)
+ * on random text and O(nm) in the worst case.
+ * where:
+ * m = pattern length
+ * n = length of data to search
+ */
+public class SearchPattern
+{
+ private static final int ALPHABET_SIZE = 256;
+ private final int[] table = new int[ALPHABET_SIZE];
+ private final byte[] pattern;
+
+ /**
+ * Produces a SearchPattern instance which can be used
+ * to find matches of the pattern in data
+ *
+ * @param pattern byte array containing the pattern
+ * @return a new SearchPattern instance using the given pattern
+ */
+ public static SearchPattern compile(byte[] pattern)
+ {
+ return new SearchPattern(Arrays.copyOf(pattern, pattern.length));
+ }
+
+ /**
+ * Produces a SearchPattern instance which can be used
+ * to find matches of the pattern in data
+ *
+ * @param pattern string containing the pattern
+ * @return a new SearchPattern instance using the given pattern
+ */
+ public static SearchPattern compile(String pattern)
+ {
+ return new SearchPattern(pattern.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * @param pattern byte array containing the pattern used for matching
+ */
+ private SearchPattern(byte[] pattern)
+ {
+ this.pattern = pattern;
+ if (pattern.length == 0)
+ throw new IllegalArgumentException("Empty Pattern");
+
+ //Build up the pre-processed table for this pattern.
+ Arrays.fill(table, pattern.length);
+ for (int i = 0; i < pattern.length - 1; ++i)
+ {
+ table[0xff & pattern[i]] = pattern.length - 1 - i;
+ }
+ }
+
+ /**
+ * Search for a complete match of the pattern within the data
+ *
+ * @param data The data in which to search for. The data may be arbitrary binary data,
+ * but the pattern will always be {@link StandardCharsets#US_ASCII} encoded.
+ * @param offset The offset within the data to start the search
+ * @param length The length of the data to search
+ * @return The index within the data array at which the first instance of the pattern or -1 if not found
+ */
+ public int match(byte[] data, int offset, int length)
+ {
+ validateArgs(data, offset, length);
+
+ int skip = offset;
+ while (skip <= offset + length - pattern.length)
+ {
+ for (int i = pattern.length - 1; data[skip + i] == pattern[i]; i--)
+ {
+ if (i == 0)
+ return skip;
+ }
+
+ skip += table[0xff & data[skip + pattern.length - 1]];
+ }
+
+ return -1;
+ }
+
+ /**
+ * Search for a partial match of the pattern at the end of the data.
+ *
+ * @param data The data in which to search for. The data may be arbitrary binary data,
+ * but the pattern will always be {@link StandardCharsets#US_ASCII} encoded.
+ * @param offset The offset within the data to start the search
+ * @param length The length of the data to search
+ * @return the length of the partial pattern matched and 0 for no match.
+ */
+ public int endsWith(byte[] data, int offset, int length)
+ {
+ validateArgs(data, offset, length);
+
+ int skip = (pattern.length <= length) ? (offset + length - pattern.length) : offset;
+ while (skip < offset + length)
+ {
+ for (int i = (offset + length - 1) - skip; data[skip + i] == pattern[i]; --i)
+ {
+ if (i == 0)
+ return (offset + length - skip);
+ }
+
+ // We can't use the pre-processed table as we are not matching on the full pattern.
+ skip++;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Search for a possibly partial match of the pattern at the start of the data.
+ *
+ * @param data The data in which to search for. The data may be arbitrary binary data,
+ * but the pattern will always be {@link StandardCharsets#US_ASCII} encoded.
+ * @param offset The offset within the data to start the search
+ * @param length The length of the data to search
+ * @param matched The length of the partial pattern already matched
+ * @return the length of the partial pattern matched and 0 for no match.
+ */
+ public int startsWith(byte[] data, int offset, int length, int matched)
+ {
+ validateArgs(data, offset, length);
+
+ int matchedCount = 0;
+ for (int i = 0; i < pattern.length - matched && i < length; i++)
+ {
+ if (data[offset + i] == pattern[i + matched])
+ matchedCount++;
+ else
+ return 0;
+ }
+
+ return matched + matchedCount;
+ }
+
+ /**
+ * Performs legality checks for standard arguments input into SearchPattern methods.
+ *
+ * @param data The data in which to search for. The data may be arbitrary binary data,
+ * but the pattern will always be {@link StandardCharsets#US_ASCII} encoded.
+ * @param offset The offset within the data to start the search
+ * @param length The length of the data to search
+ */
+ private void validateArgs(byte[] data, int offset, int length)
+ {
+ if (offset < 0)
+ throw new IllegalArgumentException("offset was negative");
+ else if (length < 0)
+ throw new IllegalArgumentException("length was negative");
+ else if (offset + length > data.length)
+ throw new IllegalArgumentException("(offset+length) out of bounds of data[]");
+ }
+
+ /**
+ * @return The length of the pattern in bytes.
+ */
+ public int getLength()
+ {
+ return pattern.length;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/SharedBlockingCallback.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/SharedBlockingCallback.java
new file mode 100644
index 0000000..daad08c
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/SharedBlockingCallback.java
@@ -0,0 +1,331 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.Objects;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Provides a reusable {@link Callback} that can block the thread
+ * while waiting to be completed.
+ * <p>
+ * A typical usage pattern is:
+ * <pre>
+ * void someBlockingCall(Object... args) throws IOException
+ * {
+ * try(Blocker blocker = sharedBlockingCallback.acquire())
+ * {
+ * someAsyncCall(args, blocker);
+ * blocker.block();
+ * }
+ * }
+ * </pre>
+ */
+public class SharedBlockingCallback
+{
+ private static final Logger LOG = Log.getLogger(SharedBlockingCallback.class);
+
+ private static final Throwable IDLE = new ConstantThrowable("IDLE");
+ private static final Throwable SUCCEEDED = new ConstantThrowable("SUCCEEDED");
+
+ private static final Throwable FAILED = new ConstantThrowable("FAILED");
+
+ private final ReentrantLock _lock = new ReentrantLock();
+ private final Condition _idle = _lock.newCondition();
+ private final Condition _complete = _lock.newCondition();
+ private Blocker _blocker = new Blocker();
+
+ @Deprecated
+ protected long getIdleTimeout()
+ {
+ return -1;
+ }
+
+ public Blocker acquire() throws IOException
+ {
+ long idle = getIdleTimeout();
+ _lock.lock();
+ try
+ {
+ while (_blocker._state != IDLE)
+ {
+ if (idle > 0 && (idle < Long.MAX_VALUE / 2))
+ {
+ // Wait a little bit longer than the blocker might block
+ if (!_idle.await(idle * 2, TimeUnit.MILLISECONDS))
+ throw new IOException(new TimeoutException());
+ }
+ else
+ _idle.await();
+ }
+ _blocker._state = null;
+ return _blocker;
+ }
+ catch (InterruptedException x)
+ {
+ throw new InterruptedIOException();
+ }
+ finally
+ {
+ _lock.unlock();
+ }
+ }
+
+ public boolean fail(Throwable cause)
+ {
+ Objects.requireNonNull(cause);
+ _lock.lock();
+ try
+ {
+ if (_blocker._state == null)
+ {
+ _blocker._state = new BlockerFailedException(cause);
+ _complete.signalAll();
+ return true;
+ }
+ }
+ finally
+ {
+ _lock.unlock();
+ }
+ return false;
+ }
+
+ protected void notComplete(Blocker blocker)
+ {
+ LOG.warn("Blocker not complete {}", blocker);
+ if (LOG.isDebugEnabled())
+ LOG.debug(new Throwable());
+ }
+
+ /**
+ * A Closeable Callback.
+ * Uses the auto close mechanism to check block has been called OK.
+ * <p>Implements {@link Callback} because calls to this
+ * callback do not blocak, rather they wakeup the thread that is blocked
+ * in {@link #block()}
+ */
+ public class Blocker implements Callback, Closeable
+ {
+ private Throwable _state = IDLE;
+
+ protected Blocker()
+ {
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return InvocationType.NON_BLOCKING;
+ }
+
+ @Override
+ public void succeeded()
+ {
+ _lock.lock();
+ try
+ {
+ if (_state == null)
+ {
+ _state = SUCCEEDED;
+ _complete.signalAll();
+ }
+ else
+ {
+ LOG.warn("Succeeded after {}", _state.toString());
+ if (LOG.isDebugEnabled())
+ LOG.debug(_state);
+ }
+ }
+ finally
+ {
+ _lock.unlock();
+ }
+ }
+
+ @Override
+ public void failed(Throwable cause)
+ {
+ _lock.lock();
+ try
+ {
+ if (_state == null)
+ {
+ if (cause == null)
+ _state = FAILED;
+ else if (cause instanceof BlockerTimeoutException)
+ // Not this blockers timeout
+ _state = new IOException(cause);
+ else
+ _state = cause;
+ _complete.signalAll();
+ }
+ else if (_state instanceof BlockerTimeoutException || _state instanceof BlockerFailedException)
+ {
+ // Failure arrived late, block() already
+ // modified the state, nothing more to do.
+ if (LOG.isDebugEnabled())
+ LOG.debug("Failed after {}", _state);
+ }
+ else
+ {
+ LOG.warn("Failed after {}: {}", _state, cause);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug(_state);
+ LOG.debug(cause);
+ }
+ }
+ }
+ finally
+ {
+ _lock.unlock();
+ }
+ }
+
+ /**
+ * Block until the Callback has succeeded or failed and after the return leave in the state to allow reuse. This is useful for code that wants to
+ * repeatable use a FutureCallback to convert an asynchronous API to a blocking API.
+ *
+ * @throws IOException if exception was caught during blocking, or callback was cancelled
+ */
+ public void block() throws IOException
+ {
+ long idle = getIdleTimeout();
+ _lock.lock();
+ try
+ {
+ while (_state == null)
+ {
+ if (idle > 0)
+ {
+ // Waiting here may compete with the idle timeout mechanism,
+ // so here we wait a little bit longer to favor the normal
+ // idle timeout mechanism that will call failed(Throwable).
+ long excess = Math.min(idle / 2, 1000);
+ if (!_complete.await(idle + excess, TimeUnit.MILLISECONDS))
+ {
+ // Method failed(Throwable) has not been called yet,
+ // so we will synthesize a special TimeoutException.
+ _state = new BlockerTimeoutException();
+ }
+ }
+ else
+ {
+ _complete.await();
+ }
+ }
+
+ if (_state == SUCCEEDED)
+ return;
+ if (_state == IDLE)
+ throw new IllegalStateException("IDLE");
+ if (_state instanceof IOException)
+ throw (IOException)_state;
+ if (_state instanceof CancellationException)
+ throw (CancellationException)_state;
+ if (_state instanceof RuntimeException)
+ throw (RuntimeException)_state;
+ if (_state instanceof Error)
+ throw (Error)_state;
+ throw new IOException(_state);
+ }
+ catch (final InterruptedException e)
+ {
+ _state = e;
+ throw new InterruptedIOException();
+ }
+ finally
+ {
+ _lock.unlock();
+ }
+ }
+
+ /**
+ * Check the Callback has succeeded or failed and after the return leave in the state to allow reuse.
+ */
+ @Override
+ public void close()
+ {
+ _lock.lock();
+ try
+ {
+ if (_state == IDLE)
+ throw new IllegalStateException("IDLE");
+ if (_state == null)
+ notComplete(this);
+ }
+ finally
+ {
+ try
+ {
+ // If we have a failure
+ if (_state != null && _state != SUCCEEDED)
+ // create a new Blocker
+ _blocker = new Blocker();
+ else
+ // else reuse Blocker
+ _state = IDLE;
+ _idle.signalAll();
+ _complete.signalAll();
+ }
+ finally
+ {
+ _lock.unlock();
+ }
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ _lock.lock();
+ try
+ {
+ return String.format("%s@%x{%s}", Blocker.class.getSimpleName(), hashCode(), _state);
+ }
+ finally
+ {
+ _lock.unlock();
+ }
+ }
+ }
+
+ private static class BlockerTimeoutException extends TimeoutException
+ {
+ }
+
+ private static class BlockerFailedException extends Exception
+ {
+ public BlockerFailedException(Throwable cause)
+ {
+ super(cause);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/SocketAddressResolver.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/SocketAddressResolver.java
new file mode 100644
index 0000000..699315d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/SocketAddressResolver.java
@@ -0,0 +1,202 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * <p>Creates {@link SocketAddress} instances, returning them through a {@link Promise}.</p>
+ */
+public interface SocketAddressResolver
+{
+ /**
+ * Resolves the given host and port, returning a {@link SocketAddress} through the given {@link Promise}
+ * with the default timeout.
+ *
+ * @param host the host to resolve
+ * @param port the port of the resulting socket address
+ * @param promise the callback invoked when the resolution succeeds or fails
+ */
+ void resolve(String host, int port, Promise<List<InetSocketAddress>> promise);
+
+ /**
+ * <p>Creates {@link SocketAddress} instances synchronously in the caller thread.</p>
+ */
+ @ManagedObject("The synchronous address resolver")
+ class Sync implements SocketAddressResolver
+ {
+ @Override
+ public void resolve(String host, int port, Promise<List<InetSocketAddress>> promise)
+ {
+ try
+ {
+ InetAddress[] addresses = InetAddress.getAllByName(host);
+
+ List<InetSocketAddress> result = new ArrayList<>(addresses.length);
+ for (InetAddress address : addresses)
+ {
+ result.add(new InetSocketAddress(address, port));
+ }
+
+ if (result.isEmpty())
+ promise.failed(new UnknownHostException());
+ else
+ promise.succeeded(result);
+ }
+ catch (Throwable x)
+ {
+ promise.failed(x);
+ }
+ }
+ }
+
+ /**
+ * <p>Creates {@link SocketAddress} instances asynchronously in a different thread.</p>
+ * <p>{@link InetSocketAddress#InetSocketAddress(String, int)} attempts to perform a DNS
+ * resolution of the host name, and this may block for several seconds.
+ * This class creates the {@link InetSocketAddress} in a separate thread and provides the result
+ * through a {@link Promise}, with the possibility to specify a timeout for the operation.</p>
+ * <p>Example usage:</p>
+ * <pre>
+ * SocketAddressResolver resolver = new SocketAddressResolver.Async(executor, scheduler, timeout);
+ * resolver.resolve("www.google.com", 80, new Promise<SocketAddress>()
+ * {
+ * public void succeeded(SocketAddress result)
+ * {
+ * // The address was resolved
+ * }
+ *
+ * public void failed(Throwable failure)
+ * {
+ * // The address resolution failed
+ * }
+ * });
+ * </pre>
+ */
+ @ManagedObject("The asynchronous address resolver")
+ class Async implements SocketAddressResolver
+ {
+ private static final Logger LOG = Log.getLogger(SocketAddressResolver.class);
+
+ private final Executor executor;
+ private final Scheduler scheduler;
+ private final long timeout;
+
+ /**
+ * Creates a new instance with the given executor (to perform DNS resolution in a separate thread),
+ * the given scheduler (to cancel the operation if it takes too long) and the given timeout, in milliseconds.
+ *
+ * @param executor the thread pool to use to perform DNS resolution in pooled threads
+ * @param scheduler the scheduler to schedule tasks to cancel DNS resolution if it takes too long
+ * @param timeout the timeout, in milliseconds, for the DNS resolution to complete
+ */
+ public Async(Executor executor, Scheduler scheduler, long timeout)
+ {
+ this.executor = executor;
+ this.scheduler = scheduler;
+ this.timeout = timeout;
+ }
+
+ public Executor getExecutor()
+ {
+ return executor;
+ }
+
+ public Scheduler getScheduler()
+ {
+ return scheduler;
+ }
+
+ @ManagedAttribute(value = "The timeout, in milliseconds, to resolve an address", readonly = true)
+ public long getTimeout()
+ {
+ return timeout;
+ }
+
+ @Override
+ public void resolve(final String host, final int port, final Promise<List<InetSocketAddress>> promise)
+ {
+ executor.execute(() ->
+ {
+ Scheduler.Task task = null;
+ final AtomicBoolean complete = new AtomicBoolean();
+ if (timeout > 0)
+ {
+ final Thread thread = Thread.currentThread();
+ task = scheduler.schedule(() ->
+ {
+ if (complete.compareAndSet(false, true))
+ {
+ promise.failed(new TimeoutException("DNS timeout " + getTimeout() + " ms"));
+ thread.interrupt();
+ }
+ }, timeout, TimeUnit.MILLISECONDS);
+ }
+
+ try
+ {
+ long start = System.nanoTime();
+ InetAddress[] addresses = InetAddress.getAllByName(host);
+ long elapsed = System.nanoTime() - start;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Resolved {} in {} ms", host, TimeUnit.NANOSECONDS.toMillis(elapsed));
+
+ List<InetSocketAddress> result = new ArrayList<>(addresses.length);
+ for (InetAddress address : addresses)
+ {
+ result.add(new InetSocketAddress(address, port));
+ }
+
+ if (complete.compareAndSet(false, true))
+ {
+ if (result.isEmpty())
+ promise.failed(new UnknownHostException());
+ else
+ promise.succeeded(result);
+ }
+ }
+ catch (Throwable x)
+ {
+ if (complete.compareAndSet(false, true))
+ promise.failed(x);
+ }
+ finally
+ {
+ if (task != null)
+ task.cancel();
+ }
+ });
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java
new file mode 100644
index 0000000..3453caa
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java
@@ -0,0 +1,1190 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Fast String Utilities.
+ *
+ * These string utilities provide both convenience methods and
+ * performance improvements over most standard library versions. The
+ * main aim of the optimizations is to avoid object creation unless
+ * absolutely required.
+ */
+public class StringUtil
+{
+ private static final Trie<String> CHARSETS = new ArrayTrie<>(256);
+
+ public static final String ALL_INTERFACES = "0.0.0.0";
+ public static final String CRLF = IO.CRLF;
+
+ /**
+ * @deprecated use {@link System#lineSeparator()} instead
+ */
+ @Deprecated
+ public static final String __LINE_SEPARATOR = System.lineSeparator();
+
+ public static final String __ISO_8859_1 = "iso-8859-1";
+ public static final String __UTF8 = "utf-8";
+ public static final String __UTF16 = "utf-16";
+
+ static
+ {
+ CHARSETS.put("utf-8", __UTF8);
+ CHARSETS.put("utf8", __UTF8);
+ CHARSETS.put("utf-16", __UTF16);
+ CHARSETS.put("utf16", __UTF16);
+ CHARSETS.put("iso-8859-1", __ISO_8859_1);
+ CHARSETS.put("iso_8859_1", __ISO_8859_1);
+ }
+
+ /**
+ * Convert alternate charset names (eg utf8) to normalized
+ * name (eg UTF-8).
+ *
+ * @param s the charset to normalize
+ * @return the normalized charset (or null if normalized version not found)
+ */
+ public static String normalizeCharset(String s)
+ {
+ String n = CHARSETS.get(s);
+ return (n == null) ? s : n;
+ }
+
+ /**
+ * Convert alternate charset names (eg utf8) to normalized
+ * name (eg UTF-8).
+ *
+ * @param s the charset to normalize
+ * @param offset the offset in the charset
+ * @param length the length of the charset in the input param
+ * @return the normalized charset (or null if not found)
+ */
+ public static String normalizeCharset(String s, int offset, int length)
+ {
+ String n = CHARSETS.get(s, offset, length);
+ return (n == null) ? s.substring(offset, offset + length) : n;
+ }
+
+ // @checkstyle-disable-check : IllegalTokenTextCheck
+
+ public static final char[] lowercases = {
+ '\000', '\001', '\002', '\003', '\004', '\005', '\006', '\007',
+ '\010', '\011', '\012', '\013', '\014', '\015', '\016', '\017',
+ '\020', '\021', '\022', '\023', '\024', '\025', '\026', '\027',
+ '\030', '\031', '\032', '\033', '\034', '\035', '\036', '\037',
+ '\040', '\041', '\042', '\043', '\044', '\045', '\046', '\047',
+ '\050', '\051', '\052', '\053', '\054', '\055', '\056', '\057',
+ '\060', '\061', '\062', '\063', '\064', '\065', '\066', '\067',
+ '\070', '\071', '\072', '\073', '\074', '\075', '\076', '\077',
+ '\100', '\141', '\142', '\143', '\144', '\145', '\146', '\147',
+ '\150', '\151', '\152', '\153', '\154', '\155', '\156', '\157',
+ '\160', '\161', '\162', '\163', '\164', '\165', '\166', '\167',
+ '\170', '\171', '\172', '\133', '\134', '\135', '\136', '\137',
+ '\140', '\141', '\142', '\143', '\144', '\145', '\146', '\147',
+ '\150', '\151', '\152', '\153', '\154', '\155', '\156', '\157',
+ '\160', '\161', '\162', '\163', '\164', '\165', '\166', '\167',
+ '\170', '\171', '\172', '\173', '\174', '\175', '\176', '\177'
+ };
+
+ /**
+ * fast lower case conversion. Only works on ascii (not unicode)
+ *
+ * @param s the string to convert
+ * @return a lower case version of s
+ */
+ public static String asciiToLowerCase(String s)
+ {
+ if (s == null)
+ return null;
+
+ char[] c = null;
+ int i = s.length();
+ // look for first conversion
+ while (i-- > 0)
+ {
+ char c1 = s.charAt(i);
+ if (c1 <= 127)
+ {
+ char c2 = lowercases[c1];
+ if (c1 != c2)
+ {
+ c = s.toCharArray();
+ c[i] = c2;
+ break;
+ }
+ }
+ }
+ while (i-- > 0)
+ {
+ if (c[i] <= 127)
+ c[i] = lowercases[c[i]];
+ }
+
+ return c == null ? s : new String(c);
+ }
+
+ /**
+ * Replace all characters from input string that are known to have
+ * special meaning in various filesystems.
+ *
+ * <p>
+ * This will replace all of the following characters
+ * with a "{@code _}" (underscore).
+ * </p>
+ * <ul>
+ * <li>Control Characters</li>
+ * <li>Anything not 7-bit printable ASCII</li>
+ * <li>Special characters: pipe, redirect, combine, slash, equivalence, bang, glob, selection, etc...</li>
+ * <li>Space</li>
+ * </ul>
+ *
+ * @param str the raw input string
+ * @return the sanitized output string. or null if {@code str} is null.
+ */
+ public static String sanitizeFileSystemName(String str)
+ {
+ if (str == null)
+ return null;
+
+ char[] chars = str.toCharArray();
+ int len = chars.length;
+ for (int i = 0; i < len; i++)
+ {
+ char c = chars[i];
+ if ((c <= 0x1F) || // control characters
+ (c >= 0x7F) || // over 7-bit printable ASCII
+ // piping : special meaning on unix / osx / windows
+ (c == '|') || (c == '>') || (c == '<') || (c == '/') || (c == '&') ||
+ // special characters on windows
+ (c == '\\') || (c == '.') || (c == ':') ||
+ // special characters on osx
+ (c == '=') || (c == '"') || (c == ',') ||
+ // glob / selection characters on most OS's
+ (c == '*') || (c == '?') ||
+ // bang execution on unix / osx
+ (c == '!') ||
+ // spaces are just generally difficult to work with
+ (c == ' '))
+ {
+ chars[i] = '_';
+ }
+ }
+ return String.valueOf(chars);
+ }
+
+ public static boolean startsWithIgnoreCase(String s, String w)
+ {
+ if (w == null)
+ return true;
+
+ if (s == null || s.length() < w.length())
+ return false;
+
+ for (int i = 0; i < w.length(); i++)
+ {
+ char c1 = s.charAt(i);
+ char c2 = w.charAt(i);
+ if (c1 != c2)
+ {
+ if (c1 <= 127)
+ c1 = lowercases[c1];
+ if (c2 <= 127)
+ c2 = lowercases[c2];
+ if (c1 != c2)
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static boolean endsWithIgnoreCase(String s, String w)
+ {
+ if (w == null)
+ return true;
+ if (s == null)
+ return false;
+
+ int sl = s.length();
+ int wl = w.length();
+
+ if (sl < wl)
+ return false;
+
+ for (int i = wl; i-- > 0; )
+ {
+ char c1 = s.charAt(--sl);
+ char c2 = w.charAt(i);
+ if (c1 != c2)
+ {
+ if (c1 <= 127)
+ c1 = lowercases[c1];
+ if (c2 <= 127)
+ c2 = lowercases[c2];
+ if (c1 != c2)
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * returns the next index of a character from the chars string
+ *
+ * @param s the input string to search
+ * @param chars the chars to look for
+ * @return the index of the character in the input stream found.
+ */
+ public static int indexFrom(String s, String chars)
+ {
+ for (int i = 0; i < s.length(); i++)
+ {
+ if (chars.indexOf(s.charAt(i)) >= 0)
+ return i;
+ }
+ return -1;
+ }
+
+ /**
+ * Replace chars within string.
+ * <p>
+ * Fast replacement for {@code java.lang.String#}{@link String#replace(char, char)}
+ * </p>
+ *
+ * @param str the input string
+ * @param find the char to look for
+ * @param with the char to replace with
+ * @return the now replaced string
+ */
+ public static String replace(String str, char find, char with)
+ {
+ if (str == null)
+ return null;
+
+ if (find == with)
+ return str;
+
+ int c = 0;
+ int idx = str.indexOf(find, c);
+ if (idx == -1)
+ {
+ return str;
+ }
+ char[] chars = str.toCharArray();
+ int len = chars.length;
+ for (int i = idx; i < len; i++)
+ {
+ if (chars[i] == find)
+ chars[i] = with;
+ }
+ return String.valueOf(chars);
+ }
+
+ /**
+ * Replace substrings within string.
+ * <p>
+ * Fast replacement for {@code java.lang.String#}{@link String#replace(CharSequence, CharSequence)}
+ * </p>
+ *
+ * @param s the input string
+ * @param sub the string to look for
+ * @param with the string to replace with
+ * @return the now replaced string
+ */
+ public static String replace(String s, String sub, String with)
+ {
+ if (s == null)
+ return null;
+
+ int c = 0;
+ int i = s.indexOf(sub, c);
+ if (i == -1)
+ {
+ return s;
+ }
+ StringBuilder buf = new StringBuilder(s.length() + with.length());
+ do
+ {
+ buf.append(s, c, i);
+ buf.append(with);
+ c = i + sub.length();
+ }
+ while ((i = s.indexOf(sub, c)) != -1);
+ if (c < s.length())
+ {
+ buf.append(s.substring(c));
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Replace first substrings within string.
+ * <p>
+ * Fast replacement for {@code java.lang.String#}{@link String#replaceFirst(String, String)}, but without
+ * Regex support.
+ * </p>
+ *
+ * @param original the original string
+ * @param target the target string to look for
+ * @param replacement the replacement string to use
+ * @return the replaced string
+ */
+ public static String replaceFirst(String original, String target, String replacement)
+ {
+ int idx = original.indexOf(target);
+ if (idx == -1)
+ return original;
+
+ int offset = 0;
+ int originalLen = original.length();
+ StringBuilder buf = new StringBuilder(originalLen + replacement.length());
+ buf.append(original, offset, idx);
+ offset += idx + target.length();
+ buf.append(replacement);
+ buf.append(original, offset, originalLen);
+
+ return buf.toString();
+ }
+
+ /**
+ * Remove single or double quotes.
+ *
+ * @param s the input string
+ * @return the string with quotes removed
+ */
+ @Deprecated
+ public static String unquote(String s)
+ {
+ return QuotedStringTokenizer.unquote(s);
+ }
+
+ /**
+ * Append substring to StringBuilder
+ *
+ * @param buf StringBuilder to append to
+ * @param s String to append from
+ * @param offset The offset of the substring
+ * @param length The length of the substring
+ */
+ public static void append(StringBuilder buf,
+ String s,
+ int offset,
+ int length)
+ {
+ synchronized (buf)
+ {
+ int end = offset + length;
+ for (int i = offset; i < end; i++)
+ {
+ if (i >= s.length())
+ break;
+ buf.append(s.charAt(i));
+ }
+ }
+ }
+
+ /**
+ * append hex digit
+ *
+ * @param buf the buffer to append to
+ * @param b the byte to append
+ * @param base the base of the hex output (almost always 16).
+ */
+ public static void append(StringBuilder buf, byte b, int base)
+ {
+ int bi = 0xff & b;
+ int c = '0' + (bi / base) % base;
+ if (c > '9')
+ c = 'a' + (c - '0' - 10);
+ buf.append((char)c);
+ c = '0' + bi % base;
+ if (c > '9')
+ c = 'a' + (c - '0' - 10);
+ buf.append((char)c);
+ }
+
+ /**
+ * Append 2 digits (zero padded) to the StringBuffer
+ *
+ * @param buf the buffer to append to
+ * @param i the value to append
+ */
+ public static void append2digits(StringBuffer buf, int i)
+ {
+ if (i < 100)
+ {
+ buf.append((char)(i / 10 + '0'));
+ buf.append((char)(i % 10 + '0'));
+ }
+ }
+
+ /**
+ * Append 2 digits (zero padded) to the StringBuilder
+ *
+ * @param buf the buffer to append to
+ * @param i the value to append
+ */
+ public static void append2digits(StringBuilder buf, int i)
+ {
+ if (i < 100)
+ {
+ buf.append((char)(i / 10 + '0'));
+ buf.append((char)(i % 10 + '0'));
+ }
+ }
+
+ /**
+ * Return a non null string.
+ *
+ * @param s String
+ * @return The string passed in or empty string if it is null.
+ */
+ public static String nonNull(String s)
+ {
+ if (s == null)
+ return "";
+ return s;
+ }
+
+ public static boolean equals(String s, char[] buf, int offset, int length)
+ {
+ if (s.length() != length)
+ return false;
+ for (int i = 0; i < length; i++)
+ {
+ if (buf[offset + i] != s.charAt(i))
+ return false;
+ }
+ return true;
+ }
+
+ public static String toUTF8String(byte[] b, int offset, int length)
+ {
+ return new String(b, offset, length, StandardCharsets.UTF_8);
+ }
+
+ public static String toString(byte[] b, int offset, int length, String charset)
+ {
+ try
+ {
+ return new String(b, offset, length, charset);
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Find the index of a control characters in String
+ * <p>
+ * This will return a result on the first occurrence of a control character, regardless if
+ * there are more than one.
+ * </p>
+ * <p>
+ * Note: uses codepoint version of {@link Character#isISOControl(int)} to support Unicode better.
+ * </p>
+ *
+ * <pre>
+ * indexOfControlChars(null) == -1
+ * indexOfControlChars("") == -1
+ * indexOfControlChars("\r\n") == 0
+ * indexOfControlChars("\t") == 0
+ * indexOfControlChars(" ") == -1
+ * indexOfControlChars("a") == -1
+ * indexOfControlChars(".") == -1
+ * indexOfControlChars(";\n") == 1
+ * indexOfControlChars("abc\f") == 3
+ * indexOfControlChars("z\010") == 1
+ * indexOfControlChars(":\u001c") == 1
+ * </pre>
+ *
+ * @param str the string to test.
+ * @return the index of first control character in string, -1 if no control characters encountered
+ */
+ public static int indexOfControlChars(String str)
+ {
+ if (str == null)
+ {
+ return -1;
+ }
+ int len = str.length();
+ for (int i = 0; i < len; i++)
+ {
+ if (Character.isISOControl(str.codePointAt(i)))
+ {
+ // found a control character, we can stop searching now
+ return i;
+ }
+ }
+ // no control characters
+ return -1;
+ }
+
+ /**
+ * Test if a string is null or only has whitespace characters in it.
+ * <p>
+ * Note: uses codepoint version of {@link Character#isWhitespace(int)} to support Unicode better.
+ *
+ * <pre>
+ * isBlank(null) == true
+ * isBlank("") == true
+ * isBlank("\r\n") == true
+ * isBlank("\t") == true
+ * isBlank(" ") == true
+ * isBlank("a") == false
+ * isBlank(".") == false
+ * isBlank(";\n") == false
+ * </pre>
+ *
+ * @param str the string to test.
+ * @return true if string is null or only whitespace characters, false if non-whitespace characters encountered.
+ */
+ public static boolean isBlank(String str)
+ {
+ if (str == null)
+ {
+ return true;
+ }
+ int len = str.length();
+ for (int i = 0; i < len; i++)
+ {
+ if (!Character.isWhitespace(str.codePointAt(i)))
+ {
+ // found a non-whitespace, we can stop searching now
+ return false;
+ }
+ }
+ // only whitespace
+ return true;
+ }
+
+ /**
+ * <p>Checks if a String is empty ("") or null.</p>
+ *
+ * <pre>
+ * isEmpty(null) == true
+ * isEmpty("") == true
+ * isEmpty("\r\n") == false
+ * isEmpty("\t") == false
+ * isEmpty(" ") == false
+ * isEmpty("a") == false
+ * isEmpty(".") == false
+ * isEmpty(";\n") == false
+ * </pre>
+ *
+ * @param str the string to test.
+ * @return true if string is null or empty.
+ */
+ public static boolean isEmpty(String str)
+ {
+ return str == null || str.isEmpty();
+ }
+
+ /**
+ * Test if a string is not null and contains at least 1 non-whitespace characters in it.
+ * <p>
+ * Note: uses codepoint version of {@link Character#isWhitespace(int)} to support Unicode better.
+ *
+ * <pre>
+ * isNotBlank(null) == false
+ * isNotBlank("") == false
+ * isNotBlank("\r\n") == false
+ * isNotBlank("\t") == false
+ * isNotBlank(" ") == false
+ * isNotBlank("a") == true
+ * isNotBlank(".") == true
+ * isNotBlank(";\n") == true
+ * </pre>
+ *
+ * @param str the string to test.
+ * @return true if string is not null and has at least 1 non-whitespace character, false if null or all-whitespace characters.
+ */
+ public static boolean isNotBlank(String str)
+ {
+ if (str == null)
+ {
+ return false;
+ }
+ int len = str.length();
+ for (int i = 0; i < len; i++)
+ {
+ if (!Character.isWhitespace(str.codePointAt(i)))
+ {
+ // found a non-whitespace, we can stop searching now
+ return true;
+ }
+ }
+ // only whitespace
+ return false;
+ }
+
+ public static boolean isUTF8(String charset)
+ {
+ return __UTF8.equalsIgnoreCase(charset) || __UTF8.equalsIgnoreCase(normalizeCharset(charset));
+ }
+
+ public static boolean isHex(String str, int offset, int length)
+ {
+ if (offset + length > str.length())
+ {
+ return false;
+ }
+
+ for (int i = offset; i < (offset + length); i++)
+ {
+ char c = str.charAt(i);
+ if (!(((c >= 'a') && (c <= 'f')) ||
+ ((c >= 'A') && (c <= 'F')) ||
+ ((c >= '0') && (c <= '9'))))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static String printable(String name)
+ {
+ if (name == null)
+ return null;
+ StringBuilder buf = new StringBuilder(name.length());
+ for (int i = 0; i < name.length(); i++)
+ {
+ char c = name.charAt(i);
+ if (!Character.isISOControl(c))
+ buf.append(c);
+ }
+ return buf.toString();
+ }
+
+ public static String printable(byte[] b)
+ {
+ StringBuilder buf = new StringBuilder();
+ for (int i = 0; i < b.length; i++)
+ {
+ char c = (char)b[i];
+ if (Character.isWhitespace(c) || c > ' ' && c < 0x7f)
+ buf.append(c);
+ else
+ {
+ buf.append("0x");
+ TypeUtil.toHex(b[i], buf);
+ }
+ }
+ return buf.toString();
+ }
+
+ public static byte[] getBytes(String s)
+ {
+ return s.getBytes(StandardCharsets.ISO_8859_1);
+ }
+
+ public static byte[] getUtf8Bytes(String s)
+ {
+ return s.getBytes(StandardCharsets.UTF_8);
+ }
+
+ public static byte[] getBytes(String s, String charset)
+ {
+ try
+ {
+ return s.getBytes(charset);
+ }
+ catch (Exception e)
+ {
+ return s.getBytes();
+ }
+ }
+
+ /**
+ * Converts a binary SID to a string SID
+ *
+ * http://en.wikipedia.org/wiki/Security_Identifier
+ *
+ * S-1-IdentifierAuthority-SubAuthority1-SubAuthority2-...-SubAuthorityn
+ *
+ * @param sidBytes the SID bytes to build from
+ * @return the string SID
+ */
+ @Deprecated
+ public static String sidBytesToString(byte[] sidBytes)
+ {
+ StringBuilder sidString = new StringBuilder();
+
+ // Identify this as a SID
+ sidString.append("S-");
+
+ // Add SID revision level (expect 1 but may change someday)
+ sidString.append(sidBytes[0]).append('-');
+
+ StringBuilder tmpBuilder = new StringBuilder();
+
+ // crunch the six bytes of issuing authority value
+ for (int i = 2; i <= 7; ++i)
+ {
+ tmpBuilder.append(Integer.toHexString(sidBytes[i] & 0xFF));
+ }
+
+ sidString.append(Long.parseLong(tmpBuilder.toString(), 16)); // '-' is in the subauth loop
+
+ // the number of subAuthorities we need to attach
+ int subAuthorityCount = sidBytes[1];
+ // attach each of the subAuthorities
+ for (int i = 0; i < subAuthorityCount; ++i)
+ {
+ int offset = i * 4;
+ tmpBuilder.setLength(0);
+ // these need to be zero padded hex and little endian
+ tmpBuilder.append(String.format("%02X%02X%02X%02X",
+ (sidBytes[11 + offset] & 0xFF),
+ (sidBytes[10 + offset] & 0xFF),
+ (sidBytes[9 + offset] & 0xFF),
+ (sidBytes[8 + offset] & 0xFF)));
+ sidString.append('-').append(Long.parseLong(tmpBuilder.toString(), 16));
+ }
+
+ return sidString.toString();
+ }
+
+ /**
+ * Converts a string SID to a binary SID
+ *
+ * http://en.wikipedia.org/wiki/Security_Identifier
+ *
+ * S-1-IdentifierAuthority-SubAuthority1-SubAuthority2-...-SubAuthorityn
+ *
+ * @param sidString the string SID
+ * @return the binary SID
+ */
+ @Deprecated
+ public static byte[] sidStringToBytes(String sidString)
+ {
+ String[] sidTokens = sidString.split("-");
+
+ int subAuthorityCount = sidTokens.length - 3; // S-Rev-IdAuth-
+
+ int byteCount = 0;
+ byte[] sidBytes = new byte[1 + 1 + 6 + (4 * subAuthorityCount)];
+
+ // the revision byte
+ sidBytes[byteCount++] = (byte)Integer.parseInt(sidTokens[1]);
+ // the # of sub authorities byte
+ sidBytes[byteCount++] = (byte)subAuthorityCount;
+ // the certAuthority
+ String hexStr = Long.toHexString(Long.parseLong(sidTokens[2]));
+
+ while (hexStr.length() < 12) // pad to 12 characters
+ {
+ hexStr = "0" + hexStr;
+ }
+ // place the certAuthority 6 bytes
+ for (int i = 0; i < hexStr.length(); i = i + 2)
+ {
+ sidBytes[byteCount++] = (byte)Integer.parseInt(hexStr.substring(i, i + 2), 16);
+ }
+
+ for (int i = 3; i < sidTokens.length; ++i)
+ {
+ hexStr = Long.toHexString(Long.parseLong(sidTokens[i]));
+
+ while (hexStr.length() < 8) // pad to 8 characters
+ {
+ hexStr = "0" + hexStr;
+ }
+
+ // place the inverted sub authorities, 4 bytes each
+ for (int j = hexStr.length(); j > 0; j = j - 2)
+ {
+ sidBytes[byteCount++] = (byte)Integer.parseInt(hexStr.substring(j - 2, j), 16);
+ }
+ }
+
+ return sidBytes;
+ }
+
+ /**
+ * Convert String to an integer. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown
+ *
+ * @param string A String containing an integer.
+ * @param from The index to start parsing from
+ * @return an int
+ */
+ public static int toInt(String string, int from)
+ {
+ int val = 0;
+ boolean started = false;
+ boolean minus = false;
+ for (int i = from; i < string.length(); i++)
+ {
+ char b = string.charAt(i);
+ if (b <= ' ')
+ {
+ if (started)
+ break;
+ }
+ else if (b >= '0' && b <= '9')
+ {
+ val = val * 10 + (b - '0');
+ started = true;
+ }
+ else if (b == '-' && !started)
+ {
+ minus = true;
+ }
+ else
+ break;
+ }
+ if (started)
+ return minus ? (-val) : val;
+ throw new NumberFormatException(string);
+ }
+
+ /**
+ * Convert String to an long. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown
+ *
+ * @param string A String containing an integer.
+ * @return an int
+ */
+ public static long toLong(String string)
+ {
+ long val = 0;
+ boolean started = false;
+ boolean minus = false;
+ for (int i = 0; i < string.length(); i++)
+ {
+ char b = string.charAt(i);
+ if (b <= ' ')
+ {
+ if (started)
+ break;
+ }
+ else if (b >= '0' && b <= '9')
+ {
+ val = val * 10L + (b - '0');
+ started = true;
+ }
+ else if (b == '-' && !started)
+ {
+ minus = true;
+ }
+ else
+ break;
+ }
+ if (started)
+ return minus ? (-val) : val;
+ throw new NumberFormatException(string);
+ }
+
+ /**
+ * Truncate a string to a max size.
+ *
+ * @param str the string to possibly truncate
+ * @param maxSize the maximum size of the string
+ * @return the truncated string. if <code>str</code> param is null, then the returned string will also be null.
+ */
+ public static String truncate(String str, int maxSize)
+ {
+ if (str == null)
+ {
+ return null;
+ }
+ if (str.length() <= maxSize)
+ {
+ return str;
+ }
+ return str.substring(0, maxSize);
+ }
+
+ /**
+ * Parse the string representation of a list using {@link #csvSplit(List, String, int, int)}
+ *
+ * @param s The string to parse, expected to be enclosed as '[...]'
+ * @return An array of parsed values.
+ */
+ public static String[] arrayFromString(String s)
+ {
+ if (s == null)
+ return new String[]{};
+ if (!s.startsWith("[") || !s.endsWith("]"))
+ throw new IllegalArgumentException();
+ if (s.length() == 2)
+ return new String[]{};
+ return csvSplit(s, 1, s.length() - 2);
+ }
+
+ /**
+ * Parse a CSV string using {@link #csvSplit(List, String, int, int)}
+ *
+ * @param s The string to parse
+ * @return An array of parsed values.
+ */
+ public static String[] csvSplit(String s)
+ {
+ if (s == null)
+ return null;
+ return csvSplit(s, 0, s.length());
+ }
+
+ /**
+ * Parse a CSV string using {@link #csvSplit(List, String, int, int)}
+ *
+ * @param s The string to parse
+ * @param off The offset into the string to start parsing
+ * @param len The len in characters to parse
+ * @return An array of parsed values.
+ */
+ public static String[] csvSplit(String s, int off, int len)
+ {
+ if (s == null)
+ return null;
+ if (off < 0 || len < 0 || off > s.length())
+ throw new IllegalArgumentException();
+ List<String> list = new ArrayList<>();
+ csvSplit(list, s, off, len);
+ return list.toArray(new String[list.size()]);
+ }
+
+ enum CsvSplitState
+ {
+ PRE_DATA, QUOTE, SLOSH, DATA, WHITE, POST_DATA
+ }
+
+ /**
+ * Split a quoted comma separated string to a list
+ * <p>Handle <a href="https://www.ietf.org/rfc/rfc4180.txt">rfc4180</a>-like
+ * CSV strings, with the exceptions:<ul>
+ * <li>quoted values may contain double quotes escaped with back-slash
+ * <li>Non-quoted values are trimmed of leading trailing white space
+ * <li>trailing commas are ignored
+ * <li>double commas result in a empty string value
+ * </ul>
+ *
+ * @param list The Collection to split to (or null to get a new list)
+ * @param s The string to parse
+ * @param off The offset into the string to start parsing
+ * @param len The len in characters to parse
+ * @return list containing the parsed list values
+ */
+ public static List<String> csvSplit(List<String> list, String s, int off, int len)
+ {
+ if (list == null)
+ list = new ArrayList<>();
+ CsvSplitState state = CsvSplitState.PRE_DATA;
+ StringBuilder out = new StringBuilder();
+ int last = -1;
+ while (len > 0)
+ {
+ char ch = s.charAt(off++);
+ len--;
+
+ switch (state)
+ {
+ case PRE_DATA:
+ if (Character.isWhitespace(ch))
+ continue;
+ if ('"' == ch)
+ {
+ state = CsvSplitState.QUOTE;
+ continue;
+ }
+
+ if (',' == ch)
+ {
+ list.add("");
+ continue;
+ }
+ state = CsvSplitState.DATA;
+ out.append(ch);
+ continue;
+ case DATA:
+ if (Character.isWhitespace(ch))
+ {
+ last = out.length();
+ out.append(ch);
+ state = CsvSplitState.WHITE;
+ continue;
+ }
+
+ if (',' == ch)
+ {
+ list.add(out.toString());
+ out.setLength(0);
+ state = CsvSplitState.PRE_DATA;
+ continue;
+ }
+ out.append(ch);
+ continue;
+
+ case WHITE:
+ if (Character.isWhitespace(ch))
+ {
+ out.append(ch);
+ continue;
+ }
+
+ if (',' == ch)
+ {
+ out.setLength(last);
+ list.add(out.toString());
+ out.setLength(0);
+ state = CsvSplitState.PRE_DATA;
+ continue;
+ }
+
+ state = CsvSplitState.DATA;
+ out.append(ch);
+ last = -1;
+ continue;
+ case QUOTE:
+ if ('\\' == ch)
+ {
+ state = CsvSplitState.SLOSH;
+ continue;
+ }
+ if ('"' == ch)
+ {
+ list.add(out.toString());
+ out.setLength(0);
+ state = CsvSplitState.POST_DATA;
+ continue;
+ }
+ out.append(ch);
+ continue;
+
+ case SLOSH:
+ out.append(ch);
+ state = CsvSplitState.QUOTE;
+ continue;
+
+ case POST_DATA:
+ if (',' == ch)
+ {
+ state = CsvSplitState.PRE_DATA;
+ continue;
+ }
+ continue;
+ }
+ }
+ switch (state)
+ {
+ case PRE_DATA:
+ case POST_DATA:
+ break;
+ case DATA:
+ case QUOTE:
+ case SLOSH:
+ list.add(out.toString());
+ break;
+
+ case WHITE:
+ out.setLength(last);
+ list.add(out.toString());
+ break;
+ }
+
+ return list;
+ }
+
+ public static String sanitizeXmlString(String html)
+ {
+ if (html == null)
+ return null;
+
+ int i = 0;
+
+ // Are there any characters that need sanitizing?
+ loop:
+ for (; i < html.length(); i++)
+ {
+ char c = html.charAt(i);
+ switch (c)
+ {
+ case '&':
+ case '<':
+ case '>':
+ case '\'':
+ case '"':
+ break loop;
+ default:
+ if (Character.isISOControl(c) && !Character.isWhitespace(c))
+ break loop;
+ }
+ }
+ // No characters need sanitizing, so return original string
+ if (i == html.length())
+ return html;
+
+ // Create builder with OK content so far
+ StringBuilder out = new StringBuilder(html.length() * 4 / 3);
+ out.append(html, 0, i);
+
+ // sanitize remaining content
+ for (; i < html.length(); i++)
+ {
+ char c = html.charAt(i);
+ switch (c)
+ {
+ case '&':
+ out.append("&");
+ break;
+ case '<':
+ out.append("<");
+ break;
+ case '>':
+ out.append(">");
+ break;
+ case '\'':
+ out.append("'");
+ break;
+ case '"':
+ out.append(""");
+ break;
+ default:
+ if (Character.isISOControl(c) && !Character.isWhitespace(c))
+ out.append('?');
+ else
+ out.append(c);
+ }
+ }
+ return out.toString();
+ }
+
+ public static String strip(String str, String find)
+ {
+ return StringUtil.replace(str, find, "");
+ }
+
+ /**
+ * The String value of an Object
+ * <p>This method calls {@link String#valueOf(Object)} unless the object is null,
+ * in which case null is returned</p>
+ *
+ * @param object The object
+ * @return String value or null
+ */
+ public static String valueOf(Object object)
+ {
+ return object == null ? null : String.valueOf(object);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/TopologicalSort.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/TopologicalSort.java
new file mode 100644
index 0000000..6a693d6
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/TopologicalSort.java
@@ -0,0 +1,221 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * Topological sort a list or array.
+ * <p>A Topological sort is used when you have a partial ordering expressed as
+ * dependencies between elements (also often represented as edges in a directed
+ * acyclic graph). A Topological sort should not be used when you have a total
+ * ordering expressed as a {@link Comparator} over the items. The algorithm has
+ * the additional characteristic that dependency sets are sorted by the original
+ * list order so that order is preserved when possible.</p>
+ * <p>
+ * The sort algorithm works by recursively visiting every item, once and
+ * only once. On each visit, the items dependencies are first visited and then the
+ * item is added to the sorted list. Thus the algorithm ensures that dependency
+ * items are always added before dependent items.</p>
+ *
+ * @param <T> The type to be sorted. It must be able to be added to a {@link HashSet}
+ */
+public class TopologicalSort<T>
+{
+ private final Map<T, Set<T>> _dependencies = new HashMap<>();
+
+ /**
+ * Add a dependency to be considered in the sort.
+ *
+ * @param dependent The dependent item will be sorted after all its dependencies
+ * @param dependency The dependency item, will be sorted before its dependent item
+ */
+ public void addDependency(T dependent, T dependency)
+ {
+ Set<T> set = _dependencies.get(dependent);
+ if (set == null)
+ {
+ set = new HashSet<>();
+ _dependencies.put(dependent, set);
+ }
+ set.add(dependency);
+ }
+
+ /**
+ * Sort the passed array according to dependencies previously set with
+ * {@link #addDependency(Object, Object)}. Where possible, ordering will be
+ * preserved if no dependency
+ *
+ * @param array The array to be sorted.
+ */
+ public void sort(T[] array)
+ {
+ List<T> sorted = new ArrayList<>();
+ Set<T> visited = new HashSet<>();
+ Comparator<T> comparator = new InitialOrderComparator<>(array);
+
+ // Visit all items in the array
+ for (T t : array)
+ {
+ visit(t, visited, sorted, comparator);
+ }
+
+ sorted.toArray(array);
+ }
+
+ /**
+ * Sort the passed list according to dependencies previously set with
+ * {@link #addDependency(Object, Object)}. Where possible, ordering will be
+ * preserved if no dependency
+ *
+ * @param list The list to be sorted.
+ */
+ public void sort(Collection<T> list)
+ {
+ List<T> sorted = new ArrayList<>();
+ Set<T> visited = new HashSet<>();
+ Comparator<T> comparator = new InitialOrderComparator<>(list);
+
+ // Visit all items in the list
+ for (T t : list)
+ {
+ visit(t, visited, sorted, comparator);
+ }
+
+ list.clear();
+ list.addAll(sorted);
+ }
+
+ /**
+ * Visit an item to be sorted.
+ *
+ * @param item The item to be visited
+ * @param visited The Set of items already visited
+ * @param sorted The list to sort items into
+ * @param comparator A comparator used to sort dependencies.
+ */
+ private void visit(T item, Set<T> visited, List<T> sorted, Comparator<T> comparator)
+ {
+ // If the item has not been visited
+ if (!visited.contains(item))
+ {
+ // We are visiting it now, so add it to the visited set
+ visited.add(item);
+
+ // Lookup the items dependencies
+ Set<T> dependencies = _dependencies.get(item);
+ if (dependencies != null)
+ {
+ // Sort the dependencies
+ SortedSet<T> orderedDeps = new TreeSet<>(comparator);
+ orderedDeps.addAll(dependencies);
+
+ // recursively visit each dependency
+ try
+ {
+ for (T d : orderedDeps)
+ {
+ visit(d, visited, sorted, comparator);
+ }
+ }
+ catch (CyclicException e)
+ {
+ throw new CyclicException(item, e);
+ }
+ }
+
+ // Now that we have visited all our dependencies, they and their
+ // dependencies will have been added to the sorted list. So we can
+ // now add the current item and it will be after its dependencies
+ sorted.add(item);
+ }
+ else if (!sorted.contains(item))
+ // If we have already visited an item, but it has not yet been put in the
+ // sorted list, then we must be in a cycle!
+ throw new CyclicException(item);
+ }
+
+ /**
+ * A comparator that is used to sort dependencies in the order they
+ * were in the original list. This ensures that dependencies are visited
+ * in the original order and no needless reordering takes place.
+ */
+ private static class InitialOrderComparator<T> implements Comparator<T>
+ {
+ private final Map<T, Integer> _indexes = new HashMap<>();
+
+ InitialOrderComparator(T[] initial)
+ {
+ int i = 0;
+ for (T t : initial)
+ {
+ _indexes.put(t, i++);
+ }
+ }
+
+ InitialOrderComparator(Collection<T> initial)
+ {
+ int i = 0;
+ for (T t : initial)
+ {
+ _indexes.put(t, i++);
+ }
+ }
+
+ @Override
+ public int compare(T o1, T o2)
+ {
+ Integer i1 = _indexes.get(o1);
+ Integer i2 = _indexes.get(o2);
+ if (i1 == null || i2 == null || i1.equals(o2))
+ return 0;
+ if (i1 < i2)
+ return -1;
+ return 1;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return "TopologicalSort " + _dependencies;
+ }
+
+ private static class CyclicException extends IllegalStateException
+ {
+ CyclicException(Object item)
+ {
+ super("cyclic at " + item);
+ }
+
+ CyclicException(Object item, CyclicException e)
+ {
+ super("cyclic at " + item, e);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/TreeTrie.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/TreeTrie.java
new file mode 100644
index 0000000..da0ec61
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/TreeTrie.java
@@ -0,0 +1,403 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A Trie String lookup data structure using a tree
+ * <p>This implementation is always case insensitive and is optimal for
+ * a variable number of fixed strings with few special characters.
+ * </p>
+ * <p>This Trie is stored in a Tree and is unlimited in capacity</p>
+ *
+ * <p>This Trie is not Threadsafe and contains no mutual exclusion
+ * or deliberate memory barriers. It is intended for an TreeTrie to be
+ * built by a single thread and then used concurrently by multiple threads
+ * and not mutated during that access. If concurrent mutations of the
+ * Trie is required external locks need to be applied.
+ * </p>
+ *
+ * @param <V> the entry type
+ */
+public class TreeTrie<V> extends AbstractTrie<V>
+{
+ private static final int[] LOOKUP =
+ {
+ // 0 1 2 3 4 5 6 7 8 9 A B C D E F
+ /*0*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ /*1*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ /*2*/31, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 26, -1, 27, 30, -1,
+ /*3*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 28, 29, -1, -1, -1, -1,
+ /*4*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ /*5*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
+ /*6*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ /*7*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1
+ };
+ private static final int INDEX = 32;
+ private final TreeTrie<V>[] _nextIndex;
+ private final List<TreeTrie<V>> _nextOther = new ArrayList<>();
+ private final char _c;
+ private String _key;
+ private V _value;
+
+ public TreeTrie()
+ {
+ super(true);
+ _nextIndex = new TreeTrie[INDEX];
+ _c = 0;
+ }
+
+ private TreeTrie(char c)
+ {
+ super(true);
+ _nextIndex = new TreeTrie[INDEX];
+ this._c = c;
+ }
+
+ @Override
+ public void clear()
+ {
+ Arrays.fill(_nextIndex, null);
+ _nextOther.clear();
+ _key = null;
+ _value = null;
+ }
+
+ @Override
+ public boolean put(String s, V v)
+ {
+ TreeTrie<V> t = this;
+ int limit = s.length();
+ for (int k = 0; k < limit; k++)
+ {
+ char c = s.charAt(k);
+
+ int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1;
+ if (index >= 0)
+ {
+ if (t._nextIndex[index] == null)
+ t._nextIndex[index] = new TreeTrie<V>(c);
+ t = t._nextIndex[index];
+ }
+ else
+ {
+ TreeTrie<V> n = null;
+ for (int i = t._nextOther.size(); i-- > 0; )
+ {
+ n = t._nextOther.get(i);
+ if (n._c == c)
+ break;
+ n = null;
+ }
+ if (n == null)
+ {
+ n = new TreeTrie<V>(c);
+ t._nextOther.add(n);
+ }
+ t = n;
+ }
+ }
+ t._key = v == null ? null : s;
+ t._value = v;
+ return true;
+ }
+
+ @Override
+ public V get(String s, int offset, int len)
+ {
+ TreeTrie<V> t = this;
+ for (int i = 0; i < len; i++)
+ {
+ char c = s.charAt(offset + i);
+ int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1;
+ if (index >= 0)
+ {
+ if (t._nextIndex[index] == null)
+ return null;
+ t = t._nextIndex[index];
+ }
+ else
+ {
+ TreeTrie<V> n = null;
+ for (int j = t._nextOther.size(); j-- > 0; )
+ {
+ n = t._nextOther.get(j);
+ if (n._c == c)
+ break;
+ n = null;
+ }
+ if (n == null)
+ return null;
+ t = n;
+ }
+ }
+ return t._value;
+ }
+
+ @Override
+ public V get(ByteBuffer b, int offset, int len)
+ {
+ TreeTrie<V> t = this;
+ for (int i = 0; i < len; i++)
+ {
+ byte c = b.get(offset + i);
+ int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1;
+ if (index >= 0)
+ {
+ if (t._nextIndex[index] == null)
+ return null;
+ t = t._nextIndex[index];
+ }
+ else
+ {
+ TreeTrie<V> n = null;
+ for (int j = t._nextOther.size(); j-- > 0; )
+ {
+ n = t._nextOther.get(j);
+ if (n._c == c)
+ break;
+ n = null;
+ }
+ if (n == null)
+ return null;
+ t = n;
+ }
+ }
+ return t._value;
+ }
+
+ @Override
+ public V getBest(byte[] b, int offset, int len)
+ {
+ TreeTrie<V> t = this;
+ for (int i = 0; i < len; i++)
+ {
+ byte c = b[offset + i];
+ int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1;
+ if (index >= 0)
+ {
+ if (t._nextIndex[index] == null)
+ break;
+ t = t._nextIndex[index];
+ }
+ else
+ {
+ TreeTrie<V> n = null;
+ for (int j = t._nextOther.size(); j-- > 0; )
+ {
+ n = t._nextOther.get(j);
+ if (n._c == c)
+ break;
+ n = null;
+ }
+ if (n == null)
+ break;
+ t = n;
+ }
+
+ // Is the next Trie is a match
+ if (t._key != null)
+ {
+ // Recurse so we can remember this possibility
+ V best = t.getBest(b, offset + i + 1, len - i - 1);
+ if (best != null)
+ return best;
+ break;
+ }
+ }
+ return t._value;
+ }
+
+ @Override
+ public V getBest(String s, int offset, int len)
+ {
+ TreeTrie<V> t = this;
+ for (int i = 0; i < len; i++)
+ {
+ byte c = (byte)(0xff & s.charAt(offset + i));
+ int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1;
+ if (index >= 0)
+ {
+ if (t._nextIndex[index] == null)
+ break;
+ t = t._nextIndex[index];
+ }
+ else
+ {
+ TreeTrie<V> n = null;
+ for (int j = t._nextOther.size(); j-- > 0; )
+ {
+ n = t._nextOther.get(j);
+ if (n._c == c)
+ break;
+ n = null;
+ }
+ if (n == null)
+ break;
+ t = n;
+ }
+
+ // Is the next Trie is a match
+ if (t._key != null)
+ {
+ // Recurse so we can remember this possibility
+ V best = t.getBest(s, offset + i + 1, len - i - 1);
+ if (best != null)
+ return best;
+ break;
+ }
+ }
+ return t._value;
+ }
+
+ @Override
+ public V getBest(ByteBuffer b, int offset, int len)
+ {
+ if (b.hasArray())
+ return getBest(b.array(), b.arrayOffset() + b.position() + offset, len);
+ return getBestByteBuffer(b, offset, len);
+ }
+
+ private V getBestByteBuffer(ByteBuffer b, int offset, int len)
+ {
+ TreeTrie<V> t = this;
+ int pos = b.position() + offset;
+ for (int i = 0; i < len; i++)
+ {
+ byte c = b.get(pos++);
+ int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1;
+ if (index >= 0)
+ {
+ if (t._nextIndex[index] == null)
+ break;
+ t = t._nextIndex[index];
+ }
+ else
+ {
+ TreeTrie<V> n = null;
+ for (int j = t._nextOther.size(); j-- > 0; )
+ {
+ n = t._nextOther.get(j);
+ if (n._c == c)
+ break;
+ n = null;
+ }
+ if (n == null)
+ break;
+ t = n;
+ }
+
+ // Is the next Trie is a match
+ if (t._key != null)
+ {
+ // Recurse so we can remember this possibility
+ V best = t.getBest(b, offset + i + 1, len - i - 1);
+ if (best != null)
+ return best;
+ break;
+ }
+ }
+ return t._value;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder buf = new StringBuilder();
+ toString(buf, this);
+
+ if (buf.length() == 0)
+ return "{}";
+
+ buf.setCharAt(0, '{');
+ buf.append('}');
+ return buf.toString();
+ }
+
+ private static <V> void toString(Appendable out, TreeTrie<V> t)
+ {
+ if (t != null)
+ {
+ if (t._value != null)
+ {
+ try
+ {
+ out.append(',');
+ out.append(t._key);
+ out.append('=');
+ out.append(t._value.toString());
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ for (int i = 0; i < INDEX; i++)
+ {
+ if (t._nextIndex[i] != null)
+ toString(out, t._nextIndex[i]);
+ }
+ for (int i = t._nextOther.size(); i-- > 0; )
+ {
+ toString(out, t._nextOther.get(i));
+ }
+ }
+ }
+
+ @Override
+ public Set<String> keySet()
+ {
+ Set<String> keys = new HashSet<>();
+ keySet(keys, this);
+ return keys;
+ }
+
+ private static <V> void keySet(Set<String> set, TreeTrie<V> t)
+ {
+ if (t != null)
+ {
+ if (t._key != null)
+ set.add(t._key);
+
+ for (int i = 0; i < INDEX; i++)
+ {
+ if (t._nextIndex[i] != null)
+ keySet(set, t._nextIndex[i]);
+ }
+ for (int i = t._nextOther.size(); i-- > 0; )
+ {
+ keySet(set, t._nextOther.get(i));
+ }
+ }
+ }
+
+ @Override
+ public boolean isFull()
+ {
+ return false;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Trie.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Trie.java
new file mode 100644
index 0000000..fb25ca5
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Trie.java
@@ -0,0 +1,230 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * A Trie String lookup data structure.
+ *
+ * @param <V> the Trie entry type
+ */
+public interface Trie<V>
+{
+
+ /**
+ * Put an entry into the Trie
+ *
+ * @param s The key for the entry
+ * @param v The value of the entry
+ * @return True if the Trie had capacity to add the field.
+ */
+ boolean put(String s, V v);
+
+ /**
+ * Put a value as both a key and a value.
+ *
+ * @param v The value and key
+ * @return True if the Trie had capacity to add the field.
+ */
+ boolean put(V v);
+
+ V remove(String s);
+
+ /**
+ * Get an exact match from a String key
+ *
+ * @param s The key
+ * @return the value for the string key
+ */
+ V get(String s);
+
+ /**
+ * Get an exact match from a String key
+ *
+ * @param s The key
+ * @param offset The offset within the string of the key
+ * @param len the length of the key
+ * @return the value for the string / offset / length
+ */
+ V get(String s, int offset, int len);
+
+ /**
+ * Get an exact match from a segment of a ByteBuufer as key
+ *
+ * @param b The buffer
+ * @return The value or null if not found
+ */
+ V get(ByteBuffer b);
+
+ /**
+ * Get an exact match from a segment of a ByteBuufer as key
+ *
+ * @param b The buffer
+ * @param offset The offset within the buffer of the key
+ * @param len the length of the key
+ * @return The value or null if not found
+ */
+ V get(ByteBuffer b, int offset, int len);
+
+ /**
+ * Get the best match from key in a String.
+ *
+ * @param s The string
+ * @return The value or null if not found
+ */
+ V getBest(String s);
+
+ /**
+ * Get the best match from key in a String.
+ *
+ * @param s The string
+ * @param offset The offset within the string of the key
+ * @param len the length of the key
+ * @return The value or null if not found
+ */
+ V getBest(String s, int offset, int len);
+
+ /**
+ * Get the best match from key in a byte array.
+ * The key is assumed to by ISO_8859_1 characters.
+ *
+ * @param b The buffer
+ * @param offset The offset within the array of the key
+ * @param len the length of the key
+ * @return The value or null if not found
+ */
+ V getBest(byte[] b, int offset, int len);
+
+ /**
+ * Get the best match from key in a byte buffer.
+ * The key is assumed to by ISO_8859_1 characters.
+ *
+ * @param b The buffer
+ * @param offset The offset within the buffer of the key
+ * @param len the length of the key
+ * @return The value or null if not found
+ */
+ V getBest(ByteBuffer b, int offset, int len);
+
+ Set<String> keySet();
+
+ boolean isFull();
+
+ boolean isCaseInsensitive();
+
+ void clear();
+
+ static <T> Trie<T> empty(final boolean caseInsensitive)
+ {
+ return new Trie<T>()
+ {
+ @Override
+ public boolean put(String s, Object o)
+ {
+ return false;
+ }
+
+ @Override
+ public boolean put(Object o)
+ {
+ return false;
+ }
+
+ @Override
+ public T remove(String s)
+ {
+ return null;
+ }
+
+ @Override
+ public T get(String s)
+ {
+ return null;
+ }
+
+ @Override
+ public T get(String s, int offset, int len)
+ {
+ return null;
+ }
+
+ @Override
+ public T get(ByteBuffer b)
+ {
+ return null;
+ }
+
+ @Override
+ public T get(ByteBuffer b, int offset, int len)
+ {
+ return null;
+ }
+
+ @Override
+ public T getBest(String s)
+ {
+ return null;
+ }
+
+ @Override
+ public T getBest(String s, int offset, int len)
+ {
+ return null;
+ }
+
+ @Override
+ public T getBest(byte[] b, int offset, int len)
+ {
+ return null;
+ }
+
+ @Override
+ public T getBest(ByteBuffer b, int offset, int len)
+ {
+ return null;
+ }
+
+ @Override
+ public Set<String> keySet()
+ {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public boolean isFull()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean isCaseInsensitive()
+ {
+ return caseInsensitive;
+ }
+
+ @Override
+ public void clear()
+ {
+ }
+ };
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java
new file mode 100644
index 0000000..65121f7
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java
@@ -0,0 +1,718 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.AccessController;
+import java.security.CodeSource;
+import java.security.PrivilegedAction;
+import java.security.ProtectionDomain;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * TYPE Utilities.
+ * Provides various static utility methods for manipulating types and their
+ * string representations.
+ *
+ * @since Jetty 4.1
+ */
+public class TypeUtil
+{
+ private static final Logger LOG = Log.getLogger(TypeUtil.class);
+ public static final Class<?>[] NO_ARGS = new Class[]{};
+ public static final int CR = '\r';
+ public static final int LF = '\n';
+
+ private static final HashMap<String, Class<?>> name2Class = new HashMap<>();
+
+ static
+ {
+ name2Class.put("boolean", java.lang.Boolean.TYPE);
+ name2Class.put("byte", java.lang.Byte.TYPE);
+ name2Class.put("char", java.lang.Character.TYPE);
+ name2Class.put("double", java.lang.Double.TYPE);
+ name2Class.put("float", java.lang.Float.TYPE);
+ name2Class.put("int", java.lang.Integer.TYPE);
+ name2Class.put("long", java.lang.Long.TYPE);
+ name2Class.put("short", java.lang.Short.TYPE);
+ name2Class.put("void", java.lang.Void.TYPE);
+
+ name2Class.put("java.lang.Boolean.TYPE", java.lang.Boolean.TYPE);
+ name2Class.put("java.lang.Byte.TYPE", java.lang.Byte.TYPE);
+ name2Class.put("java.lang.Character.TYPE", java.lang.Character.TYPE);
+ name2Class.put("java.lang.Double.TYPE", java.lang.Double.TYPE);
+ name2Class.put("java.lang.Float.TYPE", java.lang.Float.TYPE);
+ name2Class.put("java.lang.Integer.TYPE", java.lang.Integer.TYPE);
+ name2Class.put("java.lang.Long.TYPE", java.lang.Long.TYPE);
+ name2Class.put("java.lang.Short.TYPE", java.lang.Short.TYPE);
+ name2Class.put("java.lang.Void.TYPE", java.lang.Void.TYPE);
+
+ name2Class.put("java.lang.Boolean", java.lang.Boolean.class);
+ name2Class.put("java.lang.Byte", java.lang.Byte.class);
+ name2Class.put("java.lang.Character", java.lang.Character.class);
+ name2Class.put("java.lang.Double", java.lang.Double.class);
+ name2Class.put("java.lang.Float", java.lang.Float.class);
+ name2Class.put("java.lang.Integer", java.lang.Integer.class);
+ name2Class.put("java.lang.Long", java.lang.Long.class);
+ name2Class.put("java.lang.Short", java.lang.Short.class);
+
+ name2Class.put("Boolean", java.lang.Boolean.class);
+ name2Class.put("Byte", java.lang.Byte.class);
+ name2Class.put("Character", java.lang.Character.class);
+ name2Class.put("Double", java.lang.Double.class);
+ name2Class.put("Float", java.lang.Float.class);
+ name2Class.put("Integer", java.lang.Integer.class);
+ name2Class.put("Long", java.lang.Long.class);
+ name2Class.put("Short", java.lang.Short.class);
+
+ name2Class.put(null, java.lang.Void.TYPE);
+ name2Class.put("string", java.lang.String.class);
+ name2Class.put("String", java.lang.String.class);
+ name2Class.put("java.lang.String", java.lang.String.class);
+ }
+
+ private static final HashMap<Class<?>, String> class2Name = new HashMap<>();
+
+ static
+ {
+ class2Name.put(java.lang.Boolean.TYPE, "boolean");
+ class2Name.put(java.lang.Byte.TYPE, "byte");
+ class2Name.put(java.lang.Character.TYPE, "char");
+ class2Name.put(java.lang.Double.TYPE, "double");
+ class2Name.put(java.lang.Float.TYPE, "float");
+ class2Name.put(java.lang.Integer.TYPE, "int");
+ class2Name.put(java.lang.Long.TYPE, "long");
+ class2Name.put(java.lang.Short.TYPE, "short");
+ class2Name.put(java.lang.Void.TYPE, "void");
+
+ class2Name.put(java.lang.Boolean.class, "java.lang.Boolean");
+ class2Name.put(java.lang.Byte.class, "java.lang.Byte");
+ class2Name.put(java.lang.Character.class, "java.lang.Character");
+ class2Name.put(java.lang.Double.class, "java.lang.Double");
+ class2Name.put(java.lang.Float.class, "java.lang.Float");
+ class2Name.put(java.lang.Integer.class, "java.lang.Integer");
+ class2Name.put(java.lang.Long.class, "java.lang.Long");
+ class2Name.put(java.lang.Short.class, "java.lang.Short");
+
+ class2Name.put(null, "void");
+ class2Name.put(java.lang.String.class, "java.lang.String");
+ }
+
+ private static final HashMap<Class<?>, Method> class2Value = new HashMap<>();
+
+ static
+ {
+ try
+ {
+ Class<?>[] s = {java.lang.String.class};
+
+ class2Value.put(java.lang.Boolean.TYPE,
+ java.lang.Boolean.class.getMethod("valueOf", s));
+ class2Value.put(java.lang.Byte.TYPE,
+ java.lang.Byte.class.getMethod("valueOf", s));
+ class2Value.put(java.lang.Double.TYPE,
+ java.lang.Double.class.getMethod("valueOf", s));
+ class2Value.put(java.lang.Float.TYPE,
+ java.lang.Float.class.getMethod("valueOf", s));
+ class2Value.put(java.lang.Integer.TYPE,
+ java.lang.Integer.class.getMethod("valueOf", s));
+ class2Value.put(java.lang.Long.TYPE,
+ java.lang.Long.class.getMethod("valueOf", s));
+ class2Value.put(java.lang.Short.TYPE,
+ java.lang.Short.class.getMethod("valueOf", s));
+
+ class2Value.put(java.lang.Boolean.class,
+ java.lang.Boolean.class.getMethod("valueOf", s));
+ class2Value.put(java.lang.Byte.class,
+ java.lang.Byte.class.getMethod("valueOf", s));
+ class2Value.put(java.lang.Double.class,
+ java.lang.Double.class.getMethod("valueOf", s));
+ class2Value.put(java.lang.Float.class,
+ java.lang.Float.class.getMethod("valueOf", s));
+ class2Value.put(java.lang.Integer.class,
+ java.lang.Integer.class.getMethod("valueOf", s));
+ class2Value.put(java.lang.Long.class,
+ java.lang.Long.class.getMethod("valueOf", s));
+ class2Value.put(java.lang.Short.class,
+ java.lang.Short.class.getMethod("valueOf", s));
+ }
+ catch (Exception e)
+ {
+ throw new Error(e);
+ }
+ }
+
+ private static final List<Function<Class<?>, URI>> LOCATION_METHODS = new ArrayList<>();
+ private static final Function<Class<?>, URI> MODULE_LOCATION;
+
+ static
+ {
+ // Lookup order in LOCATION_METHODS is important.
+ LOCATION_METHODS.add(TypeUtil::getCodeSourceLocation);
+ Function<Class<?>, URI> moduleFunc = null;
+ try
+ {
+ Class<?> clazzModuleLocation = TypeUtil.class.getClassLoader().loadClass(TypeUtil.class.getPackage().getName() + ".ModuleLocation");
+ Object obj = clazzModuleLocation.getConstructor().newInstance();
+ if (obj instanceof Function)
+ {
+ //noinspection unchecked
+ moduleFunc = (Function<Class<?>, URI>)obj;
+ LOCATION_METHODS.add(moduleFunc);
+ }
+ }
+ catch (Throwable t)
+ {
+ LOG.debug("This JVM Runtime does not support Modules, disabling Jetty internal support");
+ }
+ MODULE_LOCATION = moduleFunc;
+ LOCATION_METHODS.add(TypeUtil::getClassLoaderLocation);
+ LOCATION_METHODS.add(TypeUtil::getSystemClassLoaderLocation);
+ }
+
+ /**
+ * Array to List.
+ * <p>
+ * Works like {@link Arrays#asList(Object...)}, but handles null arrays.
+ *
+ * @param a the array to convert to a list
+ * @param <T> the array and list entry type
+ * @return a list backed by the array.
+ */
+ public static <T> List<T> asList(T[] a)
+ {
+ if (a == null)
+ return Collections.emptyList();
+ return Arrays.asList(a);
+ }
+
+ /**
+ * Class from a canonical name for a type.
+ *
+ * @param name A class or type name.
+ * @return A class , which may be a primitive TYPE field..
+ */
+ public static Class<?> fromName(String name)
+ {
+ return name2Class.get(name);
+ }
+
+ /**
+ * Canonical name for a type.
+ *
+ * @param type A class , which may be a primitive TYPE field.
+ * @return Canonical name.
+ */
+ public static String toName(Class<?> type)
+ {
+ return class2Name.get(type);
+ }
+
+ /**
+ * Return the Classpath / Classloader reference for the
+ * provided class file.
+ *
+ * <p>
+ * Convenience method for the code
+ * </p>
+ *
+ * <pre>
+ * String ref = myObject.getClass().getName().replace('.','/') + ".class";
+ * </pre>
+ *
+ * @param clazz the class to reference
+ * @return the classpath reference syntax for the class file
+ */
+ public static String toClassReference(Class<?> clazz)
+ {
+ return TypeUtil.toClassReference(clazz.getName());
+ }
+
+ /**
+ * Return the Classpath / Classloader reference for the
+ * provided class file.
+ *
+ * <p>
+ * Convenience method for the code
+ * </p>
+ *
+ * <pre>
+ * String ref = myClassName.replace('.','/') + ".class";
+ * </pre>
+ *
+ * @param className the class to reference
+ * @return the classpath reference syntax for the class file
+ */
+ public static String toClassReference(String className)
+ {
+ return StringUtil.replace(className, '.', '/').concat(".class");
+ }
+
+ /**
+ * Convert String value to instance.
+ *
+ * @param type The class of the instance, which may be a primitive TYPE field.
+ * @param value The value as a string.
+ * @return The value as an Object.
+ */
+ public static Object valueOf(Class<?> type, String value)
+ {
+ try
+ {
+ if (type.equals(java.lang.String.class))
+ return value;
+
+ Method m = class2Value.get(type);
+ if (m != null)
+ return m.invoke(null, value);
+
+ if (type.equals(java.lang.Character.TYPE) ||
+ type.equals(java.lang.Character.class))
+ return value.charAt(0);
+
+ Constructor<?> c = type.getConstructor(java.lang.String.class);
+ return c.newInstance(value);
+ }
+ catch (NoSuchMethodException | IllegalAccessException | InstantiationException x)
+ {
+ LOG.ignore(x);
+ }
+ catch (InvocationTargetException x)
+ {
+ if (x.getTargetException() instanceof Error)
+ throw (Error)x.getTargetException();
+ LOG.ignore(x);
+ }
+ return null;
+ }
+
+ /**
+ * Convert String value to instance.
+ *
+ * @param type classname or type (eg int)
+ * @param value The value as a string.
+ * @return The value as an Object.
+ */
+ public static Object valueOf(String type, String value)
+ {
+ return valueOf(fromName(type), value);
+ }
+
+ /**
+ * Parse an int from a substring.
+ * Negative numbers are not handled.
+ *
+ * @param s String
+ * @param offset Offset within string
+ * @param length Length of integer or -1 for remainder of string
+ * @param base base of the integer
+ * @return the parsed integer
+ * @throws NumberFormatException if the string cannot be parsed
+ */
+ public static int parseInt(String s, int offset, int length, int base)
+ throws NumberFormatException
+ {
+ int value = 0;
+
+ if (length < 0)
+ length = s.length() - offset;
+
+ for (int i = 0; i < length; i++)
+ {
+ char c = s.charAt(offset + i);
+
+ int digit = convertHexDigit((int)c);
+ if (digit < 0 || digit >= base)
+ throw new NumberFormatException(s.substring(offset, offset + length));
+ value = value * base + digit;
+ }
+ return value;
+ }
+
+ /**
+ * Parse an int from a byte array of ascii characters.
+ * Negative numbers are not handled.
+ *
+ * @param b byte array
+ * @param offset Offset within string
+ * @param length Length of integer or -1 for remainder of string
+ * @param base base of the integer
+ * @return the parsed integer
+ * @throws NumberFormatException if the array cannot be parsed into an integer
+ */
+ public static int parseInt(byte[] b, int offset, int length, int base)
+ throws NumberFormatException
+ {
+ int value = 0;
+
+ if (length < 0)
+ length = b.length - offset;
+
+ for (int i = 0; i < length; i++)
+ {
+ char c = (char)(0xff & b[offset + i]);
+
+ int digit = c - '0';
+ if (digit < 0 || digit >= base || digit >= 10)
+ {
+ digit = 10 + c - 'A';
+ if (digit < 10 || digit >= base)
+ digit = 10 + c - 'a';
+ }
+ if (digit < 0 || digit >= base)
+ throw new NumberFormatException(new String(b, offset, length));
+ value = value * base + digit;
+ }
+ return value;
+ }
+
+ public static byte[] parseBytes(String s, int base)
+ {
+ byte[] bytes = new byte[s.length() / 2];
+ for (int i = 0; i < s.length(); i += 2)
+ {
+ bytes[i / 2] = (byte)TypeUtil.parseInt(s, i, 2, base);
+ }
+ return bytes;
+ }
+
+ public static String toString(byte[] bytes, int base)
+ {
+ StringBuilder buf = new StringBuilder();
+ for (byte b : bytes)
+ {
+ int bi = 0xff & b;
+ int c = '0' + (bi / base) % base;
+ if (c > '9')
+ c = 'a' + (c - '0' - 10);
+ buf.append((char)c);
+ c = '0' + bi % base;
+ if (c > '9')
+ c = 'a' + (c - '0' - 10);
+ buf.append((char)c);
+ }
+ return buf.toString();
+ }
+
+ /**
+ * @param c An ASCII encoded character 0-9 a-f A-F
+ * @return The byte value of the character 0-16.
+ */
+ public static byte convertHexDigit(byte c)
+ {
+ byte b = (byte)((c & 0x1f) + ((c >> 6) * 0x19) - 0x10);
+ if (b < 0 || b > 15)
+ throw new NumberFormatException("!hex " + c);
+ return b;
+ }
+
+ /**
+ * @param c An ASCII encoded character 0-9 a-f A-F
+ * @return The byte value of the character 0-16.
+ */
+ public static int convertHexDigit(char c)
+ {
+ int d = ((c & 0x1f) + ((c >> 6) * 0x19) - 0x10);
+ if (d < 0 || d > 15)
+ throw new NumberFormatException("!hex " + c);
+ return d;
+ }
+
+ /**
+ * @param c An ASCII encoded character 0-9 a-f A-F
+ * @return The byte value of the character 0-16.
+ */
+ public static int convertHexDigit(int c)
+ {
+ int d = ((c & 0x1f) + ((c >> 6) * 0x19) - 0x10);
+ if (d < 0 || d > 15)
+ throw new NumberFormatException("!hex " + c);
+ return d;
+ }
+
+ public static void toHex(byte b, Appendable buf)
+ {
+ try
+ {
+ int d = 0xf & ((0xF0 & b) >> 4);
+ buf.append((char)((d > 9 ? ('A' - 10) : '0') + d));
+ d = 0xf & b;
+ buf.append((char)((d > 9 ? ('A' - 10) : '0') + d));
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void toHex(int value, Appendable buf) throws IOException
+ {
+ int d = 0xf & ((0xF0000000 & value) >> 28);
+ buf.append((char)((d > 9 ? ('A' - 10) : '0') + d));
+ d = 0xf & ((0x0F000000 & value) >> 24);
+ buf.append((char)((d > 9 ? ('A' - 10) : '0') + d));
+ d = 0xf & ((0x00F00000 & value) >> 20);
+ buf.append((char)((d > 9 ? ('A' - 10) : '0') + d));
+ d = 0xf & ((0x000F0000 & value) >> 16);
+ buf.append((char)((d > 9 ? ('A' - 10) : '0') + d));
+ d = 0xf & ((0x0000F000 & value) >> 12);
+ buf.append((char)((d > 9 ? ('A' - 10) : '0') + d));
+ d = 0xf & ((0x00000F00 & value) >> 8);
+ buf.append((char)((d > 9 ? ('A' - 10) : '0') + d));
+ d = 0xf & ((0x000000F0 & value) >> 4);
+ buf.append((char)((d > 9 ? ('A' - 10) : '0') + d));
+ d = 0xf & value;
+ buf.append((char)((d > 9 ? ('A' - 10) : '0') + d));
+
+ Integer.toString(0, 36);
+ }
+
+ public static void toHex(long value, Appendable buf) throws IOException
+ {
+ toHex((int)(value >> 32), buf);
+ toHex((int)value, buf);
+ }
+
+ public static String toHexString(byte b)
+ {
+ return toHexString(new byte[]{b}, 0, 1);
+ }
+
+ public static String toHexString(byte[] b)
+ {
+ return toHexString(b, 0, b.length);
+ }
+
+ public static String toHexString(byte[] b, int offset, int length)
+ {
+ StringBuilder buf = new StringBuilder();
+ for (int i = offset; i < offset + length; i++)
+ {
+ int bi = 0xff & b[i];
+ int c = '0' + (bi / 16) % 16;
+ if (c > '9')
+ c = 'A' + (c - '0' - 10);
+ buf.append((char)c);
+ c = '0' + bi % 16;
+ if (c > '9')
+ c = 'a' + (c - '0' - 10);
+ buf.append((char)c);
+ }
+ return buf.toString();
+ }
+
+ public static byte[] fromHexString(String s)
+ {
+ if (s.length() % 2 != 0)
+ throw new IllegalArgumentException(s);
+ byte[] array = new byte[s.length() / 2];
+ for (int i = 0; i < array.length; i++)
+ {
+ int b = Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16);
+ array[i] = (byte)(0xff & b);
+ }
+ return array;
+ }
+
+ public static void dump(Class<?> c)
+ {
+ System.err.println("Dump: " + c);
+ dump(c.getClassLoader());
+ }
+
+ public static void dump(ClassLoader cl)
+ {
+ System.err.println("Dump Loaders:");
+ while (cl != null)
+ {
+ System.err.println(" loader " + cl);
+ cl = cl.getParent();
+ }
+ }
+
+ @Deprecated
+ public static Object call(Class<?> oClass, String methodName, Object obj, Object[] arg) throws InvocationTargetException, NoSuchMethodException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Deprecated
+ public static Object construct(Class<?> klass, Object[] arguments) throws InvocationTargetException, NoSuchMethodException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Deprecated
+ public static Object construct(Class<?> klass, Object[] arguments, Map<String, Object> namedArgMap) throws InvocationTargetException, NoSuchMethodException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * @param o Object to test for true
+ * @return True if passed object is not null and is either a Boolean with value true or evaluates to a string that evaluates to true.
+ */
+ public static boolean isTrue(Object o)
+ {
+ if (o == null)
+ return false;
+ if (o instanceof Boolean)
+ return ((Boolean)o).booleanValue();
+ return Boolean.parseBoolean(o.toString());
+ }
+
+ /**
+ * @param o Object to test for false
+ * @return True if passed object is not null and is either a Boolean with value false or evaluates to a string that evaluates to false.
+ */
+ public static boolean isFalse(Object o)
+ {
+ if (o == null)
+ return false;
+ if (o instanceof Boolean)
+ return !((Boolean)o).booleanValue();
+ return "false".equalsIgnoreCase(o.toString());
+ }
+
+ /**
+ * Attempt to find the Location of a loaded Class.
+ * <p>
+ * This can be null for primitives, void, and in-memory classes.
+ * </p>
+ *
+ * @param clazz the loaded class to find a location for.
+ * @return the location as a URI (this is a URI pointing to a holder of the class: a directory,
+ * a jar file, a {@code jrt://} resource, etc), or null of no location available.
+ */
+ public static URI getLocationOfClass(Class<?> clazz)
+ {
+ for (Function<Class<?>, URI> locationFunction : LOCATION_METHODS)
+ {
+ try
+ {
+ URI location = locationFunction.apply(clazz);
+ if (location != null)
+ {
+ return location;
+ }
+ }
+ catch (Throwable cause)
+ {
+ cause.printStackTrace(System.err);
+ }
+ }
+ return null;
+ }
+
+ public static URI getClassLoaderLocation(Class<?> clazz)
+ {
+ return getClassLoaderLocation(clazz, clazz.getClassLoader());
+ }
+
+ public static URI getSystemClassLoaderLocation(Class<?> clazz)
+ {
+ return getClassLoaderLocation(clazz, ClassLoader.getSystemClassLoader());
+ }
+
+ public static URI getClassLoaderLocation(Class<?> clazz, ClassLoader loader)
+ {
+ if (loader == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ String resourceName = TypeUtil.toClassReference(clazz);
+ if (loader != null)
+ {
+ URL url = loader.getResource(resourceName);
+ if (url != null)
+ {
+ URI uri = url.toURI();
+ String uriStr = uri.toASCIIString();
+ if (uriStr.startsWith("jar:file:"))
+ {
+ uriStr = uriStr.substring(4);
+ int idx = uriStr.indexOf("!/");
+ if (idx > 0)
+ {
+ return URI.create(uriStr.substring(0, idx));
+ }
+ }
+ return uri;
+ }
+ }
+ }
+ catch (URISyntaxException ignored)
+ {
+ }
+ return null;
+ }
+
+ public static URI getCodeSourceLocation(Class<?> clazz)
+ {
+ try
+ {
+ ProtectionDomain domain = AccessController.doPrivileged((PrivilegedAction<ProtectionDomain>)() -> clazz.getProtectionDomain());
+ if (domain != null)
+ {
+ CodeSource source = domain.getCodeSource();
+ if (source != null)
+ {
+ URL location = source.getLocation();
+
+ if (location != null)
+ {
+ return location.toURI();
+ }
+ }
+ }
+ }
+ catch (URISyntaxException ignored)
+ {
+ }
+ return null;
+ }
+
+ public static URI getModuleLocation(Class<?> clazz)
+ {
+ // In Jetty 10, this method can be implemented directly, without reflection
+ if (MODULE_LOCATION != null)
+ {
+ return MODULE_LOCATION.apply(clazz);
+ }
+ return null;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java
new file mode 100644
index 0000000..227db98
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java
@@ -0,0 +1,1386 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * URI Utility methods.
+ * <p>
+ * This class assists with the decoding and encoding or HTTP URI's.
+ * It differs from the java.net.URL class as it does not provide
+ * communications ability, but it does assist with query string
+ * formatting.
+ * </p>
+ *
+ * @see UrlEncoded
+ */
+public class URIUtil
+ implements Cloneable
+{
+ private static final Logger LOG = Log.getLogger(URIUtil.class);
+ public static final String SLASH = "/";
+ public static final String HTTP = "http";
+ public static final String HTTPS = "https";
+
+ // Use UTF-8 as per http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars
+ public static final Charset __CHARSET = StandardCharsets.UTF_8;
+
+ private URIUtil()
+ {
+ }
+
+ /**
+ * Encode a URI path.
+ * This is the same encoding offered by URLEncoder, except that
+ * the '/' character is not encoded.
+ *
+ * @param path The path the encode
+ * @return The encoded path
+ */
+ public static String encodePath(String path)
+ {
+ if (path == null || path.length() == 0)
+ return path;
+
+ StringBuilder buf = encodePath(null, path, 0);
+ return buf == null ? path : buf.toString();
+ }
+
+ /**
+ * Encode a URI path.
+ *
+ * @param path The path the encode
+ * @param buf StringBuilder to encode path into (or null)
+ * @return The StringBuilder or null if no substitutions required.
+ */
+ public static StringBuilder encodePath(StringBuilder buf, String path)
+ {
+ return encodePath(buf, path, 0);
+ }
+
+ /**
+ * Encode a URI path.
+ *
+ * @param path The path the encode
+ * @param buf StringBuilder to encode path into (or null)
+ * @return The StringBuilder or null if no substitutions required.
+ */
+ private static StringBuilder encodePath(StringBuilder buf, String path, int offset)
+ {
+ byte[] bytes = null;
+ if (buf == null)
+ {
+ loop:
+ for (int i = offset; i < path.length(); i++)
+ {
+ char c = path.charAt(i);
+ switch (c)
+ {
+ case '%':
+ case '?':
+ case ';':
+ case '#':
+ case '"':
+ case '\'':
+ case '<':
+ case '>':
+ case ' ':
+ case '[':
+ case '\\':
+ case ']':
+ case '^':
+ case '`':
+ case '{':
+ case '|':
+ case '}':
+ buf = new StringBuilder(path.length() * 2);
+ break loop;
+ default:
+ if (c < 0x20 || c >= 0x7f)
+ {
+ bytes = path.getBytes(URIUtil.__CHARSET);
+ buf = new StringBuilder(path.length() * 2);
+ break loop;
+ }
+ }
+ }
+ if (buf == null)
+ return null;
+ }
+
+ int i;
+
+ loop:
+ for (i = offset; i < path.length(); i++)
+ {
+ char c = path.charAt(i);
+ switch (c)
+ {
+ case '%':
+ buf.append("%25");
+ continue;
+ case '?':
+ buf.append("%3F");
+ continue;
+ case ';':
+ buf.append("%3B");
+ continue;
+ case '#':
+ buf.append("%23");
+ continue;
+ case '"':
+ buf.append("%22");
+ continue;
+ case '\'':
+ buf.append("%27");
+ continue;
+ case '<':
+ buf.append("%3C");
+ continue;
+ case '>':
+ buf.append("%3E");
+ continue;
+ case ' ':
+ buf.append("%20");
+ continue;
+ case '[':
+ buf.append("%5B");
+ continue;
+ case '\\':
+ buf.append("%5C");
+ continue;
+ case ']':
+ buf.append("%5D");
+ continue;
+ case '^':
+ buf.append("%5E");
+ continue;
+ case '`':
+ buf.append("%60");
+ continue;
+ case '{':
+ buf.append("%7B");
+ continue;
+ case '|':
+ buf.append("%7C");
+ continue;
+ case '}':
+ buf.append("%7D");
+ continue;
+
+ default:
+ if (c < 0x20 || c >= 0x7f)
+ {
+ bytes = path.getBytes(URIUtil.__CHARSET);
+ break loop;
+ }
+ buf.append(c);
+ }
+ }
+
+ if (bytes != null)
+ {
+ for (; i < bytes.length; i++)
+ {
+ byte c = bytes[i];
+ switch (c)
+ {
+ case '%':
+ buf.append("%25");
+ continue;
+ case '?':
+ buf.append("%3F");
+ continue;
+ case ';':
+ buf.append("%3B");
+ continue;
+ case '#':
+ buf.append("%23");
+ continue;
+ case '"':
+ buf.append("%22");
+ continue;
+ case '\'':
+ buf.append("%27");
+ continue;
+ case '<':
+ buf.append("%3C");
+ continue;
+ case '>':
+ buf.append("%3E");
+ continue;
+ case ' ':
+ buf.append("%20");
+ continue;
+ case '[':
+ buf.append("%5B");
+ continue;
+ case '\\':
+ buf.append("%5C");
+ continue;
+ case ']':
+ buf.append("%5D");
+ continue;
+ case '^':
+ buf.append("%5E");
+ continue;
+ case '`':
+ buf.append("%60");
+ continue;
+ case '{':
+ buf.append("%7B");
+ continue;
+ case '|':
+ buf.append("%7C");
+ continue;
+ case '}':
+ buf.append("%7D");
+ continue;
+ default:
+ if (c < 0x20 || c >= 0x7f)
+ {
+ buf.append('%');
+ TypeUtil.toHex(c, buf);
+ }
+ else
+ buf.append((char)c);
+ }
+ }
+ }
+
+ return buf;
+ }
+
+ /**
+ * Encode a raw URI String and convert any raw spaces to
+ * their "%20" equivalent.
+ *
+ * @param str input raw string
+ * @return output with spaces converted to "%20"
+ */
+ public static String encodeSpaces(String str)
+ {
+ return StringUtil.replace(str, " ", "%20");
+ }
+
+ /**
+ * Encode a raw String and convert any specific characters to their URI encoded equivalent.
+ *
+ * @param str input raw string
+ * @param charsToEncode the list of raw characters that need to be encoded (if encountered)
+ * @return output with specified characters encoded.
+ */
+ @SuppressWarnings("Duplicates")
+ public static String encodeSpecific(String str, String charsToEncode)
+ {
+ if ((str == null) || (str.length() == 0))
+ return null;
+
+ if ((charsToEncode == null) || (charsToEncode.length() == 0))
+ return str;
+
+ char[] find = charsToEncode.toCharArray();
+ int len = str.length();
+ StringBuilder ret = new StringBuilder((int)(len * 0.20d));
+ for (int i = 0; i < len; i++)
+ {
+ char c = str.charAt(i);
+ boolean escaped = false;
+ for (char f : find)
+ {
+ if (c == f)
+ {
+ escaped = true;
+ ret.append('%');
+ int d = 0xf & ((0xF0 & c) >> 4);
+ ret.append((char)((d > 9 ? ('A' - 10) : '0') + d));
+ d = 0xf & c;
+ ret.append((char)((d > 9 ? ('A' - 10) : '0') + d));
+ break;
+ }
+ }
+ if (!escaped)
+ {
+ ret.append(c);
+ }
+ }
+ return ret.toString();
+ }
+
+ /**
+ * Decode a raw String and convert any specific URI encoded sequences into characters.
+ *
+ * @param str input raw string
+ * @param charsToDecode the list of raw characters that need to be decoded (if encountered), leaving all other encoded sequences alone.
+ * @return output with specified characters decoded.
+ */
+ @SuppressWarnings("Duplicates")
+ public static String decodeSpecific(String str, String charsToDecode)
+ {
+ if ((str == null) || (str.length() == 0))
+ return null;
+
+ if ((charsToDecode == null) || (charsToDecode.length() == 0))
+ return str;
+
+ int idx = str.indexOf('%');
+ if (idx == -1)
+ {
+ // no hits
+ return str;
+ }
+
+ char[] find = charsToDecode.toCharArray();
+ int len = str.length();
+ Utf8StringBuilder ret = new Utf8StringBuilder(len);
+ ret.append(str, 0, idx);
+
+ for (int i = idx; i < len; i++)
+ {
+ char c = str.charAt(i);
+ switch (c)
+ {
+ case '%':
+ if ((i + 2) < len)
+ {
+ char u = str.charAt(i + 1);
+ char l = str.charAt(i + 2);
+ char result = (char)(0xff & (TypeUtil.convertHexDigit(u) * 16 + TypeUtil.convertHexDigit(l)));
+ boolean decoded = false;
+ for (char f : find)
+ {
+ if (f == result)
+ {
+ ret.append(result);
+ decoded = true;
+ break;
+ }
+ }
+ if (decoded)
+ {
+ i += 2;
+ }
+ else
+ {
+ ret.append(c);
+ }
+ }
+ else
+ {
+ throw new IllegalArgumentException("Bad URI % encoding");
+ }
+ break;
+ default:
+ ret.append(c);
+ break;
+ }
+ }
+ return ret.toString();
+ }
+
+ /**
+ * Encode a URI path.
+ *
+ * @param path The path the encode
+ * @param buf StringBuilder to encode path into (or null)
+ * @param encode String of characters to encode. % is always encoded.
+ * @return The StringBuilder or null if no substitutions required.
+ */
+ public static StringBuilder encodeString(StringBuilder buf,
+ String path,
+ String encode)
+ {
+ if (buf == null)
+ {
+ for (int i = 0; i < path.length(); i++)
+ {
+ char c = path.charAt(i);
+ if (c == '%' || encode.indexOf(c) >= 0)
+ {
+ buf = new StringBuilder(path.length() << 1);
+ break;
+ }
+ }
+ if (buf == null)
+ return null;
+ }
+
+ for (int i = 0; i < path.length(); i++)
+ {
+ char c = path.charAt(i);
+ if (c == '%' || encode.indexOf(c) >= 0)
+ {
+ buf.append('%');
+ StringUtil.append(buf, (byte)(0xff & c), 16);
+ }
+ else
+ buf.append(c);
+ }
+
+ return buf;
+ }
+
+ /* Decode a URI path and strip parameters
+ */
+ public static String decodePath(String path)
+ {
+ return decodePath(path, 0, path.length());
+ }
+
+ /* Decode a URI path and strip parameters of UTF-8 path
+ */
+ public static String decodePath(String path, int offset, int length)
+ {
+ try
+ {
+ Utf8StringBuilder builder = null;
+ int end = offset + length;
+ for (int i = offset; i < end; i++)
+ {
+ char c = path.charAt(i);
+ switch (c)
+ {
+ case '%':
+ if (builder == null)
+ {
+ builder = new Utf8StringBuilder(path.length());
+ builder.append(path, offset, i - offset);
+ }
+ if ((i + 2) < end)
+ {
+ char u = path.charAt(i + 1);
+ if (u == 'u')
+ {
+ // In Jetty-10 UTF16 encoding is only supported with UriCompliance.Violation.UTF16_ENCODINGS.
+ // This is wrong. This is a codepoint not a char
+ builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16)));
+ i += 5;
+ }
+ else
+ {
+ builder.append((byte)(0xff & (TypeUtil.convertHexDigit(u) * 16 + TypeUtil.convertHexDigit(path.charAt(i + 2)))));
+ i += 2;
+ }
+ }
+ else
+ {
+ throw new IllegalArgumentException("Bad URI % encoding");
+ }
+
+ break;
+
+ case ';':
+ if (builder == null)
+ {
+ builder = new Utf8StringBuilder(path.length());
+ builder.append(path, offset, i - offset);
+ }
+
+ while (++i < end)
+ {
+ if (path.charAt(i) == '/')
+ {
+ builder.append('/');
+ break;
+ }
+ }
+
+ break;
+
+ default:
+ if (builder != null)
+ builder.append(c);
+ break;
+ }
+ }
+
+ if (builder != null)
+ return builder.toString();
+ if (offset == 0 && length == path.length())
+ return path;
+ return path.substring(offset, end);
+ }
+ catch (NotUtf8Exception e)
+ {
+ LOG.debug(path.substring(offset, offset + length) + " " + e);
+ return decodeISO88591Path(path, offset, length);
+ }
+ catch (IllegalArgumentException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new IllegalArgumentException("cannot decode URI", e);
+ }
+ }
+
+ /* Decode a URI path and strip parameters of ISO-8859-1 path
+ */
+ private static String decodeISO88591Path(String path, int offset, int length)
+ {
+ StringBuilder builder = null;
+ int end = offset + length;
+ for (int i = offset; i < end; i++)
+ {
+ char c = path.charAt(i);
+ switch (c)
+ {
+ case '%':
+ if (builder == null)
+ {
+ builder = new StringBuilder(path.length());
+ builder.append(path, offset, i - offset);
+ }
+ if ((i + 2) < end)
+ {
+ char u = path.charAt(i + 1);
+ if (u == 'u')
+ {
+ // In Jetty-10 UTF16 encoding is only supported with UriCompliance.Violation.UTF16_ENCODINGS. // This is wrong. This is a codepoint not a char
+ builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16)));
+ i += 5;
+ }
+ else
+ {
+ builder.append((char)(0xff & (TypeUtil.convertHexDigit(u) * 16 + TypeUtil.convertHexDigit(path.charAt(i + 2)))));
+ i += 2;
+ }
+ }
+ else
+ {
+ throw new IllegalArgumentException();
+ }
+
+ break;
+
+ case ';':
+ if (builder == null)
+ {
+ builder = new StringBuilder(path.length());
+ builder.append(path, offset, i - offset);
+ }
+ while (++i < end)
+ {
+ if (path.charAt(i) == '/')
+ {
+ builder.append('/');
+ break;
+ }
+ }
+ break;
+
+ default:
+ if (builder != null)
+ builder.append(c);
+ break;
+ }
+ }
+
+ if (builder != null)
+ return builder.toString();
+ if (offset == 0 && length == path.length())
+ return path;
+ return path.substring(offset, end);
+ }
+
+ /**
+ * Add two encoded URI path segments.
+ * Handles null and empty paths, path and query params
+ * (eg ?a=b or ;JSESSIONID=xxx) and avoids duplicate '/'
+ *
+ * @param p1 URI path segment (should be encoded)
+ * @param p2 URI path segment (should be encoded)
+ * @return Legally combined path segments.
+ */
+ public static String addEncodedPaths(String p1, String p2)
+ {
+ if (p1 == null || p1.length() == 0)
+ {
+ if (p1 != null && p2 == null)
+ return p1;
+ return p2;
+ }
+ if (p2 == null || p2.length() == 0)
+ return p1;
+
+ int split = p1.indexOf(';');
+ if (split < 0)
+ split = p1.indexOf('?');
+ if (split == 0)
+ return p2 + p1;
+ if (split < 0)
+ split = p1.length();
+
+ StringBuilder buf = new StringBuilder(p1.length() + p2.length() + 2);
+ buf.append(p1);
+
+ if (buf.charAt(split - 1) == '/')
+ {
+ if (p2.startsWith(URIUtil.SLASH))
+ {
+ buf.deleteCharAt(split - 1);
+ buf.insert(split - 1, p2);
+ }
+ else
+ buf.insert(split, p2);
+ }
+ else
+ {
+ if (p2.startsWith(URIUtil.SLASH))
+ buf.insert(split, p2);
+ else
+ {
+ buf.insert(split, '/');
+ buf.insert(split + 1, p2);
+ }
+ }
+
+ return buf.toString();
+ }
+
+ /**
+ * Add two Decoded URI path segments.
+ * Handles null and empty paths. Path and query params (eg ?a=b or
+ * ;JSESSIONID=xxx) are not handled
+ *
+ * @param p1 URI path segment (should be decoded)
+ * @param p2 URI path segment (should be decoded)
+ * @return Legally combined path segments.
+ */
+ public static String addPaths(String p1, String p2)
+ {
+ if (p1 == null || p1.length() == 0)
+ {
+ if (p1 != null && p2 == null)
+ return p1;
+ return p2;
+ }
+ if (p2 == null || p2.length() == 0)
+ return p1;
+
+ boolean p1EndsWithSlash = p1.endsWith(SLASH);
+ boolean p2StartsWithSlash = p2.startsWith(SLASH);
+
+ if (p1EndsWithSlash && p2StartsWithSlash)
+ {
+ if (p2.length() == 1)
+ return p1;
+ if (p1.length() == 1)
+ return p2;
+ }
+
+ StringBuilder buf = new StringBuilder(p1.length() + p2.length() + 2);
+ buf.append(p1);
+
+ if (p1.endsWith(SLASH))
+ {
+ if (p2.startsWith(SLASH))
+ buf.setLength(buf.length() - 1);
+ }
+ else
+ {
+ if (!p2.startsWith(SLASH))
+ buf.append(SLASH);
+ }
+ buf.append(p2);
+
+ return buf.toString();
+ }
+
+ /** Add a path and a query string
+ * @param path The path which may already contain contain a query
+ * @param query The query string or null if no query to be added
+ * @return The path with any non null query added after a '?' or '&' as appropriate.
+ */
+ public static String addPathQuery(String path, String query)
+ {
+ if (query == null)
+ return path;
+ if (path.indexOf('?') >= 0)
+ return path + '&' + query;
+ return path + '?' + query;
+ }
+
+ /**
+ * Given a URI, attempt to get the last segment.
+ * <p>
+ * If this is a {@code jar:file://} style URI, then
+ * the JAR filename is returned (not the deep {@code !/path} location)
+ * </p>
+ *
+ * @param uri the URI to look in
+ * @return the last segment.
+ */
+ public static String getUriLastPathSegment(URI uri)
+ {
+ String ssp = uri.getSchemeSpecificPart();
+ // strip off deep jar:file: reference information
+ int idx = ssp.indexOf("!/");
+ if (idx != -1)
+ {
+ ssp = ssp.substring(0, idx);
+ }
+
+ // Strip off trailing '/' if present
+ if (ssp.endsWith("/"))
+ {
+ ssp = ssp.substring(0, ssp.length() - 1);
+ }
+
+ // Only interested in last segment
+ idx = ssp.lastIndexOf('/');
+ if (idx != -1)
+ {
+ ssp = ssp.substring(idx + 1);
+ }
+
+ return ssp;
+ }
+
+ /**
+ * Return the parent Path.
+ * Treat a URI like a directory path and return the parent directory.
+ *
+ * @param p the path to return a parent reference to
+ * @return the parent path of the URI
+ */
+ public static String parentPath(String p)
+ {
+ if (p == null || URIUtil.SLASH.equals(p))
+ return null;
+ int slash = p.lastIndexOf('/', p.length() - 2);
+ if (slash >= 0)
+ return p.substring(0, slash + 1);
+ return null;
+ }
+
+ /**
+ * Convert a partial URI to a canonical form.
+ * <p>
+ * All segments of "." and ".." are factored out.
+ * Null is returned if the path tries to .. above its root.
+ * </p>
+ *
+ * @param uri the encoded URI from the path onwards, which may contain query strings and/or fragments
+ * @return the canonical path, or null if path traversal above root.
+ * @see #canonicalPath(String)
+ */
+ public static String canonicalURI(String uri)
+ {
+ if (uri == null || uri.isEmpty())
+ return uri;
+
+ boolean slash = true;
+ int end = uri.length();
+ int i = 0;
+
+ // Initially just loop looking if we may need to normalize
+ loop: while (i < end)
+ {
+ char c = uri.charAt(i);
+ switch (c)
+ {
+ case '/':
+ slash = true;
+ break;
+
+ case '.':
+ if (slash)
+ break loop;
+ slash = false;
+ break;
+
+ case '?':
+ case '#':
+ // Nothing to normalize so return original path
+ return uri;
+
+ default:
+ slash = false;
+ }
+
+ i++;
+ }
+
+ // Nothing to normalize so return original path
+ if (i == end)
+ return uri;
+
+ // We probably need to normalize, so copy to path so far into builder
+ StringBuilder canonical = new StringBuilder(uri.length());
+ canonical.append(uri, 0, i);
+
+ // Loop looking for single and double dot segments
+ int dots = 1;
+ i++;
+ loop : while (i < end)
+ {
+ char c = uri.charAt(i);
+ switch (c)
+ {
+ case '/':
+ if (doDotsSlash(canonical, dots))
+ return null;
+ slash = true;
+ dots = 0;
+ break;
+
+ case '?':
+ case '#':
+ // finish normalization at a query
+ break loop;
+
+ case '.':
+ // Count dots only if they are leading in the segment
+ if (dots > 0)
+ dots++;
+ else if (slash)
+ dots = 1;
+ else
+ canonical.append('.');
+ slash = false;
+ break;
+
+ default:
+ // Add leading dots to the path
+ while (dots-- > 0)
+ canonical.append('.');
+ canonical.append(c);
+ dots = 0;
+ slash = false;
+ }
+ i++;
+ }
+
+ // process any remaining dots
+ if (doDots(canonical, dots))
+ return null;
+
+ // append any query
+ if (i < end)
+ canonical.append(uri, i, end);
+
+ return canonical.toString();
+ }
+
+ /**
+ * @param path the encoded URI from the path onwards, which may contain query strings and/or fragments
+ * @return the canonical path, or null if path traversal above root.
+ * @deprecated Use {@link #canonicalURI(String)}
+ */
+ @Deprecated
+ public static String canonicalEncodedPath(String path)
+ {
+ return canonicalURI(path);
+ }
+
+ /**
+ * Convert a decoded URI path to a canonical form.
+ * <p>
+ * All segments of "." and ".." are factored out.
+ * Null is returned if the path tries to .. above its root.
+ * </p>
+ *
+ * @param path the decoded URI path to convert. Any special characters (e.g. '?', "#") are assumed to be part of
+ * the path segments.
+ * @return the canonical path, or null if path traversal above root.
+ * @see #canonicalURI(String)
+ */
+ public static String canonicalPath(String path)
+ {
+ if (path == null || path.isEmpty())
+ return path;
+
+ boolean slash = true;
+ int end = path.length();
+ int i = 0;
+
+ // Initially just loop looking if we may need to normalize
+ loop: while (i < end)
+ {
+ char c = path.charAt(i);
+ switch (c)
+ {
+ case '/':
+ slash = true;
+ break;
+
+ case '.':
+ if (slash)
+ break loop;
+ slash = false;
+ break;
+
+ default:
+ slash = false;
+ }
+
+ i++;
+ }
+
+ // Nothing to normalize so return original path
+ if (i == end)
+ return path;
+
+ // We probably need to normalize, so copy to path so far into builder
+ StringBuilder canonical = new StringBuilder(path.length());
+ canonical.append(path, 0, i);
+
+ // Loop looking for single and double dot segments
+ int dots = 1;
+ i++;
+ while (i < end)
+ {
+ char c = path.charAt(i);
+ switch (c)
+ {
+ case '/':
+ if (doDotsSlash(canonical, dots))
+ return null;
+ slash = true;
+ dots = 0;
+ break;
+
+ case '.':
+ // Count dots only if they are leading in the segment
+ if (dots > 0)
+ dots++;
+ else if (slash)
+ dots = 1;
+ else
+ canonical.append('.');
+ slash = false;
+ break;
+
+ default:
+ // Add leading dots to the path
+ while (dots-- > 0)
+ canonical.append('.');
+ canonical.append(c);
+ dots = 0;
+ slash = false;
+ }
+ i++;
+ }
+
+ // process any remaining dots
+ if (doDots(canonical, dots))
+ return null;
+
+ return canonical.toString();
+ }
+
+ private static boolean doDots(StringBuilder canonical, int dots)
+ {
+ switch (dots)
+ {
+ case 0:
+ case 1:
+ break;
+ case 2:
+ if (canonical.length() < 2)
+ return true;
+ canonical.setLength(canonical.length() - 1);
+ canonical.setLength(canonical.lastIndexOf("/") + 1);
+ break;
+ default:
+ while (dots-- > 0)
+ canonical.append('.');
+ }
+ return false;
+ }
+
+ private static boolean doDotsSlash(StringBuilder canonical, int dots)
+ {
+ switch (dots)
+ {
+ case 0:
+ canonical.append('/');
+ break;
+ case 1:
+ break;
+ case 2:
+ if (canonical.length() < 2)
+ return true;
+ canonical.setLength(canonical.length() - 1);
+ canonical.setLength(canonical.lastIndexOf("/") + 1);
+ break;
+ default:
+ while (dots-- > 0)
+ canonical.append('.');
+ canonical.append('/');
+ }
+ return false;
+ }
+
+ /**
+ * Convert a path to a compact form.
+ * All instances of "//" and "///" etc. are factored out to single "/"
+ *
+ * @param path the path to compact
+ * @return the compacted path
+ */
+ public static String compactPath(String path)
+ {
+ if (path == null || path.length() == 0)
+ return path;
+
+ int state = 0;
+ int end = path.length();
+ int i = 0;
+
+ loop:
+ while (i < end)
+ {
+ char c = path.charAt(i);
+ switch (c)
+ {
+ case '?':
+ return path;
+ case '/':
+ state++;
+ if (state == 2)
+ break loop;
+ break;
+ default:
+ state = 0;
+ }
+ i++;
+ }
+
+ if (state < 2)
+ return path;
+
+ StringBuilder buf = new StringBuilder(path.length());
+ buf.append(path, 0, i);
+
+ loop2:
+ while (i < end)
+ {
+ char c = path.charAt(i);
+ switch (c)
+ {
+ case '?':
+ buf.append(path, i, end);
+ break loop2;
+ case '/':
+ if (state++ == 0)
+ buf.append(c);
+ break;
+ default:
+ state = 0;
+ buf.append(c);
+ }
+ i++;
+ }
+
+ return buf.toString();
+ }
+
+ /**
+ * @param uri URI
+ * @return True if the uri has a scheme
+ */
+ public static boolean hasScheme(String uri)
+ {
+ for (int i = 0; i < uri.length(); i++)
+ {
+ char c = uri.charAt(i);
+ if (c == ':')
+ {
+ return true;
+ }
+ if (!(c >= 'a' && c <= 'z' ||
+ c >= 'A' && c <= 'Z' ||
+ (i > 0 && (c >= '0' && c <= '9' || c == '.' || c == '+' || c == '-'))))
+ {
+ break;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Create a new URI from the arguments, handling IPv6 host encoding and default ports
+ *
+ * @param scheme the URI scheme
+ * @param server the URI server
+ * @param port the URI port
+ * @param path the URI path
+ * @param query the URI query
+ * @return A String URI
+ */
+ public static String newURI(String scheme, String server, int port, String path, String query)
+ {
+ StringBuilder builder = newURIBuilder(scheme, server, port);
+ builder.append(path);
+ if (query != null && query.length() > 0)
+ builder.append('?').append(query);
+ return builder.toString();
+ }
+
+ /**
+ * Create a new URI StringBuilder from the arguments, handling IPv6 host encoding and default ports
+ *
+ * @param scheme the URI scheme
+ * @param server the URI server
+ * @param port the URI port
+ * @return a StringBuilder containing URI prefix
+ */
+ public static StringBuilder newURIBuilder(String scheme, String server, int port)
+ {
+ StringBuilder builder = new StringBuilder();
+ appendSchemeHostPort(builder, scheme, server, port);
+ return builder;
+ }
+
+ /**
+ * Append scheme, host and port URI prefix, handling IPv6 address encoding and default ports
+ *
+ * @param url StringBuilder to append to
+ * @param scheme the URI scheme
+ * @param server the URI server
+ * @param port the URI port
+ */
+ public static void appendSchemeHostPort(StringBuilder url, String scheme, String server, int port)
+ {
+ url.append(scheme).append("://").append(HostPort.normalizeHost(server));
+
+ if (port > 0)
+ {
+ switch (scheme)
+ {
+ case "http":
+ if (port != 80)
+ url.append(':').append(port);
+ break;
+
+ case "https":
+ if (port != 443)
+ url.append(':').append(port);
+ break;
+
+ default:
+ url.append(':').append(port);
+ }
+ }
+ }
+
+ /**
+ * Append scheme, host and port URI prefix, handling IPv6 address encoding and default ports
+ *
+ * @param url StringBuffer to append to
+ * @param scheme the URI scheme
+ * @param server the URI server
+ * @param port the URI port
+ */
+ public static void appendSchemeHostPort(StringBuffer url, String scheme, String server, int port)
+ {
+ synchronized (url)
+ {
+ url.append(scheme).append("://").append(HostPort.normalizeHost(server));
+
+ if (port > 0)
+ {
+ switch (scheme)
+ {
+ case "http":
+ if (port != 80)
+ url.append(':').append(port);
+ break;
+
+ case "https":
+ if (port != 443)
+ url.append(':').append(port);
+ break;
+
+ default:
+ url.append(':').append(port);
+ }
+ }
+ }
+ }
+
+ public static boolean equalsIgnoreEncodings(String uriA, String uriB)
+ {
+ int lenA = uriA.length();
+ int lenB = uriB.length();
+ int a = 0;
+ int b = 0;
+
+ while (a < lenA && b < lenB)
+ {
+ int oa = uriA.charAt(a++);
+ int ca = oa;
+ if (ca == '%')
+ {
+ ca = lenientPercentDecode(uriA, a);
+ if (ca == (-1))
+ {
+ ca = '%';
+ }
+ else
+ {
+ a += 2;
+ }
+ }
+
+ int ob = uriB.charAt(b++);
+ int cb = ob;
+ if (cb == '%')
+ {
+ cb = lenientPercentDecode(uriB, b);
+ if (cb == (-1))
+ {
+ cb = '%';
+ }
+ else
+ {
+ b += 2;
+ }
+ }
+
+ // Don't match on encoded slash
+ if (ca == '/' && oa != ob)
+ return false;
+
+ if (ca != cb)
+ return false;
+ }
+ return a == lenA && b == lenB;
+ }
+
+ private static int lenientPercentDecode(String str, int offset)
+ {
+ if (offset >= str.length())
+ return -1;
+
+ if (StringUtil.isHex(str, offset, 2))
+ {
+ return TypeUtil.parseInt(str, offset, 2, 16);
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ public static boolean equalsIgnoreEncodings(URI uriA, URI uriB)
+ {
+ if (uriA.equals(uriB))
+ return true;
+
+ if (uriA.getScheme() == null)
+ {
+ if (uriB.getScheme() != null)
+ return false;
+ }
+ else if (!uriA.getScheme().equalsIgnoreCase(uriB.getScheme()))
+ return false;
+
+ if ("jar".equalsIgnoreCase(uriA.getScheme()))
+ {
+ // at this point we know that both uri's are "jar:"
+ URI uriAssp = URI.create(uriA.getSchemeSpecificPart());
+ URI uriBssp = URI.create(uriB.getSchemeSpecificPart());
+ return equalsIgnoreEncodings(uriAssp, uriBssp);
+ }
+
+ if (uriA.getAuthority() == null)
+ {
+ if (uriB.getAuthority() != null)
+ return false;
+ }
+ else if (!uriA.getAuthority().equals(uriB.getAuthority()))
+ return false;
+
+ return equalsIgnoreEncodings(uriA.getPath(), uriB.getPath());
+ }
+
+ /**
+ * @param uri A URI to add the path to
+ * @param path A decoded path element
+ * @return URI with path added.
+ */
+ public static URI addPath(URI uri, String path)
+ {
+ String base = uri.toASCIIString();
+ StringBuilder buf = new StringBuilder(base.length() + path.length() * 3);
+ buf.append(base);
+ if (buf.charAt(base.length() - 1) != '/')
+ buf.append('/');
+
+ int offset = path.charAt(0) == '/' ? 1 : 0;
+ encodePath(buf, path, offset);
+
+ return URI.create(buf.toString());
+ }
+
+ /**
+ * Combine two query strings into one. Each query string should not contain the beginning '?' character, but
+ * may contain multiple parameters separated by the '{@literal &}' character.
+ * @param query1 the first query string.
+ * @param query2 the second query string.
+ * @return the combination of the two query strings.
+ */
+ public static String addQueries(String query1, String query2)
+ {
+ if (StringUtil.isEmpty(query1))
+ return query2;
+ if (StringUtil.isEmpty(query2))
+ return query1;
+ return query1 + '&' + query2;
+ }
+
+ public static URI getJarSource(URI uri)
+ {
+ try
+ {
+ if (!"jar".equals(uri.getScheme()))
+ return uri;
+ // Get SSP (retaining encoded form)
+ String s = uri.getRawSchemeSpecificPart();
+ int bangSlash = s.indexOf("!/");
+ if (bangSlash >= 0)
+ s = s.substring(0, bangSlash);
+ return new URI(s);
+ }
+ catch (URISyntaxException e)
+ {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public static String getJarSource(String uri)
+ {
+ if (!uri.startsWith("jar:"))
+ return uri;
+ int bangSlash = uri.indexOf("!/");
+ return (bangSlash >= 0) ? uri.substring(4, bangSlash) : uri.substring(4);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Uptime.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Uptime.java
new file mode 100644
index 0000000..8eda723
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Uptime.java
@@ -0,0 +1,132 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Provide for a Uptime class that is compatible with Android, GAE, and the new Java 8 compact profiles
+ */
+public class Uptime
+{
+ public static final int NOIMPL = -1;
+
+ public interface Impl
+ {
+ long getUptime();
+ }
+
+ public static class DefaultImpl implements Impl
+ {
+ public Object mxBean;
+ public Method uptimeMethod;
+
+ public DefaultImpl()
+ {
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ try
+ {
+ Class<?> mgmtFactory = Class.forName("java.lang.management.ManagementFactory", true, cl);
+ Class<?> runtimeClass = Class.forName("java.lang.management.RuntimeMXBean", true, cl);
+ Class<?>[] noparams = new Class<?>[0];
+ Method mxBeanMethod = mgmtFactory.getMethod("getRuntimeMXBean", noparams);
+ if (mxBeanMethod == null)
+ {
+ throw new UnsupportedOperationException("method getRuntimeMXBean() not found");
+ }
+ mxBean = mxBeanMethod.invoke(mgmtFactory);
+ if (mxBean == null)
+ {
+ throw new UnsupportedOperationException("getRuntimeMXBean() method returned null");
+ }
+ uptimeMethod = runtimeClass.getMethod("getUptime", noparams);
+ if (mxBean == null)
+ {
+ throw new UnsupportedOperationException("method getUptime() not found");
+ }
+ }
+ catch (ClassNotFoundException |
+ NoClassDefFoundError |
+ NoSuchMethodException |
+ SecurityException |
+ IllegalAccessException |
+ IllegalArgumentException |
+ InvocationTargetException e)
+ {
+ throw new UnsupportedOperationException("Implementation not available in this environment", e);
+ }
+ }
+
+ @Override
+ public long getUptime()
+ {
+ try
+ {
+ return (long)uptimeMethod.invoke(mxBean);
+ }
+ catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
+ {
+ return NOIMPL;
+ }
+ }
+ }
+
+ private static final Uptime INSTANCE = new Uptime();
+
+ public static Uptime getInstance()
+ {
+ return INSTANCE;
+ }
+
+ private Impl impl;
+
+ private Uptime()
+ {
+ try
+ {
+ impl = new DefaultImpl();
+ }
+ catch (UnsupportedOperationException e)
+ {
+ System.err.printf("Defaulting Uptime to NOIMPL due to (%s) %s%n", e.getClass().getName(), e.getMessage());
+ impl = null;
+ }
+ }
+
+ public Impl getImpl()
+ {
+ return impl;
+ }
+
+ public void setImpl(Impl impl)
+ {
+ this.impl = impl;
+ }
+
+ public static long getUptime()
+ {
+ Uptime u = getInstance();
+ if (u == null || u.impl == null)
+ {
+ return NOIMPL;
+ }
+ return u.impl.getUptime();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java
new file mode 100644
index 0000000..da51768
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java
@@ -0,0 +1,984 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+import static org.eclipse.jetty.util.TypeUtil.convertHexDigit;
+
+/**
+ * Handles coding of MIME "x-www-form-urlencoded".
+ * <p>
+ * This class handles the encoding and decoding for either
+ * the query string of a URL or the _content of a POST HTTP request.
+ * </p>
+ * <b>Notes</b>
+ * <p>
+ * The UTF-8 charset is assumed, unless otherwise defined by either
+ * passing a parameter or setting the "org.eclipse.jetty.util.UrlEncoding.charset"
+ * System property.
+ * </p>
+ * <p>
+ * The hashtable either contains String single values, vectors
+ * of String or arrays of Strings.
+ * </p>
+ * <p>
+ * This class is only partially synchronised. In particular, simple
+ * get operations are not protected from concurrent updates.
+ * </p>
+ *
+ * @see java.net.URLEncoder
+ */
+@SuppressWarnings("serial")
+public class UrlEncoded extends MultiMap<String> implements Cloneable
+{
+ static final Logger LOG = Log.getLogger(UrlEncoded.class);
+
+ public static final Charset ENCODING;
+
+ static
+ {
+ Charset encoding;
+ try
+ {
+ String charset = System.getProperty("org.eclipse.jetty.util.UrlEncoding.charset");
+ encoding = charset == null ? StandardCharsets.UTF_8 : Charset.forName(charset);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ encoding = StandardCharsets.UTF_8;
+ }
+ ENCODING = encoding;
+ }
+
+ public UrlEncoded(UrlEncoded url)
+ {
+ super(url);
+ }
+
+ public UrlEncoded()
+ {
+ }
+
+ public UrlEncoded(String query)
+ {
+ decodeTo(query, this, ENCODING);
+ }
+
+ public void decode(String query)
+ {
+ decodeTo(query, this, ENCODING);
+ }
+
+ public void decode(String query, Charset charset)
+ {
+ decodeTo(query, this, charset);
+ }
+
+ /**
+ * Encode MultiMap with % encoding for UTF8 sequences.
+ *
+ * @return the MultiMap as a string with % encoding
+ */
+ public String encode()
+ {
+ return encode(ENCODING, false);
+ }
+
+ /**
+ * Encode MultiMap with % encoding for arbitrary Charset sequences.
+ *
+ * @param charset the charset to use for encoding
+ * @return the MultiMap as a string encoded with % encodings
+ */
+ public String encode(Charset charset)
+ {
+ return encode(charset, false);
+ }
+
+ /**
+ * Encode MultiMap with % encoding.
+ *
+ * @param charset the charset to encode with
+ * @param equalsForNullValue if True, then an '=' is always used, even
+ * for parameters without a value. e.g. <code>"blah?a=&b=&c="</code>.
+ * @return the MultiMap as a string encoded with % encodings
+ */
+ public synchronized String encode(Charset charset, boolean equalsForNullValue)
+ {
+ return encode(this, charset, equalsForNullValue);
+ }
+
+ /**
+ * Encode MultiMap with % encoding.
+ *
+ * @param map the map to encode
+ * @param charset the charset to use for encoding (uses default encoding if null)
+ * @param equalsForNullValue if True, then an '=' is always used, even
+ * for parameters without a value. e.g. <code>"blah?a=&b=&c="</code>.
+ * @return the MultiMap as a string encoded with % encodings.
+ */
+ public static String encode(MultiMap<String> map, Charset charset, boolean equalsForNullValue)
+ {
+ if (charset == null)
+ charset = ENCODING;
+
+ StringBuilder result = new StringBuilder(128);
+
+ boolean delim = false;
+ for (Map.Entry<String, List<String>> entry : map.entrySet())
+ {
+ String key = entry.getKey();
+ List<String> list = entry.getValue();
+ int s = list.size();
+
+ if (delim)
+ {
+ result.append('&');
+ }
+
+ if (s == 0)
+ {
+ result.append(encodeString(key, charset));
+ if (equalsForNullValue)
+ result.append('=');
+ }
+ else
+ {
+ for (int i = 0; i < s; i++)
+ {
+ if (i > 0)
+ result.append('&');
+ String val = list.get(i);
+ result.append(encodeString(key, charset));
+
+ if (val != null)
+ {
+ String str = val;
+ if (str.length() > 0)
+ {
+ result.append('=');
+ result.append(encodeString(str, charset));
+ }
+ else if (equalsForNullValue)
+ result.append('=');
+ }
+ else if (equalsForNullValue)
+ result.append('=');
+ }
+ }
+ delim = true;
+ }
+ return result.toString();
+ }
+
+ /**
+ * Decoded parameters to Map.
+ *
+ * @param content the string containing the encoded parameters
+ * @param map the MultiMap to put parsed query parameters into
+ * @param charset the charset to use for decoding
+ */
+ public static void decodeTo(String content, MultiMap<String> map, String charset)
+ {
+ decodeTo(content, map, charset == null ? null : Charset.forName(charset));
+ }
+
+ /**
+ * Decoded parameters to Map.
+ *
+ * @param content the string containing the encoded parameters
+ * @param map the MultiMap to put parsed query parameters into
+ * @param charset the charset to use for decoding
+ */
+ public static void decodeTo(String content, MultiMap<String> map, Charset charset)
+ {
+ if (charset == null)
+ charset = ENCODING;
+
+ if (StandardCharsets.UTF_8.equals(charset))
+ {
+ decodeUtf8To(content, 0, content.length(), map);
+ return;
+ }
+
+ synchronized (map)
+ {
+ String key = null;
+ String value;
+ int mark = -1;
+ boolean encoded = false;
+ for (int i = 0; i < content.length(); i++)
+ {
+ char c = content.charAt(i);
+ switch (c)
+ {
+ case '&':
+ int l = i - mark - 1;
+ value = l == 0 ? "" : (encoded ? decodeString(content, mark + 1, l, charset) : content.substring(mark + 1, i));
+ mark = i;
+ encoded = false;
+ if (key != null)
+ {
+ map.add(key, value);
+ }
+ else if (value != null && value.length() > 0)
+ {
+ map.add(value, "");
+ }
+ key = null;
+ value = null;
+ break;
+ case '=':
+ if (key != null)
+ break;
+ key = encoded ? decodeString(content, mark + 1, i - mark - 1, charset) : content.substring(mark + 1, i);
+ mark = i;
+ encoded = false;
+ break;
+ case '+':
+ encoded = true;
+ break;
+ case '%':
+ encoded = true;
+ break;
+ }
+ }
+
+ if (key != null)
+ {
+ int l = content.length() - mark - 1;
+ value = l == 0 ? "" : (encoded ? decodeString(content, mark + 1, l, charset) : content.substring(mark + 1));
+ map.add(key, value);
+ }
+ else if (mark < content.length())
+ {
+ key = encoded
+ ? decodeString(content, mark + 1, content.length() - mark - 1, charset)
+ : content.substring(mark + 1);
+ if (key != null && key.length() > 0)
+ {
+ map.add(key, "");
+ }
+ }
+ }
+ }
+
+ public static void decodeUtf8To(String query, MultiMap<String> map)
+ {
+ decodeUtf8To(query, 0, query.length(), map);
+ }
+
+ /**
+ * Decoded parameters to Map.
+ *
+ * @param query the string containing the encoded parameters
+ * @param offset the offset within raw to decode from
+ * @param length the length of the section to decode
+ * @param map the {@link MultiMap} to populate
+ */
+ public static void decodeUtf8To(String query, int offset, int length, MultiMap<String> map)
+ {
+ Utf8StringBuilder buffer = new Utf8StringBuilder();
+ synchronized (map)
+ {
+ String key = null;
+ String value = null;
+
+ int end = offset + length;
+ for (int i = offset; i < end; i++)
+ {
+ char c = query.charAt(i);
+ switch (c)
+ {
+ case '&':
+ value = buffer.toReplacedString();
+ buffer.reset();
+ if (key != null)
+ {
+ map.add(key, value);
+ }
+ else if (value != null && value.length() > 0)
+ {
+ map.add(value, "");
+ }
+ key = null;
+ value = null;
+ break;
+
+ case '=':
+ if (key != null)
+ {
+ buffer.append(c);
+ break;
+ }
+ key = buffer.toReplacedString();
+ buffer.reset();
+ break;
+
+ case '+':
+ buffer.append((byte)' ');
+ break;
+
+ case '%':
+ if (i + 2 < end)
+ {
+ char hi = query.charAt(++i);
+ char lo = query.charAt(++i);
+ buffer.append(decodeHexByte(hi, lo));
+ }
+ else
+ {
+ throw new Utf8Appendable.NotUtf8Exception("Incomplete % encoding");
+ }
+ break;
+
+ default:
+ buffer.append(c);
+ break;
+ }
+ }
+
+ if (key != null)
+ {
+ value = buffer.toReplacedString();
+ buffer.reset();
+ map.add(key, value);
+ }
+ else if (buffer.length() > 0)
+ {
+ map.add(buffer.toReplacedString(), "");
+ }
+ }
+ }
+
+ /**
+ * Decoded parameters to MultiMap, using ISO8859-1 encodings.
+ *
+ * @param in InputSteam to read
+ * @param map MultiMap to add parameters to
+ * @param maxLength maximum length of form to read or -1 for no limit
+ * @param maxKeys maximum number of keys to read or -1 for no limit
+ * @throws IOException if unable to decode the InputStream as ISO8859-1
+ */
+ public static void decode88591To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys)
+ throws IOException
+ {
+ synchronized (map)
+ {
+ StringBuilder buffer = new StringBuilder();
+ String key = null;
+ String value = null;
+
+ int b;
+
+ int totalLength = 0;
+ while ((b = in.read()) >= 0)
+ {
+ switch ((char)b)
+ {
+ case '&':
+ value = buffer.length() == 0 ? "" : buffer.toString();
+ buffer.setLength(0);
+ if (key != null)
+ {
+ map.add(key, value);
+ }
+ else if (value.length() > 0)
+ {
+ map.add(value, "");
+ }
+ key = null;
+ value = null;
+ checkMaxKeys(map, maxKeys);
+ break;
+
+ case '=':
+ if (key != null)
+ {
+ buffer.append((char)b);
+ break;
+ }
+ key = buffer.toString();
+ buffer.setLength(0);
+ break;
+
+ case '+':
+ buffer.append(' ');
+ break;
+
+ case '%':
+ int code0 = in.read();
+ int code1 = in.read();
+ buffer.append(decodeHexChar(code0, code1));
+ break;
+
+ default:
+ buffer.append((char)b);
+ break;
+ }
+ checkMaxLength(++totalLength, maxLength);
+ }
+
+ if (key != null)
+ {
+ value = buffer.length() == 0 ? "" : buffer.toString();
+ buffer.setLength(0);
+ map.add(key, value);
+ }
+ else if (buffer.length() > 0)
+ {
+ map.add(buffer.toString(), "");
+ }
+ checkMaxKeys(map, maxKeys);
+ }
+ }
+
+ /**
+ * Decoded parameters to Map.
+ *
+ * @param in InputSteam to read
+ * @param map MultiMap to add parameters to
+ * @param maxLength maximum form length to decode or -1 for no limit
+ * @param maxKeys the maximum number of keys to read or -1 for no limit
+ * @throws IOException if unable to decode the input stream
+ */
+ public static void decodeUtf8To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys)
+ throws IOException
+ {
+ synchronized (map)
+ {
+ Utf8StringBuilder buffer = new Utf8StringBuilder();
+ String key = null;
+ String value = null;
+
+ int b;
+
+ int totalLength = 0;
+ while ((b = in.read()) >= 0)
+ {
+ switch ((char)b)
+ {
+ case '&':
+ value = buffer.toReplacedString();
+ buffer.reset();
+ if (key != null)
+ {
+ map.add(key, value);
+ }
+ else if (value != null && value.length() > 0)
+ {
+ map.add(value, "");
+ }
+ key = null;
+ value = null;
+ checkMaxKeys(map, maxKeys);
+ break;
+
+ case '=':
+ if (key != null)
+ {
+ buffer.append((byte)b);
+ break;
+ }
+ key = buffer.toReplacedString();
+ buffer.reset();
+ break;
+
+ case '+':
+ buffer.append((byte)' ');
+ break;
+
+ case '%':
+ char code0 = (char)in.read();
+ char code1 = (char)in.read();
+ buffer.append(decodeHexByte(code0, code1));
+ break;
+
+ default:
+ buffer.append((byte)b);
+ break;
+ }
+ checkMaxLength(++totalLength, maxLength);
+ }
+
+ if (key != null)
+ {
+ value = buffer.toReplacedString();
+ buffer.reset();
+ map.add(key, value);
+ }
+ else if (buffer.length() > 0)
+ {
+ map.add(buffer.toReplacedString(), "");
+ }
+ checkMaxKeys(map, maxKeys);
+ }
+ }
+
+ public static void decodeUtf16To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys) throws IOException
+ {
+ InputStreamReader input = new InputStreamReader(in, StandardCharsets.UTF_16);
+ StringWriter buf = new StringWriter(8192);
+ IO.copy(input, buf, maxLength);
+
+ // TODO implement maxKeys
+ decodeTo(buf.getBuffer().toString(), map, StandardCharsets.UTF_16);
+ }
+
+ /**
+ * Decoded parameters to Map.
+ *
+ * @param in the stream containing the encoded parameters
+ * @param map the MultiMap to decode into
+ * @param charset the charset to use for decoding
+ * @param maxLength the maximum length of the form to decode or -1 for no limit
+ * @param maxKeys the maximum number of keys to decode or -1 for no limit
+ * @throws IOException if unable to decode the input stream
+ */
+ public static void decodeTo(InputStream in, MultiMap<String> map, String charset, int maxLength, int maxKeys)
+ throws IOException
+ {
+ if (charset == null)
+ {
+ if (ENCODING.equals(StandardCharsets.UTF_8))
+ decodeUtf8To(in, map, maxLength, maxKeys);
+ else
+ decodeTo(in, map, ENCODING, maxLength, maxKeys);
+ }
+ else if (StringUtil.__UTF8.equalsIgnoreCase(charset))
+ decodeUtf8To(in, map, maxLength, maxKeys);
+ else if (StringUtil.__ISO_8859_1.equalsIgnoreCase(charset))
+ decode88591To(in, map, maxLength, maxKeys);
+ else if (StringUtil.__UTF16.equalsIgnoreCase(charset))
+ decodeUtf16To(in, map, maxLength, maxKeys);
+ else
+ decodeTo(in, map, Charset.forName(charset), maxLength, maxKeys);
+ }
+
+ /**
+ * Decoded parameters to Map.
+ *
+ * @param in the stream containing the encoded parameters
+ * @param map the MultiMap to decode into
+ * @param charset the charset to use for decoding
+ * @param maxLength the maximum length of the form to decode
+ * @param maxKeys the maximum number of keys to decode
+ * @throws IOException if unable to decode input stream
+ */
+ public static void decodeTo(InputStream in, MultiMap<String> map, Charset charset, int maxLength, int maxKeys)
+ throws IOException
+ {
+ //no charset present, use the configured default
+ if (charset == null)
+ charset = ENCODING;
+
+ if (StandardCharsets.UTF_8.equals(charset))
+ {
+ decodeUtf8To(in, map, maxLength, maxKeys);
+ return;
+ }
+
+ if (StandardCharsets.ISO_8859_1.equals(charset))
+ {
+ decode88591To(in, map, maxLength, maxKeys);
+ return;
+ }
+
+ if (StandardCharsets.UTF_16.equals(charset)) // Should be all 2 byte encodings
+ {
+ decodeUtf16To(in, map, maxLength, maxKeys);
+ return;
+ }
+
+ synchronized (map)
+ {
+ String key = null;
+ String value = null;
+
+ int c;
+
+ int totalLength = 0;
+
+ try (ByteArrayOutputStream2 output = new ByteArrayOutputStream2())
+ {
+ int size = 0;
+
+ while ((c = in.read()) > 0)
+ {
+ switch ((char)c)
+ {
+ case '&':
+ size = output.size();
+ value = size == 0 ? "" : output.toString(charset);
+ output.setCount(0);
+ if (key != null)
+ {
+ map.add(key, value);
+ }
+ else if (value != null && value.length() > 0)
+ {
+ map.add(value, "");
+ }
+ key = null;
+ value = null;
+ checkMaxKeys(map, maxKeys);
+ break;
+ case '=':
+ if (key != null)
+ {
+ output.write(c);
+ break;
+ }
+ size = output.size();
+ key = size == 0 ? "" : output.toString(charset);
+ output.setCount(0);
+ break;
+ case '+':
+ output.write(' ');
+ break;
+ case '%':
+ int code0 = in.read();
+ int code1 = in.read();
+ output.write(decodeHexChar(code0, code1));
+ break;
+ default:
+ output.write(c);
+ break;
+ }
+ checkMaxLength(++totalLength, maxLength);
+ }
+
+ size = output.size();
+ if (key != null)
+ {
+ value = size == 0 ? "" : output.toString(charset);
+ output.setCount(0);
+ map.add(key, value);
+ }
+ else if (size > 0)
+ {
+ map.add(output.toString(charset), "");
+ }
+ checkMaxKeys(map, maxKeys);
+ }
+ }
+ }
+
+ private static void checkMaxKeys(MultiMap<String> map, int maxKeys)
+ {
+ int size = map.size();
+ if (maxKeys >= 0 && size > maxKeys)
+ throw new IllegalStateException(String.format("Form with too many keys [%d > %d]", size, maxKeys));
+ }
+
+ private static void checkMaxLength(int length, int maxLength)
+ {
+ if (maxLength >= 0 && length > maxLength)
+ throw new IllegalStateException("Form is larger than max length " + maxLength);
+ }
+
+ /**
+ * Decode String with % encoding.
+ * This method makes the assumption that the majority of calls
+ * will need no decoding.
+ *
+ * @param encoded the encoded string to decode
+ * @return the decoded string
+ */
+ public static String decodeString(String encoded)
+ {
+ return decodeString(encoded, 0, encoded.length(), ENCODING);
+ }
+
+ /**
+ * Decode String with % encoding.
+ * This method makes the assumption that the majority of calls
+ * will need no decoding.
+ *
+ * @param encoded the encoded string to decode
+ * @param offset the offset in the encoded string to decode from
+ * @param length the length of characters in the encoded string to decode
+ * @param charset the charset to use for decoding
+ * @return the decoded string
+ */
+ public static String decodeString(String encoded, int offset, int length, Charset charset)
+ {
+ if (charset == null || StandardCharsets.UTF_8.equals(charset))
+ {
+ Utf8StringBuffer buffer = null;
+
+ for (int i = 0; i < length; i++)
+ {
+ char c = encoded.charAt(offset + i);
+ if (c < 0 || c > 0xff)
+ {
+ if (buffer == null)
+ {
+ buffer = new Utf8StringBuffer(length);
+ buffer.getStringBuffer().append(encoded, offset, offset + i + 1);
+ }
+ else
+ buffer.getStringBuffer().append(c);
+ }
+ else if (c == '+')
+ {
+ if (buffer == null)
+ {
+ buffer = new Utf8StringBuffer(length);
+ buffer.getStringBuffer().append(encoded, offset, offset + i);
+ }
+
+ buffer.getStringBuffer().append(' ');
+ }
+ else if (c == '%')
+ {
+ if (buffer == null)
+ {
+ buffer = new Utf8StringBuffer(length);
+ buffer.getStringBuffer().append(encoded, offset, offset + i);
+ }
+
+ if ((i + 2) < length)
+ {
+ int o = offset + i + 1;
+ i += 2;
+ byte b = (byte)TypeUtil.parseInt(encoded, o, 2, 16);
+ buffer.append(b);
+ }
+ else
+ {
+ buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT);
+ i = length;
+ }
+ }
+ else if (buffer != null)
+ buffer.getStringBuffer().append(c);
+ }
+
+ if (buffer == null)
+ {
+ if (offset == 0 && encoded.length() == length)
+ return encoded;
+ return encoded.substring(offset, offset + length);
+ }
+
+ return buffer.toReplacedString();
+ }
+ else
+ {
+ StringBuffer buffer = null;
+
+ for (int i = 0; i < length; i++)
+ {
+ char c = encoded.charAt(offset + i);
+ if (c < 0 || c > 0xff)
+ {
+ if (buffer == null)
+ {
+ buffer = new StringBuffer(length);
+ buffer.append(encoded, offset, offset + i + 1);
+ }
+ else
+ buffer.append(c);
+ }
+ else if (c == '+')
+ {
+ if (buffer == null)
+ {
+ buffer = new StringBuffer(length);
+ buffer.append(encoded, offset, offset + i);
+ }
+
+ buffer.append(' ');
+ }
+ else if (c == '%')
+ {
+ if (buffer == null)
+ {
+ buffer = new StringBuffer(length);
+ buffer.append(encoded, offset, offset + i);
+ }
+
+ byte[] ba = new byte[length];
+ int n = 0;
+ while (c >= 0 && c <= 0xff)
+ {
+ if (c == '%')
+ {
+ if (i + 2 < length)
+ {
+ int o = offset + i + 1;
+ i += 3;
+ ba[n] = (byte)TypeUtil.parseInt(encoded, o, 2, 16);
+ n++;
+ }
+ else
+ {
+ ba[n++] = (byte)'?';
+ i = length;
+ }
+ }
+ else if (c == '+')
+ {
+ ba[n++] = (byte)' ';
+ i++;
+ }
+ else
+ {
+ ba[n++] = (byte)c;
+ i++;
+ }
+
+ if (i >= length)
+ break;
+ c = encoded.charAt(offset + i);
+ }
+
+ i--;
+ buffer.append(new String(ba, 0, n, charset));
+ }
+ else if (buffer != null)
+ buffer.append(c);
+ }
+
+ if (buffer == null)
+ {
+ if (offset == 0 && encoded.length() == length)
+ return encoded;
+ return encoded.substring(offset, offset + length);
+ }
+
+ return buffer.toString();
+ }
+ }
+
+ private static char decodeHexChar(int hi, int lo)
+ {
+ try
+ {
+ return (char)((convertHexDigit(hi) << 4) + convertHexDigit(lo));
+ }
+ catch (NumberFormatException e)
+ {
+ throw new IllegalArgumentException("Not valid encoding '%" + (char)hi + (char)lo + "'");
+ }
+ }
+
+ private static byte decodeHexByte(char hi, char lo)
+ {
+ try
+ {
+ return (byte)((convertHexDigit(hi) << 4) + convertHexDigit(lo));
+ }
+ catch (NumberFormatException e)
+ {
+ throw new IllegalArgumentException("Not valid encoding '%" + hi + lo + "'");
+ }
+ }
+
+ /**
+ * Perform URL encoding.
+ *
+ * @param string the string to encode
+ * @return encoded string.
+ */
+ public static String encodeString(String string)
+ {
+ return encodeString(string, ENCODING);
+ }
+
+ /**
+ * Perform URL encoding.
+ *
+ * @param string the string to encode
+ * @param charset the charset to use for encoding
+ * @return encoded string.
+ */
+ public static String encodeString(String string, Charset charset)
+ {
+ if (charset == null)
+ charset = ENCODING;
+ byte[] bytes = null;
+ bytes = string.getBytes(charset);
+
+ int len = bytes.length;
+ byte[] encoded = new byte[bytes.length * 3];
+ int n = 0;
+ boolean noEncode = true;
+
+ for (int i = 0; i < len; i++)
+ {
+ byte b = bytes[i];
+
+ if (b == ' ')
+ {
+ noEncode = false;
+ encoded[n++] = (byte)'+';
+ }
+ else if (b >= 'a' && b <= 'z' ||
+ b >= 'A' && b <= 'Z' ||
+ b >= '0' && b <= '9' ||
+ b == '-' || b == '.' || b == '_' || b == '~')
+ {
+ encoded[n++] = b;
+ }
+ else
+ {
+ noEncode = false;
+ encoded[n++] = (byte)'%';
+ byte nibble = (byte)((b & 0xf0) >> 4);
+ if (nibble >= 10)
+ encoded[n++] = (byte)('A' + nibble - 10);
+ else
+ encoded[n++] = (byte)('0' + nibble);
+ nibble = (byte)(b & 0xf);
+ if (nibble >= 10)
+ encoded[n++] = (byte)('A' + nibble - 10);
+ else
+ encoded[n++] = (byte)('0' + nibble);
+ }
+ }
+
+ if (noEncode)
+ return string;
+
+ return new String(encoded, 0, n, charset);
+ }
+
+ /**
+ *
+ */
+ @Override
+ public Object clone()
+ {
+ return new UrlEncoded(this);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8Appendable.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8Appendable.java
new file mode 100644
index 0000000..ad73b86
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8Appendable.java
@@ -0,0 +1,333 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Utf8 Appendable abstract base class
+ *
+ * This abstract class wraps a standard {@link java.lang.Appendable} and provides methods to append UTF-8 encoded bytes, that are converted into characters.
+ *
+ * This class is stateful and up to 4 calls to {@link #append(byte)} may be needed before state a character is appended to the string buffer.
+ *
+ * The UTF-8 decoding is done by this class and no additional buffers or Readers are used. The UTF-8 code was inspired by
+ * http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
+ *
+ * License information for Bjoern Hoehrmann's code:
+ *
+ * Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ **/
+public abstract class Utf8Appendable
+{
+ protected static final Logger LOG = Log.getLogger(Utf8Appendable.class);
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+ public static final char REPLACEMENT = '\ufffd';
+ public static final byte[] REPLACEMENT_UTF8 = new byte[]{(byte)0xEF, (byte)0xBF, (byte)0xBD};
+ private static final int UTF8_ACCEPT = 0;
+ private static final int UTF8_REJECT = 12;
+
+ protected final Appendable _appendable;
+ protected int _state = UTF8_ACCEPT;
+
+ private static final byte[] BYTE_TABLE =
+ {
+ // The first part of the table maps bytes to character classes that
+ // to reduce the size of the transition table and create bitmasks.
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 11, 6, 6, 6, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8
+ };
+
+ private static final byte[] TRANS_TABLE =
+ {
+ // The second part is a transition table that maps a combination
+ // of a state of the automaton and a character class to a state.
+ 0, 12, 24, 36, 60, 96, 84, 12, 12, 12, 48, 72, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
+ 12, 0, 12, 12, 12, 12, 12, 0, 12, 0, 12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 24, 12, 12,
+ 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12,
+ 12, 12, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12,
+ 12, 36, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12
+ };
+
+ private int _codep;
+
+ public Utf8Appendable(Appendable appendable)
+ {
+ _appendable = appendable;
+ }
+
+ public abstract int length();
+
+ protected void reset()
+ {
+ _state = UTF8_ACCEPT;
+ }
+
+ private void checkCharAppend() throws IOException
+ {
+ if (_state != UTF8_ACCEPT)
+ {
+ _appendable.append(REPLACEMENT);
+ int state = _state;
+ _state = UTF8_ACCEPT;
+ throw new NotUtf8Exception("char appended in state " + state);
+ }
+ }
+
+ public void append(char c)
+ {
+ try
+ {
+ checkCharAppend();
+ _appendable.append(c);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void append(String s)
+ {
+ try
+ {
+ checkCharAppend();
+ _appendable.append(s);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void append(String s, int offset, int length)
+ {
+ try
+ {
+ checkCharAppend();
+ _appendable.append(s, offset, offset + length);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void append(byte b)
+ {
+ try
+ {
+ appendByte(b);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void append(ByteBuffer buf)
+ {
+ try
+ {
+ while (buf.remaining() > 0)
+ {
+ appendByte(buf.get());
+ }
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void append(byte[] b)
+ {
+ append(b, 0, b.length);
+ }
+
+ public void append(byte[] b, int offset, int length)
+ {
+ try
+ {
+ int end = offset + length;
+ for (int i = offset; i < end; i++)
+ {
+ appendByte(b[i]);
+ }
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean append(byte[] b, int offset, int length, int maxChars)
+ {
+ try
+ {
+ int end = offset + length;
+ for (int i = offset; i < end; i++)
+ {
+ if (length() > maxChars)
+ return false;
+ appendByte(b[i]);
+ }
+ return true;
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected void appendByte(byte b) throws IOException
+ {
+ if (b > 0 && _state == UTF8_ACCEPT)
+ {
+ _appendable.append((char)(b & 0xFF));
+ }
+ else
+ {
+ int i = b & 0xFF;
+ int type = BYTE_TABLE[i];
+ _codep = _state == UTF8_ACCEPT ? (0xFF >> type) & i : (i & 0x3F) | (_codep << 6);
+ int next = TRANS_TABLE[_state + type];
+
+ switch (next)
+ {
+ case UTF8_ACCEPT:
+ _state = next;
+ if (_codep < Character.MIN_HIGH_SURROGATE)
+ {
+ _appendable.append((char)_codep);
+ }
+ else
+ {
+ for (char c : Character.toChars(_codep))
+ {
+ _appendable.append(c);
+ }
+ }
+ break;
+
+ case UTF8_REJECT:
+ String reason = "byte " + TypeUtil.toHexString(b) + " in state " + (_state / 12);
+ _codep = 0;
+ _state = UTF8_ACCEPT;
+ _appendable.append(REPLACEMENT);
+ throw new NotUtf8Exception(reason);
+
+ default:
+ _state = next;
+ }
+ }
+ }
+
+ public boolean isUtf8SequenceComplete()
+ {
+ return _state == UTF8_ACCEPT;
+ }
+
+ @SuppressWarnings("serial")
+ public static class NotUtf8Exception extends IllegalArgumentException
+ {
+ public NotUtf8Exception(String reason)
+ {
+ super("Not valid UTF8! " + reason);
+ }
+ }
+
+ protected void checkState()
+ {
+ if (!isUtf8SequenceComplete())
+ {
+ _codep = 0;
+ _state = UTF8_ACCEPT;
+ try
+ {
+ _appendable.append(REPLACEMENT);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ throw new NotUtf8Exception("incomplete UTF8 sequence");
+ }
+ }
+
+ /**
+ * @return The UTF8 so far decoded, ignoring partial code points
+ */
+ public abstract String getPartialString();
+
+ /**
+ * Take the partial string an reset in internal buffer, but retain
+ * partial code points.
+ *
+ * @return The UTF8 so far decoded, ignoring partial code points
+ */
+ public String takePartialString()
+ {
+ String partial = getPartialString();
+ int save = _state;
+ reset();
+ _state = save;
+ return partial;
+ }
+
+ public String toReplacedString()
+ {
+ if (!isUtf8SequenceComplete())
+ {
+ _codep = 0;
+ _state = UTF8_ACCEPT;
+ try
+ {
+ _appendable.append(REPLACEMENT);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ Throwable th = new NotUtf8Exception("incomplete UTF8 sequence");
+ LOG.warn(th.toString());
+ LOG.debug(th);
+ }
+ return _appendable.toString();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8LineParser.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8LineParser.java
new file mode 100644
index 0000000..6c8f9ef
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8LineParser.java
@@ -0,0 +1,99 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception;
+
+/**
+ * Stateful parser for lines of UTF8 formatted text, looking for <code>"\n"</code> as a line termination character.
+ * <p>
+ * For use with new IO framework that is based on ByteBuffer parsing.
+ */
+public class Utf8LineParser
+{
+ private enum State
+ {
+ START,
+ PARSE,
+ END
+ }
+
+ private State state;
+ private Utf8StringBuilder utf;
+
+ public Utf8LineParser()
+ {
+ this.state = State.START;
+ }
+
+ /**
+ * Parse a ByteBuffer (could be a partial buffer), and return once a complete line of UTF8 parsed text has been reached.
+ *
+ * @param buf the buffer to parse (could be an incomplete buffer)
+ * @return the line of UTF8 parsed text, or null if no line end termination has been reached within the {@link ByteBuffer#remaining() remaining} bytes of
+ * the provided ByteBuffer. (In the case of a null, a subsequent ByteBuffer with a line end termination should be provided)
+ * @throws NotUtf8Exception if the input buffer has bytes that do not conform to UTF8 validation (validation performed by {@link Utf8StringBuilder}
+ */
+ public String parse(ByteBuffer buf)
+ {
+ byte b;
+ while (buf.remaining() > 0)
+ {
+ b = buf.get();
+ if (parseByte(b))
+ {
+ state = State.START;
+ return utf.toString();
+ }
+ }
+ // have not reached end of line (yet)
+ return null;
+ }
+
+ private boolean parseByte(byte b)
+ {
+ switch (state)
+ {
+ case START:
+ utf = new Utf8StringBuilder();
+ state = State.PARSE;
+ return parseByte(b);
+ case PARSE:
+ // not waiting on more UTF sequence parts.
+ if (utf.isUtf8SequenceComplete() && ((b == '\r') || (b == '\n')))
+ {
+ state = State.END;
+ return parseByte(b);
+ }
+ utf.append(b);
+ break;
+ case END:
+ if (b == '\n')
+ {
+ // we've reached the end
+ state = State.START;
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8StringBuffer.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8StringBuffer.java
new file mode 100644
index 0000000..4d8b1e4
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8StringBuffer.java
@@ -0,0 +1,80 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+/**
+ * UTF-8 StringBuffer.
+ *
+ * This class wraps a standard {@link java.lang.StringBuffer} and provides methods to append
+ * UTF-8 encoded bytes, that are converted into characters.
+ *
+ * This class is stateful and up to 4 calls to {@link #append(byte)} may be needed before
+ * state a character is appended to the string buffer.
+ *
+ * The UTF-8 decoding is done by this class and no additional buffers or Readers are used.
+ * The UTF-8 code was inspired by http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
+ */
+public class Utf8StringBuffer extends Utf8Appendable
+{
+ final StringBuffer _buffer;
+
+ public Utf8StringBuffer()
+ {
+ super(new StringBuffer());
+ _buffer = (StringBuffer)_appendable;
+ }
+
+ public Utf8StringBuffer(int capacity)
+ {
+ super(new StringBuffer(capacity));
+ _buffer = (StringBuffer)_appendable;
+ }
+
+ @Override
+ public int length()
+ {
+ return _buffer.length();
+ }
+
+ @Override
+ public void reset()
+ {
+ super.reset();
+ _buffer.setLength(0);
+ }
+
+ @Override
+ public String getPartialString()
+ {
+ return _buffer.toString();
+ }
+
+ public StringBuffer getStringBuffer()
+ {
+ checkState();
+ return _buffer;
+ }
+
+ @Override
+ public String toString()
+ {
+ checkState();
+ return _buffer.toString();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8StringBuilder.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8StringBuilder.java
new file mode 100644
index 0000000..ad11914
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8StringBuilder.java
@@ -0,0 +1,80 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+/**
+ * UTF-8 StringBuilder.
+ *
+ * This class wraps a standard {@link java.lang.StringBuilder} and provides methods to append
+ * UTF-8 encoded bytes, that are converted into characters.
+ *
+ * This class is stateful and up to 4 calls to {@link #append(byte)} may be needed before
+ * state a character is appended to the string buffer.
+ *
+ * The UTF-8 decoding is done by this class and no additional buffers or Readers are used.
+ * The UTF-8 code was inspired by http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
+ */
+public class Utf8StringBuilder extends Utf8Appendable
+{
+ final StringBuilder _buffer;
+
+ public Utf8StringBuilder()
+ {
+ super(new StringBuilder());
+ _buffer = (StringBuilder)_appendable;
+ }
+
+ public Utf8StringBuilder(int capacity)
+ {
+ super(new StringBuilder(capacity));
+ _buffer = (StringBuilder)_appendable;
+ }
+
+ @Override
+ public int length()
+ {
+ return _buffer.length();
+ }
+
+ @Override
+ public void reset()
+ {
+ super.reset();
+ _buffer.setLength(0);
+ }
+
+ @Override
+ public String getPartialString()
+ {
+ return _buffer.toString();
+ }
+
+ public StringBuilder getStringBuilder()
+ {
+ checkState();
+ return _buffer;
+ }
+
+ @Override
+ public String toString()
+ {
+ checkState();
+ return _buffer.toString();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/ManagedAttribute.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/ManagedAttribute.java
new file mode 100644
index 0000000..82459d0
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/ManagedAttribute.java
@@ -0,0 +1,78 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The <code>@ManagedAttribute</code> annotation is used to indicate that a given method
+ * exposes a JMX attribute. This annotation is placed always on the reader
+ * method of a given attribute. Unless it is marked as read-only in the
+ * configuration of the annotation a corresponding setter is looked for
+ * following normal naming conventions. For example if this annotation is
+ * on a method called getFoo() then a method called setFoo() would be looked
+ * for and if found wired automatically into the jmx attribute.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Target({ElementType.METHOD})
+public @interface ManagedAttribute
+{
+ /**
+ * Description of the Managed Attribute
+ *
+ * @return value
+ */
+ String value() default "Not Specified";
+
+ /**
+ * name to use for the attribute
+ *
+ * @return the name of the attribute
+ */
+ String name() default "";
+
+ /**
+ * Is the managed field read-only?
+ *
+ * Required only when a setter exists but should not be exposed via JMX
+ *
+ * @return true if readonly
+ */
+ boolean readonly() default false;
+
+ /**
+ * Does the managed field exist on a proxy object?
+ *
+ * @return true if a proxy object is involved
+ */
+ boolean proxied() default false;
+
+ /**
+ * If is a field references a setter that doesn't conform to standards for discovery
+ * it can be set here.
+ *
+ * @return the full name of the setter in question
+ */
+ String setter() default "";
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/ManagedObject.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/ManagedObject.java
new file mode 100644
index 0000000..fbf6a8d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/ManagedObject.java
@@ -0,0 +1,45 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The <code>@ManagedObject</code> annotation is used on a class at the top level to
+ * indicate that it should be exposed as an mbean. It has only one attribute
+ * to it which is used as the description of the MBean. Should multiple
+ * <code>@ManagedObject</code> annotations be found in the chain of influence then the
+ * first description is used.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Target({ElementType.TYPE})
+public @interface ManagedObject
+{
+ /**
+ * Description of the Managed Object
+ *
+ * @return value
+ */
+ String value() default "Not Specified";
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/ManagedOperation.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/ManagedOperation.java
new file mode 100644
index 0000000..ced24e7
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/ManagedOperation.java
@@ -0,0 +1,60 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The <code>@ManagedOperation</code> annotation is used to indicate that a given method
+ * should be considered a JMX operation.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Target({ElementType.METHOD})
+public @interface ManagedOperation
+{
+ /**
+ * Description of the Managed Object
+ *
+ * @return value
+ */
+ String value() default "Not Specified";
+
+ /**
+ * The impact of an operation.
+ *
+ * NOTE: Valid values are UNKNOWN, ACTION, INFO, ACTION_INFO
+ *
+ * NOTE: applies to METHOD
+ *
+ * @return String representing the impact of the operation
+ */
+ String impact() default "UNKNOWN";
+
+ /**
+ * Does the managed field exist on a proxy object?
+ *
+ * @return true if a proxy object is involved
+ */
+ boolean proxied() default false;
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/Name.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/Name.java
new file mode 100644
index 0000000..322b633
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/Name.java
@@ -0,0 +1,52 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation is used to describe variables in method
+ * signatures so that when rendered into tools like JConsole
+ * it is clear what the parameters are. For example:
+ *
+ * public void doodle(@Name(value="doodle", description="A description of the argument") String doodle)
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Target({ElementType.PARAMETER})
+public @interface Name
+{
+ /**
+ * the name of the parameter
+ *
+ * @return the value
+ */
+ String value();
+
+ /**
+ * the description of the parameter
+ *
+ * @return the description
+ */
+ String description() default "";
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/package-info.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/package-info.java
new file mode 100644
index 0000000..660e626
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/annotation/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Util : Common Utility Annotations
+ */
+package org.eclipse.jetty.util.annotation;
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/AbstractLifeCycle.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/AbstractLifeCycle.java
new file mode 100644
index 0000000..4c28fdc
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/AbstractLifeCycle.java
@@ -0,0 +1,289 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.Uptime;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Basic implementation of the life cycle interface for components.
+ */
+@ManagedObject("Abstract Implementation of LifeCycle")
+public abstract class AbstractLifeCycle implements LifeCycle
+{
+ private static final Logger LOG = Log.getLogger(AbstractLifeCycle.class);
+
+ public static final String STOPPED = "STOPPED";
+ public static final String FAILED = "FAILED";
+ public static final String STARTING = "STARTING";
+ public static final String STARTED = "STARTED";
+ public static final String STOPPING = "STOPPING";
+ public static final String RUNNING = "RUNNING";
+
+ private final CopyOnWriteArrayList<LifeCycle.Listener> _listeners = new CopyOnWriteArrayList<LifeCycle.Listener>();
+ private final Object _lock = new Object();
+ private static final int STATE_FAILED = -1;
+ private static final int STATE_STOPPED = 0;
+ private static final int STATE_STARTING = 1;
+ private static final int STATE_STARTED = 2;
+ private static final int STATE_STOPPING = 3;
+ private volatile int _state = STATE_STOPPED;
+ private long _stopTimeout = 30000;
+
+ protected void doStart() throws Exception
+ {
+ }
+
+ protected void doStop() throws Exception
+ {
+ }
+
+ @Override
+ public final void start() throws Exception
+ {
+ synchronized (_lock)
+ {
+ try
+ {
+ if (_state == STATE_STARTED || _state == STATE_STARTING)
+ return;
+ setStarting();
+ doStart();
+ setStarted();
+ }
+ catch (Throwable e)
+ {
+ setFailed(e);
+ throw e;
+ }
+ }
+ }
+
+ @Override
+ public final void stop() throws Exception
+ {
+ synchronized (_lock)
+ {
+ try
+ {
+ if (_state == STATE_STOPPING || _state == STATE_STOPPED)
+ return;
+ setStopping();
+ doStop();
+ setStopped();
+ }
+ catch (Throwable e)
+ {
+ setFailed(e);
+ throw e;
+ }
+ }
+ }
+
+ @Override
+ public boolean isRunning()
+ {
+ final int state = _state;
+
+ return state == STATE_STARTED || state == STATE_STARTING;
+ }
+
+ @Override
+ public boolean isStarted()
+ {
+ return _state == STATE_STARTED;
+ }
+
+ @Override
+ public boolean isStarting()
+ {
+ return _state == STATE_STARTING;
+ }
+
+ @Override
+ public boolean isStopping()
+ {
+ return _state == STATE_STOPPING;
+ }
+
+ @Override
+ public boolean isStopped()
+ {
+ return _state == STATE_STOPPED;
+ }
+
+ @Override
+ public boolean isFailed()
+ {
+ return _state == STATE_FAILED;
+ }
+
+ @Override
+ public void addLifeCycleListener(LifeCycle.Listener listener)
+ {
+ _listeners.add(listener);
+ }
+
+ @Override
+ public void removeLifeCycleListener(LifeCycle.Listener listener)
+ {
+ _listeners.remove(listener);
+ }
+
+ @ManagedAttribute(value = "Lifecycle State for this instance", readonly = true)
+ public String getState()
+ {
+ switch (_state)
+ {
+ case STATE_FAILED:
+ return FAILED;
+ case STATE_STARTING:
+ return STARTING;
+ case STATE_STARTED:
+ return STARTED;
+ case STATE_STOPPING:
+ return STOPPING;
+ case STATE_STOPPED:
+ return STOPPED;
+ }
+ return null;
+ }
+
+ public static String getState(LifeCycle lc)
+ {
+ if (lc.isStarting())
+ return STARTING;
+ if (lc.isStarted())
+ return STARTED;
+ if (lc.isStopping())
+ return STOPPING;
+ if (lc.isStopped())
+ return STOPPED;
+ return FAILED;
+ }
+
+ private void setStarted()
+ {
+ _state = STATE_STARTED;
+ if (LOG.isDebugEnabled())
+ LOG.debug(STARTED + " @{}ms {}", Uptime.getUptime(), this);
+ for (Listener listener : _listeners)
+ {
+ listener.lifeCycleStarted(this);
+ }
+ }
+
+ private void setStarting()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("starting {}", this);
+ _state = STATE_STARTING;
+ for (Listener listener : _listeners)
+ {
+ listener.lifeCycleStarting(this);
+ }
+ }
+
+ private void setStopping()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("stopping {}", this);
+ _state = STATE_STOPPING;
+ for (Listener listener : _listeners)
+ {
+ listener.lifeCycleStopping(this);
+ }
+ }
+
+ private void setStopped()
+ {
+ _state = STATE_STOPPED;
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} {}", STOPPED, this);
+ for (Listener listener : _listeners)
+ {
+ listener.lifeCycleStopped(this);
+ }
+ }
+
+ private void setFailed(Throwable th)
+ {
+ _state = STATE_FAILED;
+ if (LOG.isDebugEnabled())
+ LOG.warn(FAILED + " " + this + ": " + th, th);
+ for (Listener listener : _listeners)
+ {
+ listener.lifeCycleFailure(this, th);
+ }
+ }
+
+ @ManagedAttribute(value = "The stop timeout in milliseconds")
+ public long getStopTimeout()
+ {
+ return _stopTimeout;
+ }
+
+ public void setStopTimeout(long stopTimeout)
+ {
+ this._stopTimeout = stopTimeout;
+ }
+
+ public abstract static class AbstractLifeCycleListener implements LifeCycle.Listener
+ {
+ @Override
+ public void lifeCycleFailure(LifeCycle event, Throwable cause)
+ {
+ }
+
+ @Override
+ public void lifeCycleStarted(LifeCycle event)
+ {
+ }
+
+ @Override
+ public void lifeCycleStarting(LifeCycle event)
+ {
+ }
+
+ @Override
+ public void lifeCycleStopped(LifeCycle event)
+ {
+ }
+
+ @Override
+ public void lifeCycleStopping(LifeCycle event)
+ {
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ String name = getClass().getSimpleName();
+ if (StringUtil.isBlank(name) && getClass().getSuperclass() != null)
+ name = getClass().getSuperclass().getSimpleName();
+ return String.format("%s@%x{%s}", name, hashCode(), getState());
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/AttributeContainerMap.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/AttributeContainerMap.java
new file mode 100644
index 0000000..ca7ea3b
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/AttributeContainerMap.java
@@ -0,0 +1,89 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jetty.util.Attributes;
+
+/**
+ * An Attributes implementation that holds it's values in an immutable {@link ContainerLifeCycle}
+ */
+public class AttributeContainerMap extends ContainerLifeCycle implements Attributes
+{
+ private final Map<String, Object> _map = new HashMap<>();
+
+ @Override
+ public synchronized void setAttribute(String name, Object attribute)
+ {
+ Object old = _map.put(name, attribute);
+ updateBean(old, attribute);
+ }
+
+ @Override
+ public synchronized void removeAttribute(String name)
+ {
+ Object removed = _map.remove(name);
+ if (removed != null)
+ removeBean(removed);
+ }
+
+ @Override
+ public synchronized Object getAttribute(String name)
+ {
+ return _map.get(name);
+ }
+
+ @Override
+ public synchronized Enumeration<String> getAttributeNames()
+ {
+ return Collections.enumeration(_map.keySet());
+ }
+
+ @Override
+ public Set<String> getAttributeNameSet()
+ {
+ return _map.keySet();
+ }
+
+ @Override
+ public synchronized void clearAttributes()
+ {
+ _map.clear();
+ this.removeBeans();
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ Dumpable.dumpObject(out, this);
+ Dumpable.dumpMapEntries(out, indent, _map, true);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{size=%d}", this.getClass().getSimpleName(), hashCode(), _map.size());
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/Container.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/Container.java
new file mode 100644
index 0000000..e460e25
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/Container.java
@@ -0,0 +1,144 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.util.Collection;
+
+/**
+ * A Container
+ */
+public interface Container
+{
+ /**
+ * Add a bean. If the bean is-a {@link Listener}, then also do an implicit {@link #addEventListener(Listener)}.
+ *
+ * @param o the bean object to add
+ * @return true if the bean was added, false if it was already present
+ */
+ boolean addBean(Object o);
+
+ /**
+ * @return the collection of beans known to this aggregate, in the order they were added.
+ * @see #getBean(Class)
+ */
+ Collection<Object> getBeans();
+
+ /**
+ * @param clazz the class of the beans
+ * @param <T> the Bean type
+ * @return a list of beans of the given class (or subclass), in the order they were added.
+ * @see #getBeans()
+ * @see #getContainedBeans(Class)
+ */
+ <T> Collection<T> getBeans(Class<T> clazz);
+
+ /**
+ * @param clazz the class of the bean
+ * @param <T> the Bean type
+ * @return the first bean (in order added) of a specific class (or subclass), or null if no such bean exist
+ */
+ <T> T getBean(Class<T> clazz);
+
+ /**
+ * Removes the given bean.
+ * If the bean is-a {@link Listener}, then also do an implicit {@link #removeEventListener(Listener)}.
+ *
+ * @param o the bean to remove
+ * @return whether the bean was removed
+ */
+ boolean removeBean(Object o);
+
+ /**
+ * Add an event listener.
+ *
+ * @param listener the listener to add
+ * @see Container#addBean(Object)
+ */
+ void addEventListener(Listener listener);
+
+ /**
+ * Remove an event listener.
+ *
+ * @param listener the listener to remove
+ * @see Container#removeBean(Object)
+ */
+ void removeEventListener(Listener listener);
+
+ /**
+ * Unmanages a bean already contained by this aggregate, so that it is not started/stopped/destroyed with this
+ * aggregate.
+ *
+ * @param bean The bean to unmanage (must already have been added).
+ */
+ void unmanage(Object bean);
+
+ /**
+ * Manages a bean already contained by this aggregate, so that it is started/stopped/destroyed with this
+ * aggregate.
+ *
+ * @param bean The bean to manage (must already have been added).
+ */
+ void manage(Object bean);
+
+ /**
+ * Test if this container manages a bean
+ *
+ * @param bean the bean to test
+ * @return whether this aggregate contains and manages the bean
+ */
+ boolean isManaged(Object bean);
+
+ /**
+ * Adds the given bean, explicitly managing it or not.
+ *
+ * @param o The bean object to add
+ * @param managed whether to managed the lifecycle of the bean
+ * @return true if the bean was added, false if it was already present
+ */
+ boolean addBean(Object o, boolean managed);
+
+ /**
+ * A listener for Container events.
+ * If an added bean implements this interface it will receive the events
+ * for this container.
+ */
+ interface Listener
+ {
+ void beanAdded(Container parent, Object child);
+
+ void beanRemoved(Container parent, Object child);
+ }
+
+ /**
+ * Inherited Listener.
+ * If an added bean implements this interface, then it will
+ * be added to all contained beans that are themselves Containers
+ */
+ interface InheritedListener extends Listener
+ {
+ }
+
+ /**
+ * @param clazz the class of the beans
+ * @param <T> the Bean type
+ * @return the list of beans of the given class from the entire Container hierarchy. The order is primarily depth first
+ * and secondarily added order.
+ */
+ <T> Collection<T> getContainedBeans(Class<T> clazz);
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java
new file mode 100644
index 0000000..9e2cade
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java
@@ -0,0 +1,941 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.jetty.util.MultiException;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * A ContainerLifeCycle is an {@link LifeCycle} implementation for a collection of contained beans.
+ * <p>
+ * Beans can be added to the ContainerLifeCycle either as managed beans or as unmanaged beans.
+ * A managed bean is started, stopped and destroyed with the aggregate.
+ * An unmanaged bean is associated with the aggregate for the purposes of {@link #dump()}, but its
+ * lifecycle must be managed externally.
+ * <p>
+ * When a {@link LifeCycle} bean is added without a managed state being specified the state is
+ * determined heuristically:
+ * <ul>
+ * <li>If the added bean is running, it will be added as an unmanaged bean.</li>
+ * <li>If the added bean is !running and the container is !running, it will be added as an AUTO bean (see below).</li>
+ * <li>If the added bean is !running and the container is starting, it will be added as a managed bean
+ * and will be started (this handles the frequent case of new beans added during calls to doStart).</li>
+ * <li>If the added bean is !running and the container is started, it will be added as an unmanaged bean.</li>
+ * </ul>
+ * When the container is started, then all contained managed beans will also be started.
+ * Any contained AUTO beans will be check for their status and if already started will be switched unmanaged beans,
+ * else they will be started and switched to managed beans.
+ * Beans added after a container is started are not started and their state needs to be explicitly managed.
+ * <p>
+ * When stopping the container, a contained bean will be stopped by this aggregate only if it
+ * is started by this aggregate.
+ * <p>
+ * The methods {@link #addBean(Object, boolean)}, {@link #manage(Object)} and {@link #unmanage(Object)} can be used to
+ * explicitly control the life cycle relationship.
+ * <p>
+ * If adding a bean that is shared between multiple {@link ContainerLifeCycle} instances, then it should be started
+ * before being added, so it is unmanaged, or the API must be used to explicitly set it as unmanaged.
+ * <p>
+ * This class also provides utility methods to dump deep structures of objects.
+ * In the dump, the following symbols are used to indicate the type of contained object:
+ * <pre>
+ * SomeContainerLifeCycleInstance
+ * +- contained POJO instance
+ * += contained MANAGED object, started and stopped with this instance
+ * +~ referenced UNMANAGED object, with separate lifecycle
+ * +? referenced AUTO object that could become MANAGED or UNMANAGED.
+ * </pre>
+ */
+@ManagedObject("Implementation of Container and LifeCycle")
+public class ContainerLifeCycle extends AbstractLifeCycle implements Container, Destroyable, Dumpable.DumpableContainer
+{
+ private static final Logger LOG = Log.getLogger(ContainerLifeCycle.class);
+ private final List<Bean> _beans = new CopyOnWriteArrayList<>();
+ private final List<Container.Listener> _listeners = new CopyOnWriteArrayList<>();
+ private boolean _doStarted;
+ private boolean _destroyed;
+
+ /**
+ * Starts the managed lifecycle beans in the order they were added.
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (_destroyed)
+ throw new IllegalStateException("Destroyed container cannot be restarted");
+
+ // indicate that we are started, so that addBean will start other beans added.
+ _doStarted = true;
+
+ // start our managed and auto beans
+ try
+ {
+ for (Bean b : _beans)
+ {
+ if (b._bean instanceof LifeCycle)
+ {
+ LifeCycle l = (LifeCycle)b._bean;
+ switch (b._managed)
+ {
+ case MANAGED:
+ if (l.isStopped() || l.isFailed())
+ start(l);
+ break;
+
+ case AUTO:
+ if (l.isStopped())
+ {
+ manage(b);
+ start(l);
+ }
+ else
+ {
+ unmanage(b);
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ super.doStart();
+ }
+ catch (Throwable t)
+ {
+ // on failure, stop any managed components that have been started
+ List<Bean> reverse = new ArrayList<>(_beans);
+ Collections.reverse(reverse);
+ for (Bean b : reverse)
+ {
+ if (b._bean instanceof LifeCycle && b._managed == Managed.MANAGED)
+ {
+ LifeCycle l = (LifeCycle)b._bean;
+ if (l.isRunning())
+ {
+ try
+ {
+ stop(l);
+ }
+ catch (Throwable cause2)
+ {
+ if (cause2 != t)
+ t.addSuppressed(cause2);
+ }
+ }
+ }
+ }
+ throw t;
+ }
+ }
+
+ /**
+ * Starts the given lifecycle.
+ *
+ * @param l the lifecycle to start
+ * @throws Exception if unable to start lifecycle
+ */
+ protected void start(LifeCycle l) throws Exception
+ {
+ l.start();
+ }
+
+ /**
+ * Stops the given lifecycle.
+ *
+ * @param l the lifecycle to stop
+ * @throws Exception if unable to stop the lifecycle
+ */
+ protected void stop(LifeCycle l) throws Exception
+ {
+ l.stop();
+ }
+
+ /**
+ * Stops the managed lifecycle beans in the reverse order they were added.
+ */
+ @Override
+ protected void doStop() throws Exception
+ {
+ _doStarted = false;
+ super.doStop();
+ List<Bean> reverse = new ArrayList<>(_beans);
+ Collections.reverse(reverse);
+ MultiException mex = new MultiException();
+ for (Bean b : reverse)
+ {
+ if (b._managed == Managed.MANAGED && b._bean instanceof LifeCycle)
+ {
+ LifeCycle l = (LifeCycle)b._bean;
+ try
+ {
+ stop(l);
+ }
+ catch (Throwable cause)
+ {
+ mex.add(cause);
+ }
+ }
+ }
+ mex.ifExceptionThrow();
+ }
+
+ /**
+ * Destroys the managed Destroyable beans in the reverse order they were added.
+ */
+ @Override
+ public void destroy()
+ {
+ _destroyed = true;
+ List<Bean> reverse = new ArrayList<>(_beans);
+ Collections.reverse(reverse);
+ for (Bean b : reverse)
+ {
+ if (b._bean instanceof Destroyable && (b._managed == Managed.MANAGED || b._managed == Managed.POJO))
+ {
+ Destroyable d = (Destroyable)b._bean;
+ try
+ {
+ d.destroy();
+ }
+ catch (Throwable cause)
+ {
+ LOG.warn(cause);
+ }
+ }
+ }
+ _beans.clear();
+ }
+
+ /**
+ * @param bean the bean to test
+ * @return whether this aggregate contains the bean
+ */
+ public boolean contains(Object bean)
+ {
+ for (Bean b : _beans)
+ {
+ if (b._bean == bean)
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @param bean the bean to test
+ * @return whether this aggregate contains and manages the bean
+ */
+ @Override
+ public boolean isManaged(Object bean)
+ {
+ for (Bean b : _beans)
+ {
+ if (b._bean == bean)
+ return b.isManaged();
+ }
+ return false;
+ }
+
+ /**
+ * @param bean the bean to test
+ * @return whether this aggregate contains the bean in auto state
+ */
+ public boolean isAuto(Object bean)
+ {
+ for (Bean b : _beans)
+ {
+ if (b._bean == bean)
+ return b._managed == Managed.AUTO;
+ }
+ return false;
+ }
+
+ /**
+ * @param bean the bean to test
+ * @return whether this aggregate contains the bean in auto state
+ */
+ public boolean isUnmanaged(Object bean)
+ {
+ for (Bean b : _beans)
+ {
+ if (b._bean == bean)
+ return b._managed == Managed.UNMANAGED;
+ }
+ return false;
+ }
+
+ /**
+ * Adds the given bean, detecting whether to manage it or not.
+ * If the bean is a {@link LifeCycle}, then it will be managed if it is not
+ * already started and not managed if it is already started.
+ * The {@link #addBean(Object, boolean)}
+ * method should be used if this is not correct, or the {@link #manage(Object)} and {@link #unmanage(Object)}
+ * methods may be used after an add to change the status.
+ *
+ * @param o the bean object to add
+ * @return true if the bean was added, false if it was already present
+ */
+ @Override
+ public boolean addBean(Object o)
+ {
+ if (o instanceof LifeCycle)
+ {
+ LifeCycle l = (LifeCycle)o;
+ return addBean(o, l.isRunning() ? Managed.UNMANAGED : Managed.AUTO);
+ }
+
+ return addBean(o, Managed.POJO);
+ }
+
+ /**
+ * Adds the given bean, explicitly managing it or not.
+ *
+ * @param o The bean object to add
+ * @param managed whether to managed the lifecycle of the bean
+ * @return true if the bean was added, false if it was already present
+ */
+ @Override
+ public boolean addBean(Object o, boolean managed)
+ {
+ if (o instanceof LifeCycle)
+ return addBean(o, managed ? Managed.MANAGED : Managed.UNMANAGED);
+ return addBean(o, managed ? Managed.POJO : Managed.UNMANAGED);
+ }
+
+ private boolean addBean(Object o, Managed managed)
+ {
+ if (o == null || contains(o))
+ return false;
+
+ Bean newBean = new Bean(o);
+
+ // if the bean is a Listener
+ if (o instanceof Container.Listener)
+ addEventListener((Container.Listener)o);
+
+ // Add the bean
+ _beans.add(newBean);
+
+ // Tell existing listeners about the new bean
+ for (Container.Listener l : _listeners)
+ {
+ l.beanAdded(this, o);
+ }
+
+ try
+ {
+ switch (managed)
+ {
+ case UNMANAGED:
+ unmanage(newBean);
+ break;
+
+ case MANAGED:
+ manage(newBean);
+
+ if (isStarting() && _doStarted)
+ {
+ LifeCycle l = (LifeCycle)o;
+ if (!l.isRunning())
+ start(l);
+ }
+ break;
+
+ case AUTO:
+ if (o instanceof LifeCycle)
+ {
+ LifeCycle l = (LifeCycle)o;
+ if (isStarting())
+ {
+ if (l.isRunning())
+ unmanage(newBean);
+ else if (_doStarted)
+ {
+ manage(newBean);
+ start(l);
+ }
+ else
+ newBean._managed = Managed.AUTO;
+ }
+ else if (isStarted())
+ unmanage(newBean);
+ else
+ newBean._managed = Managed.AUTO;
+ }
+ else
+ newBean._managed = Managed.POJO;
+ break;
+
+ case POJO:
+ newBean._managed = Managed.POJO;
+ }
+ }
+ catch (RuntimeException | Error e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} added {}", this, newBean);
+
+ return true;
+ }
+
+ /**
+ * Adds a managed lifecycle.
+ * <p>This is a convenience method that uses addBean(lifecycle,true)
+ * and then ensures that the added bean is started iff this container
+ * is running. Exception from nested calls to start are caught and
+ * wrapped as RuntimeExceptions
+ *
+ * @param lifecycle the managed lifecycle to add
+ */
+ public void addManaged(LifeCycle lifecycle)
+ {
+ addBean(lifecycle, true);
+ try
+ {
+ if (isRunning() && !lifecycle.isRunning())
+ start(lifecycle);
+ }
+ catch (RuntimeException | Error e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void addEventListener(Container.Listener listener)
+ {
+ if (_listeners.contains(listener))
+ return;
+
+ _listeners.add(listener);
+
+ // tell it about existing beans
+ for (Bean b : _beans)
+ {
+ listener.beanAdded(this, b._bean);
+
+ // handle inheritance
+ if (listener instanceof InheritedListener && b.isManaged() && b._bean instanceof Container)
+ {
+ if (b._bean instanceof ContainerLifeCycle)
+ ((ContainerLifeCycle)b._bean).addBean(listener, false);
+ else
+ ((Container)b._bean).addBean(listener);
+ }
+ }
+ }
+
+ /**
+ * Manages a bean already contained by this aggregate, so that it is started/stopped/destroyed with this
+ * aggregate.
+ *
+ * @param bean The bean to manage (must already have been added).
+ */
+ @Override
+ public void manage(Object bean)
+ {
+ for (Bean b : _beans)
+ {
+ if (b._bean == bean)
+ {
+ manage(b);
+ return;
+ }
+ }
+ throw new IllegalArgumentException("Unknown bean " + bean);
+ }
+
+ private void manage(Bean bean)
+ {
+ if (bean._managed != Managed.MANAGED)
+ {
+ bean._managed = Managed.MANAGED;
+
+ if (bean._bean instanceof Container)
+ {
+ for (Container.Listener l : _listeners)
+ {
+ if (l instanceof InheritedListener)
+ {
+ if (bean._bean instanceof ContainerLifeCycle)
+ ((ContainerLifeCycle)bean._bean).addBean(l, false);
+ else
+ ((Container)bean._bean).addBean(l);
+ }
+ }
+ }
+
+ if (bean._bean instanceof AbstractLifeCycle)
+ {
+ ((AbstractLifeCycle)bean._bean).setStopTimeout(getStopTimeout());
+ }
+ }
+ }
+
+ /**
+ * Unmanages a bean already contained by this aggregate, so that it is not started/stopped/destroyed with this
+ * aggregate.
+ *
+ * @param bean The bean to unmanage (must already have been added).
+ */
+ @Override
+ public void unmanage(Object bean)
+ {
+ for (Bean b : _beans)
+ {
+ if (b._bean == bean)
+ {
+ unmanage(b);
+ return;
+ }
+ }
+ throw new IllegalArgumentException("Unknown bean " + bean);
+ }
+
+ private void unmanage(Bean bean)
+ {
+ if (bean._managed != Managed.UNMANAGED)
+ {
+ if (bean._managed == Managed.MANAGED && bean._bean instanceof Container)
+ {
+ for (Container.Listener l : _listeners)
+ {
+ if (l instanceof InheritedListener)
+ ((Container)bean._bean).removeBean(l);
+ }
+ }
+ bean._managed = Managed.UNMANAGED;
+ }
+ }
+
+ @Override
+ public Collection<Object> getBeans()
+ {
+ return getBeans(Object.class);
+ }
+
+ public void setBeans(Collection<Object> beans)
+ {
+ for (Object bean : beans)
+ {
+ addBean(bean);
+ }
+ }
+
+ @Override
+ public <T> Collection<T> getBeans(Class<T> clazz)
+ {
+ ArrayList<T> beans = null;
+ for (Bean b : _beans)
+ {
+ if (clazz.isInstance(b._bean))
+ {
+ if (beans == null)
+ beans = new ArrayList<>();
+ beans.add(clazz.cast(b._bean));
+ }
+ }
+ return beans == null ? Collections.emptyList() : beans;
+ }
+
+ @Override
+ public <T> T getBean(Class<T> clazz)
+ {
+ for (Bean b : _beans)
+ {
+ if (clazz.isInstance(b._bean))
+ return clazz.cast(b._bean);
+ }
+ return null;
+ }
+
+ /**
+ * Removes all bean
+ */
+ public void removeBeans()
+ {
+ ArrayList<Bean> beans = new ArrayList<>(_beans);
+ for (Bean b : beans)
+ {
+ remove(b);
+ }
+ }
+
+ private Bean getBean(Object o)
+ {
+ for (Bean b : _beans)
+ {
+ if (b._bean == o)
+ return b;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean removeBean(Object o)
+ {
+ Bean b = getBean(o);
+ return b != null && remove(b);
+ }
+
+ private boolean remove(Bean bean)
+ {
+ if (_beans.remove(bean))
+ {
+ boolean wasManaged = bean.isManaged();
+
+ unmanage(bean);
+
+ for (Container.Listener l : _listeners)
+ {
+ l.beanRemoved(this, bean._bean);
+ }
+
+ if (bean._bean instanceof Container.Listener)
+ removeEventListener((Container.Listener)bean._bean);
+
+ // stop managed beans
+ if (wasManaged && bean._bean instanceof LifeCycle)
+ {
+ try
+ {
+ stop((LifeCycle)bean._bean);
+ }
+ catch (RuntimeException | Error e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void removeEventListener(Container.Listener listener)
+ {
+ if (_listeners.remove(listener))
+ {
+ // remove existing beans
+ for (Bean b : _beans)
+ {
+ listener.beanRemoved(this, b._bean);
+
+ if (listener instanceof InheritedListener && b.isManaged() && b._bean instanceof Container)
+ ((Container)b._bean).removeBean(listener);
+ }
+ }
+ }
+
+ @Override
+ public void setStopTimeout(long stopTimeout)
+ {
+ super.setStopTimeout(stopTimeout);
+ for (Bean bean : _beans)
+ {
+ if (bean.isManaged() && bean._bean instanceof AbstractLifeCycle)
+ ((AbstractLifeCycle)bean._bean).setStopTimeout(stopTimeout);
+ }
+ }
+
+ /**
+ * Dumps to {@link System#err}.
+ *
+ * @see #dump()
+ */
+ @ManagedOperation("Dump the object to stderr")
+ public void dumpStdErr()
+ {
+ try
+ {
+ dump(System.err, "");
+ System.err.println(Dumpable.KEY);
+ }
+ catch (IOException e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ @Override
+ @ManagedOperation("Dump the object to a string")
+ public String dump()
+ {
+ return Dumpable.dump(this);
+ }
+
+ /**
+ * @param dumpable the object to dump
+ * @return the string representation of the given Dumpable
+ * @deprecated use {@link Dumpable#dump(Dumpable)} instead
+ */
+ @Deprecated
+ public static String dump(Dumpable dumpable)
+ {
+ return Dumpable.dump(dumpable);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ dumpObjects(out, indent);
+ }
+
+ /**
+ * Dump this object to an Appendable with no indent.
+ *
+ * @param out The appendable to dump to.
+ * @throws IOException May be thrown by the Appendable
+ */
+ public void dump(Appendable out) throws IOException
+ {
+ dump(out, "");
+ }
+
+ /**
+ * Dump just this object, but not it's children. Typically used to
+ * implement {@link #dump(Appendable, String)}
+ *
+ * @param out The appendable to dump to
+ * @throws IOException May be thrown by the Appendable
+ */
+ @Deprecated
+ protected void dumpThis(Appendable out) throws IOException
+ {
+ out.append(String.valueOf(this)).append(" - ").append(getState()).append("\n");
+ }
+
+ /**
+ * @param out The Appendable to dump to
+ * @param obj The object to dump
+ * @throws IOException May be thrown by the Appendable
+ * @deprecated use {@link Dumpable#dumpObject(Appendable, Object)} instead
+ */
+ @Deprecated
+ public static void dumpObject(Appendable out, Object obj) throws IOException
+ {
+ Dumpable.dumpObject(out, obj);
+ }
+
+ /**
+ * Dump this object, it's contained beans and additional items to an Appendable
+ *
+ * @param out The appendable to dump to
+ * @param indent The indent to apply after any new lines
+ * @param items Additional items to be dumped as contained.
+ * @throws IOException May be thrown by the Appendable
+ */
+ protected void dumpObjects(Appendable out, String indent, Object... items) throws IOException
+ {
+ Dumpable.dumpObjects(out, indent, this, items);
+ }
+
+ /**
+ * @param out The appendable to dump to
+ * @param indent The indent to apply after any new lines
+ * @param items Additional collections to be dumped
+ * @throws IOException May be thrown by the Appendable
+ * @deprecated use {@link #dumpObjects(Appendable, String, Object...)}
+ */
+ @Deprecated
+ protected void dumpBeans(Appendable out, String indent, Collection<?>... items) throws IOException
+ {
+ dump(out, indent, items);
+ }
+
+ @Deprecated
+ public static void dump(Appendable out, String indent, Collection<?>... collections) throws IOException
+ {
+ if (collections.length == 0)
+ return;
+ int size = 0;
+ for (Collection<?> c : collections)
+ {
+ size += c.size();
+ }
+ if (size == 0)
+ return;
+
+ int i = 0;
+ for (Collection<?> c : collections)
+ {
+ for (Object o : c)
+ {
+ i++;
+ out.append(indent).append(" +- ");
+ Dumpable.dumpObjects(out, indent + (i < size ? " | " : " "), o);
+ }
+ }
+ }
+
+ enum Managed
+ {
+ POJO, MANAGED, UNMANAGED, AUTO
+ }
+
+ private static class Bean
+ {
+ private final Object _bean;
+ private volatile Managed _managed = Managed.POJO;
+
+ private Bean(Object b)
+ {
+ if (b == null)
+ throw new NullPointerException();
+ _bean = b;
+ }
+
+ public boolean isManaged()
+ {
+ return _managed == Managed.MANAGED;
+ }
+
+ public boolean isManageable()
+ {
+ switch (_managed)
+ {
+ case MANAGED:
+ return true;
+ case AUTO:
+ return _bean instanceof LifeCycle && ((LifeCycle)_bean).isStopped();
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("{%s,%s}", _bean, _managed);
+ }
+ }
+
+ public void updateBean(Object oldBean, final Object newBean)
+ {
+ if (newBean != oldBean)
+ {
+ if (oldBean != null)
+ removeBean(oldBean);
+ if (newBean != null)
+ addBean(newBean);
+ }
+ }
+
+ public void updateBean(Object oldBean, final Object newBean, boolean managed)
+ {
+ if (newBean != oldBean)
+ {
+ if (oldBean != null)
+ removeBean(oldBean);
+ if (newBean != null)
+ addBean(newBean, managed);
+ }
+ }
+
+ public void updateBeans(Object[] oldBeans, final Object[] newBeans)
+ {
+ // remove oldChildren not in newChildren
+ if (oldBeans != null)
+ {
+ loop:
+ for (Object o : oldBeans)
+ {
+ if (newBeans != null)
+ {
+ for (Object n : newBeans)
+ {
+ if (o == n)
+ continue loop;
+ }
+ }
+ removeBean(o);
+ }
+ }
+
+ // add new beans not in old
+ if (newBeans != null)
+ {
+ loop:
+ for (Object n : newBeans)
+ {
+ if (oldBeans != null)
+ {
+ for (Object o : oldBeans)
+ {
+ if (o == n)
+ continue loop;
+ }
+ }
+ addBean(n);
+ }
+ }
+ }
+
+ @Override
+ public <T> Collection<T> getContainedBeans(Class<T> clazz)
+ {
+ Set<T> beans = new HashSet<>();
+ getContainedBeans(clazz, beans);
+ return beans;
+ }
+
+ protected <T> void getContainedBeans(Class<T> clazz, Collection<T> beans)
+ {
+ beans.addAll(getBeans(clazz));
+ for (Container c : getBeans(Container.class))
+ {
+ Bean bean = getBean(c);
+ if (bean != null && bean.isManageable())
+ {
+ if (c instanceof ContainerLifeCycle)
+ ((ContainerLifeCycle)c).getContainedBeans(clazz, beans);
+ else
+ beans.addAll(c.getContainedBeans(clazz));
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/Destroyable.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/Destroyable.java
new file mode 100644
index 0000000..9d1a2b9
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/Destroyable.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+
+/**
+ * <p>A Destroyable is an object which can be destroyed.</p>
+ * <p>Typically a Destroyable is a {@link LifeCycle} component that can hold onto
+ * resources over multiple start/stop cycles. A call to destroy will release all
+ * resources and will prevent any further start/stop cycles from being successful.</p>
+ */
+@ManagedObject
+public interface Destroyable
+{
+ @ManagedOperation(value = "Destroys this component", impact = "ACTION")
+ void destroy();
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/Dumpable.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/Dumpable.java
new file mode 100644
index 0000000..9acfc97
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/Dumpable.java
@@ -0,0 +1,289 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+
+@ManagedObject("Dumpable Object")
+public interface Dumpable
+{
+ String KEY = "key: +- bean, += managed, +~ unmanaged, +? auto, +: iterable, +] array, +@ map, +> undefined";
+
+ @ManagedOperation(value = "Dump the nested Object state as a String", impact = "INFO")
+ default String dump()
+ {
+ return dump(this);
+ }
+
+ /**
+ * Dump this object (and children) into an Appendable using the provided indent after any new lines.
+ * The indent should not be applied to the first object dumped.
+ *
+ * @param out The appendable to dump to
+ * @param indent The indent to apply after any new lines.
+ * @throws IOException if unable to write to Appendable
+ */
+ void dump(Appendable out, String indent) throws IOException;
+
+ /**
+ * Utility method to implement {@link #dump()} by calling {@link #dump(Appendable, String)}
+ *
+ * @param dumpable The dumpable to dump
+ * @return The dumped string
+ */
+ static String dump(Dumpable dumpable)
+ {
+ StringBuilder b = new StringBuilder();
+ try
+ {
+ dumpable.dump(b, "");
+ }
+ catch (IOException e)
+ {
+ b.append(e.toString());
+ }
+ b.append(KEY);
+ return b.toString();
+ }
+
+ /**
+ * The description of this/self found in the dump.
+ * Allows for alternative representation of Object other then .toString()
+ * where the long form output of toString() is represented in a cleaner way
+ * within the dump infrastructure.
+ *
+ * @return the representation of self
+ */
+ default String dumpSelf()
+ {
+ return toString();
+ }
+
+ /**
+ * Dump just an Object (but not it's contained items) to an Appendable.
+ *
+ * @param out The Appendable to dump to
+ * @param o The object to dump.
+ * @throws IOException May be thrown by the Appendable
+ */
+ static void dumpObject(Appendable out, Object o) throws IOException
+ {
+ try
+ {
+ String s;
+ if (o == null)
+ s = "null";
+ else if (o instanceof Dumpable)
+ {
+ s = ((Dumpable)o).dumpSelf();
+ s = StringUtil.replace(s, "\r\n", "|");
+ s = StringUtil.replace(s, '\n', '|');
+ }
+ else if (o instanceof Collection)
+ s = String.format("%s@%x(size=%d)", o.getClass().getName(), o.hashCode(), ((Collection)o).size());
+ else if (o.getClass().isArray())
+ s = String.format("%s@%x[size=%d]", o.getClass().getComponentType(), o.hashCode(), Array.getLength(o));
+ else if (o instanceof Map)
+ s = String.format("%s@%x{size=%d}", o.getClass().getName(), o.hashCode(), ((Map<?, ?>)o).size());
+ else
+ {
+ s = String.valueOf(o);
+ s = StringUtil.replace(s, "\r\n", "|");
+ s = StringUtil.replace(s, '\n', '|');
+ }
+
+ if (o instanceof LifeCycle)
+ out.append(s).append(" - ").append((AbstractLifeCycle.getState((LifeCycle)o))).append("\n");
+ else
+ out.append(s).append("\n");
+ }
+ catch (Throwable ex)
+ {
+ out.append("=> ").append(ex.toString()).append("\n");
+ }
+ }
+
+ /**
+ * Dump an Object, it's contained items and additional items to an {@link Appendable}.
+ * If the object in an {@link Iterable} or an {@link Array}, then its contained items
+ * are also dumped.
+ *
+ * @param out the Appendable to dump to
+ * @param indent The indent to apply after any new lines
+ * @param object The object to dump. If the object is an instance
+ * of {@link Container}, {@link Stream}, {@link Iterable}, {@link Array} or {@link Map},
+ * then children of the object a recursively dumped.
+ * @param extraChildren Items to be dumped as children of the object, in addition to any discovered children of object
+ * @throws IOException May be thrown by the Appendable
+ */
+ static void dumpObjects(Appendable out, String indent, Object object, Object... extraChildren) throws IOException
+ {
+ dumpObject(out, object);
+
+ int extras = extraChildren == null ? 0 : extraChildren.length;
+
+ if (object instanceof Stream)
+ object = ((Stream)object).toArray();
+ if (object instanceof Array)
+ object = Arrays.asList((Object[])object);
+
+ if (object instanceof Container)
+ {
+ dumpContainer(out, indent, (Container)object, extras == 0);
+ }
+ if (object instanceof Iterable)
+ {
+ dumpIterable(out, indent, (Iterable<?>)object, extras == 0);
+ }
+ else if (object instanceof Map)
+ {
+ dumpMapEntries(out, indent, (Map<?, ?>)object, extras == 0);
+ }
+
+ if (extras == 0)
+ return;
+
+ int i = 0;
+ for (Object item : extraChildren)
+ {
+ i++;
+ String nextIndent = indent + (i < extras ? "| " : " ");
+ out.append(indent).append("+> ");
+ if (item instanceof Dumpable)
+ ((Dumpable)item).dump(out, nextIndent);
+ else
+ dumpObjects(out, nextIndent, item);
+ }
+ }
+
+ static void dumpContainer(Appendable out, String indent, Container object, boolean last) throws IOException
+ {
+ Container container = object;
+ ContainerLifeCycle containerLifeCycle = container instanceof ContainerLifeCycle ? (ContainerLifeCycle)container : null;
+ for (Iterator<Object> i = container.getBeans().iterator(); i.hasNext(); )
+ {
+ Object bean = i.next();
+
+ if (container instanceof DumpableContainer && !((DumpableContainer)container).isDumpable(bean))
+ continue; //won't be dumped as a child bean
+
+ String nextIndent = indent + ((i.hasNext() || !last) ? "| " : " ");
+ if (bean instanceof LifeCycle)
+ {
+ if (container.isManaged(bean))
+ {
+ out.append(indent).append("+= ");
+ if (bean instanceof Dumpable)
+ ((Dumpable)bean).dump(out, nextIndent);
+ else
+ dumpObjects(out, nextIndent, bean);
+ }
+ else if (containerLifeCycle != null && containerLifeCycle.isAuto(bean))
+ {
+ out.append(indent).append("+? ");
+ if (bean instanceof Dumpable)
+ ((Dumpable)bean).dump(out, nextIndent);
+ else
+ dumpObjects(out, nextIndent, bean);
+ }
+ else
+ {
+ out.append(indent).append("+~ ");
+ dumpObject(out, bean);
+ }
+ }
+ else if (containerLifeCycle != null && containerLifeCycle.isUnmanaged(bean))
+ {
+ out.append(indent).append("+~ ");
+ dumpObject(out, bean);
+ }
+ else
+ {
+ out.append(indent).append("+- ");
+ if (bean instanceof Dumpable)
+ ((Dumpable)bean).dump(out, nextIndent);
+ else
+ dumpObjects(out, nextIndent, bean);
+ }
+ }
+ }
+
+ static void dumpIterable(Appendable out, String indent, Iterable<?> iterable, boolean last) throws IOException
+ {
+ for (Iterator i = iterable.iterator(); i.hasNext(); )
+ {
+ Object item = i.next();
+ String nextIndent = indent + ((i.hasNext() || !last) ? "| " : " ");
+ out.append(indent).append("+: ");
+ if (item instanceof Dumpable)
+ ((Dumpable)item).dump(out, nextIndent);
+ else
+ dumpObjects(out, nextIndent, item);
+ }
+ }
+
+ static void dumpMapEntries(Appendable out, String indent, Map<?, ?> map, boolean last) throws IOException
+ {
+ for (Iterator<? extends Map.Entry<?, ?>> i = map.entrySet().iterator(); i.hasNext(); )
+ {
+ Map.Entry entry = i.next();
+ String nextIndent = indent + ((i.hasNext() || !last) ? "| " : " ");
+ out.append(indent).append("+@ ").append(String.valueOf(entry.getKey())).append(" = ");
+ Object item = entry.getValue();
+ if (item instanceof Dumpable)
+ ((Dumpable)item).dump(out, nextIndent);
+ else
+ dumpObjects(out, nextIndent, item);
+ }
+ }
+
+ static Dumpable named(String name, Object object)
+ {
+ return (out, indent) ->
+ {
+ out.append(name).append(": ");
+ Dumpable.dumpObjects(out, indent, object);
+ };
+ }
+
+ /**
+ * DumpableContainer
+ *
+ * A Dumpable that is a container of beans can implement this
+ * interface to allow it to refine which of its beans can be
+ * dumped.
+ */
+ public interface DumpableContainer extends Dumpable
+ {
+ default boolean isDumpable(Object o)
+ {
+ return true;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/DumpableCollection.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/DumpableCollection.java
new file mode 100644
index 0000000..9d1b5bc
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/DumpableCollection.java
@@ -0,0 +1,59 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+public class DumpableCollection implements Dumpable
+{
+ private final String _name;
+ private final Collection<?> _collection;
+
+ public DumpableCollection(String name, Collection<?> collection)
+ {
+ _name = name;
+ _collection = collection;
+ }
+
+ public static DumpableCollection fromArray(String name, Object[] array)
+ {
+ return new DumpableCollection(name, array == null ? Collections.emptyList() : Arrays.asList(array));
+ }
+
+ public static DumpableCollection from(String name, Object... items)
+ {
+ return new DumpableCollection(name, items == null ? Collections.emptyList() : Arrays.asList(items));
+ }
+
+ @Override
+ public String dump()
+ {
+ return Dumpable.dump(this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ Object[] array = (_collection == null ? null : _collection.toArray());
+ Dumpable.dumpObjects(out, indent, _name + " size=" + (array == null ? 0 : array.length), array);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/FileDestroyable.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/FileDestroyable.java
new file mode 100644
index 0000000..7bc8832
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/FileDestroyable.java
@@ -0,0 +1,95 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+
+public class FileDestroyable implements Destroyable
+{
+ private static final Logger LOG = Log.getLogger(FileDestroyable.class);
+ final List<File> _files = new ArrayList<File>();
+
+ public FileDestroyable()
+ {
+ }
+
+ public FileDestroyable(String file) throws IOException
+ {
+ _files.add(Resource.newResource(file).getFile());
+ }
+
+ public FileDestroyable(File file)
+ {
+ _files.add(file);
+ }
+
+ public void addFile(String file) throws IOException
+ {
+ try (Resource r = Resource.newResource(file))
+ {
+ _files.add(r.getFile());
+ }
+ }
+
+ public void addFile(File file)
+ {
+ _files.add(file);
+ }
+
+ public void addFiles(Collection<File> files)
+ {
+ _files.addAll(files);
+ }
+
+ public void removeFile(String file) throws IOException
+ {
+ try (Resource r = Resource.newResource(file))
+ {
+ _files.remove(r.getFile());
+ }
+ }
+
+ public void removeFile(File file)
+ {
+ _files.remove(file);
+ }
+
+ @Override
+ public void destroy()
+ {
+ for (File file : _files)
+ {
+ if (file.exists())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Destroy {}", file);
+ IO.delete(file);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/FileNoticeLifeCycleListener.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/FileNoticeLifeCycleListener.java
new file mode 100644
index 0000000..249ff3a
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/FileNoticeLifeCycleListener.java
@@ -0,0 +1,83 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.io.FileWriter;
+import java.io.Writer;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * A LifeCycle Listener that writes state changes to a file.
+ * <p>This can be used with the jetty.sh script to wait for successful startup.
+ */
+public class FileNoticeLifeCycleListener implements LifeCycle.Listener
+{
+ private static final Logger LOG = Log.getLogger(FileNoticeLifeCycleListener.class);
+
+ private final String _filename;
+
+ public FileNoticeLifeCycleListener(String filename)
+ {
+ _filename = filename;
+ }
+
+ private void writeState(String action, LifeCycle lifecycle)
+ {
+ try (Writer out = new FileWriter(_filename, true))
+ {
+ out.append(action).append(" ").append(lifecycle.toString()).append("\n");
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ @Override
+ public void lifeCycleStarting(LifeCycle event)
+ {
+ writeState("STARTING", event);
+ }
+
+ @Override
+ public void lifeCycleStarted(LifeCycle event)
+ {
+ writeState("STARTED", event);
+ }
+
+ @Override
+ public void lifeCycleFailure(LifeCycle event, Throwable cause)
+ {
+ writeState("FAILED", event);
+ }
+
+ @Override
+ public void lifeCycleStopping(LifeCycle event)
+ {
+ writeState("STOPPING", event);
+ }
+
+ @Override
+ public void lifeCycleStopped(LifeCycle event)
+ {
+ writeState("STOPPED", event);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/Graceful.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/Graceful.java
new file mode 100644
index 0000000..3687827
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/Graceful.java
@@ -0,0 +1,89 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.FutureCallback;
+
+/**
+ * <p>Jetty components that wish to be part of a Graceful shutdown implement this interface so that
+ * the {@link Graceful#shutdown()} method will be called to initiate a shutdown. Shutdown operations
+ * can fall into the following categories:</p>
+ * <ul>
+ * <li>Preventing new load from being accepted (eg connectors stop accepting connections)</li>
+ * <li>Preventing existing load expanding (eg stopping existing connections accepting new requests)</li>
+ * <li>Waiting for existing load to complete (eg waiting for active request count to reduce to 0)</li>
+ * <li>Performing cleanup operations that may take time (eg closing an SSL connection)</li>
+ * </ul>
+ * <p>The {@link Future} returned by the the shutdown call will be completed to indicate the shutdown operation is completed.
+ * Some shutdown operations may be instantaneous and always return a completed future.
+ * </p><p>
+ * Graceful shutdown is typically orchestrated by the doStop methods of Server or ContextHandler (for a full or partial
+ * shutdown respectively).
+ * </p>
+ */
+public interface Graceful
+{
+ Future<Void> shutdown();
+
+ boolean isShutdown();
+
+ /**
+ * A utility Graceful that uses a {@link FutureCallback} to indicate if shutdown is completed.
+ * By default the {@link FutureCallback} is returned as already completed, but the {@link #newShutdownCallback()} method
+ * can be overloaded to return a non-completed callback that will require a {@link Callback#succeeded()} or
+ * {@link Callback#failed(Throwable)} call to be completed.
+ */
+ class Shutdown implements Graceful
+ {
+ private final AtomicReference<FutureCallback> _shutdown = new AtomicReference<>();
+
+ protected FutureCallback newShutdownCallback()
+ {
+ return FutureCallback.SUCCEEDED;
+ }
+
+ @Override
+ public Future<Void> shutdown()
+ {
+ return _shutdown.updateAndGet(fcb -> fcb == null ? newShutdownCallback() : fcb);
+ }
+
+ @Override
+ public boolean isShutdown()
+ {
+ return _shutdown.get() != null;
+ }
+
+ public void cancel()
+ {
+ FutureCallback shutdown = _shutdown.getAndSet(null);
+ if (shutdown != null && !shutdown.isDone())
+ shutdown.cancel(true);
+ }
+
+ public FutureCallback get()
+ {
+ return _shutdown.get();
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/LifeCycle.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/LifeCycle.java
new file mode 100644
index 0000000..73475a5
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/LifeCycle.java
@@ -0,0 +1,162 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.util.EventListener;
+
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+
+/**
+ * The lifecycle interface for generic components.
+ * <br>
+ * Classes implementing this interface have a defined life cycle
+ * defined by the methods of this interface.
+ */
+@ManagedObject("Lifecycle Interface for startable components")
+public interface LifeCycle
+{
+
+ /**
+ * Starts the component.
+ *
+ * @throws Exception If the component fails to start
+ * @see #isStarted()
+ * @see #stop()
+ * @see #isFailed()
+ */
+ @ManagedOperation(value = "Starts the instance", impact = "ACTION")
+ void start()
+ throws Exception;
+
+ /**
+ * Stops the component.
+ * The component may wait for current activities to complete
+ * normally, but it can be interrupted.
+ *
+ * @throws Exception If the component fails to stop
+ * @see #isStopped()
+ * @see #start()
+ * @see #isFailed()
+ */
+ @ManagedOperation(value = "Stops the instance", impact = "ACTION")
+ void stop()
+ throws Exception;
+
+ /**
+ * @return true if the component is starting or has been started.
+ */
+ boolean isRunning();
+
+ /**
+ * @return true if the component has been started.
+ * @see #start()
+ * @see #isStarting()
+ */
+ boolean isStarted();
+
+ /**
+ * @return true if the component is starting.
+ * @see #isStarted()
+ */
+ boolean isStarting();
+
+ /**
+ * @return true if the component is stopping.
+ * @see #isStopped()
+ */
+ boolean isStopping();
+
+ /**
+ * @return true if the component has been stopped.
+ * @see #stop()
+ * @see #isStopping()
+ */
+ boolean isStopped();
+
+ /**
+ * @return true if the component has failed to start or has failed to stop.
+ */
+ boolean isFailed();
+
+ void addLifeCycleListener(LifeCycle.Listener listener);
+
+ void removeLifeCycleListener(LifeCycle.Listener listener);
+
+ /**
+ * Listener.
+ * A listener for Lifecycle events.
+ */
+ interface Listener extends EventListener
+ {
+ void lifeCycleStarting(LifeCycle event);
+
+ void lifeCycleStarted(LifeCycle event);
+
+ void lifeCycleFailure(LifeCycle event, Throwable cause);
+
+ void lifeCycleStopping(LifeCycle event);
+
+ void lifeCycleStopped(LifeCycle event);
+ }
+
+ /**
+ * Utility to start an object if it is a LifeCycle and to convert
+ * any exception thrown to a {@link RuntimeException}
+ *
+ * @param object The instance to start.
+ * @throws RuntimeException if the call to start throws an exception.
+ */
+ static void start(Object object)
+ {
+ if (object instanceof LifeCycle)
+ {
+ try
+ {
+ ((LifeCycle)object).start();
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Utility to stop an object if it is a LifeCycle and to convert
+ * any exception thrown to a {@link RuntimeException}
+ *
+ * @param object The instance to stop.
+ * @throws RuntimeException if the call to stop throws an exception.
+ */
+ static void stop(Object object)
+ {
+ if (object instanceof LifeCycle)
+ {
+ try
+ {
+ ((LifeCycle)object).stop();
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/StopLifeCycle.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/StopLifeCycle.java
new file mode 100644
index 0000000..bd533fd
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/StopLifeCycle.java
@@ -0,0 +1,71 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * A LifeCycle that when started will stop another LifeCycle
+ */
+public class StopLifeCycle extends AbstractLifeCycle implements LifeCycle.Listener
+{
+ private static final Logger LOG = Log.getLogger(StopLifeCycle.class);
+
+ private final LifeCycle _lifecycle;
+
+ public StopLifeCycle(LifeCycle lifecycle)
+ {
+ _lifecycle = lifecycle;
+ addLifeCycleListener(this);
+ }
+
+ @Override
+ public void lifeCycleStarting(LifeCycle lifecycle)
+ {
+ }
+
+ @Override
+ public void lifeCycleStarted(LifeCycle lifecycle)
+ {
+ try
+ {
+ _lifecycle.stop();
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ @Override
+ public void lifeCycleFailure(LifeCycle lifecycle, Throwable cause)
+ {
+ }
+
+ @Override
+ public void lifeCycleStopping(LifeCycle lifecycle)
+ {
+ }
+
+ @Override
+ public void lifeCycleStopped(LifeCycle lifecycle)
+ {
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/package-info.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/package-info.java
new file mode 100644
index 0000000..86a0f73
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/component/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Util : Jetty Lifecycle Management
+ */
+package org.eclipse.jetty.util.component;
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/compression/CompressionPool.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/compression/CompressionPool.java
new file mode 100644
index 0000000..297ea1f
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/compression/CompressionPool.java
@@ -0,0 +1,141 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.compression;
+
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+
+public abstract class CompressionPool<T> extends AbstractLifeCycle
+{
+ public static final int INFINITE_CAPACITY = -1;
+
+ private final Queue<T> _pool;
+ private final AtomicInteger _numObjects = new AtomicInteger(0);
+ private final int _capacity;
+
+ /**
+ * Create a Pool of {@link T} instances.
+ *
+ * If given a capacity equal to zero the Objects will not be pooled
+ * and will be created on acquire and ended on release.
+ * If given a negative capacity equal to zero there will be no size restrictions on the Pool
+ *
+ * @param capacity maximum number of Objects which can be contained in the pool
+ */
+ public CompressionPool(int capacity)
+ {
+ _capacity = capacity;
+ _pool = (_capacity == 0) ? null : new ConcurrentLinkedQueue<>();
+ }
+
+ protected abstract T newObject();
+
+ protected abstract void end(T object);
+
+ protected abstract void reset(T object);
+
+ /**
+ * @return Object taken from the pool if it is not empty or a newly created Object
+ */
+ public T acquire()
+ {
+ T object;
+
+ if (_capacity == 0)
+ object = newObject();
+ else
+ {
+ object = _pool.poll();
+ if (object == null)
+ object = newObject();
+ else if (_capacity > 0)
+ _numObjects.decrementAndGet();
+ }
+
+ return object;
+ }
+
+ /**
+ * @param object returns this Object to the pool or calls {@link #end(Object)} if the pool is full.
+ */
+ public void release(T object)
+ {
+ if (object == null)
+ return;
+
+ if (_capacity == 0 || !isRunning())
+ {
+ end(object);
+ return;
+ }
+ else if (_capacity < 0)
+ {
+ reset(object);
+ _pool.add(object);
+ }
+ else
+ {
+ while (true)
+ {
+ int d = _numObjects.get();
+
+ if (d >= _capacity)
+ {
+ end(object);
+ break;
+ }
+
+ if (_numObjects.compareAndSet(d, d + 1))
+ {
+ reset(object);
+ _pool.add(object);
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void doStop()
+ {
+ T t = _pool.poll();
+ while (t != null)
+ {
+ end(t);
+ t = _pool.poll();
+ }
+ _numObjects.set(0);
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder str = new StringBuilder();
+ str.append(getClass().getSimpleName());
+ str.append('@').append(Integer.toHexString(hashCode()));
+ str.append('{').append(getState());
+ str.append(",size=").append(_pool == null ? -1 : _pool.size());
+ str.append(",capacity=").append(_capacity <= 0 ? "UNLIMITED" : _capacity);
+ str.append('}');
+ return str.toString();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/compression/DeflaterPool.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/compression/DeflaterPool.java
new file mode 100644
index 0000000..a14f9aa
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/compression/DeflaterPool.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.compression;
+
+import java.util.zip.Deflater;
+
+public class DeflaterPool extends CompressionPool<Deflater>
+{
+ private final int compressionLevel;
+ private final boolean nowrap;
+
+ /**
+ * Create a Pool of {@link Deflater} instances.
+ * <p>
+ * If given a capacity equal to zero the Deflaters will not be pooled
+ * and will be created on acquire and ended on release.
+ * If given a negative capacity equal to zero there will be no size restrictions on the DeflaterPool
+ *
+ * @param capacity maximum number of Deflaters which can be contained in the pool
+ * @param compressionLevel the default compression level for new Deflater objects
+ * @param nowrap if true then use GZIP compatible compression for all new Deflater objects
+ */
+ public DeflaterPool(int capacity, int compressionLevel, boolean nowrap)
+ {
+ super(capacity);
+ this.compressionLevel = compressionLevel;
+ this.nowrap = nowrap;
+ }
+
+ @Override
+ protected Deflater newObject()
+ {
+ return new Deflater(compressionLevel, nowrap);
+ }
+
+ @Override
+ protected void end(Deflater deflater)
+ {
+ deflater.end();
+ }
+
+ @Override
+ protected void reset(Deflater deflater)
+ {
+ deflater.reset();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/compression/InflaterPool.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/compression/InflaterPool.java
new file mode 100644
index 0000000..b79244a
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/compression/InflaterPool.java
@@ -0,0 +1,60 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.compression;
+
+import java.util.zip.Inflater;
+
+public class InflaterPool extends CompressionPool<Inflater>
+{
+ private final boolean nowrap;
+
+ /**
+ * Create a Pool of {@link Inflater} instances.
+ * <p>
+ * If given a capacity equal to zero the Inflaters will not be pooled
+ * and will be created on acquire and ended on release.
+ * If given a negative capacity equal to zero there will be no size restrictions on the InflaterPool
+ *
+ * @param capacity maximum number of Inflaters which can be contained in the pool
+ * @param nowrap if true then use GZIP compatible compression for all new Inflater objects
+ */
+ public InflaterPool(int capacity, boolean nowrap)
+ {
+ super(capacity);
+ this.nowrap = nowrap;
+ }
+
+ @Override
+ protected Inflater newObject()
+ {
+ return new Inflater(nowrap);
+ }
+
+ @Override
+ protected void end(Inflater inflater)
+ {
+ inflater.end();
+ }
+
+ @Override
+ protected void reset(Inflater inflater)
+ {
+ inflater.reset();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/AbstractLogger.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/AbstractLogger.java
new file mode 100644
index 0000000..6706415
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/AbstractLogger.java
@@ -0,0 +1,257 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import java.util.Properties;
+
+/**
+ * Abstract Logger.
+ * Manages the atomic registration of the logger by name.
+ */
+public abstract class AbstractLogger implements Logger
+{
+ public static final int LEVEL_DEFAULT = -1;
+ public static final int LEVEL_ALL = 0;
+ public static final int LEVEL_DEBUG = 1;
+ public static final int LEVEL_INFO = 2;
+ public static final int LEVEL_WARN = 3;
+ public static final int LEVEL_OFF = 10;
+
+ @Override
+ public final Logger getLogger(String name)
+ {
+ if (isBlank(name))
+ return this;
+
+ final String basename = getName();
+ final String fullname = (isBlank(basename) || Log.getRootLogger() == this) ? name : (basename + "." + name);
+
+ Logger logger = Log.getLoggers().get(fullname);
+ if (logger == null)
+ {
+ Logger newlog = newLogger(fullname);
+
+ logger = Log.getMutableLoggers().putIfAbsent(fullname, newlog);
+ if (logger == null)
+ logger = newlog;
+ }
+
+ return logger;
+ }
+
+ protected abstract Logger newLogger(String fullname);
+
+ /**
+ * A more robust form of name blank test. Will return true for null names, and names that have only whitespace
+ *
+ * @param name the name to test
+ * @return true for null or blank name, false if any non-whitespace character is found.
+ */
+ private static boolean isBlank(String name)
+ {
+ if (name == null)
+ {
+ return true;
+ }
+ int size = name.length();
+ char c;
+ for (int i = 0; i < size; i++)
+ {
+ c = name.charAt(i);
+ if (!Character.isWhitespace(c))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Get the Logging Level for the provided log name. Using the FQCN first, then each package segment from longest to
+ * shortest.
+ *
+ * @param props the properties to check
+ * @param name the name to get log for
+ * @return the logging level
+ */
+ public static int lookupLoggingLevel(Properties props, final String name)
+ {
+ if ((props == null) || (props.isEmpty()) || name == null)
+ return LEVEL_DEFAULT;
+
+ // Calculate the level this named logger should operate under.
+ // Checking with FQCN first, then each package segment from longest to shortest.
+ String nameSegment = name;
+
+ while ((nameSegment != null) && (nameSegment.length() > 0))
+ {
+ String levelStr = props.getProperty(nameSegment + ".LEVEL");
+ // System.err.printf("[StdErrLog.CONFIG] Checking for property [%s.LEVEL] = %s%n",nameSegment,levelStr);
+ int level = getLevelId(nameSegment + ".LEVEL", levelStr);
+ if (level != (-1))
+ {
+ return level;
+ }
+
+ // Trim and try again.
+ int idx = nameSegment.lastIndexOf('.');
+ if (idx >= 0)
+ {
+ nameSegment = nameSegment.substring(0, idx);
+ }
+ else
+ {
+ nameSegment = null;
+ }
+ }
+
+ // Default Logging Level
+ return LEVEL_DEFAULT;
+ }
+
+ public static String getLoggingProperty(Properties props, String name, String property)
+ {
+ // Calculate the level this named logger should operate under.
+ // Checking with FQCN first, then each package segment from longest to shortest.
+ String nameSegment = name;
+
+ while ((nameSegment != null) && (nameSegment.length() > 0))
+ {
+ String s = props.getProperty(nameSegment + "." + property);
+ if (s != null)
+ return s;
+
+ // Trim and try again.
+ int idx = nameSegment.lastIndexOf('.');
+ nameSegment = (idx >= 0) ? nameSegment.substring(0, idx) : null;
+ }
+
+ return null;
+ }
+
+ protected static int getLevelId(String levelSegment, String levelName)
+ {
+ if (levelName == null)
+ {
+ return -1;
+ }
+ String levelStr = levelName.trim();
+ if ("ALL".equalsIgnoreCase(levelStr))
+ {
+ return LEVEL_ALL;
+ }
+ else if ("DEBUG".equalsIgnoreCase(levelStr))
+ {
+ return LEVEL_DEBUG;
+ }
+ else if ("INFO".equalsIgnoreCase(levelStr))
+ {
+ return LEVEL_INFO;
+ }
+ else if ("WARN".equalsIgnoreCase(levelStr))
+ {
+ return LEVEL_WARN;
+ }
+ else if ("OFF".equalsIgnoreCase(levelStr))
+ {
+ return LEVEL_OFF;
+ }
+
+ System.err.println("Unknown StdErrLog level [" + levelSegment + "]=[" + levelStr + "], expecting only [ALL, DEBUG, INFO, WARN, OFF] as values.");
+ return -1;
+ }
+
+ /**
+ * Condenses a classname by stripping down the package name to just the first character of each package name
+ * segment.Configured
+ *
+ * <pre>
+ * Examples:
+ * "org.eclipse.jetty.test.FooTest" = "oejt.FooTest"
+ * "org.eclipse.jetty.server.logging.LogTest" = "orjsl.LogTest"
+ * </pre>
+ *
+ * @param classname the fully qualified class name
+ * @return the condensed name
+ */
+ @SuppressWarnings("Duplicates")
+ protected static String condensePackageString(String classname)
+ {
+ if (classname == null || classname.isEmpty())
+ {
+ return "";
+ }
+
+ int rawLen = classname.length();
+ StringBuilder dense = new StringBuilder(rawLen);
+ boolean foundStart = false;
+ boolean hasPackage = false;
+ int startIdx = -1;
+ int endIdx = -1;
+ for (int i = 0; i < rawLen; i++)
+ {
+ char c = classname.charAt(i);
+ if (!foundStart)
+ {
+ foundStart = Character.isJavaIdentifierStart(c);
+ if (foundStart)
+ {
+ if (startIdx >= 0)
+ {
+ dense.append(classname.charAt(startIdx));
+ hasPackage = true;
+ }
+ startIdx = i;
+ }
+ }
+
+ if (foundStart)
+ {
+ if (!Character.isJavaIdentifierPart(c))
+ {
+ foundStart = false;
+ }
+ else
+ {
+ endIdx = i;
+ }
+ }
+ }
+ // append remaining from startIdx
+ if ((startIdx >= 0) && (endIdx >= startIdx))
+ {
+ if (hasPackage)
+ {
+ dense.append('.');
+ }
+ dense.append(classname, startIdx, endIdx + 1);
+ }
+
+ return dense.toString();
+ }
+
+ @Override
+ public void debug(String msg, long arg)
+ {
+ if (isDebugEnabled())
+ {
+ debug(msg, new Object[]{new Long(arg)});
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/JavaUtilLog.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/JavaUtilLog.java
new file mode 100644
index 0000000..899a3a2
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/JavaUtilLog.java
@@ -0,0 +1,304 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import java.net.URL;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.LogRecord;
+
+import org.eclipse.jetty.util.Loader;
+
+/**
+ * <p>
+ * Implementation of Jetty {@link Logger} based on {@link java.util.logging.Logger}.
+ * </p>
+ *
+ * <p>
+ * You can also set the logger level using <a href="http://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html">
+ * standard java.util.logging configuration</a>.
+ * </p>
+ *
+ * Configuration Properties:
+ * <dl>
+ * <dt>${name|hierarchy}.LEVEL=(ALL|DEBUG|INFO|WARN|OFF)</dt>
+ * <dd>
+ * Sets the level that the Logger should log at.<br>
+ * Names can be a package name, or a fully qualified class name.<br>
+ * Default: The default from the java.util.logging mechanism/configuration
+ * <br>
+ * <dt>org.eclipse.jetty.util.log.javautil.PROPERTIES=<property-resource-name></dt>
+ * <dd>If set, it is used as a classpath resource name to find a java.util.logging
+ * property file.
+ * <br>
+ * Default: null
+ * </dd>
+ * <dt>org.eclipse.jetty.util.log.javautil.SOURCE=(true|false)</dt>
+ * <dd>Set the LogRecord source class and method for JavaUtilLog.<br>
+ * Default: true
+ * </dd>
+ * <dt>org.eclipse.jetty.util.log.SOURCE=(true|false)</dt>
+ * <dd>Set the LogRecord source class and method for all Loggers.<br>
+ * Default: depends on Logger class
+ * </dd>
+ * </dl>
+ */
+public class JavaUtilLog extends AbstractLogger
+{
+ private static final String THIS_CLASS = JavaUtilLog.class.getName();
+ private static final boolean __source =
+ Boolean.parseBoolean(Log.__props.getProperty("org.eclipse.jetty.util.log.SOURCE",
+ Log.__props.getProperty("org.eclipse.jetty.util.log.javautil.SOURCE", "true")));
+
+ private static boolean _initialized = false;
+
+ private Level configuredLevel;
+ private java.util.logging.Logger _logger;
+
+ public JavaUtilLog()
+ {
+ this("org.eclipse.jetty.util.log.javautil");
+ }
+
+ public JavaUtilLog(String name)
+ {
+ synchronized (JavaUtilLog.class)
+ {
+ if (!_initialized)
+ {
+ _initialized = true;
+
+ final String properties = Log.__props.getProperty("org.eclipse.jetty.util.log.javautil.PROPERTIES", null);
+ if (properties != null)
+ {
+ AccessController.doPrivileged(new PrivilegedAction<Object>()
+ {
+ @Override
+ public Object run()
+ {
+ try
+ {
+ URL props = Loader.getResource(properties);
+ if (props != null)
+ LogManager.getLogManager().readConfiguration(props.openStream());
+ }
+ catch (Throwable e)
+ {
+ System.err.println("[WARN] Error loading logging config: " + properties);
+ e.printStackTrace(System.err);
+ }
+
+ return null;
+ }
+ });
+ }
+ }
+ }
+
+ _logger = java.util.logging.Logger.getLogger(name);
+
+ switch (lookupLoggingLevel(Log.__props, name))
+ {
+ case LEVEL_ALL:
+ _logger.setLevel(Level.ALL);
+ break;
+ case LEVEL_DEBUG:
+ _logger.setLevel(Level.FINE);
+ break;
+ case LEVEL_INFO:
+ _logger.setLevel(Level.INFO);
+ break;
+ case LEVEL_WARN:
+ _logger.setLevel(Level.WARNING);
+ break;
+ case LEVEL_OFF:
+ _logger.setLevel(Level.OFF);
+ break;
+ case LEVEL_DEFAULT:
+ default:
+ break;
+ }
+
+ configuredLevel = _logger.getLevel();
+ }
+
+ @Override
+ public String getName()
+ {
+ return _logger.getName();
+ }
+
+ protected void log(Level level, String msg, Throwable thrown)
+ {
+ LogRecord record = new LogRecord(level, msg);
+ if (thrown != null)
+ record.setThrown(thrown);
+ record.setLoggerName(_logger.getName());
+ if (__source)
+ {
+ StackTraceElement[] stack = new Throwable().getStackTrace();
+ for (int i = 0; i < stack.length; i++)
+ {
+ StackTraceElement e = stack[i];
+ if (!e.getClassName().equals(THIS_CLASS))
+ {
+ record.setSourceClassName(e.getClassName());
+ record.setSourceMethodName(e.getMethodName());
+ break;
+ }
+ }
+ }
+ _logger.log(record);
+ }
+
+ @Override
+ public void warn(String msg, Object... args)
+ {
+ if (_logger.isLoggable(Level.WARNING))
+ log(Level.WARNING, format(msg, args), null);
+ }
+
+ @Override
+ public void warn(Throwable thrown)
+ {
+ if (_logger.isLoggable(Level.WARNING))
+ log(Level.WARNING, "", thrown);
+ }
+
+ @Override
+ public void warn(String msg, Throwable thrown)
+ {
+ if (_logger.isLoggable(Level.WARNING))
+ log(Level.WARNING, msg, thrown);
+ }
+
+ @Override
+ public void info(String msg, Object... args)
+ {
+ if (_logger.isLoggable(Level.INFO))
+ log(Level.INFO, format(msg, args), null);
+ }
+
+ @Override
+ public void info(Throwable thrown)
+ {
+ if (_logger.isLoggable(Level.INFO))
+ log(Level.INFO, "", thrown);
+ }
+
+ @Override
+ public void info(String msg, Throwable thrown)
+ {
+ if (_logger.isLoggable(Level.INFO))
+ log(Level.INFO, msg, thrown);
+ }
+
+ @Override
+ public boolean isDebugEnabled()
+ {
+ return _logger.isLoggable(Level.FINE);
+ }
+
+ @Override
+ public void setDebugEnabled(boolean enabled)
+ {
+ if (enabled)
+ {
+ configuredLevel = _logger.getLevel();
+ _logger.setLevel(Level.FINE);
+ }
+ else
+ {
+ _logger.setLevel(configuredLevel);
+ }
+ }
+
+ @Override
+ public void debug(String msg, Object... args)
+ {
+ if (_logger.isLoggable(Level.FINE))
+ log(Level.FINE, format(msg, args), null);
+ }
+
+ @Override
+ public void debug(String msg, long arg)
+ {
+ if (_logger.isLoggable(Level.FINE))
+ log(Level.FINE, format(msg, arg), null);
+ }
+
+ @Override
+ public void debug(Throwable thrown)
+ {
+ if (_logger.isLoggable(Level.FINE))
+ log(Level.FINE, "", thrown);
+ }
+
+ @Override
+ public void debug(String msg, Throwable thrown)
+ {
+ if (_logger.isLoggable(Level.FINE))
+ log(Level.FINE, msg, thrown);
+ }
+
+ /**
+ * Create a Child Logger of this Logger.
+ */
+ @Override
+ protected Logger newLogger(String fullname)
+ {
+ return new JavaUtilLog(fullname);
+ }
+
+ @Override
+ public void ignore(Throwable ignored)
+ {
+ if (_logger.isLoggable(Level.FINEST))
+ log(Level.FINEST, Log.IGNORED, ignored);
+ }
+
+ private String format(String msg, Object... args)
+ {
+ msg = String.valueOf(msg); // Avoids NPE
+ String braces = "{}";
+ StringBuilder builder = new StringBuilder();
+ int start = 0;
+ for (Object arg : args)
+ {
+ int bracesIndex = msg.indexOf(braces, start);
+ if (bracesIndex < 0)
+ {
+ builder.append(msg.substring(start));
+ builder.append(" ");
+ builder.append(arg);
+ start = msg.length();
+ }
+ else
+ {
+ builder.append(msg, start, bracesIndex);
+ builder.append(arg);
+ start = bracesIndex + braces.length();
+ }
+ }
+ builder.append(msg.substring(start));
+ return builder.toString();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/JettyAwareLogger.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/JettyAwareLogger.java
new file mode 100644
index 0000000..5e63483
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/JettyAwareLogger.java
@@ -0,0 +1,629 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import org.slf4j.Marker;
+import org.slf4j.helpers.FormattingTuple;
+import org.slf4j.helpers.MessageFormatter;
+
+/**
+ * JettyAwareLogger is used to fix a FQCN bug that arises from how Jetty
+ * Log uses an indirect slf4j implementation.
+ *
+ * https://bugs.eclipse.org/bugs/show_bug.cgi?id=276670
+ */
+class JettyAwareLogger implements org.slf4j.Logger
+{
+ private static final int DEBUG = org.slf4j.spi.LocationAwareLogger.DEBUG_INT;
+ private static final int ERROR = org.slf4j.spi.LocationAwareLogger.ERROR_INT;
+ private static final int INFO = org.slf4j.spi.LocationAwareLogger.INFO_INT;
+ private static final int TRACE = org.slf4j.spi.LocationAwareLogger.TRACE_INT;
+ private static final int WARN = org.slf4j.spi.LocationAwareLogger.WARN_INT;
+
+ private static final String FQCN = Slf4jLog.class.getName();
+ private final org.slf4j.spi.LocationAwareLogger _logger;
+
+ public JettyAwareLogger(org.slf4j.spi.LocationAwareLogger logger)
+ {
+ _logger = logger;
+ }
+
+ /**
+ * @see org.slf4j.Logger#getName()
+ */
+ @Override
+ public String getName()
+ {
+ return _logger.getName();
+ }
+
+ /**
+ * @see org.slf4j.Logger#isTraceEnabled()
+ */
+ @Override
+ public boolean isTraceEnabled()
+ {
+ return _logger.isTraceEnabled();
+ }
+
+ /**
+ * @see org.slf4j.Logger#trace(java.lang.String)
+ */
+ @Override
+ public void trace(String msg)
+ {
+ log(null, TRACE, msg, null, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#trace(java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void trace(String format, Object arg)
+ {
+ log(null, TRACE, format, new Object[]{arg}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#trace(java.lang.String, java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public void trace(String format, Object arg1, Object arg2)
+ {
+ log(null, TRACE, format, new Object[]{arg1, arg2}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#trace(java.lang.String, java.lang.Object[])
+ */
+ @Override
+ public void trace(String format, Object[] argArray)
+ {
+ log(null, TRACE, format, argArray, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#trace(java.lang.String, java.lang.Throwable)
+ */
+ @Override
+ public void trace(String msg, Throwable t)
+ {
+ log(null, TRACE, msg, null, t);
+ }
+
+ /**
+ * @see org.slf4j.Logger#isTraceEnabled(org.slf4j.Marker)
+ */
+ @Override
+ public boolean isTraceEnabled(Marker marker)
+ {
+ return _logger.isTraceEnabled(marker);
+ }
+
+ /**
+ * @see org.slf4j.Logger#trace(org.slf4j.Marker, java.lang.String)
+ */
+ @Override
+ public void trace(Marker marker, String msg)
+ {
+ log(marker, TRACE, msg, null, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#trace(org.slf4j.Marker, java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void trace(Marker marker, String format, Object arg)
+ {
+ log(marker, TRACE, format, new Object[]{arg}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#trace(org.slf4j.Marker, java.lang.String, java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public void trace(Marker marker, String format, Object arg1, Object arg2)
+ {
+ log(marker, TRACE, format, new Object[]{arg1, arg2}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#trace(org.slf4j.Marker, java.lang.String, java.lang.Object[])
+ */
+ @Override
+ public void trace(Marker marker, String format, Object[] argArray)
+ {
+ log(marker, TRACE, format, argArray, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#trace(org.slf4j.Marker, java.lang.String, java.lang.Throwable)
+ */
+ @Override
+ public void trace(Marker marker, String msg, Throwable t)
+ {
+ log(marker, TRACE, msg, null, t);
+ }
+
+ /**
+ * @see org.slf4j.Logger#isDebugEnabled()
+ */
+ @Override
+ public boolean isDebugEnabled()
+ {
+ return _logger.isDebugEnabled();
+ }
+
+ /**
+ * @see org.slf4j.Logger#debug(java.lang.String)
+ */
+ @Override
+ public void debug(String msg)
+ {
+ log(null, DEBUG, msg, null, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#debug(java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void debug(String format, Object arg)
+ {
+ log(null, DEBUG, format, new Object[]{arg}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#debug(java.lang.String, java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public void debug(String format, Object arg1, Object arg2)
+ {
+ log(null, DEBUG, format, new Object[]{arg1, arg2}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#debug(java.lang.String, java.lang.Object[])
+ */
+ @Override
+ public void debug(String format, Object[] argArray)
+ {
+ log(null, DEBUG, format, argArray, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#debug(java.lang.String, java.lang.Throwable)
+ */
+ @Override
+ public void debug(String msg, Throwable t)
+ {
+ log(null, DEBUG, msg, null, t);
+ }
+
+ /**
+ * @see org.slf4j.Logger#isDebugEnabled(org.slf4j.Marker)
+ */
+ @Override
+ public boolean isDebugEnabled(Marker marker)
+ {
+ return _logger.isDebugEnabled(marker);
+ }
+
+ /**
+ * @see org.slf4j.Logger#debug(org.slf4j.Marker, java.lang.String)
+ */
+ @Override
+ public void debug(Marker marker, String msg)
+ {
+ log(marker, DEBUG, msg, null, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#debug(org.slf4j.Marker, java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void debug(Marker marker, String format, Object arg)
+ {
+ log(marker, DEBUG, format, new Object[]{arg}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#debug(org.slf4j.Marker, java.lang.String, java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public void debug(Marker marker, String format, Object arg1, Object arg2)
+ {
+ log(marker, DEBUG, format, new Object[]{arg1, arg2}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#debug(org.slf4j.Marker, java.lang.String, java.lang.Object[])
+ */
+ @Override
+ public void debug(Marker marker, String format, Object[] argArray)
+ {
+ log(marker, DEBUG, format, argArray, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#debug(org.slf4j.Marker, java.lang.String, java.lang.Throwable)
+ */
+ @Override
+ public void debug(Marker marker, String msg, Throwable t)
+ {
+ log(marker, DEBUG, msg, null, t);
+ }
+
+ /**
+ * @see org.slf4j.Logger#isInfoEnabled()
+ */
+ @Override
+ public boolean isInfoEnabled()
+ {
+ return _logger.isInfoEnabled();
+ }
+
+ /**
+ * @see org.slf4j.Logger#info(java.lang.String)
+ */
+ @Override
+ public void info(String msg)
+ {
+ log(null, INFO, msg, null, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#info(java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void info(String format, Object arg)
+ {
+ log(null, INFO, format, new Object[]{arg}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#info(java.lang.String, java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public void info(String format, Object arg1, Object arg2)
+ {
+ log(null, INFO, format, new Object[]{arg1, arg2}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#info(java.lang.String, java.lang.Object[])
+ */
+ @Override
+ public void info(String format, Object[] argArray)
+ {
+ log(null, INFO, format, argArray, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#info(java.lang.String, java.lang.Throwable)
+ */
+ @Override
+ public void info(String msg, Throwable t)
+ {
+ log(null, INFO, msg, null, t);
+ }
+
+ /**
+ * @see org.slf4j.Logger#isInfoEnabled(org.slf4j.Marker)
+ */
+ @Override
+ public boolean isInfoEnabled(Marker marker)
+ {
+ return _logger.isInfoEnabled(marker);
+ }
+
+ /**
+ * @see org.slf4j.Logger#info(org.slf4j.Marker, java.lang.String)
+ */
+ @Override
+ public void info(Marker marker, String msg)
+ {
+ log(marker, INFO, msg, null, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#info(org.slf4j.Marker, java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void info(Marker marker, String format, Object arg)
+ {
+ log(marker, INFO, format, new Object[]{arg}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#info(org.slf4j.Marker, java.lang.String, java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public void info(Marker marker, String format, Object arg1, Object arg2)
+ {
+ log(marker, INFO, format, new Object[]{arg1, arg2}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#info(org.slf4j.Marker, java.lang.String, java.lang.Object[])
+ */
+ @Override
+ public void info(Marker marker, String format, Object[] argArray)
+ {
+ log(marker, INFO, format, argArray, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#info(org.slf4j.Marker, java.lang.String, java.lang.Throwable)
+ */
+ @Override
+ public void info(Marker marker, String msg, Throwable t)
+ {
+ log(marker, INFO, msg, null, t);
+ }
+
+ /**
+ * @see org.slf4j.Logger#isWarnEnabled()
+ */
+ @Override
+ public boolean isWarnEnabled()
+ {
+ return _logger.isWarnEnabled();
+ }
+
+ /**
+ * @see org.slf4j.Logger#warn(java.lang.String)
+ */
+ @Override
+ public void warn(String msg)
+ {
+ log(null, WARN, msg, null, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#warn(java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void warn(String format, Object arg)
+ {
+ log(null, WARN, format, new Object[]{arg}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#warn(java.lang.String, java.lang.Object[])
+ */
+ @Override
+ public void warn(String format, Object[] argArray)
+ {
+ log(null, WARN, format, argArray, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#warn(java.lang.String, java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public void warn(String format, Object arg1, Object arg2)
+ {
+ log(null, WARN, format, new Object[]{arg1, arg2}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#warn(java.lang.String, java.lang.Throwable)
+ */
+ @Override
+ public void warn(String msg, Throwable t)
+ {
+ log(null, WARN, msg, null, t);
+ }
+
+ /**
+ * @see org.slf4j.Logger#isWarnEnabled(org.slf4j.Marker)
+ */
+ @Override
+ public boolean isWarnEnabled(Marker marker)
+ {
+ return _logger.isWarnEnabled(marker);
+ }
+
+ /**
+ * @see org.slf4j.Logger#warn(org.slf4j.Marker, java.lang.String)
+ */
+ @Override
+ public void warn(Marker marker, String msg)
+ {
+ log(marker, WARN, msg, null, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#warn(org.slf4j.Marker, java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void warn(Marker marker, String format, Object arg)
+ {
+ log(marker, WARN, format, new Object[]{arg}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#warn(org.slf4j.Marker, java.lang.String, java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public void warn(Marker marker, String format, Object arg1, Object arg2)
+ {
+ log(marker, WARN, format, new Object[]{arg1, arg2}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#warn(org.slf4j.Marker, java.lang.String, java.lang.Object[])
+ */
+ @Override
+ public void warn(Marker marker, String format, Object[] argArray)
+ {
+ log(marker, WARN, format, argArray, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#warn(org.slf4j.Marker, java.lang.String, java.lang.Throwable)
+ */
+ @Override
+ public void warn(Marker marker, String msg, Throwable t)
+ {
+ log(marker, WARN, msg, null, t);
+ }
+
+ /**
+ * @see org.slf4j.Logger#isErrorEnabled()
+ */
+ @Override
+ public boolean isErrorEnabled()
+ {
+ return _logger.isErrorEnabled();
+ }
+
+ /**
+ * @see org.slf4j.Logger#error(java.lang.String)
+ */
+ @Override
+ public void error(String msg)
+ {
+ log(null, ERROR, msg, null, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#error(java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void error(String format, Object arg)
+ {
+ log(null, ERROR, format, new Object[]{arg}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#error(java.lang.String, java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public void error(String format, Object arg1, Object arg2)
+ {
+ log(null, ERROR, format, new Object[]{arg1, arg2}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#error(java.lang.String, java.lang.Object[])
+ */
+ @Override
+ public void error(String format, Object[] argArray)
+ {
+ log(null, ERROR, format, argArray, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#error(java.lang.String, java.lang.Throwable)
+ */
+ @Override
+ public void error(String msg, Throwable t)
+ {
+ log(null, ERROR, msg, null, t);
+ }
+
+ /**
+ * @see org.slf4j.Logger#isErrorEnabled(org.slf4j.Marker)
+ */
+ @Override
+ public boolean isErrorEnabled(Marker marker)
+ {
+ return _logger.isErrorEnabled(marker);
+ }
+
+ /**
+ * @see org.slf4j.Logger#error(org.slf4j.Marker, java.lang.String)
+ */
+ @Override
+ public void error(Marker marker, String msg)
+ {
+ log(marker, ERROR, msg, null, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#error(org.slf4j.Marker, java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void error(Marker marker, String format, Object arg)
+ {
+ log(marker, ERROR, format, new Object[]{arg}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#error(org.slf4j.Marker, java.lang.String, java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public void error(Marker marker, String format, Object arg1, Object arg2)
+ {
+ log(marker, ERROR, format, new Object[]{arg1, arg2}, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#error(org.slf4j.Marker, java.lang.String, java.lang.Object[])
+ */
+ @Override
+ public void error(Marker marker, String format, Object[] argArray)
+ {
+ log(marker, ERROR, format, argArray, null);
+ }
+
+ /**
+ * @see org.slf4j.Logger#error(org.slf4j.Marker, java.lang.String, java.lang.Throwable)
+ */
+ @Override
+ public void error(Marker marker, String msg, Throwable t)
+ {
+ log(marker, ERROR, msg, null, t);
+ }
+
+ @Override
+ public String toString()
+ {
+ return _logger.toString();
+ }
+
+ private void log(Marker marker, int level, String msg, Object[] argArray, Throwable t)
+ {
+ if (argArray == null)
+ {
+ // Simple SLF4J Message (no args)
+ _logger.log(marker, FQCN, level, msg, null, t);
+ }
+ else
+ {
+ int loggerLevel = _logger.isTraceEnabled()
+ ? TRACE
+ : _logger.isDebugEnabled()
+ ? DEBUG
+ : _logger.isInfoEnabled()
+ ? INFO
+ : _logger.isWarnEnabled()
+ ? WARN
+ : ERROR;
+ if (loggerLevel <= level)
+ {
+ // Don't assume downstream handles argArray properly.
+ // Do it the SLF4J way here to eliminate that as a bug.
+ FormattingTuple ft = MessageFormatter.arrayFormat(msg, argArray);
+ _logger.log(marker, FQCN, level, ft.getMessage(), null, t);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/JettyLogHandler.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/JettyLogHandler.java
new file mode 100644
index 0000000..6cec4d9
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/JettyLogHandler.java
@@ -0,0 +1,198 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.text.MessageFormat;
+import java.util.ResourceBundle;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.LogRecord;
+import java.util.regex.Pattern;
+
+/**
+ * Redirect java.util.logging events to Jetty Log
+ */
+public class JettyLogHandler extends java.util.logging.Handler
+{
+ public static void config()
+ {
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ URL url = cl.getResource("logging.properties");
+ if (url != null)
+ {
+ System.err.printf("Initializing java.util.logging from %s%n", url);
+ try (InputStream in = url.openStream())
+ {
+ LogManager.getLogManager().readConfiguration(in);
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace(System.err);
+ }
+ }
+ else
+ {
+ System.err.printf("WARNING: java.util.logging failed to initialize: logging.properties not found%n");
+ }
+
+ System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.Jdk14Logger");
+ }
+
+ public JettyLogHandler()
+ {
+ if (Boolean.parseBoolean(Log.__props.getProperty("org.eclipse.jetty.util.log.DEBUG", "false")))
+ {
+ setLevel(Level.FINEST);
+ }
+
+ if (Boolean.parseBoolean(Log.__props.getProperty("org.eclipse.jetty.util.log.IGNORED", "false")))
+ {
+ setLevel(Level.ALL);
+ }
+
+ System.err.printf("%s Initialized at level [%s]%n", this.getClass().getName(), getLevel().getName());
+ }
+
+ private synchronized String formatMessage(LogRecord record)
+ {
+ String msg = getMessage(record);
+
+ try
+ {
+ Object[] params = record.getParameters();
+ if ((params == null) || (params.length == 0))
+ {
+ return msg;
+ }
+
+ if (Pattern.compile("\\{\\d+\\}").matcher(msg).find())
+ {
+ return MessageFormat.format(msg, params);
+ }
+
+ return msg;
+ }
+ catch (Exception ex)
+ {
+ return msg;
+ }
+ }
+
+ private String getMessage(LogRecord record)
+ {
+ ResourceBundle bundle = record.getResourceBundle();
+ if (bundle != null)
+ {
+ try
+ {
+ return bundle.getString(record.getMessage());
+ }
+ catch (java.util.MissingResourceException ignored)
+ {
+ }
+ }
+
+ return record.getMessage();
+ }
+
+ @Override
+ public void publish(LogRecord record)
+ {
+ org.eclipse.jetty.util.log.Logger jettyLogger = getJettyLogger(record.getLoggerName());
+
+ int level = record.getLevel().intValue();
+ if (level >= Level.OFF.intValue())
+ {
+ // nothing to log, skip it.
+ return;
+ }
+
+ Throwable cause = record.getThrown();
+ String msg = formatMessage(record);
+
+ if (level >= Level.WARNING.intValue())
+ {
+ // log at warn
+ if (cause != null)
+ {
+ jettyLogger.warn(msg, cause);
+ }
+ else
+ {
+ jettyLogger.warn(msg);
+ }
+ return;
+ }
+
+ if (level >= Level.INFO.intValue())
+ {
+ // log at info
+ if (cause != null)
+ {
+ jettyLogger.info(msg, cause);
+ }
+ else
+ {
+ jettyLogger.info(msg);
+ }
+ return;
+ }
+
+ if (level >= Level.FINEST.intValue())
+ {
+ // log at debug
+ if (cause != null)
+ {
+ jettyLogger.debug(msg, cause);
+ }
+ else
+ {
+ jettyLogger.debug(msg);
+ }
+ return;
+ }
+
+ if (level >= Level.ALL.intValue())
+ {
+ // only corresponds with ignore (in jetty speak)
+ jettyLogger.ignore(cause);
+ return;
+ }
+ }
+
+ private Logger getJettyLogger(String loggerName)
+ {
+ return org.eclipse.jetty.util.log.Log.getLogger(loggerName);
+ }
+
+ @Override
+ public void flush()
+ {
+ // ignore
+ }
+
+ @Override
+ public void close() throws SecurityException
+ {
+ // ignore
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/Log.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/Log.java
new file mode 100644
index 0000000..3862011
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/Log.java
@@ -0,0 +1,319 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.eclipse.jetty.util.Loader;
+import org.eclipse.jetty.util.Uptime;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+
+/**
+ * Logging.
+ * This class provides a static logging interface. If an instance of the
+ * org.slf4j.Logger class is found on the classpath, the static log methods
+ * are directed to a slf4j logger for "org.eclipse.log". Otherwise the logs
+ * are directed to stderr.
+ * <p>
+ * The "org.eclipse.jetty.util.log.class" system property can be used
+ * to select a specific logging implementation.
+ * <p>
+ * If the system property org.eclipse.jetty.util.log.IGNORED is set,
+ * then ignored exceptions are logged in detail.
+ *
+ * @see StdErrLog
+ * @see Slf4jLog
+ */
+public class Log
+{
+ public static final String EXCEPTION = "EXCEPTION ";
+ public static final String IGNORED = "IGNORED EXCEPTION ";
+ /**
+ * The {@link Logger} implementation class name
+ */
+ public static String __logClass;
+ /**
+ * Legacy flag indicating if {@link Logger#ignore(Throwable)} methods produce any output in the {@link Logger}s
+ */
+ public static boolean __ignored;
+ /**
+ * Logging Configuration Properties
+ */
+ protected static final Properties __props = new Properties();
+ private static final ConcurrentMap<String, Logger> __loggers = new ConcurrentHashMap<>();
+ private static boolean __initialized;
+ private static Logger LOG;
+
+ static
+ {
+ AccessController.doPrivileged(new PrivilegedAction<Object>()
+ {
+ @Override
+ public Object run()
+ {
+ // First see if the jetty-logging.properties object exists in the classpath.
+ // * This is an optional feature used by embedded mode use, and test cases to allow for early
+ // * configuration of the Log class in situations where access to the System.properties are
+ // * either too late or just impossible.
+ loadProperties("jetty-logging.properties", __props);
+
+ // Next see if an OS specific jetty-logging.properties object exists in the classpath.
+ // This really for setting up test specific logging behavior based on OS.
+ String osName = System.getProperty("os.name");
+ if (osName != null && osName.length() > 0)
+ {
+ // NOTE: cannot use jetty-util's StringUtil.replace() as it may initialize logging itself.
+ osName = osName.toLowerCase(Locale.ENGLISH).replace(' ', '-');
+ loadProperties("jetty-logging-" + osName + ".properties", __props);
+ }
+
+ // Now load the System.properties as-is into the __props,
+ // these values will override any key conflicts in __props.
+ @SuppressWarnings("unchecked")
+ Enumeration<String> systemKeyEnum = (Enumeration<String>)System.getProperties().propertyNames();
+ while (systemKeyEnum.hasMoreElements())
+ {
+ String key = systemKeyEnum.nextElement();
+ String val = System.getProperty(key);
+ // Protect against application code insertion of non-String values (returned as null).
+ if (val != null)
+ __props.setProperty(key, val);
+ }
+
+ // Now use the configuration properties to configure the Log statics.
+ __logClass = __props.getProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.Slf4jLog");
+ __ignored = Boolean.parseBoolean(__props.getProperty("org.eclipse.jetty.util.log.IGNORED", "false"));
+ return null;
+ }
+ });
+ }
+
+ private static void loadProperties(String resourceName, Properties props)
+ {
+ URL testProps = Loader.getResource(resourceName);
+ if (testProps != null)
+ {
+ try (InputStream in = testProps.openStream())
+ {
+ Properties p = new Properties();
+ p.load(in);
+ for (Object key : p.keySet())
+ {
+ Object value = p.get(key);
+ if (value != null)
+ props.put(key, value);
+ }
+ }
+ catch (IOException e)
+ {
+ System.err.println("[WARN] Error loading logging config: " + testProps);
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static void initialized()
+ {
+ synchronized (Log.class)
+ {
+ if (__initialized)
+ return;
+ __initialized = true;
+
+ boolean announce = Boolean.parseBoolean(__props.getProperty("org.eclipse.jetty.util.log.announce", "true"));
+ try
+ {
+ Class<?> logClass = Loader.loadClass(Log.class, __logClass);
+ if (LOG == null || !LOG.getClass().equals(logClass))
+ {
+ LOG = (Logger)logClass.getDeclaredConstructor().newInstance();
+ if (announce)
+ LOG.debug("Logging to {} via {}", LOG, logClass.getName());
+ }
+ }
+ catch (Throwable e)
+ {
+ // Unable to load specified Logger implementation, default to standard logging.
+ initStandardLogging(e);
+ }
+
+ if (announce && LOG != null)
+ LOG.info(String.format("Logging initialized @%dms to %s", Uptime.getUptime(), LOG.getClass().getName()));
+ }
+ Objects.requireNonNull(LOG, "Root Logger may not be null");
+ }
+
+ private static void initStandardLogging(Throwable e)
+ {
+ if (__ignored)
+ e.printStackTrace();
+
+ if (LOG == null)
+ LOG = new StdErrLog();
+ }
+
+ public static Logger getLog()
+ {
+ initialized();
+ return LOG;
+ }
+
+ /**
+ * Set the root logger.
+ * <p>
+ * Note that if any classes have statically obtained their logger instance prior to this call, their Logger will not
+ * be affected by this call.
+ *
+ * @param log the root logger implementation to set
+ */
+ public static void setLog(Logger log)
+ {
+ Log.LOG = Objects.requireNonNull(log, "Root Logger may not be null");
+ __logClass = null;
+ }
+
+ /**
+ * Get the root logger.
+ *
+ * @return the root logger
+ */
+ public static Logger getRootLogger()
+ {
+ initialized();
+ return LOG;
+ }
+
+ static boolean isIgnored()
+ {
+ return __ignored;
+ }
+
+ /**
+ * Set Log to parent Logger.
+ * <p>
+ * If there is a different Log class available from a parent classloader,
+ * call {@link #getLogger(String)} on it and construct a {@link LoggerLog} instance
+ * as this Log's Logger, so that logging is delegated to the parent Log.
+ * <p>
+ * This should be used if a webapp is using Log, but wishes the logging to be
+ * directed to the containers log.
+ * <p>
+ * If there is not parent Log, then this call is equivalent to<pre>
+ * Log.setLog(Log.getLogger(name));
+ * </pre>
+ *
+ * @param name Logger name
+ */
+ public static void setLogToParent(String name)
+ {
+ ClassLoader loader = Log.class.getClassLoader();
+ if (loader != null && loader.getParent() != null)
+ {
+ try
+ {
+ Class<?> uberlog = loader.getParent().loadClass("org.eclipse.jetty.util.log.Log");
+ Method getLogger = uberlog.getMethod("getLogger", String.class);
+ Object logger = getLogger.invoke(null, name);
+ setLog(new LoggerLog(logger));
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ else
+ {
+ setLog(getLogger(name));
+ }
+ }
+
+ /**
+ * Obtain a named Logger based on the fully qualified class name.
+ *
+ * @param clazz the class to base the Logger name off of
+ * @return the Logger with the given name
+ */
+ public static Logger getLogger(Class<?> clazz)
+ {
+ return getLogger(clazz.getName());
+ }
+
+ /**
+ * Obtain a named Logger or the default Logger if null is passed.
+ *
+ * @param name the Logger name
+ * @return the Logger with the given name
+ */
+ public static Logger getLogger(String name)
+ {
+ initialized();
+
+ Logger logger = null;
+
+ // Return root
+ if (name == null)
+ logger = LOG;
+
+ // use cache
+ if (logger == null)
+ logger = __loggers.get(name);
+
+ // create new logger
+ if (logger == null && LOG != null)
+ logger = LOG.getLogger(name);
+
+ Objects.requireNonNull(logger, "Logger with name [" + name + "]");
+
+ return logger;
+ }
+
+ static ConcurrentMap<String, Logger> getMutableLoggers()
+ {
+ return __loggers;
+ }
+
+ /**
+ * Get a map of all configured {@link Logger} instances.
+ *
+ * @return a map of all configured {@link Logger} instances
+ */
+ @ManagedAttribute("list of all instantiated loggers")
+ public static Map<String, Logger> getLoggers()
+ {
+ return Collections.unmodifiableMap(__loggers);
+ }
+
+ public static Properties getProperties()
+ {
+ return __props;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/Logger.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/Logger.java
new file mode 100644
index 0000000..6b3670b
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/Logger.java
@@ -0,0 +1,134 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+/**
+ * A simple logging facade that is intended simply to capture the style of logging as used by Jetty.
+ */
+public interface Logger
+{
+ /**
+ * @return the name of this logger
+ */
+ String getName();
+
+ /**
+ * Formats and logs at warn level.
+ *
+ * @param msg the formatting string
+ * @param args the optional arguments
+ */
+ void warn(String msg, Object... args);
+
+ /**
+ * Logs the given Throwable information at warn level
+ *
+ * @param thrown the Throwable to log
+ */
+ void warn(Throwable thrown);
+
+ /**
+ * Logs the given message at warn level, with Throwable information.
+ *
+ * @param msg the message to log
+ * @param thrown the Throwable to log
+ */
+ void warn(String msg, Throwable thrown);
+
+ /**
+ * Formats and logs at info level.
+ *
+ * @param msg the formatting string
+ * @param args the optional arguments
+ */
+ void info(String msg, Object... args);
+
+ /**
+ * Logs the given Throwable information at info level
+ *
+ * @param thrown the Throwable to log
+ */
+ void info(Throwable thrown);
+
+ /**
+ * Logs the given message at info level, with Throwable information.
+ *
+ * @param msg the message to log
+ * @param thrown the Throwable to log
+ */
+ void info(String msg, Throwable thrown);
+
+ /**
+ * @return whether the debug level is enabled
+ */
+ boolean isDebugEnabled();
+
+ /**
+ * Mutator used to turn debug on programmatically.
+ *
+ * @param enabled whether to enable the debug level
+ */
+ void setDebugEnabled(boolean enabled);
+
+ /**
+ * Formats and logs at debug level.
+ *
+ * @param msg the formatting string
+ * @param args the optional arguments
+ */
+ void debug(String msg, Object... args);
+
+ /**
+ * Formats and logs at debug level.
+ * avoids autoboxing of integers
+ *
+ * @param msg the formatting string
+ * @param value long value
+ */
+ void debug(String msg, long value);
+
+ /**
+ * Logs the given Throwable information at debug level
+ *
+ * @param thrown the Throwable to log
+ */
+ void debug(Throwable thrown);
+
+ /**
+ * Logs the given message at debug level, with Throwable information.
+ *
+ * @param msg the message to log
+ * @param thrown the Throwable to log
+ */
+ void debug(String msg, Throwable thrown);
+
+ /**
+ * @param name the name of the logger
+ * @return a logger with the given name
+ */
+ Logger getLogger(String name);
+
+ /**
+ * Ignore an exception.
+ * <p>This should be used rather than an empty catch block.
+ *
+ * @param ignored the throwable to log as ignored
+ */
+ void ignore(Throwable ignored);
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/LoggerLog.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/LoggerLog.java
new file mode 100644
index 0000000..640801a
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/LoggerLog.java
@@ -0,0 +1,243 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import java.lang.reflect.Method;
+
+/**
+ *
+ */
+public class LoggerLog extends AbstractLogger
+{
+ private final Object _logger;
+ private final Method _debugMT;
+ private final Method _debugMAA;
+ private final Method _infoMT;
+ private final Method _infoMAA;
+ private final Method _warnMT;
+ private final Method _warnMAA;
+ private final Method _setDebugEnabledE;
+ private final Method _getLoggerN;
+ private final Method _getName;
+ private volatile boolean _debug;
+
+ public LoggerLog(Object logger)
+ {
+ try
+ {
+ _logger = logger;
+ Class<?> lc = logger.getClass();
+ _debugMT = lc.getMethod("debug", String.class, Throwable.class);
+ _debugMAA = lc.getMethod("debug", String.class, Object[].class);
+ _infoMT = lc.getMethod("info", String.class, Throwable.class);
+ _infoMAA = lc.getMethod("info", String.class, Object[].class);
+ _warnMT = lc.getMethod("warn", String.class, Throwable.class);
+ _warnMAA = lc.getMethod("warn", String.class, Object[].class);
+ Method isDebugEnabled = lc.getMethod("isDebugEnabled");
+ _setDebugEnabledE = lc.getMethod("setDebugEnabled", Boolean.TYPE);
+ _getLoggerN = lc.getMethod("getLogger", String.class);
+ _getName = lc.getMethod("getName");
+
+ _debug = (Boolean)isDebugEnabled.invoke(_logger);
+ }
+ catch (Exception x)
+ {
+ throw new IllegalStateException(x);
+ }
+ }
+
+ @Override
+ public String getName()
+ {
+ try
+ {
+ return (String)_getName.invoke(_logger);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ @Override
+ public void warn(String msg, Object... args)
+ {
+ try
+ {
+ _warnMAA.invoke(_logger, args);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void warn(Throwable thrown)
+ {
+ warn("", thrown);
+ }
+
+ @Override
+ public void warn(String msg, Throwable thrown)
+ {
+ try
+ {
+ _warnMT.invoke(_logger, msg, thrown);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void info(String msg, Object... args)
+ {
+ try
+ {
+ _infoMAA.invoke(_logger, args);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void info(Throwable thrown)
+ {
+ info("", thrown);
+ }
+
+ @Override
+ public void info(String msg, Throwable thrown)
+ {
+ try
+ {
+ _infoMT.invoke(_logger, msg, thrown);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public boolean isDebugEnabled()
+ {
+ return _debug;
+ }
+
+ @Override
+ public void setDebugEnabled(boolean enabled)
+ {
+ try
+ {
+ _setDebugEnabledE.invoke(_logger, enabled);
+ _debug = enabled;
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void debug(String msg, Object... args)
+ {
+ if (!_debug)
+ return;
+
+ try
+ {
+ _debugMAA.invoke(_logger, args);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void debug(Throwable thrown)
+ {
+ debug("", thrown);
+ }
+
+ @Override
+ public void debug(String msg, Throwable th)
+ {
+ if (!_debug)
+ return;
+
+ try
+ {
+ _debugMT.invoke(_logger, msg, th);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void debug(String msg, long value)
+ {
+ if (!_debug)
+ return;
+
+ try
+ {
+ _debugMAA.invoke(_logger, new Long(value));
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void ignore(Throwable ignored)
+ {
+ if (Log.isIgnored())
+ {
+ debug(Log.IGNORED, ignored);
+ }
+ }
+
+ /**
+ * Create a Child Logger of this Logger.
+ */
+ @Override
+ protected Logger newLogger(String fullname)
+ {
+ try
+ {
+ Object logger = _getLoggerN.invoke(_logger, fullname);
+ return new LoggerLog(logger);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ return this;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/Slf4jLog.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/Slf4jLog.java
new file mode 100644
index 0000000..d2dfd2e
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/Slf4jLog.java
@@ -0,0 +1,152 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+/**
+ * Slf4jLog Logger
+ */
+public class Slf4jLog extends AbstractLogger
+{
+ private final org.slf4j.Logger _logger;
+
+ public Slf4jLog() throws Exception
+ {
+ this("org.eclipse.jetty.util.log");
+ }
+
+ public Slf4jLog(String name)
+ {
+ //NOTE: if only an slf4j-api jar is on the classpath, slf4j will use a NOPLogger
+ org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(name);
+
+ // Fix LocationAwareLogger use to indicate FQCN of this class -
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=276670
+ if (logger instanceof org.slf4j.spi.LocationAwareLogger)
+ {
+ _logger = new JettyAwareLogger((org.slf4j.spi.LocationAwareLogger)logger);
+ }
+ else
+ {
+ _logger = logger;
+ }
+ }
+
+ @Override
+ public String getName()
+ {
+ return _logger.getName();
+ }
+
+ @Override
+ public void warn(String msg, Object... args)
+ {
+ _logger.warn(msg, args);
+ }
+
+ @Override
+ public void warn(Throwable thrown)
+ {
+ warn("", thrown);
+ }
+
+ @Override
+ public void warn(String msg, Throwable thrown)
+ {
+ _logger.warn(msg, thrown);
+ }
+
+ @Override
+ public void info(String msg, Object... args)
+ {
+ _logger.info(msg, args);
+ }
+
+ @Override
+ public void info(Throwable thrown)
+ {
+ info("", thrown);
+ }
+
+ @Override
+ public void info(String msg, Throwable thrown)
+ {
+ _logger.info(msg, thrown);
+ }
+
+ @Override
+ public void debug(String msg, Object... args)
+ {
+ _logger.debug(msg, args);
+ }
+
+ @Override
+ public void debug(String msg, long arg)
+ {
+ if (isDebugEnabled())
+ _logger.debug(msg, new Object[]{new Long(arg)});
+ }
+
+ @Override
+ public void debug(Throwable thrown)
+ {
+ debug("", thrown);
+ }
+
+ @Override
+ public void debug(String msg, Throwable thrown)
+ {
+ _logger.debug(msg, thrown);
+ }
+
+ @Override
+ public boolean isDebugEnabled()
+ {
+ return _logger.isDebugEnabled();
+ }
+
+ @Override
+ public void setDebugEnabled(boolean enabled)
+ {
+ warn("setDebugEnabled not implemented", null, null);
+ }
+
+ /**
+ * Create a Child Logger of this Logger.
+ */
+ @Override
+ protected Logger newLogger(String fullname)
+ {
+ return new Slf4jLog(fullname);
+ }
+
+ @Override
+ public void ignore(Throwable ignored)
+ {
+ if (Log.isIgnored())
+ {
+ debug(Log.IGNORED, ignored);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return _logger.toString();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/StacklessLogging.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/StacklessLogging.java
new file mode 100644
index 0000000..1fc2358
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/StacklessLogging.java
@@ -0,0 +1,86 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A try-with-resources compatible layer for {@link StdErrLog#setHideStacks(boolean) hiding stacktraces} within the scope of the <code>try</code> block when
+ * logging with {@link StdErrLog} implementation.
+ * <p>
+ * Use of other logging implementation cause no effect when using this class
+ * <p>
+ * Example:
+ *
+ * <pre>
+ * try (StacklessLogging scope = new StacklessLogging(EventDriver.class,Noisy.class))
+ * {
+ * doActionThatCausesStackTraces();
+ * }
+ * </pre>
+ */
+public class StacklessLogging implements AutoCloseable
+{
+ private final Set<StdErrLog> squelched = new HashSet<>();
+
+ public StacklessLogging(Class<?>... classesToSquelch)
+ {
+ for (Class<?> clazz : classesToSquelch)
+ {
+ Logger log = Log.getLogger(clazz);
+ // only operate on loggers that are of type StdErrLog
+ if (log instanceof StdErrLog && !log.isDebugEnabled())
+ {
+ StdErrLog stdErrLog = ((StdErrLog)log);
+ if (!stdErrLog.isHideStacks())
+ {
+ stdErrLog.setHideStacks(true);
+ squelched.add(stdErrLog);
+ }
+ }
+ }
+ }
+
+ public StacklessLogging(Logger... logs)
+ {
+ for (Logger log : logs)
+ {
+ // only operate on loggers that are of type StdErrLog
+ if (log instanceof StdErrLog && !log.isDebugEnabled())
+ {
+ StdErrLog stdErrLog = ((StdErrLog)log);
+ if (!stdErrLog.isHideStacks())
+ {
+ stdErrLog.setHideStacks(true);
+ squelched.add(stdErrLog);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ for (StdErrLog log : squelched)
+ {
+ log.setHideStacks(false);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/StdErrLog.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/StdErrLog.java
new file mode 100644
index 0000000..56bc36e
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/StdErrLog.java
@@ -0,0 +1,704 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import java.io.PrintStream;
+import java.security.AccessControlException;
+import java.util.Properties;
+
+import org.eclipse.jetty.util.DateCache;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+
+/**
+ * StdErr Logging implementation.
+ * <p>
+ * A Jetty {@link Logger} that sends all logs to STDERR ({@link System#err}) with basic formatting.
+ * <p>
+ * Supports named loggers, and properties based configuration.
+ * <p>
+ * Configuration Properties:
+ * <dl>
+ * <dt>${name|hierarchy}.LEVEL=(ALL|DEBUG|INFO|WARN|OFF)</dt>
+ * <dd>
+ * Sets the level that the Logger should log at.<br>
+ * Names can be a package name, or a fully qualified class name.<br>
+ * Default: INFO<br>
+ * <br>
+ * Examples:
+ * <dl>
+ * <dt>org.eclipse.jetty.LEVEL=WARN</dt>
+ * <dd>indicates that all of the jetty specific classes, in any package that
+ * starts with <code>org.eclipse.jetty</code> should log at level WARN.</dd>
+ * <dt>org.eclipse.jetty.io.ChannelEndPoint.LEVEL=ALL</dt>
+ * <dd>indicates that the specific class, ChannelEndPoint, should log all
+ * logging events that it can generate, including DEBUG, INFO, WARN (and even special
+ * internally ignored exception cases).</dd>
+ * </dl>
+ * </dd>
+ *
+ * <dt>${name}.SOURCE=(true|false)</dt>
+ * <dd>
+ * Logger specific, attempt to print the java source file name and line number
+ * where the logging event originated from.<br>
+ * Name must be a fully qualified class name (package name hierarchy is not supported
+ * by this configurable)<br>
+ * Warning: this is a slow operation and will have an impact on performance!<br>
+ * Default: false
+ * </dd>
+ *
+ * <dt>${name}.STACKS=(true|false)</dt>
+ * <dd>
+ * Logger specific, control the display of stacktraces.<br>
+ * Name must be a fully qualified class name (package name hierarchy is not supported
+ * by this configurable)<br>
+ * Default: true
+ * </dd>
+ *
+ * <dt>org.eclipse.jetty.util.log.stderr.SOURCE=(true|false)</dt>
+ * <dd>Special Global Configuration, attempt to print the java source file name and line number
+ * where the logging event originated from.<br>
+ * Default: false
+ * </dd>
+ *
+ * <dt>org.eclipse.jetty.util.log.stderr.LONG=(true|false)</dt>
+ * <dd>Special Global Configuration, when true, output logging events to STDERR using
+ * long form, fully qualified class names. when false, use abbreviated package names<br>
+ * Default: false
+ * </dd>
+ * <dt>org.eclipse.jetty.util.log.stderr.ESCAPE=(true|false)</dt>
+ * <dd>Global Configuration, when true output logging events to STDERR are always
+ * escaped so that control characters are replaced with '?"; '\r' with '<' and '\n' replaced '|'<br>
+ * Default: true
+ * </dd>
+ * </dl>
+ */
+@ManagedObject("Jetty StdErr Logging Implementation")
+public class StdErrLog extends AbstractLogger
+{
+ private static final String EOL = System.lineSeparator();
+ private static final Object[] EMPTY_ARGS = new Object[0];
+ // Do not change output format lightly, people rely on this output format now.
+ private static int __tagpad = Integer.parseInt(Log.__props.getProperty("org.eclipse.jetty.util.log.StdErrLog.TAG_PAD", "0"));
+ private static DateCache _dateCache;
+
+ private static final boolean __source = Boolean.parseBoolean(Log.__props.getProperty("org.eclipse.jetty.util.log.SOURCE",
+ Log.__props.getProperty("org.eclipse.jetty.util.log.stderr.SOURCE", "false")));
+ private static final boolean __long = Boolean.parseBoolean(Log.__props.getProperty("org.eclipse.jetty.util.log.stderr.LONG", "false"));
+ private static final boolean __escape = Boolean.parseBoolean(Log.__props.getProperty("org.eclipse.jetty.util.log.stderr.ESCAPE", "true"));
+
+ static
+ {
+ String[] deprecatedProperties =
+ {"DEBUG", "org.eclipse.jetty.util.log.DEBUG", "org.eclipse.jetty.util.log.stderr.DEBUG"};
+
+ // Toss a message to users about deprecated system properties
+ for (String deprecatedProp : deprecatedProperties)
+ {
+ if (System.getProperty(deprecatedProp) != null)
+ {
+ System.err.printf("System Property [%s] has been deprecated! (Use org.eclipse.jetty.LEVEL=DEBUG instead)%n", deprecatedProp);
+ }
+ }
+
+ try
+ {
+ _dateCache = new DateCache("yyyy-MM-dd HH:mm:ss");
+ }
+ catch (Exception x)
+ {
+ x.printStackTrace(System.err);
+ }
+ }
+
+ public static void setTagPad(int pad)
+ {
+ __tagpad = pad;
+ }
+
+ private int _level;
+ // Level that this Logger was configured as (remembered in special case of .setDebugEnabled())
+ private int _configuredLevel;
+ // The alternate stream to print to (if set)
+ private PrintStream _altStream;
+ private boolean _source;
+ // Print the long form names, otherwise use abbreviated
+ private boolean _printLongNames = __long;
+ // The full log name, as provided by the system.
+ private final String _name;
+ // The abbreviated log name (used by default, unless _long is specified)
+ protected final String _abbrevname;
+ private boolean _hideStacks = false;
+
+ public static int getLoggingLevel(Properties props, String name)
+ {
+ int level = lookupLoggingLevel(props, name);
+ if (level == LEVEL_DEFAULT)
+ {
+ level = lookupLoggingLevel(props, "log");
+ if (level == LEVEL_DEFAULT)
+ level = LEVEL_INFO;
+ }
+ return level;
+ }
+
+ /**
+ * Obtain a StdErrLog reference for the specified class, a convenience method used most often during testing to allow for control over a specific logger.
+ * <p>
+ * Must be actively using StdErrLog as the Logger implementation.
+ *
+ * @param clazz the Class reference for the logger to use.
+ * @return the StdErrLog logger
+ * @throws RuntimeException if StdErrLog is not the active Logger implementation.
+ */
+ public static StdErrLog getLogger(Class<?> clazz)
+ {
+ Logger log = Log.getLogger(clazz);
+ if (log instanceof StdErrLog)
+ {
+ return (StdErrLog)log;
+ }
+ throw new RuntimeException("Logger for " + clazz + " is not of type StdErrLog");
+ }
+
+ /**
+ * Construct an anonymous StdErrLog (no name).
+ * <p>
+ * NOTE: Discouraged usage!
+ */
+ public StdErrLog()
+ {
+ this(null);
+ }
+
+ /**
+ * Construct a named StdErrLog using the {@link Log} defined properties
+ *
+ * @param name the name of the logger
+ */
+ public StdErrLog(String name)
+ {
+ this(name, null);
+ }
+
+ /**
+ * Construct a named Logger using the provided properties to configure logger.
+ *
+ * @param name the name of the logger
+ * @param props the configuration properties
+ */
+ public StdErrLog(String name, Properties props)
+ {
+ @SuppressWarnings("ReferenceEquality")
+ boolean sameObject = (props != Log.__props);
+ if (props != null && sameObject)
+ Log.__props.putAll(props);
+ _name = name == null ? "" : name;
+ _abbrevname = condensePackageString(this._name);
+ _level = getLoggingLevel(Log.__props, this._name);
+ _configuredLevel = _level;
+
+ try
+ {
+ String source = getLoggingProperty(Log.__props, _name, "SOURCE");
+ _source = source == null ? __source : Boolean.parseBoolean(source);
+ }
+ catch (AccessControlException ace)
+ {
+ _source = __source;
+ }
+
+ try
+ {
+ // allow stacktrace display to be controlled by properties as well
+ String stacks = getLoggingProperty(Log.__props, _name, "STACKS");
+ _hideStacks = stacks != null && !Boolean.parseBoolean(stacks);
+ }
+ catch (AccessControlException ignore)
+ {
+ /* ignore */
+ }
+ }
+
+ @Override
+ public String getName()
+ {
+ return _name;
+ }
+
+ public void setPrintLongNames(boolean printLongNames)
+ {
+ this._printLongNames = printLongNames;
+ }
+
+ public boolean isPrintLongNames()
+ {
+ return this._printLongNames;
+ }
+
+ public boolean isHideStacks()
+ {
+ return _hideStacks;
+ }
+
+ public void setHideStacks(boolean hideStacks)
+ {
+ _hideStacks = hideStacks;
+ }
+
+ /**
+ * Is the source of a log, logged
+ *
+ * @return true if the class, method, file and line number of a log is logged.
+ */
+ public boolean isSource()
+ {
+ return _source;
+ }
+
+ /**
+ * Set if a log source is logged.
+ *
+ * @param source true if the class, method, file and line number of a log is logged.
+ */
+ public void setSource(boolean source)
+ {
+ _source = source;
+ }
+
+ @Override
+ public void warn(String msg, Object... args)
+ {
+ if (_level <= LEVEL_WARN)
+ {
+ StringBuilder builder = new StringBuilder(64);
+ format(builder, ":WARN:", msg, args);
+ println(builder);
+ }
+ }
+
+ @Override
+ public void warn(Throwable thrown)
+ {
+ warn("", thrown);
+ }
+
+ @Override
+ public void warn(String msg, Throwable thrown)
+ {
+ if (_level <= LEVEL_WARN)
+ {
+ StringBuilder builder = new StringBuilder(64);
+ format(builder, ":WARN:", msg, thrown);
+ println(builder);
+ }
+ }
+
+ @Override
+ public void info(String msg, Object... args)
+ {
+ if (_level <= LEVEL_INFO)
+ {
+ StringBuilder builder = new StringBuilder(64);
+ format(builder, ":INFO:", msg, args);
+ println(builder);
+ }
+ }
+
+ @Override
+ public void info(Throwable thrown)
+ {
+ info("", thrown);
+ }
+
+ @Override
+ public void info(String msg, Throwable thrown)
+ {
+ if (_level <= LEVEL_INFO)
+ {
+ StringBuilder builder = new StringBuilder(64);
+ format(builder, ":INFO:", msg, thrown);
+ println(builder);
+ }
+ }
+
+ @ManagedAttribute("is debug enabled for root logger Log.LOG")
+ @Override
+ public boolean isDebugEnabled()
+ {
+ return (_level <= LEVEL_DEBUG);
+ }
+
+ /**
+ * Legacy interface where a programmatic configuration of the logger level
+ * is done as a wholesale approach.
+ */
+ @Override
+ public void setDebugEnabled(boolean enabled)
+ {
+ int level = enabled ? LEVEL_DEBUG : this.getConfiguredLevel();
+ this.setLevel(level);
+
+ String name = getName();
+ for (Logger log : Log.getLoggers().values())
+ {
+ if (log.getName().startsWith(name) && log instanceof StdErrLog)
+ {
+ StdErrLog logger = (StdErrLog)log;
+ level = enabled ? LEVEL_DEBUG : logger.getConfiguredLevel();
+ logger.setLevel(level);
+ }
+ }
+ }
+
+ private int getConfiguredLevel()
+ {
+ return _configuredLevel;
+ }
+
+ public int getLevel()
+ {
+ return _level;
+ }
+
+ /**
+ * Set the level for this logger.
+ * <p>
+ * Available values ({@link StdErrLog#LEVEL_ALL}, {@link StdErrLog#LEVEL_DEBUG}, {@link StdErrLog#LEVEL_INFO},
+ * {@link StdErrLog#LEVEL_WARN})
+ *
+ * @param level the level to set the logger to
+ */
+ public void setLevel(int level)
+ {
+ this._level = level;
+ }
+
+ /**
+ * The alternate stream to use for STDERR.
+ *
+ * @param stream the stream of choice, or {@code null} to use {@link System#err}
+ */
+ public void setStdErrStream(PrintStream stream)
+ {
+ this._altStream = stream;
+ }
+
+ @Override
+ public void debug(String msg, Object... args)
+ {
+ if (isDebugEnabled())
+ {
+ StringBuilder builder = new StringBuilder(64);
+ format(builder, ":DBUG:", msg, args);
+ println(builder);
+ }
+ }
+
+ @Override
+ public void debug(String msg, long arg)
+ {
+ if (isDebugEnabled())
+ {
+ StringBuilder builder = new StringBuilder(64);
+ format(builder, ":DBUG:", msg, arg);
+ println(builder);
+ }
+ }
+
+ @Override
+ public void debug(Throwable thrown)
+ {
+ debug("", thrown);
+ }
+
+ @Override
+ public void debug(String msg, Throwable thrown)
+ {
+ if (isDebugEnabled())
+ {
+ StringBuilder builder = new StringBuilder(64);
+ format(builder, ":DBUG:", msg, thrown);
+ println(builder);
+ }
+ }
+
+ private void println(StringBuilder builder)
+ {
+ if (_altStream != null)
+ _altStream.println(builder);
+ else
+ {
+ // We always use the PrintStream stored in System.err,
+ // just in case someone has replaced it with a call to System.setErr(PrintStream)
+ System.err.println(builder);
+ }
+ }
+
+ private void format(StringBuilder builder, String level, String msg, Object... inArgs)
+ {
+ long now = System.currentTimeMillis();
+ int ms = (int)(now % 1000);
+ String d = _dateCache.formatNow(now);
+ tag(builder, d, ms, level);
+
+ Object[] msgArgs = EMPTY_ARGS;
+ int msgArgsLen = 0;
+ Throwable cause = null;
+
+ if (inArgs != null)
+ {
+ msgArgs = inArgs;
+ msgArgsLen = inArgs.length;
+ if (msgArgsLen > 0 && inArgs[msgArgsLen - 1] instanceof Throwable)
+ {
+ cause = (Throwable)inArgs[msgArgsLen - 1];
+ msgArgsLen--;
+ }
+ }
+
+ if (msg == null)
+ {
+ msg = "";
+ for (int i = 0; i < msgArgsLen; i++)
+ {
+ //noinspection StringConcatenationInLoop
+ msg += "{} ";
+ }
+ }
+ String braces = "{}";
+ int start = 0;
+ for (int i = 0; i < msgArgsLen; i++)
+ {
+ Object arg = msgArgs[i];
+ int bracesIndex = msg.indexOf(braces, start);
+ if (bracesIndex < 0)
+ {
+ escape(builder, msg.substring(start));
+ builder.append(" ");
+ if (arg != null)
+ builder.append(arg);
+ start = msg.length();
+ }
+ else
+ {
+ escape(builder, msg.substring(start, bracesIndex));
+ if (arg != null)
+ builder.append(arg);
+ start = bracesIndex + braces.length();
+ }
+ }
+ escape(builder, msg.substring(start));
+
+ if (cause != null)
+ {
+ if (isHideStacks())
+ {
+ builder.append(": ").append(cause);
+ }
+ else
+ {
+ formatCause(builder, cause, "");
+ }
+ }
+ }
+
+ private void formatCause(StringBuilder builder, Throwable cause, String indent)
+ {
+ builder.append(EOL).append(indent);
+ escape(builder, cause.toString());
+ StackTraceElement[] elements = cause.getStackTrace();
+ for (int i = 0; elements != null && i < elements.length; i++)
+ {
+ builder.append(EOL).append(indent).append("\tat ");
+ escape(builder, elements[i].toString());
+ }
+
+ for (Throwable suppressed : cause.getSuppressed())
+ {
+ builder.append(EOL).append(indent).append("Suppressed: ");
+ formatCause(builder, suppressed, "\t|" + indent);
+ }
+
+ Throwable by = cause.getCause();
+ if (by != null && by != cause)
+ {
+ builder.append(EOL).append(indent).append("Caused by: ");
+ formatCause(builder, by, indent);
+ }
+ }
+
+ private void escape(StringBuilder builder, String str)
+ {
+ if (__escape)
+ {
+ for (int i = 0; i < str.length(); ++i)
+ {
+ char c = str.charAt(i);
+ if (Character.isISOControl(c))
+ {
+ if (c == '\n')
+ {
+ builder.append('|');
+ }
+ else if (c == '\r')
+ {
+ builder.append('<');
+ }
+ else
+ {
+ builder.append('?');
+ }
+ }
+ else
+ {
+ builder.append(c);
+ }
+ }
+ }
+ else
+ builder.append(str);
+ }
+
+ private void tag(StringBuilder builder, String d, int ms, String tag)
+ {
+ builder.setLength(0);
+ builder.append(d);
+ if (ms > 99)
+ {
+ builder.append('.');
+ }
+ else if (ms > 9)
+ {
+ builder.append(".0");
+ }
+ else
+ {
+ builder.append(".00");
+ }
+ builder.append(ms).append(tag);
+
+ String name = _printLongNames ? _name : _abbrevname;
+ String tname = Thread.currentThread().getName();
+
+ int p = __tagpad > 0 ? (name.length() + tname.length() - __tagpad) : 0;
+
+ if (p < 0)
+ {
+ builder
+ .append(name)
+ .append(':')
+ .append(" ", 0, -p)
+ .append(tname);
+ }
+ else if (p == 0)
+ {
+ builder.append(name).append(':').append(tname);
+ }
+ builder.append(':');
+
+ if (_source)
+ {
+ Throwable source = new Throwable();
+ StackTraceElement[] frames = source.getStackTrace();
+ for (final StackTraceElement frame : frames)
+ {
+ String clazz = frame.getClassName();
+ if (clazz.equals(StdErrLog.class.getName()) || clazz.equals(Log.class.getName()))
+ {
+ continue;
+ }
+ if (!_printLongNames && clazz.startsWith("org.eclipse.jetty."))
+ {
+ builder.append(condensePackageString(clazz));
+ }
+ else
+ {
+ builder.append(clazz);
+ }
+ builder.append('#').append(frame.getMethodName());
+ if (frame.getFileName() != null)
+ {
+ builder.append('(').append(frame.getFileName()).append(':').append(frame.getLineNumber()).append(')');
+ }
+ builder.append(':');
+ break;
+ }
+ }
+
+ builder.append(' ');
+ }
+
+ /**
+ * Create a Child Logger of this Logger.
+ */
+ @Override
+ protected Logger newLogger(String fullname)
+ {
+ StdErrLog logger = new StdErrLog(fullname);
+ // Preserve configuration for new loggers configuration
+ logger.setPrintLongNames(_printLongNames);
+ logger._altStream = this._altStream;
+
+ // Force the child to have any programmatic configuration
+ if (_level != _configuredLevel)
+ logger._level = _level;
+
+ return logger;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder s = new StringBuilder();
+ s.append("StdErrLog:");
+ s.append(_name);
+ s.append(":LEVEL=");
+ switch (_level)
+ {
+ case LEVEL_ALL:
+ s.append("ALL");
+ break;
+ case LEVEL_DEBUG:
+ s.append("DEBUG");
+ break;
+ case LEVEL_INFO:
+ s.append("INFO");
+ break;
+ case LEVEL_WARN:
+ s.append("WARN");
+ break;
+ default:
+ s.append("?");
+ break;
+ }
+ return s.toString();
+ }
+
+ @Override
+ public void ignore(Throwable ignored)
+ {
+ if (_level <= LEVEL_ALL)
+ {
+ StringBuilder builder = new StringBuilder(64);
+ format(builder, ":IGNORED:", "", ignored);
+ println(builder);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/package-info.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/package-info.java
new file mode 100644
index 0000000..86b1fca
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/log/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Util : Common Logging Integrations
+ */
+package org.eclipse.jetty.util.log;
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/package-info.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/package-info.java
new file mode 100644
index 0000000..cd189a4
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Util : Common Utility Classes
+ */
+package org.eclipse.jetty.util;
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/AWTLeakPreventer.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/AWTLeakPreventer.java
new file mode 100644
index 0000000..2717ee9
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/AWTLeakPreventer.java
@@ -0,0 +1,45 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.preventers;
+
+import java.awt.Toolkit;
+
+/**
+ * AWTLeakPreventer
+ *
+ * See https://issues.jboss.org/browse/AS7-3733
+ *
+ * The java.awt.Toolkit class has a static field that is the default toolkit.
+ * Creating the default toolkit causes the creation of an EventQueue, which has a
+ * classloader field initialized by the thread context class loader.
+ */
+public class AWTLeakPreventer extends AbstractLeakPreventer
+{
+
+ /**
+ * @see org.eclipse.jetty.util.preventers.AbstractLeakPreventer#prevent(java.lang.ClassLoader)
+ */
+ @Override
+ public void prevent(ClassLoader loader)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Pinning classloader for java.awt.EventQueue using " + loader);
+ Toolkit.getDefaultToolkit();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/AbstractLeakPreventer.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/AbstractLeakPreventer.java
new file mode 100644
index 0000000..3c64418
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/AbstractLeakPreventer.java
@@ -0,0 +1,57 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.preventers;
+
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * AbstractLeakPreventer
+ *
+ * Abstract base class for code that seeks to avoid pinning of webapp classloaders by using the jetty classloader to
+ * proactively call the code that pins them (generally pinned as static data members, or as static
+ * data members that are daemon threads (which use the context classloader)).
+ *
+ * Instances of subclasses of this class should be set with Server.addBean(), which will
+ * ensure that they are called when the Server instance starts up, which will have the jetty
+ * classloader in scope.
+ */
+public abstract class AbstractLeakPreventer extends AbstractLifeCycle
+{
+ protected static final Logger LOG = Log.getLogger(AbstractLeakPreventer.class);
+
+ public abstract void prevent(ClassLoader loader);
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ try
+ {
+ Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
+ prevent(getClass().getClassLoader());
+ super.doStart();
+ }
+ finally
+ {
+ Thread.currentThread().setContextClassLoader(loader);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/AppContextLeakPreventer.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/AppContextLeakPreventer.java
new file mode 100644
index 0000000..9f5f246
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/AppContextLeakPreventer.java
@@ -0,0 +1,41 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.preventers;
+
+import javax.imageio.ImageIO;
+
+/**
+ * AppContextLeakPreventer
+ *
+ * Cause the classloader that is pinned by AppContext.getAppContext() to be
+ * a container or system classloader, not a webapp classloader.
+ *
+ * Inspired by Tomcat JreMemoryLeakPrevention.
+ */
+public class AppContextLeakPreventer extends AbstractLeakPreventer
+{
+
+ @Override
+ public void prevent(ClassLoader loader)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Pinning classloader for AppContext.getContext() with " + loader);
+ ImageIO.getUseCache();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/DOMLeakPreventer.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/DOMLeakPreventer.java
new file mode 100644
index 0000000..1a3b0a0
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/DOMLeakPreventer.java
@@ -0,0 +1,56 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.preventers;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+
+/**
+ * DOMLeakPreventer
+ *
+ * See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6916498
+ *
+ * Prevent the RuntimeException that is a static member of AbstractDOMParser
+ * from pinning a webapp classloader by causing it to be set here by a non-webapp classloader.
+ *
+ * Note that according to the bug report, a heap dump may not identify the GCRoot, making
+ * it difficult to identify the cause of the leak.
+ *
+ * @deprecated reported as fixed in jdk 7, see https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6916498
+ */
+@Deprecated
+public class DOMLeakPreventer extends AbstractLeakPreventer
+{
+
+ /**
+ * @see org.eclipse.jetty.util.preventers.AbstractLeakPreventer#prevent(java.lang.ClassLoader)
+ */
+ @Override
+ public void prevent(ClassLoader loader)
+ {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ try
+ {
+ factory.newDocumentBuilder();
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/DriverManagerLeakPreventer.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/DriverManagerLeakPreventer.java
new file mode 100644
index 0000000..524b106
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/DriverManagerLeakPreventer.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.preventers;
+
+import java.sql.DriverManager;
+
+/**
+ * DriverManagerLeakPreventer
+ *
+ * Cause DriverManager.getCallerClassLoader() to be called, which will pin the classloader.
+ *
+ * Inspired by Tomcat JreMemoryLeakPrevention.
+ */
+public class DriverManagerLeakPreventer extends AbstractLeakPreventer
+{
+
+ @Override
+ public void prevent(ClassLoader loader)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Pinning DriverManager classloader with " + loader);
+ DriverManager.getDrivers();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/GCThreadLeakPreventer.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/GCThreadLeakPreventer.java
new file mode 100644
index 0000000..8a3828c
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/GCThreadLeakPreventer.java
@@ -0,0 +1,65 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.preventers;
+
+import java.lang.reflect.Method;
+
+/**
+ * GCThreadLeakPreventer
+ *
+ * Prevents a call to sun.misc.GC.requestLatency pinning a webapp classloader
+ * by calling it with a non-webapp classloader. The problem appears to be that
+ * when this method is called, a daemon thread is created which takes the
+ * context classloader. A known caller of this method is the RMI impl. See
+ * http://stackoverflow.com/questions/6626680/does-java-garbage-collection-log-entry-full-gc-system-mean-some-class-called
+ *
+ * This preventer will start the thread with the longest possible interval, although
+ * subsequent calls can vary that. Recommend to only use this class if you're doing
+ * RMI.
+ *
+ * Inspired by Tomcat JreMemoryLeakPrevention.
+ *
+ * @deprecated fixed in jdvm 9b130, see https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8157570
+ */
+@Deprecated
+public class GCThreadLeakPreventer extends AbstractLeakPreventer
+{
+
+ /**
+ * @see org.eclipse.jetty.util.preventers.AbstractLeakPreventer#prevent(java.lang.ClassLoader)
+ */
+ @Override
+ public void prevent(ClassLoader loader)
+ {
+ try
+ {
+ Class<?> clazz = Class.forName("sun.misc.GC");
+ Method requestLatency = clazz.getMethod("requestLatency", long.class);
+ requestLatency.invoke(null, Long.valueOf(Long.MAX_VALUE - 1));
+ }
+ catch (ClassNotFoundException e)
+ {
+ LOG.ignore(e);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/Java2DLeakPreventer.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/Java2DLeakPreventer.java
new file mode 100644
index 0000000..0eb738d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/Java2DLeakPreventer.java
@@ -0,0 +1,51 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.preventers;
+
+/**
+ * Java2DLeakPreventer
+ *
+ * Prevent pinning of webapp classloader by pre-loading sun.java2d.Disposer class
+ * before webapp classloaders are created.
+ *
+ * See https://issues.apache.org/bugzilla/show_bug.cgi?id=51687
+ *
+ * @deprecated fixed in jdk 9, see https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6489540
+ *
+ */
+@Deprecated
+public class Java2DLeakPreventer extends AbstractLeakPreventer
+{
+
+ /**
+ * @see org.eclipse.jetty.util.preventers.AbstractLeakPreventer#prevent(java.lang.ClassLoader)
+ */
+ @Override
+ public void prevent(ClassLoader loader)
+ {
+ try
+ {
+ Class.forName("sun.java2d.Disposer", true, loader);
+ }
+ catch (ClassNotFoundException e)
+ {
+ LOG.ignore(e);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/LDAPLeakPreventer.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/LDAPLeakPreventer.java
new file mode 100644
index 0000000..bb9b35c
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/LDAPLeakPreventer.java
@@ -0,0 +1,52 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.preventers;
+
+/**
+ * LDAPLeakPreventer
+ *
+ * If com.sun.jndi.LdapPoolManager class is loaded and the system property
+ * com.sun.jndi.ldap.connect.pool.timeout is set to a nonzero value, a daemon
+ * thread is started which can pin a webapp classloader if it is the first to
+ * load the LdapPoolManager.
+ *
+ * Inspired by Tomcat JreMemoryLeakPrevention
+ *
+ * @deprecated fixed in jdk 8u192
+ */
+@Deprecated
+public class LDAPLeakPreventer extends AbstractLeakPreventer
+{
+
+ /**
+ * @see org.eclipse.jetty.util.preventers.AbstractLeakPreventer#prevent(java.lang.ClassLoader)
+ */
+ @Override
+ public void prevent(ClassLoader loader)
+ {
+ try
+ {
+ Class.forName("com.sun.jndi.LdapPoolManager", true, loader);
+ }
+ catch (ClassNotFoundException e)
+ {
+ LOG.ignore(e);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/LoginConfigurationLeakPreventer.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/LoginConfigurationLeakPreventer.java
new file mode 100644
index 0000000..052f6f9
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/LoginConfigurationLeakPreventer.java
@@ -0,0 +1,50 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.preventers;
+
+/**
+ * LoginConfigurationLeakPreventer
+ *
+ * The javax.security.auth.login.Configuration class keeps a static reference to the
+ * thread context classloader. We prevent a webapp context classloader being used for
+ * that by invoking the classloading here.
+ *
+ * Inspired by Tomcat JreMemoryLeakPrevention
+ * @deprecated classloader does not seem to be held any more
+ */
+@Deprecated
+public class LoginConfigurationLeakPreventer extends AbstractLeakPreventer
+{
+
+ /**
+ * @see org.eclipse.jetty.util.preventers.AbstractLeakPreventer#prevent(java.lang.ClassLoader)
+ */
+ @Override
+ public void prevent(ClassLoader loader)
+ {
+ try
+ {
+ Class.forName("javax.security.auth.login.Configuration", true, loader);
+ }
+ catch (ClassNotFoundException e)
+ {
+ LOG.warn(e);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/SecurityProviderLeakPreventer.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/SecurityProviderLeakPreventer.java
new file mode 100644
index 0000000..827b808
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/SecurityProviderLeakPreventer.java
@@ -0,0 +1,46 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.preventers;
+
+import java.security.Security;
+
+/**
+ * SecurityProviderLeakPreventer
+ *
+ * Some security providers, such as sun.security.pkcs11.SunPKCS11 start a deamon thread,
+ * which will use the thread context classloader. Load them here to ensure the classloader
+ * is not a webapp classloader.
+ *
+ * Inspired by Tomcat JreMemoryLeakPrevention
+ *
+ * @deprecated sun.security.pkcs11.SunPKCS11 class explicitly sets thread classloader to null
+ */
+@Deprecated
+public class SecurityProviderLeakPreventer extends AbstractLeakPreventer
+{
+
+ /**
+ * @see org.eclipse.jetty.util.preventers.AbstractLeakPreventer#prevent(java.lang.ClassLoader)
+ */
+ @Override
+ public void prevent(ClassLoader loader)
+ {
+ Security.getProviders();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/package-info.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/package-info.java
new file mode 100644
index 0000000..382ba4a
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/preventers/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Util : Common Memory Leak Prevention Tooling
+ */
+package org.eclipse.jetty.util.preventers;
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/BadResource.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/BadResource.java
new file mode 100644
index 0000000..71c4aec
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/BadResource.java
@@ -0,0 +1,112 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+/**
+ * Bad Resource.
+ *
+ * A Resource that is returned for a bade URL. Acts as a resource
+ * that does not exist and throws appropriate exceptions.
+ */
+class BadResource extends URLResource
+{
+
+ private String _message = null;
+
+ BadResource(URL url, String message)
+ {
+ super(url, null);
+ _message = message;
+ }
+
+ @Override
+ public boolean exists()
+ {
+ return false;
+ }
+
+ @Override
+ public long lastModified()
+ {
+ return -1;
+ }
+
+ @Override
+ public boolean isDirectory()
+ {
+ return false;
+ }
+
+ @Override
+ public long length()
+ {
+ return -1;
+ }
+
+ @Override
+ public File getFile()
+ {
+ return null;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException
+ {
+ throw new FileNotFoundException(_message);
+ }
+
+ @Override
+ public boolean delete()
+ throws SecurityException
+ {
+ throw new SecurityException(_message);
+ }
+
+ @Override
+ public boolean renameTo(Resource dest)
+ throws SecurityException
+ {
+ throw new SecurityException(_message);
+ }
+
+ @Override
+ public String[] list()
+ {
+ return null;
+ }
+
+ @Override
+ public void copyTo(File destination)
+ throws IOException
+ {
+ throw new SecurityException(_message);
+ }
+
+ @Override
+ public String toString()
+ {
+ return super.toString() + "; BadResource=" + _message;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/EmptyResource.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/EmptyResource.java
new file mode 100644
index 0000000..6068b0d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/EmptyResource.java
@@ -0,0 +1,129 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.channels.ReadableByteChannel;
+
+/**
+ * EmptyResource
+ *
+ * Represents a resource that does does not refer to any file, url, jar etc.
+ */
+public class EmptyResource extends Resource
+{
+ public static final Resource INSTANCE = new EmptyResource();
+
+ private EmptyResource()
+ {
+ }
+
+ @Override
+ public boolean isContainedIn(Resource r) throws MalformedURLException
+ {
+ return false;
+ }
+
+ @Override
+ public void close()
+ {
+ }
+
+ @Override
+ public boolean exists()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean isDirectory()
+ {
+ return false;
+ }
+
+ @Override
+ public long lastModified()
+ {
+ return 0;
+ }
+
+ @Override
+ public long length()
+ {
+ return 0;
+ }
+
+ @Override
+ public URL getURL()
+ {
+ return null;
+ }
+
+ @Override
+ public File getFile() throws IOException
+ {
+ return null;
+ }
+
+ @Override
+ public String getName()
+ {
+ return null;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException
+ {
+ return null;
+ }
+
+ @Override
+ public ReadableByteChannel getReadableByteChannel() throws IOException
+ {
+ return null;
+ }
+
+ @Override
+ public boolean delete() throws SecurityException
+ {
+ return false;
+ }
+
+ @Override
+ public boolean renameTo(Resource dest) throws SecurityException
+ {
+ return false;
+ }
+
+ @Override
+ public String[] list()
+ {
+ return null;
+ }
+
+ @Override
+ public Resource addPath(String path) throws IOException
+ {
+ return null;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileResource.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileResource.java
new file mode 100644
index 0000000..c3d77b3
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileResource.java
@@ -0,0 +1,489 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.security.Permission;
+
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * File Resource.
+ *
+ * Handle resources of implied or explicit file type.
+ * This class can check for aliasing in the filesystem (eg case
+ * insensitivity). By default this is turned on, or it can be controlled
+ * by calling the static method @see FileResource#setCheckAliases(boolean)
+ *
+ * @deprecated Use {@link PathResource}
+ */
+@Deprecated
+public class FileResource extends Resource
+{
+ private static final Logger LOG = Log.getLogger(FileResource.class);
+
+ private final File _file;
+ private final URI _uri;
+ private final URI _alias;
+
+ public FileResource(URL url)
+ throws IOException, URISyntaxException
+ {
+ File file;
+ try
+ {
+ // Try standard API to convert URL to file.
+ file = new File(url.toURI());
+ assertValidPath(file.toString());
+ }
+ catch (URISyntaxException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ if (!url.toString().startsWith("file:"))
+ throw new IllegalArgumentException("!file:");
+
+ LOG.ignore(e);
+ try
+ {
+ // Assume that File.toURL produced unencoded chars. So try encoding them.
+ String fileUrl = "file:" + URIUtil.encodePath(url.toString().substring(5));
+ URI uri = new URI(fileUrl);
+ if (uri.getAuthority() == null)
+ file = new File(uri);
+ else
+ file = new File("//" + uri.getAuthority() + URIUtil.decodePath(url.getFile()));
+ }
+ catch (Exception ex2)
+ {
+ LOG.ignore(ex2);
+ // Still can't get the file. Doh! try good old hack!
+ URLConnection connection = url.openConnection();
+ Permission perm = connection.getPermission();
+ file = new File(perm == null ? url.getFile() : perm.getName());
+ }
+ }
+
+ _file = file;
+ _uri = normalizeURI(_file, url.toURI());
+ _alias = checkFileAlias(_uri, _file);
+ }
+
+ public FileResource(URI uri)
+ {
+ File file = new File(uri);
+ _file = file;
+ try
+ {
+ URI fileUri = _file.toURI();
+ _uri = normalizeURI(_file, uri);
+ assertValidPath(file.toString());
+
+ // Is it a URI alias?
+ if (!URIUtil.equalsIgnoreEncodings(_uri.toASCIIString(), fileUri.toString()))
+ _alias = _file.toURI();
+ else
+ _alias = checkFileAlias(_uri, _file);
+ }
+ catch (URISyntaxException e)
+ {
+ throw new InvalidPathException(_file.toString(), e.getMessage())
+ {
+ {
+ initCause(e);
+ }
+ };
+ }
+ }
+
+ public FileResource(File file)
+ {
+ assertValidPath(file.toString());
+ _file = file;
+ try
+ {
+ _uri = normalizeURI(_file, _file.toURI());
+ }
+ catch (URISyntaxException e)
+ {
+ throw new InvalidPathException(_file.toString(), e.getMessage())
+ {
+ {
+ initCause(e);
+ }
+ };
+ }
+ _alias = checkFileAlias(_uri, _file);
+ }
+
+ public FileResource(File base, String childPath)
+ {
+ String encoded = URIUtil.encodePath(childPath);
+
+ _file = new File(base, childPath);
+
+ // The encoded path should be a suffix of the resource (give or take a directory / )
+ URI uri;
+ try
+ {
+ if (base.isDirectory())
+ {
+ // treat all paths being added as relative
+ uri = new URI(URIUtil.addEncodedPaths(base.toURI().toASCIIString(), encoded));
+ }
+ else
+ {
+ uri = new URI(base.toURI().toASCIIString() + encoded);
+ }
+ }
+ catch (final URISyntaxException e)
+ {
+ throw new InvalidPathException(base.toString() + childPath, e.getMessage())
+ {
+ {
+ initCause(e);
+ }
+ };
+ }
+
+ _uri = uri;
+ _alias = checkFileAlias(_uri, _file);
+ }
+
+ @Override
+ public boolean isSame(Resource resource)
+ {
+ try
+ {
+ if (resource instanceof PathResource)
+ {
+ Path path = ((PathResource)resource).getPath();
+ return Files.isSameFile(getFile().toPath(), path);
+ }
+ if (resource instanceof FileResource)
+ {
+ Path path = ((FileResource)resource).getFile().toPath();
+ return Files.isSameFile(getFile().toPath(), path);
+ }
+ }
+ catch (IOException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("ignored", e);
+ }
+ return false;
+ }
+
+ private static URI normalizeURI(File file, URI uri) throws URISyntaxException
+ {
+ String u = uri.toASCIIString();
+ if (file.isDirectory())
+ {
+ if (!u.endsWith("/"))
+ u += "/";
+ }
+ else if (file.exists() && u.endsWith("/"))
+ u = u.substring(0, u.length() - 1);
+ return new URI(u);
+ }
+
+ private static URI checkFileAlias(final URI uri, final File file)
+ {
+ try
+ {
+ if (!URIUtil.equalsIgnoreEncodings(uri, file.toURI()))
+ {
+ // Return alias pointing to Java File normalized URI
+ return new File(uri).getAbsoluteFile().toURI();
+ }
+
+ String abs = file.getAbsolutePath();
+ String can = file.getCanonicalPath();
+
+ if (!abs.equals(can))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("ALIAS abs={} can={}", abs, can);
+
+ URI alias = new File(can).toURI();
+ // Have to encode the path as File.toURI does not!
+ return new URI("file://" + URIUtil.encodePath(alias.getPath()));
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn("bad alias for {}: {}", file, e.toString());
+ LOG.debug(e);
+ try
+ {
+ return new URI("https://eclipse.org/bad/canonical/alias");
+ }
+ catch (Exception ex2)
+ {
+ LOG.ignore(ex2);
+ throw new RuntimeException(e);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public Resource addPath(String path)
+ throws IOException
+ {
+ assertValidPath(path);
+
+ // Check that the path is within the root,
+ // but use the original path to create the
+ // resource, to preserve aliasing.
+ if (URIUtil.canonicalPath(path) == null)
+ throw new MalformedURLException(path);
+
+ if ("/".equals(path))
+ return this;
+
+ return new FileResource(_file, path);
+ }
+
+ private void assertValidPath(String path)
+ {
+ int idx = StringUtil.indexOfControlChars(path);
+ if (idx >= 0)
+ {
+ throw new InvalidPathException(path, "Invalid Character at index " + idx);
+ }
+ }
+
+ @Override
+ public URI getAlias()
+ {
+ return _alias;
+ }
+
+ /**
+ * Returns true if the resource exists.
+ */
+ @Override
+ public boolean exists()
+ {
+ return _file.exists();
+ }
+
+ /**
+ * Returns the last modified time
+ */
+ @Override
+ public long lastModified()
+ {
+ return _file.lastModified();
+ }
+
+ /**
+ * Returns true if the resource is a container/directory.
+ */
+ @Override
+ public boolean isDirectory()
+ {
+ return _file.exists() && _file.isDirectory() || _uri.toASCIIString().endsWith("/");
+ }
+
+ /**
+ * Return the length of the resource
+ */
+ @Override
+ public long length()
+ {
+ return _file.length();
+ }
+
+ /**
+ * Returns the name of the resource
+ */
+ @Override
+ public String getName()
+ {
+ return _file.getAbsolutePath();
+ }
+
+ /**
+ * Returns an File representing the given resource or NULL if this
+ * is not possible.
+ */
+ @Override
+ public File getFile()
+ {
+ return _file;
+ }
+
+ /**
+ * Returns an input stream to the resource
+ */
+ @Override
+ public InputStream getInputStream() throws IOException
+ {
+ return new FileInputStream(_file);
+ }
+
+ @Override
+ public ReadableByteChannel getReadableByteChannel() throws IOException
+ {
+ return FileChannel.open(_file.toPath(), StandardOpenOption.READ);
+ }
+
+ /**
+ * Deletes the given resource
+ */
+ @Override
+ public boolean delete()
+ throws SecurityException
+ {
+ return _file.delete();
+ }
+
+ /**
+ * Rename the given resource
+ */
+ @Override
+ public boolean renameTo(Resource dest)
+ throws SecurityException
+ {
+ if (dest instanceof FileResource)
+ return _file.renameTo(((FileResource)dest)._file);
+ else
+ return false;
+ }
+
+ /**
+ * Returns a list of resources contained in the given resource
+ */
+ @Override
+ public String[] list()
+ {
+ String[] list = _file.list();
+ if (list == null)
+ return null;
+ for (int i = list.length; i-- > 0; )
+ {
+ if (new File(_file, list[i]).isDirectory() &&
+ !list[i].endsWith("/"))
+ list[i] += "/";
+ }
+ return list;
+ }
+
+ /**
+ * @param o the object to compare against this instance
+ * @return <code>true</code> of the object <code>o</code> is a {@link FileResource} pointing to the same file as this resource.
+ */
+ @Override
+ @SuppressWarnings("ReferenceEquality")
+ public boolean equals(Object o)
+ {
+ if (this == o)
+ return true;
+
+ if (null == o || !(o instanceof FileResource))
+ return false;
+
+ FileResource f = (FileResource)o;
+ return f._file == _file || (null != _file && _file.equals(f._file));
+ }
+
+ /**
+ * @return the hashcode.
+ */
+ @Override
+ public int hashCode()
+ {
+ return null == _file ? super.hashCode() : _file.hashCode();
+ }
+
+ @Override
+ public void copyTo(File destination)
+ throws IOException
+ {
+ if (isDirectory())
+ {
+ IO.copyDir(getFile(), destination);
+ }
+ else
+ {
+ if (destination.exists())
+ throw new IllegalArgumentException(destination + " exists");
+ IO.copy(getFile(), destination);
+ }
+ }
+
+ @Override
+ public boolean isContainedIn(Resource r) throws MalformedURLException
+ {
+ return false;
+ }
+
+ @Override
+ public void close()
+ {
+ }
+
+ @Override
+ public URL getURL()
+ {
+ try
+ {
+ return _uri.toURL();
+ }
+ catch (MalformedURLException e)
+ {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Override
+ public URI getURI()
+ {
+ return _uri;
+ }
+
+ @Override
+ public String toString()
+ {
+ return _uri.toString();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java
new file mode 100644
index 0000000..c3b20d7
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java
@@ -0,0 +1,414 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.JarURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class JarFileResource extends JarResource
+{
+ private static final Logger LOG = Log.getLogger(JarFileResource.class);
+ private JarFile _jarFile;
+ private File _file;
+ private String[] _list;
+ private JarEntry _entry;
+ private boolean _directory;
+ private String _jarUrl;
+ private String _path;
+ private boolean _exists;
+
+ protected JarFileResource(URL url)
+ {
+ super(url);
+ }
+
+ protected JarFileResource(URL url, boolean useCaches)
+ {
+ super(url, useCaches);
+ }
+
+ @Override
+ public synchronized void close()
+ {
+ _exists = false;
+ _list = null;
+ _entry = null;
+ _file = null;
+ //if the jvm is not doing url caching, then the JarFiles will not be cached either,
+ //and so they are safe to close
+ if (!getUseCaches())
+ {
+ if (_jarFile != null)
+ {
+ try
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Closing JarFile " + _jarFile.getName());
+ _jarFile.close();
+ }
+ catch (IOException ioe)
+ {
+ LOG.ignore(ioe);
+ }
+ }
+ }
+ _jarFile = null;
+ super.close();
+ }
+
+ @Override
+ protected synchronized boolean checkConnection()
+ {
+ try
+ {
+ super.checkConnection();
+ }
+ finally
+ {
+ if (_jarConnection == null)
+ {
+ _entry = null;
+ _file = null;
+ _jarFile = null;
+ _list = null;
+ }
+ }
+ return _jarFile != null;
+ }
+
+ @Override
+ protected synchronized void newConnection()
+ throws IOException
+ {
+ super.newConnection();
+
+ _entry = null;
+ _file = null;
+ _jarFile = null;
+ _list = null;
+
+ // Work with encoded URL path (_urlString is assumed to be encoded)
+ int sep = _urlString.lastIndexOf("!/");
+ _jarUrl = _urlString.substring(0, sep + 2);
+ _path = URIUtil.decodePath(_urlString.substring(sep + 2));
+ if (_path.length() == 0)
+ _path = null;
+ _jarFile = _jarConnection.getJarFile();
+ _file = new File(_jarFile.getName());
+ }
+
+ /**
+ * Returns true if the represented resource exists.
+ */
+ @Override
+ public boolean exists()
+ {
+ if (_exists)
+ return true;
+
+ if (_urlString.endsWith("!/"))
+ {
+ String fileUrl = _urlString.substring(4, _urlString.length() - 2);
+ try
+ {
+ return newResource(fileUrl).exists();
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ return false;
+ }
+ }
+
+ boolean check = checkConnection();
+
+ // Is this a root URL?
+ if (_jarUrl != null && _path == null)
+ {
+ // Then if it exists it is a directory
+ _directory = check;
+ return true;
+ }
+ else
+ {
+ // Can we find a file for it?
+ boolean closeJarFile = false;
+ JarFile jarFile = null;
+ if (check)
+ // Yes
+ jarFile = _jarFile;
+ else
+ {
+ // No - so lets look if the root entry exists.
+ try
+ {
+ JarURLConnection c = (JarURLConnection)((new URL(_jarUrl)).openConnection());
+ c.setUseCaches(getUseCaches());
+ jarFile = c.getJarFile();
+ closeJarFile = !getUseCaches();
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ }
+ }
+
+ // Do we need to look more closely?
+ if (jarFile != null && _entry == null && !_directory)
+ {
+ // OK - we have a JarFile, lets look for the entry
+ JarEntry entry = jarFile.getJarEntry(_path);
+ if (entry == null)
+ {
+ // the entry does not exist
+ _exists = false;
+ }
+ else if (entry.isDirectory())
+ {
+ _directory = true;
+ _entry = entry;
+ }
+ else
+ {
+ // Let's confirm is a file
+ JarEntry directory = jarFile.getJarEntry(_path + '/');
+ if (directory != null)
+ {
+ _directory = true;
+ _entry = directory;
+ }
+ else
+ {
+ // OK is a file
+ _directory = false;
+ _entry = entry;
+ }
+ }
+ }
+
+ if (closeJarFile && jarFile != null)
+ {
+ try
+ {
+ jarFile.close();
+ }
+ catch (IOException ioe)
+ {
+ LOG.ignore(ioe);
+ }
+ }
+ }
+
+ _exists = (_directory || _entry != null);
+ return _exists;
+ }
+
+ /**
+ * Returns true if the represented resource is a container/directory.
+ * If the resource is not a file, resources ending with "/" are
+ * considered directories.
+ */
+ @Override
+ public boolean isDirectory()
+ {
+ return _urlString.endsWith("/") || exists() && _directory;
+ }
+
+ /**
+ * Returns the last modified time
+ */
+ @Override
+ public long lastModified()
+ {
+ if (checkConnection() && _file != null)
+ {
+ if (exists() && _entry != null)
+ return _entry.getTime();
+ return _file.lastModified();
+ }
+ return -1;
+ }
+
+ @Override
+ public synchronized String[] list()
+ {
+ if (isDirectory() && _list == null)
+ {
+ List<String> list = null;
+ try
+ {
+ list = listEntries();
+ }
+ catch (Exception e)
+ {
+ //Sun's JarURLConnection impl for jar: protocol will close a JarFile in its connect() method if
+ //useCaches == false (eg someone called URLConnection with defaultUseCaches==true).
+ //As their sun.net.www.protocol.jar package caches JarFiles and/or connections, we can wind up in
+ //the situation where the JarFile we have remembered in our _jarFile member has actually been closed
+ //by other code.
+ //So, do one retry to drop a connection and get a fresh JarFile
+ LOG.warn("Retrying list:" + e);
+ LOG.debug(e);
+ close();
+ list = listEntries();
+ }
+
+ if (list != null)
+ {
+ _list = new String[list.size()];
+ list.toArray(_list);
+ }
+ }
+ return _list;
+ }
+
+ private List<String> listEntries()
+ {
+ checkConnection();
+
+ ArrayList<String> list = new ArrayList<String>(32);
+ JarFile jarFile = _jarFile;
+ if (jarFile == null)
+ {
+ try
+ {
+ JarURLConnection jc = (JarURLConnection)((new URL(_jarUrl)).openConnection());
+ jc.setUseCaches(getUseCaches());
+ jarFile = jc.getJarFile();
+ }
+ catch (Exception e)
+ {
+
+ e.printStackTrace();
+ LOG.ignore(e);
+ }
+ if (jarFile == null)
+ throw new IllegalStateException();
+ }
+
+ Enumeration<JarEntry> e = jarFile.entries();
+ String encodedDir = _urlString.substring(_urlString.lastIndexOf("!/") + 2);
+ String dir = URIUtil.decodePath(encodedDir);
+ while (e.hasMoreElements())
+ {
+ JarEntry entry = e.nextElement();
+ String name = entry.getName();
+ if (!name.startsWith(dir) || name.length() == dir.length())
+ {
+ continue;
+ }
+ String listName = name.substring(dir.length());
+ int dash = listName.indexOf('/');
+ if (dash >= 0)
+ {
+ //when listing jar:file urls, you get back one
+ //entry for the dir itself, which we ignore
+ if (dash == 0 && listName.length() == 1)
+ continue;
+ //when listing jar:file urls, all files and
+ //subdirs have a leading /, which we remove
+ if (dash == 0)
+ listName = listName.substring(dash + 1);
+ else
+ listName = listName.substring(0, dash + 1);
+
+ if (list.contains(listName))
+ continue;
+ }
+
+ list.add(listName);
+ }
+
+ return list;
+ }
+
+ /**
+ * Return the length of the resource
+ */
+ @Override
+ public long length()
+ {
+ if (isDirectory())
+ return -1;
+
+ if (_entry != null)
+ return _entry.getSize();
+
+ return -1;
+ }
+
+ /**
+ * Take a Resource that possibly might use URLConnection caching
+ * and turn it into one that doesn't.
+ *
+ * @param resource the JarFileResource to obtain without URLConnection caching.
+ * @return the non-caching resource
+ */
+ public static Resource getNonCachingResource(Resource resource)
+ {
+ if (!(resource instanceof JarFileResource))
+ return resource;
+
+ JarFileResource oldResource = (JarFileResource)resource;
+
+ JarFileResource newResource = new JarFileResource(oldResource.getURL(), false);
+ return newResource;
+ }
+
+ /**
+ * Check if this jar:file: resource is contained in the
+ * named resource. Eg <code>jar:file:///a/b/c/foo.jar!/x.html</code> isContainedIn <code>file:///a/b/c/foo.jar</code>
+ *
+ * @param resource the resource to test for
+ * @return true if resource is contained in the named resource
+ * @throws MalformedURLException if unable to process is contained due to invalid URL format
+ */
+ @Override
+ public boolean isContainedIn(Resource resource)
+ throws MalformedURLException
+ {
+ String string = _urlString;
+ int index = string.lastIndexOf("!/");
+ if (index > 0)
+ string = string.substring(0, index);
+ if (string.startsWith("jar:"))
+ string = string.substring(4);
+ URL url = new URL(string);
+ return url.sameFile(resource.getURI().toURL());
+ }
+
+ public File getJarFile()
+ {
+ if (_file != null)
+ return _file;
+ return null;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarResource.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarResource.java
new file mode 100644
index 0000000..c82c2f5
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarResource.java
@@ -0,0 +1,250 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class JarResource extends URLResource
+{
+ private static final Logger LOG = Log.getLogger(JarResource.class);
+ protected JarURLConnection _jarConnection;
+
+ protected JarResource(URL url)
+ {
+ super(url, null);
+ }
+
+ protected JarResource(URL url, boolean useCaches)
+ {
+ super(url, null, useCaches);
+ }
+
+ @Override
+ public synchronized void close()
+ {
+ _jarConnection = null;
+ super.close();
+ }
+
+ @Override
+ protected synchronized boolean checkConnection()
+ {
+ super.checkConnection();
+ try
+ {
+ if (_jarConnection != _connection)
+ newConnection();
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ _jarConnection = null;
+ }
+
+ return _jarConnection != null;
+ }
+
+ /**
+ * @throws IOException Sub-classes of <code>JarResource</code> may throw an IOException (or subclass)
+ */
+ protected void newConnection() throws IOException
+ {
+ _jarConnection = (JarURLConnection)_connection;
+ }
+
+ /**
+ * Returns true if the represented resource exists.
+ */
+ @Override
+ public boolean exists()
+ {
+ if (_urlString.endsWith("!/"))
+ return checkConnection();
+ else
+ return super.exists();
+ }
+
+ @Override
+ public File getFile()
+ throws IOException
+ {
+ return null;
+ }
+
+ @Override
+ public InputStream getInputStream()
+ throws java.io.IOException
+ {
+ checkConnection();
+ if (!_urlString.endsWith("!/"))
+ return new FilterInputStream(getInputStream(false))
+ {
+ @Override
+ public void close()
+ {
+ this.in = IO.getClosedStream();
+ }
+ };
+
+ URL url = new URL(_urlString.substring(4, _urlString.length() - 2));
+ InputStream is = url.openStream();
+ return is;
+ }
+
+ @Override
+ public void copyTo(File directory)
+ throws IOException
+ {
+ if (!exists())
+ return;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Extract " + this + " to " + directory);
+
+ String urlString = this.getURL().toExternalForm().trim();
+ int endOfJarUrl = urlString.indexOf("!/");
+ int startOfJarUrl = (endOfJarUrl >= 0 ? 4 : 0);
+
+ if (endOfJarUrl < 0)
+ throw new IOException("Not a valid jar url: " + urlString);
+
+ URL jarFileURL = new URL(urlString.substring(startOfJarUrl, endOfJarUrl));
+ String subEntryName = (endOfJarUrl + 2 < urlString.length() ? urlString.substring(endOfJarUrl + 2) : null);
+ boolean subEntryIsDir = (subEntryName != null && subEntryName.endsWith("/"));
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Extracting entry = " + subEntryName + " from jar " + jarFileURL);
+ URLConnection c = jarFileURL.openConnection();
+ c.setUseCaches(false);
+ try (InputStream is = c.getInputStream();
+ JarInputStream jin = new JarInputStream(is))
+ {
+ JarEntry entry;
+ boolean shouldExtract;
+ while ((entry = jin.getNextJarEntry()) != null)
+ {
+ String entryName = entry.getName();
+ if ((subEntryName != null) && (entryName.startsWith(subEntryName)))
+ {
+ // is the subentry really a dir?
+ if (!subEntryIsDir && subEntryName.length() + 1 == entryName.length() && entryName.endsWith("/"))
+ subEntryIsDir = true;
+
+ //if there is a particular subEntry that we are looking for, only
+ //extract it.
+ if (subEntryIsDir)
+ {
+ //if it is a subdirectory we are looking for, then we
+ //are looking to extract its contents into the target
+ //directory. Remove the name of the subdirectory so
+ //that we don't wind up creating it too.
+ entryName = entryName.substring(subEntryName.length());
+ //the entry is
+ shouldExtract = !entryName.equals("");
+ }
+ else
+ shouldExtract = true;
+ }
+ else
+ shouldExtract = (subEntryName == null) || (entryName.startsWith(subEntryName));
+
+ if (!shouldExtract)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Skipping entry: " + entryName);
+ continue;
+ }
+
+ String dotCheck = StringUtil.replace(entryName, '\\', '/');
+ dotCheck = URIUtil.canonicalPath(dotCheck);
+ if (dotCheck == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Invalid entry: " + entryName);
+ continue;
+ }
+
+ File file = new File(directory, entryName);
+
+ if (entry.isDirectory())
+ {
+ // Make directory
+ if (!file.exists())
+ file.mkdirs();
+ }
+ else
+ {
+ // make directory (some jars don't list dirs)
+ File dir = new File(file.getParent());
+ if (!dir.exists())
+ dir.mkdirs();
+
+ // Make file
+ try (OutputStream fout = new FileOutputStream(file))
+ {
+ IO.copy(jin, fout);
+ }
+
+ // touch the file.
+ if (entry.getTime() >= 0)
+ file.setLastModified(entry.getTime());
+ }
+ }
+
+ if ((subEntryName == null) || (subEntryName != null && subEntryName.equalsIgnoreCase("META-INF/MANIFEST.MF")))
+ {
+ Manifest manifest = jin.getManifest();
+ if (manifest != null)
+ {
+ File metaInf = new File(directory, "META-INF");
+ metaInf.mkdir();
+ File f = new File(metaInf, "MANIFEST.MF");
+ try (OutputStream fout = new FileOutputStream(f))
+ {
+ manifest.write(fout);
+ }
+ }
+ }
+ }
+ }
+
+ public static Resource newJarResource(Resource resource) throws IOException
+ {
+ if (resource instanceof JarResource)
+ return resource;
+ return Resource.newResource("jar:" + resource + "!/");
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
new file mode 100644
index 0000000..3c58739
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
@@ -0,0 +1,735 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.File;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.DirectoryIteratorException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.FileTime;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Java NIO Path equivalent of FileResource.
+ */
+public class PathResource extends Resource
+{
+ private static final Logger LOG = Log.getLogger(PathResource.class);
+ private static final LinkOption[] NO_FOLLOW_LINKS = new LinkOption[]{LinkOption.NOFOLLOW_LINKS};
+ private static final LinkOption[] FOLLOW_LINKS = new LinkOption[]{};
+
+ private final Path path;
+ private final Path alias;
+ private final URI uri;
+ private final boolean belongsToDefaultFileSystem;
+
+ private Path checkAliasPath()
+ {
+ Path abs = path;
+
+ /* Catch situation where the Path class has already normalized
+ * the URI eg. input path "aa./foo.txt"
+ * from an #addPath(String) is normalized away during
+ * the creation of a Path object reference.
+ * If the URI is different then the Path.toUri() then
+ * we will just use the original URI to construct the
+ * alias reference Path.
+ */
+ if (!URIUtil.equalsIgnoreEncodings(uri, path.toUri()))
+ {
+ try
+ {
+ return Paths.get(uri).toRealPath(FOLLOW_LINKS);
+ }
+ catch (IOException ignored)
+ {
+ // If the toRealPath() call fails, then let
+ // the alias checking routines continue on
+ // to other techniques.
+ LOG.ignore(ignored);
+ }
+ }
+
+ if (!abs.isAbsolute())
+ abs = path.toAbsolutePath();
+
+ // Any normalization difference means it's an alias,
+ // and we don't want to bother further to follow
+ // symlinks as it's an alias anyway.
+ Path normal = path.normalize();
+ if (!isSameName(abs, normal))
+ return normal;
+
+ try
+ {
+ if (Files.isSymbolicLink(path))
+ return path.getParent().resolve(Files.readSymbolicLink(path));
+ if (Files.exists(path))
+ {
+ Path real = abs.toRealPath(FOLLOW_LINKS);
+ if (!isSameName(abs, real))
+ return real;
+ }
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ }
+ catch (Exception e)
+ {
+ LOG.warn("bad alias ({} {}) for {}", e.getClass().getName(), e.getMessage(), path);
+ }
+ return null;
+ }
+
+ /**
+ * Test if the paths are the same name.
+ *
+ * <p>
+ * If the real path is not the same as the absolute path
+ * then we know that the real path is the alias for the
+ * provided path.
+ * </p>
+ *
+ * <p>
+ * For OS's that are case insensitive, this should
+ * return the real (on-disk / case correct) version
+ * of the path.
+ * </p>
+ *
+ * <p>
+ * We have to be careful on Windows and OSX.
+ * </p>
+ *
+ * <p>
+ * Assume we have the following scenario:
+ * </p>
+ *
+ * <pre>
+ * Path a = new File("foo").toPath();
+ * Files.createFile(a);
+ * Path b = new File("FOO").toPath();
+ * </pre>
+ *
+ * <p>
+ * There now exists a file called {@code foo} on disk.
+ * Using Windows or OSX, with a Path reference of
+ * {@code FOO}, {@code Foo}, {@code fOO}, etc.. means the following
+ * </p>
+ *
+ * <pre>
+ * | OSX | Windows | Linux
+ * -----------------------+---------+------------+---------
+ * Files.exists(a) | True | True | True
+ * Files.exists(b) | True | True | False
+ * Files.isSameFile(a,b) | True | True | False
+ * a.equals(b) | False | True | False
+ * </pre>
+ *
+ * <p>
+ * See the javadoc for Path.equals() for details about this FileSystem
+ * behavior difference
+ * </p>
+ *
+ * <p>
+ * We also cannot rely on a.compareTo(b) as this is roughly equivalent
+ * in implementation to a.equals(b)
+ * </p>
+ */
+ public static boolean isSameName(Path pathA, Path pathB)
+ {
+ int aCount = pathA.getNameCount();
+ int bCount = pathB.getNameCount();
+ if (aCount != bCount)
+ {
+ // different number of segments
+ return false;
+ }
+
+ // compare each segment of path, backwards
+ for (int i = bCount; i-- > 0; )
+ {
+ if (!pathA.getName(i).toString().equals(pathB.getName(i).toString()))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Construct a new PathResource from a File object.
+ * <p>
+ * An invocation of this convenience constructor of the form.
+ * </p>
+ * <pre>
+ * new PathResource(file);
+ * </pre>
+ * <p>
+ * behaves in exactly the same way as the expression
+ * </p>
+ * <pre>
+ * new PathResource(file.toPath());
+ * </pre>
+ *
+ * @param file the file to use
+ */
+ public PathResource(File file)
+ {
+ this(file.toPath());
+ }
+
+ /**
+ * Construct a new PathResource from a Path object.
+ *
+ * @param path the path to use
+ */
+ public PathResource(Path path)
+ {
+ Path absPath = path;
+ try
+ {
+ absPath = path.toRealPath(NO_FOLLOW_LINKS);
+ }
+ catch (IOError | IOException e)
+ {
+ // Not able to resolve real/canonical path from provided path
+ // This could be due to a glob reference, or a reference
+ // to a path that doesn't exist (yet)
+ if (LOG.isDebugEnabled())
+ LOG.debug("Unable to get real/canonical path for {}", path, e);
+ }
+
+ this.path = absPath;
+
+ assertValidPath(path);
+ this.uri = this.path.toUri();
+ this.alias = checkAliasPath();
+ this.belongsToDefaultFileSystem = this.path.getFileSystem() == FileSystems.getDefault();
+ }
+
+ /**
+ * Construct a new PathResource from a parent PathResource
+ * and child sub path
+ *
+ * @param parent the parent path resource
+ * @param childPath the child sub path
+ */
+ private PathResource(PathResource parent, String childPath)
+ {
+ // Calculate the URI and the path separately, so that any aliasing done by
+ // FileSystem.getPath(path,childPath) is visible as a difference to the URI
+ // obtained via URIUtil.addDecodedPath(uri,childPath)
+ // The checkAliasPath normalization checks will only work correctly if the getPath implementation here does not normalize.
+ this.path = parent.path.getFileSystem().getPath(parent.path.toString(), childPath);
+ if (isDirectory() && !childPath.endsWith("/"))
+ childPath += "/";
+ this.uri = URIUtil.addPath(parent.uri, childPath);
+ this.alias = checkAliasPath();
+ this.belongsToDefaultFileSystem = this.path.getFileSystem() == FileSystems.getDefault();
+ }
+
+ /**
+ * Construct a new PathResource from a URI object.
+ * <p>
+ * Must be an absolute URI using the <code>file</code> scheme.
+ *
+ * @param uri the URI to build this PathResource from.
+ * @throws IOException if unable to construct the PathResource from the URI.
+ */
+ public PathResource(URI uri) throws IOException
+ {
+ if (!uri.isAbsolute())
+ {
+ throw new IllegalArgumentException("not an absolute uri");
+ }
+
+ if (!uri.getScheme().equalsIgnoreCase("file"))
+ {
+ throw new IllegalArgumentException("not file: scheme");
+ }
+
+ Path path;
+ try
+ {
+ path = Paths.get(uri);
+ }
+ catch (IllegalArgumentException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ throw new IOException("Unable to build Path from: " + uri, e);
+ }
+
+ this.path = path.toAbsolutePath();
+ this.uri = path.toUri();
+ this.alias = checkAliasPath();
+ this.belongsToDefaultFileSystem = this.path.getFileSystem() == FileSystems.getDefault();
+ }
+
+ /**
+ * Create a new PathResource from a provided URL object.
+ * <p>
+ * An invocation of this convenience constructor of the form.
+ * </p>
+ * <pre>
+ * new PathResource(url);
+ * </pre>
+ * <p>
+ * behaves in exactly the same way as the expression
+ * </p>
+ * <pre>
+ * new PathResource(url.toURI());
+ * </pre>
+ *
+ * @param url the url to attempt to create PathResource from
+ * @throws IOException if URL doesn't point to a location that can be transformed to a PathResource
+ * @throws URISyntaxException if the provided URL was malformed
+ */
+ public PathResource(URL url) throws IOException, URISyntaxException
+ {
+ this(url.toURI());
+ }
+
+ @Override
+ public boolean isSame(Resource resource)
+ {
+ try
+ {
+ if (resource instanceof PathResource)
+ {
+ Path path = ((PathResource)resource).getPath();
+ return Files.isSameFile(getPath(), path);
+ }
+ if (resource instanceof FileResource)
+ {
+ Path path = ((FileResource)resource).getFile().toPath();
+ return Files.isSameFile(getPath(), path);
+ }
+ }
+ catch (IOException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("ignored", e);
+ }
+ return false;
+ }
+
+ @Override
+ public Resource addPath(final String subPath) throws IOException
+ {
+ // Check that the path is within the root,
+ // but use the original path to create the
+ // resource, to preserve aliasing.
+ if (URIUtil.canonicalPath(subPath) == null)
+ throw new MalformedURLException(subPath);
+
+ if ("/".equals(subPath))
+ return this;
+
+ // Sub-paths are always under PathResource
+ // compensate for input sub-paths like "/subdir"
+ // where default resolve behavior would be
+ // to treat that like an absolute path
+
+ return new PathResource(this, subPath);
+ }
+
+ private void assertValidPath(Path path)
+ {
+ // TODO merged from 9.2, check if necessary
+ String str = path.toString();
+ int idx = StringUtil.indexOfControlChars(str);
+ if (idx >= 0)
+ {
+ throw new InvalidPathException(str, "Invalid Character at index " + idx);
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ // not applicable for FileSytem / Path
+ }
+
+ @Override
+ public boolean delete() throws SecurityException
+ {
+ try
+ {
+ return Files.deleteIfExists(path);
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ return false;
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ {
+ return true;
+ }
+ if (obj == null)
+ {
+ return false;
+ }
+ if (getClass() != obj.getClass())
+ {
+ return false;
+ }
+ PathResource other = (PathResource)obj;
+ if (path == null)
+ {
+ return other.path == null;
+ }
+ else
+ return path.equals(other.path);
+ }
+
+ @Override
+ public boolean exists()
+ {
+ return Files.exists(path, NO_FOLLOW_LINKS);
+ }
+
+ @Override
+ public File getFile() throws IOException
+ {
+ if (!belongsToDefaultFileSystem)
+ return null;
+ return path.toFile();
+ }
+
+ /**
+ * @return the {@link Path} of the resource
+ */
+ public Path getPath()
+ {
+ return path;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException
+ {
+ return Files.newInputStream(path, StandardOpenOption.READ);
+ }
+
+ @Override
+ public String getName()
+ {
+ return path.toAbsolutePath().toString();
+ }
+
+ @Override
+ public ReadableByteChannel getReadableByteChannel() throws IOException
+ {
+ return newSeekableByteChannel();
+ }
+
+ public SeekableByteChannel newSeekableByteChannel() throws IOException
+ {
+ return Files.newByteChannel(path, StandardOpenOption.READ);
+ }
+
+ @Override
+ public URI getURI()
+ {
+ return this.uri;
+ }
+
+ @Override
+ public URL getURL()
+ {
+ try
+ {
+ return path.toUri().toURL();
+ }
+ catch (MalformedURLException e)
+ {
+ return null;
+ }
+ }
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = (prime * result) + ((path == null) ? 0 : path.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean isContainedIn(Resource r) throws MalformedURLException
+ {
+ // not applicable for FileSystem / path
+ return false;
+ }
+
+ @Override
+ public boolean isDirectory()
+ {
+ return Files.isDirectory(path, FOLLOW_LINKS);
+ }
+
+ @Override
+ public long lastModified()
+ {
+ try
+ {
+ FileTime ft = Files.getLastModifiedTime(path, FOLLOW_LINKS);
+ return ft.toMillis();
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ return 0;
+ }
+ }
+
+ @Override
+ public long length()
+ {
+ try
+ {
+ return Files.size(path);
+ }
+ catch (IOException e)
+ {
+ // in case of error, use File.length logic of 0L
+ return 0L;
+ }
+ }
+
+ @Override
+ public boolean isAlias()
+ {
+ return this.alias != null;
+ }
+
+ /**
+ * The Alias as a Path.
+ * <p>
+ * Note: this cannot return the alias as a DIFFERENT path in 100% of situations,
+ * due to Java's internal Path/File normalization.
+ * </p>
+ *
+ * @return the alias as a path.
+ */
+ public Path getAliasPath()
+ {
+ return this.alias;
+ }
+
+ @Override
+ public URI getAlias()
+ {
+ return this.alias == null ? null : this.alias.toUri();
+ }
+
+ @Override
+ public String[] list()
+ {
+ try (DirectoryStream<Path> dir = Files.newDirectoryStream(path))
+ {
+ List<String> entries = new ArrayList<>();
+ for (Path entry : dir)
+ {
+ String name = entry.getFileName().toString();
+
+ if (Files.isDirectory(entry))
+ {
+ name += "/";
+ }
+
+ entries.add(name);
+ }
+ int size = entries.size();
+ return entries.toArray(new String[size]);
+ }
+ catch (DirectoryIteratorException | IOException e)
+ {
+ LOG.debug(e);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean renameTo(Resource dest) throws SecurityException
+ {
+ if (dest instanceof PathResource)
+ {
+ PathResource destRes = (PathResource)dest;
+ try
+ {
+ Path result = Files.move(path, destRes.path);
+ return Files.exists(result, NO_FOLLOW_LINKS);
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ return false;
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ @Override
+ public void copyTo(File destination) throws IOException
+ {
+ if (isDirectory())
+ {
+ IO.copyDir(this.path.toFile(), destination);
+ }
+ else
+ {
+ Files.copy(this.path, destination.toPath());
+ }
+ }
+
+ /**
+ * @param outputStream the output stream to write to
+ * @param start First byte to write
+ * @param count Bytes to write or -1 for all of them.
+ * @throws IOException if unable to copy the Resource to the output
+ */
+ @Override
+ public void writeTo(OutputStream outputStream, long start, long count)
+ throws IOException
+ {
+ long length = count;
+
+ if (count < 0)
+ {
+ length = Files.size(path) - start;
+ }
+
+ try (SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ))
+ {
+ ByteBuffer buffer = BufferUtil.allocate(IO.bufferSize);
+ skipTo(channel, buffer, start);
+
+ // copy from channel to output stream
+ long readTotal = 0;
+ while (readTotal < length)
+ {
+ BufferUtil.clearToFill(buffer);
+ int size = (int)Math.min(IO.bufferSize, length - readTotal);
+ buffer.limit(size);
+ int readLen = channel.read(buffer);
+ BufferUtil.flipToFlush(buffer, 0);
+ BufferUtil.writeTo(buffer, outputStream);
+ readTotal += readLen;
+ }
+ }
+ }
+
+ private void skipTo(SeekableByteChannel channel, ByteBuffer buffer, long skipTo) throws IOException
+ {
+ try
+ {
+ if (channel.position() != skipTo)
+ {
+ channel.position(skipTo);
+ }
+ }
+ catch (UnsupportedOperationException e)
+ {
+ final int NO_PROGRESS_LIMIT = 3;
+
+ if (skipTo > 0)
+ {
+ long pos = 0;
+ long readLen;
+ int noProgressLoopLimit = NO_PROGRESS_LIMIT;
+ // loop till we reach desired point, break out on lack of progress.
+ while (noProgressLoopLimit > 0 && pos < skipTo)
+ {
+ BufferUtil.clearToFill(buffer);
+ int len = (int)Math.min(IO.bufferSize, (skipTo - pos));
+ buffer.limit(len);
+ readLen = channel.read(buffer);
+ if (readLen == 0)
+ {
+ noProgressLoopLimit--;
+ }
+ else if (readLen > 0)
+ {
+ pos += readLen;
+ noProgressLoopLimit = NO_PROGRESS_LIMIT;
+ }
+ else
+ {
+ // negative values means the stream was closed or reached EOF
+ // either way, we've hit a state where we can no longer
+ // fulfill the requested range write.
+ throw new IOException("EOF reached before SeekableByteChannel skip destination");
+ }
+ }
+
+ if (noProgressLoopLimit <= 0)
+ {
+ throw new IOException("No progress made to reach SeekableByteChannel skip position " + skipTo);
+ }
+ }
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return this.uri.toASCIIString();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
new file mode 100644
index 0000000..d3a32b5
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
@@ -0,0 +1,1012 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.Loader;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.UrlEncoded;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Abstract resource class.
+ * <p>
+ * This class provides a resource abstraction, where a resource may be
+ * a file, a URL or an entry in a jar file.
+ * </p>
+ */
+public abstract class Resource implements ResourceFactory, Closeable
+{
+ private static final Logger LOG = Log.getLogger(Resource.class);
+ public static boolean __defaultUseCaches = true;
+ volatile Object _associate;
+
+ /**
+ * Change the default setting for url connection caches.
+ * Subsequent URLConnections will use this default.
+ *
+ * @param useCaches true to enable URL connection caches, false otherwise.
+ */
+ public static void setDefaultUseCaches(boolean useCaches)
+ {
+ __defaultUseCaches = useCaches;
+ }
+
+ public static boolean getDefaultUseCaches()
+ {
+ return __defaultUseCaches;
+ }
+
+ /**
+ * Construct a resource from a uri.
+ *
+ * @param uri A URI.
+ * @return A Resource object.
+ * @throws MalformedURLException Problem accessing URI
+ */
+ public static Resource newResource(URI uri)
+ throws MalformedURLException
+ {
+ return newResource(uri.toURL());
+ }
+
+ /**
+ * Construct a resource from a url.
+ *
+ * @param url A URL.
+ * @return A Resource object.
+ */
+ public static Resource newResource(URL url)
+ {
+ return newResource(url, __defaultUseCaches);
+ }
+
+ /**
+ * Construct a resource from a url.
+ *
+ * @param url the url for which to make the resource
+ * @param useCaches true enables URLConnection caching if applicable to the type of resource
+ */
+ static Resource newResource(URL url, boolean useCaches)
+ {
+ if (url == null)
+ return null;
+
+ String urlString = url.toExternalForm();
+ if (urlString.startsWith("file:"))
+ {
+ try
+ {
+ return new PathResource(url);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(Log.EXCEPTION, e);
+ return new BadResource(url, e.toString());
+ }
+ }
+ else if (urlString.startsWith("jar:file:"))
+ {
+ return new JarFileResource(url, useCaches);
+ }
+ else if (urlString.startsWith("jar:"))
+ {
+ return new JarResource(url, useCaches);
+ }
+
+ return new URLResource(url, null, useCaches);
+ }
+
+ /**
+ * Construct a resource from a string.
+ *
+ * @param resource A URL or filename.
+ * @return A Resource object.
+ * @throws MalformedURLException Problem accessing URI
+ */
+ public static Resource newResource(String resource)
+ throws IOException
+ {
+ return newResource(resource, __defaultUseCaches);
+ }
+
+ /**
+ * Construct a resource from a string.
+ *
+ * @param resource A URL or filename.
+ * @param useCaches controls URLConnection caching
+ * @return A Resource object.
+ * @throws MalformedURLException Problem accessing URI
+ */
+ public static Resource newResource(String resource, boolean useCaches)
+ throws IOException
+ {
+ URL url = null;
+ try
+ {
+ // Try to format as a URL?
+ url = new URL(resource);
+ }
+ catch (MalformedURLException e)
+ {
+ if (!resource.startsWith("ftp:") &&
+ !resource.startsWith("file:") &&
+ !resource.startsWith("jar:"))
+ {
+ // It's likely a file/path reference.
+ return new PathResource(Paths.get(resource));
+ }
+ else
+ {
+ LOG.warn("Bad Resource: " + resource);
+ throw e;
+ }
+ }
+
+ return newResource(url, useCaches);
+ }
+
+ public static Resource newResource(File file)
+ {
+ return new PathResource(file.toPath());
+ }
+
+ /**
+ * Construct a Resource from provided path
+ *
+ * @param path the path
+ * @return the Resource for the provided path
+ * @since 9.4.10
+ */
+ public static Resource newResource(Path path)
+ {
+ return new PathResource(path);
+ }
+
+ /**
+ * Construct a system resource from a string.
+ * The resource is tried as classloader resource before being
+ * treated as a normal resource.
+ *
+ * @param resource Resource as string representation
+ * @return The new Resource
+ * @throws IOException Problem accessing resource.
+ */
+ public static Resource newSystemResource(String resource)
+ throws IOException
+ {
+ URL url = null;
+ // Try to format as a URL?
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ if (loader != null)
+ {
+ try
+ {
+ url = loader.getResource(resource);
+ if (url == null && resource.startsWith("/"))
+ url = loader.getResource(resource.substring(1));
+ }
+ catch (IllegalArgumentException e)
+ {
+ LOG.ignore(e);
+ // Catches scenario where a bad Windows path like "C:\dev" is
+ // improperly escaped, which various downstream classloaders
+ // tend to have a problem with
+ url = null;
+ }
+ }
+ if (url == null)
+ {
+ loader = Resource.class.getClassLoader();
+ if (loader != null)
+ {
+ url = loader.getResource(resource);
+ if (url == null && resource.startsWith("/"))
+ url = loader.getResource(resource.substring(1));
+ }
+ }
+
+ if (url == null)
+ {
+ url = ClassLoader.getSystemResource(resource);
+ if (url == null && resource.startsWith("/"))
+ url = ClassLoader.getSystemResource(resource.substring(1));
+ }
+
+ if (url == null)
+ return null;
+
+ return newResource(url);
+ }
+
+ /**
+ * Find a classpath resource.
+ *
+ * @param resource the relative name of the resource
+ * @return Resource or null
+ */
+ public static Resource newClassPathResource(String resource)
+ {
+ return newClassPathResource(resource, true, false);
+ }
+
+ /**
+ * Find a classpath resource.
+ * The {@link java.lang.Class#getResource(String)} method is used to lookup the resource. If it is not
+ * found, then the {@link Loader#getResource(String)} method is used.
+ * If it is still not found, then {@link ClassLoader#getSystemResource(String)} is used.
+ * Unlike {@link ClassLoader#getSystemResource(String)} this method does not check for normal resources.
+ *
+ * @param name The relative name of the resource
+ * @param useCaches True if URL caches are to be used.
+ * @param checkParents True if forced searching of parent Classloaders is performed to work around
+ * loaders with inverted priorities
+ * @return Resource or null
+ */
+ public static Resource newClassPathResource(String name, boolean useCaches, boolean checkParents)
+ {
+ URL url = Resource.class.getResource(name);
+
+ if (url == null)
+ url = Loader.getResource(name);
+ if (url == null)
+ return null;
+ return newResource(url, useCaches);
+ }
+
+ public static boolean isContainedIn(Resource r, Resource containingResource) throws MalformedURLException
+ {
+ return r.isContainedIn(containingResource);
+ }
+
+ //@checkstyle-disable-check : NoFinalizer
+ @Override
+ protected void finalize()
+ {
+ close();
+ }
+ //@checkstyle-enable-check : NoFinalizer
+
+ public abstract boolean isContainedIn(Resource r) throws MalformedURLException;
+
+ /**
+ * Return true if the passed Resource represents the same resource as the Resource.
+ * For many resource types, this is equivalent to {@link #equals(Object)}, however
+ * for resources types that support aliasing, this maybe some other check (e.g. {@link java.nio.file.Files#isSameFile(Path, Path)}).
+ * @param resource The resource to check
+ * @return true if the passed resource represents the same resource.
+ */
+ public boolean isSame(Resource resource)
+ {
+ return equals(resource);
+ }
+
+ /**
+ * Release any temporary resources held by the resource.
+ *
+ * @deprecated use {@link #close()}
+ */
+ public final void release()
+ {
+ close();
+ }
+
+ /**
+ * Release any temporary resources held by the resource.
+ */
+ @Override
+ public abstract void close();
+
+ /**
+ * @return true if the represented resource exists.
+ */
+ public abstract boolean exists();
+
+ /**
+ * @return true if the represented resource is a container/directory.
+ * if the resource is not a file, resources ending with "/" are
+ * considered directories.
+ */
+ public abstract boolean isDirectory();
+
+ /**
+ * Time resource was last modified.
+ *
+ * @return the last modified time as milliseconds since unix epoch
+ */
+ public abstract long lastModified();
+
+ /**
+ * Length of the resource.
+ *
+ * @return the length of the resource
+ */
+ public abstract long length();
+
+ /**
+ * URL representing the resource.
+ *
+ * @return a URL representing the given resource
+ * @deprecated use {{@link #getURI()}.toURL() instead.
+ */
+ @Deprecated
+ public abstract URL getURL();
+
+ /**
+ * URI representing the resource.
+ *
+ * @return an URI representing the given resource
+ */
+ public URI getURI()
+ {
+ try
+ {
+ return getURL().toURI();
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * File representing the given resource.
+ *
+ * @return an File representing the given resource or NULL if this
+ * is not possible.
+ * @throws IOException if unable to get the resource due to permissions
+ */
+ public abstract File getFile()
+ throws IOException;
+
+ /**
+ * The name of the resource.
+ *
+ * @return the name of the resource
+ */
+ public abstract String getName();
+
+ /**
+ * Input stream to the resource
+ *
+ * @return an input stream to the resource
+ * @throws IOException if unable to open the input stream
+ */
+ public abstract InputStream getInputStream()
+ throws IOException;
+
+ /**
+ * Readable ByteChannel for the resource.
+ *
+ * @return an readable bytechannel to the resource or null if one is not available.
+ * @throws IOException if unable to open the readable bytechannel for the resource.
+ */
+ public abstract ReadableByteChannel getReadableByteChannel()
+ throws IOException;
+
+ /**
+ * Deletes the given resource
+ *
+ * @return true if resource was found and successfully deleted, false if resource didn't exist or was unable to
+ * be deleted.
+ * @throws SecurityException if unable to delete due to permissions
+ */
+ public abstract boolean delete()
+ throws SecurityException;
+
+ /**
+ * Rename the given resource
+ *
+ * @param dest the destination name for the resource
+ * @return true if the resource was renamed, false if the resource didn't exist or was unable to be renamed.
+ * @throws SecurityException if unable to rename due to permissions
+ */
+ public abstract boolean renameTo(Resource dest)
+ throws SecurityException;
+
+ /**
+ * list of resource names contained in the given resource.
+ * Ordering is unspecified, so callers may wish to sort the return value to ensure deterministic behavior.
+ *
+ * @return a list of resource names contained in the given resource, or null.
+ * Note: The resource names are not URL encoded.
+ */
+ public abstract String[] list();
+
+ /**
+ * Returns the resource contained inside the current resource with the
+ * given name.
+ *
+ * @param path The path segment to add, which is not encoded. The path may be non canonical, but if so then
+ * the resulting Resource will return true from {@link #isAlias()}.
+ * @return the Resource for the resolved path within this Resource.
+ * @throws IOException if unable to resolve the path
+ * @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed, or
+ * a relative path attempts to access above the root resource.
+ */
+ public abstract Resource addPath(String path)
+ throws IOException, MalformedURLException;
+
+ /**
+ * Get a resource from within this resource.
+ * <p>
+ * This method is essentially an alias for {@link #addPath(String)}, but without checked exceptions.
+ * This method satisfied the {@link ResourceFactory} interface.
+ *
+ * @see org.eclipse.jetty.util.resource.ResourceFactory#getResource(java.lang.String)
+ */
+ @Override
+ public Resource getResource(String path)
+ {
+ try
+ {
+ return addPath(path);
+ }
+ catch (Exception e)
+ {
+ LOG.debug(e);
+ return null;
+ }
+ }
+
+ /**
+ * @param uri the uri to encode
+ * @return null (this is deprecated)
+ * @deprecated use {@link URIUtil} or {@link UrlEncoded} instead
+ */
+ @Deprecated
+ public String encode(String uri)
+ {
+ return null;
+ }
+
+ // FIXME: this appears to not be used
+ @SuppressWarnings("javadoc")
+ public Object getAssociate()
+ {
+ return _associate;
+ }
+
+ // FIXME: this appear to not be used
+ @SuppressWarnings("javadoc")
+ public void setAssociate(Object o)
+ {
+ _associate = o;
+ }
+
+ /**
+ * @return true if this Resource is an alias to another real Resource
+ */
+ public boolean isAlias()
+ {
+ return getAlias() != null;
+ }
+
+ /**
+ * @return The canonical Alias of this resource or null if none.
+ */
+ public URI getAlias()
+ {
+ return null;
+ }
+
+ /**
+ * Get the resource list as a HTML directory listing.
+ *
+ * @param base The base URL
+ * @param parent True if the parent directory should be included
+ * @return String of HTML
+ * @throws IOException if unable to get the list of resources as HTML
+ * @deprecated use {@link #getListHTML(String, boolean, String)} instead and supply raw query string.
+ */
+ @Deprecated
+ public String getListHTML(String base, boolean parent) throws IOException
+ {
+ return getListHTML(base, parent, null);
+ }
+
+ /**
+ * Get the resource list as a HTML directory listing.
+ *
+ * @param base The base URL
+ * @param parent True if the parent directory should be included
+ * @param query query params
+ * @return String of HTML
+ */
+ public String getListHTML(String base, boolean parent, String query) throws IOException
+ {
+ // This method doesn't check aliases, so it is OK to canonicalize here.
+ base = URIUtil.canonicalPath(base);
+ if (base == null || !isDirectory())
+ return null;
+
+ String[] rawListing = list();
+ if (rawListing == null)
+ {
+ return null;
+ }
+
+ boolean sortOrderAscending = true;
+ String sortColumn = "N"; // name (or "M" for Last Modified, or "S" for Size)
+
+ // check for query
+ if (query != null)
+ {
+ MultiMap<String> params = new MultiMap<>();
+ UrlEncoded.decodeUtf8To(query, 0, query.length(), params);
+
+ String paramO = params.getString("O");
+ String paramC = params.getString("C");
+ if (StringUtil.isNotBlank(paramO))
+ {
+ if (paramO.equals("A"))
+ {
+ sortOrderAscending = true;
+ }
+ else if (paramO.equals("D"))
+ {
+ sortOrderAscending = false;
+ }
+ }
+ if (StringUtil.isNotBlank(paramC))
+ {
+ if (paramC.equals("N") || paramC.equals("M") || paramC.equals("S"))
+ {
+ sortColumn = paramC;
+ }
+ }
+ }
+
+ // Gather up entries
+ List<Resource> items = new ArrayList<>();
+ for (String l : rawListing)
+ {
+ Resource item = addPath(l);
+ items.add(item);
+ }
+
+ // Perform sort
+ if (sortColumn.equals("M"))
+ {
+ Collections.sort(items, ResourceCollators.byLastModified(sortOrderAscending));
+ }
+ else if (sortColumn.equals("S"))
+ {
+ Collections.sort(items, ResourceCollators.bySize(sortOrderAscending));
+ }
+ else
+ {
+ Collections.sort(items, ResourceCollators.byName(sortOrderAscending));
+ }
+
+ String decodedBase = URIUtil.decodePath(base);
+ String title = "Directory: " + deTag(decodedBase);
+
+ StringBuilder buf = new StringBuilder(4096);
+
+ // Doctype Declaration (HTML5)
+ buf.append("<!DOCTYPE html>\n");
+ buf.append("<html lang=\"en\">\n");
+
+ // HTML Header
+ buf.append("<head>\n");
+ buf.append("<meta charset=\"utf-8\">\n");
+ buf.append("<link href=\"jetty-dir.css\" rel=\"stylesheet\" />\n");
+ buf.append("<title>");
+ buf.append(title);
+ buf.append("</title>\n");
+ buf.append("</head>\n");
+
+ // HTML Body
+ buf.append("<body>\n");
+ buf.append("<h1 class=\"title\">").append(title).append("</h1>\n");
+
+ // HTML Table
+ final String ARROW_DOWN = " ⇩";
+ final String ARROW_UP = " ⇧";
+ String arrow;
+ String order;
+
+ buf.append("<table class=\"listing\">\n");
+ buf.append("<thead>\n");
+
+ arrow = "";
+ order = "A";
+ if (sortColumn.equals("N"))
+ {
+ if (sortOrderAscending)
+ {
+ order = "D";
+ arrow = ARROW_UP;
+ }
+ else
+ {
+ order = "A";
+ arrow = ARROW_DOWN;
+ }
+ }
+
+ buf.append("<tr><th class=\"name\"><a href=\"?C=N&O=").append(order).append("\">");
+ buf.append("Name").append(arrow);
+ buf.append("</a></th>");
+
+ arrow = "";
+ order = "A";
+ if (sortColumn.equals("M"))
+ {
+ if (sortOrderAscending)
+ {
+ order = "D";
+ arrow = ARROW_UP;
+ }
+ else
+ {
+ order = "A";
+ arrow = ARROW_DOWN;
+ }
+ }
+
+ buf.append("<th class=\"lastmodified\"><a href=\"?C=M&O=").append(order).append("\">");
+ buf.append("Last Modified").append(arrow);
+ buf.append("</a></th>");
+
+ arrow = "";
+ order = "A";
+ if (sortColumn.equals("S"))
+ {
+ if (sortOrderAscending)
+ {
+ order = "D";
+ arrow = ARROW_UP;
+ }
+ else
+ {
+ order = "A";
+ arrow = ARROW_DOWN;
+ }
+ }
+ buf.append("<th class=\"size\"><a href=\"?C=S&O=").append(order).append("\">");
+ buf.append("Size").append(arrow);
+ buf.append("</a></th></tr>\n");
+ buf.append("</thead>\n");
+
+ buf.append("<tbody>\n");
+
+ String encodedBase = hrefEncodeURI(base);
+
+ if (parent)
+ {
+ // Name
+ buf.append("<tr><td class=\"name\"><a href=\"");
+ buf.append(URIUtil.addPaths(encodedBase, "../"));
+ buf.append("\">Parent Directory</a></td>");
+ // Last Modified
+ buf.append("<td class=\"lastmodified\">-</td>");
+ // Size
+ buf.append("<td>-</td>");
+ buf.append("</tr>\n");
+ }
+
+ DateFormat dfmt = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
+ DateFormat.MEDIUM);
+ for (Resource item : items)
+ {
+ String name = item.getFileName();
+ if (StringUtil.isBlank(name))
+ {
+ continue; // skip
+ }
+
+ if (item.isDirectory() && !name.endsWith("/"))
+ {
+ name += URIUtil.SLASH;
+ }
+
+ // Name
+ buf.append("<tr><td class=\"name\"><a href=\"");
+ String path = URIUtil.addEncodedPaths(encodedBase, URIUtil.encodePath(name));
+ buf.append(path);
+ buf.append("\">");
+ buf.append(deTag(name));
+ buf.append(" ");
+ buf.append("</a></td>");
+
+ // Last Modified
+ buf.append("<td class=\"lastmodified\">");
+ long lastModified = item.lastModified();
+ if (lastModified > 0)
+ {
+ buf.append(dfmt.format(new Date(item.lastModified())));
+ }
+ buf.append(" </td>");
+
+ // Size
+ buf.append("<td class=\"size\">");
+ long length = item.length();
+ if (length >= 0)
+ {
+ buf.append(String.format("%,d bytes", item.length()));
+ }
+ buf.append(" </td></tr>\n");
+ }
+ buf.append("</tbody>\n");
+ buf.append("</table>\n");
+ buf.append("</body></html>\n");
+
+ return buf.toString();
+ }
+
+ /**
+ * Get the raw (decoded if possible) Filename for this Resource.
+ * This is the last segment of the path.
+ *
+ * @return the raw / decoded filename for this resource
+ */
+ private String getFileName()
+ {
+ try
+ {
+ // if a Resource supports File
+ File file = getFile();
+ if (file != null)
+ {
+ return file.getName();
+ }
+ }
+ catch (Throwable ignored)
+ {
+ }
+
+ // All others use raw getName
+ try
+ {
+ String rawName = getName(); // gets long name "/foo/bar/xxx"
+ int idx = rawName.lastIndexOf('/');
+ if (idx == rawName.length() - 1)
+ {
+ // hit a tail slash, aka a name for a directory "/foo/bar/"
+ idx = rawName.lastIndexOf('/', idx - 1);
+ }
+
+ String encodedFileName;
+ if (idx >= 0)
+ {
+ encodedFileName = rawName.substring(idx + 1);
+ }
+ else
+ {
+ encodedFileName = rawName; // entire name
+ }
+ return UrlEncoded.decodeString(encodedFileName, 0, encodedFileName.length(), UTF_8);
+ }
+ catch (Throwable ignored)
+ {
+ }
+
+ return null;
+ }
+
+ /**
+ * Encode any characters that could break the URI string in an HREF.
+ * Such as <a href="/path/to;<script>Window.alert("XSS"+'%20'+"here");</script>">Link</a>
+ *
+ * The above example would parse incorrectly on various browsers as the "<" or '"' characters
+ * would end the href attribute value string prematurely.
+ *
+ * @param raw the raw text to encode.
+ * @return the defanged text.
+ */
+ private static String hrefEncodeURI(String raw)
+ {
+ StringBuffer buf = null;
+
+ loop:
+ for (int i = 0; i < raw.length(); i++)
+ {
+ char c = raw.charAt(i);
+ switch (c)
+ {
+ case '\'':
+ case '"':
+ case '<':
+ case '>':
+ buf = new StringBuffer(raw.length() << 1);
+ break loop;
+ }
+ }
+ if (buf == null)
+ return raw;
+
+ for (int i = 0; i < raw.length(); i++)
+ {
+ char c = raw.charAt(i);
+ switch (c)
+ {
+ case '"':
+ buf.append("%22");
+ continue;
+ case '\'':
+ buf.append("%27");
+ continue;
+ case '<':
+ buf.append("%3C");
+ continue;
+ case '>':
+ buf.append("%3E");
+ continue;
+ default:
+ buf.append(c);
+ continue;
+ }
+ }
+
+ return buf.toString();
+ }
+
+ private static String deTag(String raw)
+ {
+ return StringUtil.sanitizeXmlString(raw);
+ }
+
+ /**
+ * @param out the output stream to write to
+ * @param start First byte to write
+ * @param count Bytes to write or -1 for all of them.
+ * @throws IOException if unable to copy the Resource to the output
+ */
+ public void writeTo(OutputStream out, long start, long count)
+ throws IOException
+ {
+ try (InputStream in = getInputStream())
+ {
+ in.skip(start);
+ if (count < 0)
+ IO.copy(in, out);
+ else
+ IO.copy(in, out, count);
+ }
+ }
+
+ /**
+ * Copy the Resource to the new destination file.
+ * <p>
+ * Will not replace existing destination file.
+ *
+ * @param destination the destination file to create
+ * @throws IOException if unable to copy the resource
+ */
+ public void copyTo(File destination)
+ throws IOException
+ {
+ if (destination.exists())
+ throw new IllegalArgumentException(destination + " exists");
+
+ try (OutputStream out = new FileOutputStream(destination))
+ {
+ writeTo(out, 0, -1);
+ }
+ }
+
+ /**
+ * Generate a weak ETag reference for this Resource.
+ *
+ * @return the weak ETag reference for this resource.
+ */
+ public String getWeakETag()
+ {
+ return getWeakETag("");
+ }
+
+ public String getWeakETag(String suffix)
+ {
+ StringBuilder b = new StringBuilder(32);
+ b.append("W/\"");
+
+ String name = getName();
+ int length = name.length();
+ long lhash = 0;
+ for (int i = 0; i < length; i++)
+ {
+ lhash = 31 * lhash + name.charAt(i);
+ }
+
+ Base64.Encoder encoder = Base64.getEncoder().withoutPadding();
+ b.append(encoder.encodeToString(longToBytes(lastModified() ^ lhash)));
+ b.append(encoder.encodeToString(longToBytes(length() ^ lhash)));
+ b.append(suffix);
+ b.append('"');
+ return b.toString();
+ }
+
+ private static byte[] longToBytes(long value)
+ {
+ byte[] result = new byte[Long.BYTES];
+ for (int i = Long.BYTES - 1; i >= 0; i--)
+ {
+ result[i] = (byte)(value & 0xFF);
+ value >>= 8;
+ }
+ return result;
+ }
+
+ public Collection<Resource> getAllResources()
+ {
+ try
+ {
+ ArrayList<Resource> deep = new ArrayList<>();
+ {
+ String[] list = list();
+ if (list != null)
+ {
+ for (String i : list)
+ {
+ Resource r = addPath(i);
+ if (r.isDirectory())
+ deep.addAll(r.getAllResources());
+ else
+ deep.add(r);
+ }
+ }
+ }
+ return deep;
+ }
+ catch (Exception e)
+ {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Generate a properly encoded URL from a {@link File} instance.
+ *
+ * @param file Target file.
+ * @return URL of the target file.
+ * @throws MalformedURLException if unable to convert File to URL
+ */
+ public static URL toURL(File file) throws MalformedURLException
+ {
+ return file.toURI().toURL();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollators.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollators.java
new file mode 100644
index 0000000..dacd3e6
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollators.java
@@ -0,0 +1,104 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.text.Collator;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Locale;
+
+public class ResourceCollators
+{
+ private static Comparator<? super Resource> BY_NAME_ASCENDING =
+ new Comparator<Resource>()
+ {
+ private final Collator collator = Collator.getInstance(Locale.ENGLISH);
+
+ @Override
+ public int compare(Resource o1, Resource o2)
+ {
+ return collator.compare(o1.getName(), o2.getName());
+ }
+ };
+
+ private static Comparator<? super Resource> BY_NAME_DESCENDING =
+ Collections.reverseOrder(BY_NAME_ASCENDING);
+
+ private static Comparator<? super Resource> BY_LAST_MODIFIED_ASCENDING =
+ new Comparator<Resource>()
+ {
+ @Override
+ public int compare(Resource o1, Resource o2)
+ {
+ return Long.compare(o1.lastModified(), o2.lastModified());
+ }
+ };
+
+ private static Comparator<? super Resource> BY_LAST_MODIFIED_DESCENDING =
+ Collections.reverseOrder(BY_LAST_MODIFIED_ASCENDING);
+
+ private static Comparator<? super Resource> BY_SIZE_ASCENDING =
+ new Comparator<Resource>()
+ {
+ @Override
+ public int compare(Resource o1, Resource o2)
+ {
+ return Long.compare(o1.length(), o2.length());
+ }
+ };
+
+ private static Comparator<? super Resource> BY_SIZE_DESCENDING =
+ Collections.reverseOrder(BY_SIZE_ASCENDING);
+
+ public static Comparator<? super Resource> byLastModified(boolean sortOrderAscending)
+ {
+ if (sortOrderAscending)
+ {
+ return BY_LAST_MODIFIED_ASCENDING;
+ }
+ else
+ {
+ return BY_LAST_MODIFIED_DESCENDING;
+ }
+ }
+
+ public static Comparator<? super Resource> byName(boolean sortOrderAscending)
+ {
+ if (sortOrderAscending)
+ {
+ return BY_NAME_ASCENDING;
+ }
+ else
+ {
+ return BY_NAME_DESCENDING;
+ }
+ }
+
+ public static Comparator<? super Resource> bySize(boolean sortOrderAscending)
+ {
+ if (sortOrderAscending)
+ {
+ return BY_SIZE_ASCENDING;
+ }
+ else
+ {
+ return BY_SIZE_DESCENDING;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollection.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollection.java
new file mode 100644
index 0000000..6612e93
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollection.java
@@ -0,0 +1,520 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.channels.ReadableByteChannel;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.eclipse.jetty.util.URIUtil;
+
+/**
+ * A collection of resources (dirs).
+ * Allows webapps to have multiple (static) sources.
+ * The first resource in the collection is the main resource.
+ * If a resource is not found in the main resource, it looks it up in
+ * the order the resources were constructed.
+ */
+public class ResourceCollection extends Resource
+{
+ private Resource[] _resources;
+
+ /**
+ * Instantiates an empty resource collection.
+ * <p>
+ * This constructor is used when configuring jetty-maven-plugin.
+ */
+ public ResourceCollection()
+ {
+ _resources = new Resource[0];
+ }
+
+ /**
+ * Instantiates a new resource collection.
+ *
+ * @param resources the resources to be added to collection
+ */
+ public ResourceCollection(Resource... resources)
+ {
+ List<Resource> list = new ArrayList<>();
+ for (Resource r : resources)
+ {
+ if (r == null)
+ {
+ continue;
+ }
+ if (r instanceof ResourceCollection)
+ {
+ Collections.addAll(list, ((ResourceCollection)r).getResources());
+ }
+ else
+ {
+ list.add(r);
+ }
+ }
+ _resources = list.toArray(new Resource[0]);
+ for (Resource r : _resources)
+ {
+ assertResourceValid(r);
+ }
+ }
+
+ /**
+ * Instantiates a new resource collection.
+ *
+ * @param resources the resource strings to be added to collection
+ */
+ public ResourceCollection(String[] resources)
+ {
+ if (resources == null || resources.length == 0)
+ {
+ _resources = null;
+ return;
+ }
+
+ ArrayList<Resource> res = new ArrayList<>();
+
+ try
+ {
+ for (String strResource : resources)
+ {
+ if (strResource == null || strResource.length() == 0)
+ {
+ throw new IllegalArgumentException("empty/null resource path not supported");
+ }
+ Resource resource = Resource.newResource(strResource);
+ assertResourceValid(resource);
+ res.add(resource);
+ }
+
+ if (res.isEmpty())
+ {
+ _resources = null;
+ return;
+ }
+
+ _resources = res.toArray(new Resource[0]);
+ }
+ catch (RuntimeException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Instantiates a new resource collection.
+ *
+ * @param csvResources the string containing comma-separated resource strings
+ */
+ public ResourceCollection(String csvResources)
+ {
+ setResourcesAsCSV(csvResources);
+ }
+
+ /**
+ * Retrieves the resource collection's resources.
+ *
+ * @return the resource array
+ */
+ public Resource[] getResources()
+ {
+ return _resources;
+ }
+
+ /**
+ * Sets the resource collection's resources.
+ *
+ * @param resources the new resource array
+ */
+ public void setResources(Resource[] resources)
+ {
+ if (resources == null || resources.length == 0)
+ {
+ _resources = null;
+ return;
+ }
+
+ List<Resource> res = new ArrayList<>();
+ for (Resource resource : resources)
+ {
+ assertResourceValid(resource);
+ res.add(resource);
+ }
+
+ if (res.isEmpty())
+ {
+ _resources = null;
+ return;
+ }
+
+ _resources = res.toArray(new Resource[0]);
+ }
+
+ /**
+ * Sets the resources as string of comma-separated values.
+ * This method should be used when configuring jetty-maven-plugin.
+ *
+ * @param csvResources the comma-separated string containing
+ * one or more resource strings.
+ */
+ public void setResourcesAsCSV(String csvResources)
+ {
+ if (csvResources == null)
+ {
+ throw new IllegalArgumentException("CSV String is null");
+ }
+
+ StringTokenizer tokenizer = new StringTokenizer(csvResources, ",;");
+ int len = tokenizer.countTokens();
+ if (len == 0)
+ {
+ throw new IllegalArgumentException("ResourceCollection@setResourcesAsCSV(String) " +
+ " argument must be a string containing one or more comma-separated resource strings.");
+ }
+
+ List<Resource> res = new ArrayList<>();
+
+ try
+ {
+ while (tokenizer.hasMoreTokens())
+ {
+ String token = tokenizer.nextToken().trim();
+ // TODO: If we want to support CSV tokens with spaces then we should not trim here
+ // However, if we decide to to this, then CVS formatting/syntax becomes more strict.
+ if (token.length() == 0)
+ {
+ continue; // skip
+ }
+ Resource resource = Resource.newResource(token);
+ assertResourceValid(resource);
+ res.add(resource);
+ }
+
+ if (res.isEmpty())
+ {
+ _resources = null;
+ return;
+ }
+
+ _resources = res.toArray(new Resource[0]);
+ }
+ catch (RuntimeException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * @param path The path segment to add
+ * @return The contained resource (found first) in the collection of resources
+ */
+ @Override
+ public Resource addPath(String path) throws IOException
+ {
+ assertResourcesSet();
+
+ if (path == null)
+ {
+ throw new MalformedURLException();
+ }
+
+ if (path.length() == 0 || URIUtil.SLASH.equals(path))
+ {
+ return this;
+ }
+
+ Resource resource = null;
+ ArrayList<Resource> resources = null;
+ int i = 0;
+ for (; i < _resources.length; i++)
+ {
+ resource = _resources[i].addPath(path);
+ if (resource.exists())
+ {
+ if (resource.isDirectory())
+ {
+ break;
+ }
+ return resource;
+ }
+ }
+
+ for (i++; i < _resources.length; i++)
+ {
+ Resource r = _resources[i].addPath(path);
+ if (r.exists() && r.isDirectory())
+ {
+ if (resources == null)
+ {
+ resources = new ArrayList<>();
+ }
+
+ if (resource != null)
+ {
+ resources.add(resource);
+ resource = null;
+ }
+
+ resources.add(r);
+ }
+ }
+
+ if (resource != null)
+ {
+ return resource;
+ }
+ if (resources != null)
+ {
+ return new ResourceCollection(resources.toArray(new Resource[0]));
+ }
+
+ throw new MalformedURLException();
+ }
+
+ @Override
+ public boolean delete() throws SecurityException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean exists()
+ {
+ assertResourcesSet();
+
+ return true;
+ }
+
+ @Override
+ public File getFile() throws IOException
+ {
+ assertResourcesSet();
+
+ for (Resource r : _resources)
+ {
+ File f = r.getFile();
+ if (f != null)
+ {
+ return f;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException
+ {
+ assertResourcesSet();
+
+ for (Resource r : _resources)
+ {
+ InputStream is = r.getInputStream();
+ if (is != null)
+ {
+ return is;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public ReadableByteChannel getReadableByteChannel() throws IOException
+ {
+ assertResourcesSet();
+
+ for (Resource r : _resources)
+ {
+ ReadableByteChannel channel = r.getReadableByteChannel();
+ if (channel != null)
+ {
+ return channel;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getName()
+ {
+ assertResourcesSet();
+
+ for (Resource r : _resources)
+ {
+ String name = r.getName();
+ if (name != null)
+ {
+ return name;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public URL getURL()
+ {
+ assertResourcesSet();
+
+ for (Resource r : _resources)
+ {
+ URL url = r.getURL();
+ if (url != null)
+ {
+ return url;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isDirectory()
+ {
+ assertResourcesSet();
+
+ return true;
+ }
+
+ @Override
+ public long lastModified()
+ {
+ assertResourcesSet();
+
+ for (Resource r : _resources)
+ {
+ long lm = r.lastModified();
+ if (lm != -1)
+ {
+ return lm;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public long length()
+ {
+ return -1;
+ }
+
+ /**
+ * @return The list of resource names(merged) contained in the collection of resources.
+ */
+ @Override
+ public String[] list()
+ {
+ assertResourcesSet();
+ HashSet<String> set = new HashSet<>();
+ for (Resource r : _resources)
+ {
+ String[] list = r.list();
+ if (list != null)
+ Collections.addAll(set, list);
+ }
+ String[] result = set.toArray(new String[0]);
+ Arrays.sort(result);
+ return result;
+ }
+
+ @Override
+ public void close()
+ {
+ assertResourcesSet();
+
+ for (Resource r : _resources)
+ {
+ r.close();
+ }
+ }
+
+ @Override
+ public boolean renameTo(Resource dest) throws SecurityException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void copyTo(File destination)
+ throws IOException
+ {
+ assertResourcesSet();
+
+ for (int r = _resources.length; r-- > 0; )
+ {
+ _resources[r].copyTo(destination);
+ }
+ }
+
+ /**
+ * @return the list of resources separated by a path separator
+ */
+ @Override
+ public String toString()
+ {
+ if (_resources == null || _resources.length == 0)
+ {
+ return "[]";
+ }
+
+ return String.valueOf(Arrays.asList(_resources));
+ }
+
+ @Override
+ public boolean isContainedIn(Resource r)
+ {
+ // TODO could look at implementing the semantic of is this collection a subset of the Resource r?
+ return false;
+ }
+
+ private void assertResourcesSet()
+ {
+ if (_resources == null || _resources.length == 0)
+ {
+ throw new IllegalStateException("*resources* not set.");
+ }
+ }
+
+ private void assertResourceValid(Resource resource)
+ {
+ if (resource == null)
+ {
+ throw new IllegalStateException("Null resource not supported");
+ }
+
+ if (!resource.exists() || !resource.isDirectory())
+ {
+ throw new IllegalArgumentException(resource + " is not an existing directory.");
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactory.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactory.java
new file mode 100644
index 0000000..eac324c
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactory.java
@@ -0,0 +1,34 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+/**
+ * ResourceFactory.
+ */
+public interface ResourceFactory
+{
+
+ /**
+ * Get a resource for a path.
+ *
+ * @param path The path to the resource
+ * @return The resource or null
+ */
+ Resource getResource(String path);
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java
new file mode 100644
index 0000000..40d68ab
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java
@@ -0,0 +1,311 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.channels.ReadableByteChannel;
+
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * URL resource class.
+ */
+public class URLResource extends Resource
+{
+ private static final Logger LOG = Log.getLogger(URLResource.class);
+ protected final URL _url;
+ protected final String _urlString;
+
+ protected URLConnection _connection;
+ protected InputStream _in = null;
+ transient boolean _useCaches = Resource.__defaultUseCaches;
+
+ protected URLResource(URL url, URLConnection connection)
+ {
+ _url = url;
+ _urlString = _url.toExternalForm();
+ _connection = connection;
+ }
+
+ protected URLResource(URL url, URLConnection connection, boolean useCaches)
+ {
+ this(url, connection);
+ _useCaches = useCaches;
+ }
+
+ protected synchronized boolean checkConnection()
+ {
+ if (_connection == null)
+ {
+ try
+ {
+ _connection = _url.openConnection();
+ _connection.setUseCaches(_useCaches);
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ }
+ }
+ return _connection != null;
+ }
+
+ /**
+ * Release any resources held by the resource.
+ */
+ @Override
+ public synchronized void close()
+ {
+ if (_in != null)
+ {
+ try
+ {
+ _in.close();
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ }
+ _in = null;
+ }
+
+ if (_connection != null)
+ _connection = null;
+ }
+
+ /**
+ * Returns true if the represented resource exists.
+ */
+ @Override
+ public boolean exists()
+ {
+ try
+ {
+ synchronized (this)
+ {
+ if (checkConnection() && _in == null)
+ _in = _connection.getInputStream();
+ }
+ }
+ catch (IOException e)
+ {
+ LOG.ignore(e);
+ }
+ return _in != null;
+ }
+
+ /**
+ * Returns true if the represented resource is a container/directory.
+ * If the resource is not a file, resources ending with "/" are
+ * considered directories.
+ */
+ @Override
+ public boolean isDirectory()
+ {
+ return exists() && _urlString.endsWith("/");
+ }
+
+ /**
+ * Returns the last modified time
+ */
+ @Override
+ public long lastModified()
+ {
+ if (checkConnection())
+ return _connection.getLastModified();
+ return -1;
+ }
+
+ /**
+ * Return the length of the resource
+ */
+ @Override
+ public long length()
+ {
+ if (checkConnection())
+ return _connection.getContentLength();
+ return -1;
+ }
+
+ /**
+ * Returns a URL representing the given resource
+ */
+ @Override
+ public URL getURL()
+ {
+ return _url;
+ }
+
+ /**
+ * Returns an File representing the given resource or NULL if this
+ * is not possible.
+ */
+ @Override
+ public File getFile()
+ throws IOException
+ {
+ return null;
+ }
+
+ /**
+ * Returns the name of the resource
+ */
+ @Override
+ public String getName()
+ {
+ return _url.toExternalForm();
+ }
+
+ /**
+ * Returns an input stream to the resource. The underlying
+ * url connection will be nulled out to prevent re-use.
+ */
+ @Override
+ public synchronized InputStream getInputStream()
+ throws java.io.IOException
+ {
+ return getInputStream(true); //backwards compatibility
+ }
+
+ /**
+ * Returns an input stream to the resource, optionally nulling
+ * out the underlying url connection. If the connection is not
+ * nulled out, a subsequent call to getInputStream() may return
+ * an existing and already in-use input stream - this depends on
+ * the url protocol. Eg JarURLConnection does not reuse inputstreams.
+ *
+ * @param resetConnection if true the connection field is set to null
+ * @return the inputstream for this resource
+ * @throws IOException if unable to open the input stream
+ */
+ protected synchronized InputStream getInputStream(boolean resetConnection)
+ throws IOException
+ {
+ if (!checkConnection())
+ throw new IOException("Invalid resource");
+
+ try
+ {
+ if (_in != null)
+ {
+ InputStream in = _in;
+ _in = null;
+ return in;
+ }
+ return _connection.getInputStream();
+ }
+ finally
+ {
+ if (resetConnection)
+ {
+ _connection = null;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Connection nulled");
+ }
+ }
+ }
+
+ @Override
+ public ReadableByteChannel getReadableByteChannel() throws IOException
+ {
+ return null;
+ }
+
+ /**
+ * Deletes the given resource
+ */
+ @Override
+ public boolean delete()
+ throws SecurityException
+ {
+ throw new SecurityException("Delete not supported");
+ }
+
+ /**
+ * Rename the given resource
+ */
+ @Override
+ public boolean renameTo(Resource dest)
+ throws SecurityException
+ {
+ throw new SecurityException("RenameTo not supported");
+ }
+
+ /**
+ * Returns a list of resource names contained in the given resource
+ */
+ @Override
+ public String[] list()
+ {
+ return null;
+ }
+
+ /**
+ * Returns the resource contained inside the current resource with the
+ * given name
+ */
+ @Override
+ public Resource addPath(String path)
+ throws IOException
+ {
+ // Check that the path is within the root,
+ // but use the original path to create the
+ // resource, to preserve aliasing.
+ if (URIUtil.canonicalPath(path) == null)
+ throw new MalformedURLException(path);
+
+ return newResource(URIUtil.addEncodedPaths(_url.toExternalForm(), URIUtil.encodePath(path)), _useCaches);
+ }
+
+ @Override
+ public String toString()
+ {
+ return _urlString;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return _urlString.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ return o instanceof URLResource && _urlString.equals(((URLResource)o)._urlString);
+ }
+
+ public boolean getUseCaches()
+ {
+ return _useCaches;
+ }
+
+ @Override
+ public boolean isContainedIn(Resource containingResource) throws MalformedURLException
+ {
+ return false;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/package-info.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/package-info.java
new file mode 100644
index 0000000..5a97b59
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/resource/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Util : Common Resource Utilities
+ */
+package org.eclipse.jetty.util.resource;
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/CertificateUtils.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/CertificateUtils.java
new file mode 100644
index 0000000..6b65f69
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/CertificateUtils.java
@@ -0,0 +1,84 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.security;
+
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.cert.CRL;
+import java.security.cert.CertificateFactory;
+import java.util.Collection;
+import java.util.Objects;
+
+import org.eclipse.jetty.util.resource.Resource;
+
+public class CertificateUtils
+{
+
+ public static KeyStore getKeyStore(Resource store, String storeType, String storeProvider, String storePassword) throws Exception
+ {
+ KeyStore keystore = null;
+
+ if (store != null)
+ {
+ Objects.requireNonNull(storeType, "storeType cannot be null");
+ if (storeProvider != null)
+ {
+ keystore = KeyStore.getInstance(storeType, storeProvider);
+ }
+ else
+ {
+ keystore = KeyStore.getInstance(storeType);
+ }
+
+ if (!store.exists())
+ throw new IllegalStateException(store.getName() + " is not a valid keystore");
+
+ try (InputStream inStream = store.getInputStream())
+ {
+ keystore.load(inStream, storePassword == null ? null : storePassword.toCharArray());
+ }
+ }
+
+ return keystore;
+ }
+
+ public static Collection<? extends CRL> loadCRL(String crlPath) throws Exception
+ {
+ Collection<? extends CRL> crlList = null;
+
+ if (crlPath != null)
+ {
+ InputStream in = null;
+ try
+ {
+ in = Resource.newResource(crlPath).getInputStream();
+ crlList = CertificateFactory.getInstance("X.509").generateCRLs(in);
+ }
+ finally
+ {
+ if (in != null)
+ {
+ in.close();
+ }
+ }
+ }
+
+ return crlList;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/CertificateValidator.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/CertificateValidator.java
new file mode 100644
index 0000000..4f33b0b
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/CertificateValidator.java
@@ -0,0 +1,346 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.security;
+
+import java.security.GeneralSecurityException;
+import java.security.InvalidParameterException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.Security;
+import java.security.cert.CRL;
+import java.security.cert.CertPathBuilder;
+import java.security.cert.CertPathBuilderResult;
+import java.security.cert.CertPathValidator;
+import java.security.cert.CertStore;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CollectionCertStoreParameters;
+import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.X509CertSelector;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Convenience class to handle validation of certificates, aliases and keystores
+ *
+ * Allows specifying Certificate Revocation List (CRL), as well as enabling
+ * CRL Distribution Points Protocol (CRLDP) certificate extension support,
+ * and also enabling On-Line Certificate Status Protocol (OCSP) support.
+ *
+ * IMPORTANT: at least one of the above mechanisms *MUST* be configured and
+ * operational, otherwise certificate validation *WILL FAIL* unconditionally.
+ */
+public class CertificateValidator
+{
+ private static final Logger LOG = Log.getLogger(CertificateValidator.class);
+ private static AtomicLong __aliasCount = new AtomicLong();
+
+ private KeyStore _trustStore;
+ private Collection<? extends CRL> _crls;
+
+ /**
+ * Maximum certification path length (n - number of intermediate certs, -1 for unlimited)
+ */
+ private int _maxCertPathLength = -1;
+ /**
+ * CRL Distribution Points (CRLDP) support
+ */
+ private boolean _enableCRLDP = false;
+ /**
+ * On-Line Certificate Status Protocol (OCSP) support
+ */
+ private boolean _enableOCSP = false;
+ /**
+ * Location of OCSP Responder
+ */
+ private String _ocspResponderURL;
+
+ /**
+ * creates an instance of the certificate validator
+ *
+ * @param trustStore the truststore to use
+ * @param crls the Certificate Revocation List to use
+ */
+ public CertificateValidator(KeyStore trustStore, Collection<? extends CRL> crls)
+ {
+ if (trustStore == null)
+ {
+ throw new InvalidParameterException("TrustStore must be specified for CertificateValidator.");
+ }
+
+ _trustStore = trustStore;
+ _crls = crls;
+ }
+
+ /**
+ * validates all aliases inside of a given keystore
+ *
+ * @param keyStore the keystore to validate
+ * @throws CertificateException if keystore error and unable to validate
+ */
+ public void validate(KeyStore keyStore) throws CertificateException
+ {
+ try
+ {
+ Enumeration<String> aliases = keyStore.aliases();
+
+ for (; aliases.hasMoreElements(); )
+ {
+ String alias = aliases.nextElement();
+
+ validate(keyStore, alias);
+ }
+ }
+ catch (KeyStoreException kse)
+ {
+ throw new CertificateException("Unable to retrieve aliases from keystore", kse);
+ }
+ }
+
+ /**
+ * validates a specific alias inside of the keystore being passed in
+ *
+ * @param keyStore the keystore to validate
+ * @param keyAlias the keyalias in the keystore to valid with
+ * @return the keyAlias if valid
+ * @throws CertificateException if keystore error and unable to validate
+ */
+ public String validate(KeyStore keyStore, String keyAlias) throws CertificateException
+ {
+ String result = null;
+
+ if (keyAlias != null)
+ {
+ try
+ {
+ validate(keyStore, keyStore.getCertificate(keyAlias));
+ }
+ catch (KeyStoreException kse)
+ {
+ LOG.debug(kse);
+ throw new CertificateException("Unable to validate certificate" +
+ " for alias [" + keyAlias + "]: " + kse.getMessage(), kse);
+ }
+ result = keyAlias;
+ }
+
+ return result;
+ }
+
+ /**
+ * validates a specific certificate inside of the keystore being passed in
+ *
+ * @param keyStore the keystore to validate against
+ * @param cert the certificate to validate
+ * @throws CertificateException if keystore error and unable to validate
+ */
+ public void validate(KeyStore keyStore, Certificate cert) throws CertificateException
+ {
+ Certificate[] certChain = null;
+
+ if (cert != null && cert instanceof X509Certificate)
+ {
+ ((X509Certificate)cert).checkValidity();
+
+ String certAlias = null;
+ try
+ {
+ if (keyStore == null)
+ {
+ throw new InvalidParameterException("Keystore cannot be null");
+ }
+
+ certAlias = keyStore.getCertificateAlias(cert);
+ if (certAlias == null)
+ {
+ certAlias = "JETTY" + String.format("%016X", __aliasCount.incrementAndGet());
+ keyStore.setCertificateEntry(certAlias, cert);
+ }
+
+ certChain = keyStore.getCertificateChain(certAlias);
+ if (certChain == null || certChain.length == 0)
+ {
+ throw new IllegalStateException("Unable to retrieve certificate chain");
+ }
+ }
+ catch (KeyStoreException kse)
+ {
+ LOG.debug(kse);
+ throw new CertificateException("Unable to validate certificate" +
+ (certAlias == null ? "" : " for alias [" + certAlias + "]") + ": " + kse.getMessage(), kse);
+ }
+
+ validate(certChain);
+ }
+ }
+
+ public void validate(Certificate[] certChain) throws CertificateException
+ {
+ try
+ {
+ ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>();
+ for (Certificate item : certChain)
+ {
+ if (item == null)
+ continue;
+
+ if (!(item instanceof X509Certificate))
+ {
+ throw new IllegalStateException("Invalid certificate type in chain");
+ }
+
+ certList.add((X509Certificate)item);
+ }
+
+ if (certList.isEmpty())
+ {
+ throw new IllegalStateException("Invalid certificate chain");
+ }
+
+ X509CertSelector certSelect = new X509CertSelector();
+ certSelect.setCertificate(certList.get(0));
+
+ // Configure certification path builder parameters
+ PKIXBuilderParameters pbParams = new PKIXBuilderParameters(_trustStore, certSelect);
+ pbParams.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(certList)));
+
+ // Set maximum certification path length
+ pbParams.setMaxPathLength(_maxCertPathLength);
+
+ // Enable revocation checking
+ pbParams.setRevocationEnabled(true);
+
+ // Set static Certificate Revocation List
+ if (_crls != null && !_crls.isEmpty())
+ {
+ pbParams.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(_crls)));
+ }
+
+ // Enable On-Line Certificate Status Protocol (OCSP) support
+ if (_enableOCSP)
+ {
+ Security.setProperty("ocsp.enable", "true");
+ }
+ // Enable Certificate Revocation List Distribution Points (CRLDP) support
+ if (_enableCRLDP)
+ {
+ System.setProperty("com.sun.security.enableCRLDP", "true");
+ }
+
+ // Build certification path
+ CertPathBuilderResult buildResult = CertPathBuilder.getInstance("PKIX").build(pbParams);
+
+ // Validate certification path
+ CertPathValidator.getInstance("PKIX").validate(buildResult.getCertPath(), pbParams);
+ }
+ catch (GeneralSecurityException gse)
+ {
+ LOG.debug(gse);
+ throw new CertificateException("Unable to validate certificate: " + gse.getMessage(), gse);
+ }
+ }
+
+ public KeyStore getTrustStore()
+ {
+ return _trustStore;
+ }
+
+ public Collection<? extends CRL> getCrls()
+ {
+ return _crls;
+ }
+
+ /**
+ * @return Maximum number of intermediate certificates in
+ * the certification path (-1 for unlimited)
+ */
+ public int getMaxCertPathLength()
+ {
+ return _maxCertPathLength;
+ }
+
+ /**
+ * @param maxCertPathLength maximum number of intermediate certificates in
+ * the certification path (-1 for unlimited)
+ */
+ public void setMaxCertPathLength(int maxCertPathLength)
+ {
+ _maxCertPathLength = maxCertPathLength;
+ }
+
+ /**
+ * @return true if CRL Distribution Points support is enabled
+ */
+ public boolean isEnableCRLDP()
+ {
+ return _enableCRLDP;
+ }
+
+ /**
+ * Enables CRL Distribution Points Support
+ *
+ * @param enableCRLDP true - turn on, false - turns off
+ */
+ public void setEnableCRLDP(boolean enableCRLDP)
+ {
+ _enableCRLDP = enableCRLDP;
+ }
+
+ /**
+ * @return true if On-Line Certificate Status Protocol support is enabled
+ */
+ public boolean isEnableOCSP()
+ {
+ return _enableOCSP;
+ }
+
+ /**
+ * Enables On-Line Certificate Status Protocol support
+ *
+ * @param enableOCSP true - turn on, false - turn off
+ */
+ public void setEnableOCSP(boolean enableOCSP)
+ {
+ _enableOCSP = enableOCSP;
+ }
+
+ /**
+ * @return Location of the OCSP Responder
+ */
+ public String getOcspResponderURL()
+ {
+ return _ocspResponderURL;
+ }
+
+ /**
+ * Set the location of the OCSP Responder.
+ *
+ * @param ocspResponderURL location of the OCSP Responder
+ */
+ public void setOcspResponderURL(String ocspResponderURL)
+ {
+ _ocspResponderURL = ocspResponderURL;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/Constraint.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/Constraint.java
new file mode 100644
index 0000000..71090bc
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/Constraint.java
@@ -0,0 +1,236 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.security;
+
+import java.io.Serializable;
+import java.util.Arrays;
+
+/**
+ * Constraint
+ *
+ * Describe an auth and/or data constraint.
+ */
+public class Constraint implements Cloneable, Serializable
+{
+
+ public static final String __BASIC_AUTH = "BASIC";
+ public static final String __FORM_AUTH = "FORM";
+ public static final String __DIGEST_AUTH = "DIGEST";
+ public static final String __CERT_AUTH = "CLIENT_CERT";
+ public static final String __CERT_AUTH2 = "CLIENT-CERT";
+ public static final String __SPNEGO_AUTH = "SPNEGO";
+ public static final String __NEGOTIATE_AUTH = "NEGOTIATE";
+ public static final String __OPENID_AUTH = "OPENID";
+
+ public static boolean validateMethod(String method)
+ {
+ if (method == null)
+ return false;
+ method = method.trim();
+ return (method.equals(__FORM_AUTH) ||
+ method.equals(__BASIC_AUTH) ||
+ method.equals(__DIGEST_AUTH) ||
+ method.equals(__CERT_AUTH) ||
+ method.equals(__CERT_AUTH2) ||
+ method.equals(__SPNEGO_AUTH) ||
+ method.equals(__NEGOTIATE_AUTH) ||
+ method.equals(__OPENID_AUTH));
+ }
+
+ public static final int DC_UNSET = -1;
+ public static final int DC_NONE = 0;
+ public static final int DC_INTEGRAL = 1;
+ public static final int DC_CONFIDENTIAL = 2;
+ public static final int DC_FORBIDDEN = 3;
+
+ public static final String NONE = "NONE";
+
+ public static final String ANY_ROLE = "*";
+
+ public static final String ANY_AUTH = "**"; //Servlet Spec 3.1 pg 140
+
+ private String _name;
+
+ private String[] _roles;
+
+ private int _dataConstraint = DC_UNSET;
+
+ private boolean _anyRole = false;
+
+ private boolean _anyAuth = false;
+
+ private boolean _authenticate = false;
+
+ /**
+ * Constructor.
+ */
+ public Constraint()
+ {
+ }
+
+ /**
+ * Convenience Constructor.
+ *
+ * @param name the name
+ * @param role the role
+ */
+ public Constraint(String name, String role)
+ {
+ setName(name);
+ setRoles(new String[]{role});
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException
+ {
+ return super.clone();
+ }
+
+ /**
+ * @param name the name
+ */
+ public void setName(String name)
+ {
+ _name = name;
+ }
+
+ public String getName()
+ {
+ return _name;
+ }
+
+ public void setRoles(String[] roles)
+ {
+ _roles = roles;
+ _anyRole = false;
+ _anyAuth = false;
+ if (roles != null)
+ {
+ for (int i = roles.length; i-- > 0; )
+ {
+ _anyRole |= ANY_ROLE.equals(roles[i]);
+ _anyAuth |= ANY_AUTH.equals(roles[i]);
+ }
+ }
+ }
+
+ /**
+ * @return True if any user role is permitted.
+ */
+ public boolean isAnyRole()
+ {
+ return _anyRole;
+ }
+
+ /**
+ * Servlet Spec 3.1, pg 140
+ *
+ * @return True if any authenticated user is permitted (ie a role "**" was specified in the constraint).
+ */
+ public boolean isAnyAuth()
+ {
+ return _anyAuth;
+ }
+
+ /**
+ * @return List of roles for this constraint.
+ */
+ public String[] getRoles()
+ {
+ return _roles;
+ }
+
+ /**
+ * @param role the role
+ * @return True if the constraint contains the role.
+ */
+ public boolean hasRole(String role)
+ {
+ if (_anyRole)
+ return true;
+ if (_roles != null)
+ for (int i = _roles.length; i-- > 0; )
+ {
+ if (role.equals(_roles[i]))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @param authenticate True if users must be authenticated
+ */
+ public void setAuthenticate(boolean authenticate)
+ {
+ _authenticate = authenticate;
+ }
+
+ /**
+ * @return True if the constraint requires request authentication
+ */
+ public boolean getAuthenticate()
+ {
+ return _authenticate;
+ }
+
+ /**
+ * @return True if authentication required but no roles set
+ */
+ public boolean isForbidden()
+ {
+ return _authenticate && !_anyRole && (_roles == null || _roles.length == 0);
+ }
+
+ /**
+ * @param c Data constrain indicator: 0=DC+NONE, 1=DC_INTEGRAL &
+ * 2=DC_CONFIDENTIAL
+ */
+ public void setDataConstraint(int c)
+ {
+ if (c < 0 || c > DC_CONFIDENTIAL)
+ throw new IllegalArgumentException("Constraint out of range");
+ _dataConstraint = c;
+ }
+
+ /**
+ * @return Data constrain indicator: 0=DC+NONE, 1=DC_INTEGRAL &
+ * 2=DC_CONFIDENTIAL
+ */
+ public int getDataConstraint()
+ {
+ return _dataConstraint;
+ }
+
+ /**
+ * @return True if a data constraint has been set.
+ */
+ public boolean hasDataConstraint()
+ {
+ return _dataConstraint >= DC_NONE;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "SC{" +
+ _name + "," +
+ (_anyRole ? "*" : (_roles == null ? "-" : Arrays.asList(_roles).toString())) + "," +
+ (_dataConstraint == DC_UNSET ? "DC_UNSET}" : (_dataConstraint == DC_NONE ? "NONE}" : (_dataConstraint == DC_INTEGRAL ? "INTEGRAL}" : "CONFIDENTIAL}")));
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/Credential.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/Credential.java
new file mode 100644
index 0000000..b41713f
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/Credential.java
@@ -0,0 +1,284 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.security;
+
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ServiceLoader;
+
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Credentials. The Credential class represents an abstract mechanism for checking authentication credentials. A credential instance either represents a secret,
+ * or some data that could only be derived from knowing the secret.
+ * <p>
+ * Often a Credential is related to a Password via a one way algorithm, so while a Password itself is a Credential, a UnixCrypt or MD5 digest of a a password is
+ * only a credential that can be checked against the password.
+ * <p>
+ * This class includes an implementation for unix Crypt an MD5 digest.
+ *
+ * @see Password
+ */
+public abstract class Credential implements Serializable
+{
+ private static final long serialVersionUID = -7760551052768181572L;
+ private static final Logger LOG = Log.getLogger(Credential.class);
+ private static final ServiceLoader<CredentialProvider> CREDENTIAL_PROVIDER_LOADER = ServiceLoader.load(CredentialProvider.class);
+
+ /**
+ * Check a credential
+ *
+ * @param credentials The credential to check against. This may either be another Credential object, a Password object or a String which is interpreted by this
+ * credential.
+ * @return True if the credentials indicated that the shared secret is known to both this Credential and the passed credential.
+ */
+ public abstract boolean check(Object credentials);
+
+ /**
+ * Get a credential from a String. If the credential String starts with a known Credential type (eg "CRYPT:" or "MD5:" ) then a Credential of that type is
+ * returned. Otherwise, it tries to find a credential provider whose prefix matches with the start of the credential String. Else the credential is assumed
+ * to be a Password.
+ *
+ * @param credential String representation of the credential
+ * @return A Credential or Password instance.
+ */
+ public static Credential getCredential(String credential)
+ {
+ if (credential.startsWith(Crypt.__TYPE))
+ return new Crypt(credential);
+ if (credential.startsWith(MD5.__TYPE))
+ return new MD5(credential);
+
+ for (CredentialProvider cp : CREDENTIAL_PROVIDER_LOADER)
+ {
+ if (credential.startsWith(cp.getPrefix()))
+ {
+ final Credential credentialObj = cp.getCredential(credential);
+ if (credentialObj != null)
+ {
+ return credentialObj;
+ }
+ }
+ }
+
+ return new Password(credential);
+ }
+
+ /**
+ * <p>Utility method that replaces String.equals() to avoid timing attacks.
+ * The length of the loop executed will always be the length of the unknown credential</p>
+ *
+ * @param known the first string to compare (should be known string)
+ * @param unknown the second string to compare (should be the unknown string)
+ * @return whether the two strings are equal
+ */
+ protected static boolean stringEquals(String known, String unknown)
+ {
+ @SuppressWarnings("ReferenceEquality")
+ boolean sameObject = (known == unknown);
+ if (sameObject)
+ return true;
+ if (known == null || unknown == null)
+ return false;
+ boolean result = true;
+ int l1 = known.length();
+ int l2 = unknown.length();
+ for (int i = 0; i < l2; ++i)
+ {
+ result &= ((l1 == 0) ? unknown.charAt(l2 - i - 1) : known.charAt(i % l1)) == unknown.charAt(i);
+ }
+ return result && l1 == l2;
+ }
+
+ /**
+ * <p>Utility method that replaces Arrays.equals() to avoid timing attacks.
+ * The length of the loop executed will always be the length of the unknown credential</p>
+ *
+ * @param known the first byte array to compare (should be known value)
+ * @param unknown the second byte array to compare (should be unknown value)
+ * @return whether the two byte arrays are equal
+ */
+ protected static boolean byteEquals(byte[] known, byte[] unknown)
+ {
+ if (known == unknown)
+ return true;
+ if (known == null || unknown == null)
+ return false;
+ boolean result = true;
+ int l1 = known.length;
+ int l2 = unknown.length;
+ for (int i = 0; i < l2; ++i)
+ {
+ result &= ((l1 == 0) ? unknown[l2 - i - 1] : known[i % l1]) == unknown[i];
+ }
+ return result && l1 == l2;
+ }
+
+ /**
+ * Unix Crypt Credentials
+ */
+ public static class Crypt extends Credential
+ {
+ private static final long serialVersionUID = -2027792997664744210L;
+ private static final String __TYPE = "CRYPT:";
+
+ private final String _cooked;
+
+ Crypt(String cooked)
+ {
+ _cooked = cooked.startsWith(Crypt.__TYPE) ? cooked.substring(__TYPE.length()) : cooked;
+ }
+
+ @Override
+ public boolean check(Object credentials)
+ {
+ if (credentials instanceof char[])
+ credentials = new String((char[])credentials);
+ if (!(credentials instanceof String) && !(credentials instanceof Password))
+ LOG.warn("Can't check " + credentials.getClass() + " against CRYPT");
+ return stringEquals(_cooked, UnixCrypt.crypt(credentials.toString(), _cooked));
+ }
+
+ @Override
+ public boolean equals(Object credential)
+ {
+ if (!(credential instanceof Crypt))
+ return false;
+ Crypt c = (Crypt)credential;
+ return stringEquals(_cooked, c._cooked);
+ }
+
+ public static String crypt(String user, String pw)
+ {
+ return __TYPE + UnixCrypt.crypt(pw, user);
+ }
+ }
+
+ /**
+ * MD5 Credentials
+ */
+ public static class MD5 extends Credential
+ {
+ private static final long serialVersionUID = 5533846540822684240L;
+ private static final String __TYPE = "MD5:";
+ private static final Object __md5Lock = new Object();
+ private static MessageDigest __md;
+
+ private final byte[] _digest;
+
+ MD5(String digest)
+ {
+ digest = digest.startsWith(__TYPE) ? digest.substring(__TYPE.length()) : digest;
+ _digest = TypeUtil.parseBytes(digest, 16);
+ }
+
+ public byte[] getDigest()
+ {
+ return _digest;
+ }
+
+ @Override
+ public boolean check(Object credentials)
+ {
+ try
+ {
+ if (credentials instanceof char[])
+ credentials = new String((char[])credentials);
+ if (credentials instanceof Password || credentials instanceof String)
+ {
+ byte[] digest;
+ synchronized (__md5Lock)
+ {
+ if (__md == null)
+ __md = MessageDigest.getInstance("MD5");
+ __md.reset();
+ __md.update(credentials.toString().getBytes(StandardCharsets.ISO_8859_1));
+ digest = __md.digest();
+ }
+ return byteEquals(_digest, digest);
+ }
+ else if (credentials instanceof MD5)
+ {
+ return equals(credentials);
+ }
+ else if (credentials instanceof Credential)
+ {
+ // Allow credential to attempt check - i.e. this'll work
+ // for DigestAuthModule$Digest credentials
+ return ((Credential)credentials).check(this);
+ }
+ else
+ {
+ LOG.warn("Can't check " + credentials.getClass() + " against MD5");
+ return false;
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ return false;
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (obj instanceof MD5)
+ return byteEquals(_digest, ((MD5)obj)._digest);
+ return false;
+ }
+
+ public static String digest(String password)
+ {
+ try
+ {
+ byte[] digest;
+ synchronized (__md5Lock)
+ {
+ if (__md == null)
+ {
+ try
+ {
+ __md = MessageDigest.getInstance("MD5");
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ return null;
+ }
+ }
+
+ __md.reset();
+ __md.update(password.getBytes(StandardCharsets.ISO_8859_1));
+ digest = __md.digest();
+ }
+
+ return __TYPE + TypeUtil.toString(digest, 16);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ return null;
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/CredentialProvider.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/CredentialProvider.java
new file mode 100644
index 0000000..216f383
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/CredentialProvider.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.security;
+
+/**
+ * Provider of credentials, it converts a String into a credential if it starts with a given prefix
+ */
+public interface CredentialProvider
+{
+ /**
+ * Get a credential from a String
+ *
+ * @param credential String representation of the credential
+ * @return A Credential or Password instance.
+ */
+ Credential getCredential(String credential);
+
+ /**
+ * Get the prefix of the credential strings convertible into credentials
+ *
+ * @return prefix of the credential strings convertible into credentials
+ */
+ String getPrefix();
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/Password.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/Password.java
new file mode 100644
index 0000000..1824fff
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/Password.java
@@ -0,0 +1,256 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.security;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Password utility class.
+ *
+ * This utility class gets a password or pass phrase either by:
+ *
+ * <PRE>
+ * + Password is set as a system property.
+ * + The password is prompted for and read from standard input
+ * + A program is run to get the password.
+ * </pre>
+ *
+ * Passwords that begin with OBF: are de obfuscated. Passwords can be obfuscated
+ * by run org.eclipse.util.Password as a main class. Obfuscated password are
+ * required if a system needs to recover the full password (eg. so that it may
+ * be passed to another system). They are not secure, but prevent casual
+ * observation.
+ * <p>
+ * Passwords that begin with CRYPT: are oneway encrypted with UnixCrypt. The
+ * real password cannot be retrieved, but comparisons can be made to other
+ * passwords. A Crypt can be generated by running org.eclipse.util.UnixCrypt as
+ * a main class, passing password and then the username. Checksum passwords are
+ * a secure(ish) way to store passwords that only need to be checked rather than
+ * recovered. Note that it is not strong security - specially if simple
+ * passwords are used.
+ */
+public class Password extends Credential
+{
+ private static final Logger LOG = Log.getLogger(Password.class);
+
+ private static final long serialVersionUID = 5062906681431569445L;
+
+ public static final String __OBFUSCATE = "OBF:";
+
+ private String _pw;
+
+ /**
+ * Constructor.
+ *
+ * @param password The String password.
+ */
+ public Password(String password)
+ {
+ _pw = password;
+
+ // expand password
+ while (_pw != null && _pw.startsWith(__OBFUSCATE))
+ {
+ _pw = deobfuscate(_pw);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return _pw;
+ }
+
+ public String toStarString()
+ {
+ return "*****************************************************".substring(0, _pw.length());
+ }
+
+ @Override
+ public boolean check(Object credentials)
+ {
+ if (this == credentials)
+ return true;
+
+ if (credentials instanceof Password)
+ return credentials.equals(_pw);
+
+ if (credentials instanceof String)
+ return stringEquals(_pw, (String)credentials);
+
+ if (credentials instanceof char[])
+ return stringEquals(_pw, new String((char[])credentials));
+
+ if (credentials instanceof Credential)
+ return ((Credential)credentials).check(_pw);
+
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o)
+ return true;
+
+ if (null == o)
+ return false;
+
+ if (o instanceof Password)
+ return stringEquals(_pw, ((Password)o)._pw);
+
+ if (o instanceof String)
+ return stringEquals(_pw, (String)o);
+
+ return false;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return null == _pw ? super.hashCode() : _pw.hashCode();
+ }
+
+ public static String obfuscate(String s)
+ {
+ StringBuilder buf = new StringBuilder();
+ byte[] b = s.getBytes(StandardCharsets.UTF_8);
+
+ buf.append(__OBFUSCATE);
+ for (int i = 0; i < b.length; i++)
+ {
+ byte b1 = b[i];
+ byte b2 = b[b.length - (i + 1)];
+ if (b1 < 0 || b2 < 0)
+ {
+ int i0 = (0xff & b1) * 256 + (0xff & b2);
+ String x = Integer.toString(i0, 36).toLowerCase(Locale.ENGLISH);
+ buf.append("U0000", 0, 5 - x.length());
+ buf.append(x);
+ }
+ else
+ {
+ int i1 = 127 + b1 + b2;
+ int i2 = 127 + b1 - b2;
+ int i0 = i1 * 256 + i2;
+ String x = Integer.toString(i0, 36).toLowerCase(Locale.ENGLISH);
+
+ int j0 = Integer.parseInt(x, 36);
+ int j1 = (i0 / 256);
+ int j2 = (i0 % 256);
+ byte bx = (byte)((j1 + j2 - 254) / 2);
+
+ buf.append("000", 0, 4 - x.length());
+ buf.append(x);
+ }
+ }
+ return buf.toString();
+ }
+
+ public static String deobfuscate(String s)
+ {
+ if (s.startsWith(__OBFUSCATE))
+ s = s.substring(4);
+
+ byte[] b = new byte[s.length() / 2];
+ int l = 0;
+ for (int i = 0; i < s.length(); i += 4)
+ {
+ if (s.charAt(i) == 'U')
+ {
+ i++;
+ String x = s.substring(i, i + 4);
+ int i0 = Integer.parseInt(x, 36);
+ byte bx = (byte)(i0 >> 8);
+ b[l++] = bx;
+ }
+ else
+ {
+ String x = s.substring(i, i + 4);
+ int i0 = Integer.parseInt(x, 36);
+ int i1 = (i0 / 256);
+ int i2 = (i0 % 256);
+ byte bx = (byte)((i1 + i2 - 254) / 2);
+ b[l++] = bx;
+ }
+ }
+
+ return new String(b, 0, l, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Get a password. A password is obtained by trying
+ * <UL>
+ * <LI>Calling <Code>System.getProperty(realm,dft)</Code>
+ * <LI>Prompting for a password
+ * <LI>Using promptDft if nothing was entered.
+ * </UL>
+ *
+ * @param realm The realm name for the password, used as a SystemProperty
+ * name.
+ * @param dft The default password.
+ * @param promptDft The default to use if prompting for the password.
+ * @return Password
+ */
+ public static Password getPassword(String realm, String dft, String promptDft)
+ {
+ String passwd = System.getProperty(realm, dft);
+ if (passwd == null || passwd.length() == 0)
+ {
+ try
+ {
+ System.out.print(realm + ((promptDft != null && promptDft.length() > 0) ? " [dft]" : "") + " : ");
+ System.out.flush();
+ byte[] buf = new byte[512];
+ int len = System.in.read(buf);
+ if (len > 0)
+ passwd = new String(buf, 0, len).trim();
+ }
+ catch (IOException e)
+ {
+ LOG.warn(Log.EXCEPTION, e);
+ }
+ if (passwd == null || passwd.length() == 0)
+ passwd = promptDft;
+ }
+ return new Password(passwd);
+ }
+
+ public static void main(String[] arg)
+ {
+ if (arg.length != 1 && arg.length != 2)
+ {
+ System.err.println("Usage - java " + Password.class.getName() + " [<user>] <password>");
+ System.err.println("If the password is ?, the user will be prompted for the password");
+ System.exit(1);
+ }
+ String p = arg[arg.length == 1 ? 0 : 1];
+ Password pw = new Password(p);
+ System.err.println(pw.toString());
+ System.err.println(obfuscate(pw.toString()));
+ System.err.println(Credential.MD5.digest(p));
+ if (arg.length == 2)
+ System.err.println(Credential.Crypt.crypt(arg[0], pw.toString()));
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/UnixCrypt.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/UnixCrypt.java
new file mode 100644
index 0000000..b18d24b
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/UnixCrypt.java
@@ -0,0 +1,586 @@
+/*
+ * @(#)UnixCrypt.java 0.9 96/11/25
+ *
+ * Copyright (c) 1996 Aki Yoshida. All rights reserved.
+ *
+ * Permission to use, copy, modify and distribute this software
+ * for non-commercial or commercial purposes and without fee is
+ * hereby granted provided that this copyright notice appears in
+ * all copies.
+ */
+
+/**
+ * Unix crypt(3C) utility
+ *
+ * @version 0.9, 11/25/96
+ * @author Aki Yoshida
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ *
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ */
+
+/**
+ * modified April 2001
+ * by Iris Van den Broeke, Daniel Deville
+ */
+
+package org.eclipse.jetty.util.security;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Unix Crypt. Implements the one way cryptography used by Unix systems for
+ * simple password protection.
+ *
+ * @version $Id: UnixCrypt.java,v 1.1 2005/10/05 14:09:14 janb Exp $
+ * @author Greg Wilkins (gregw)
+ */
+public class UnixCrypt
+{
+
+ /* (mostly) Standard DES Tables from Tom Truscott */
+ private static final byte[] IP = {
+ /* initial permutation */
+ 58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14,
+ 6, 64, 56, 48, 40, 32, 24, 16, 8, 57, 49, 41, 33, 25, 17, 9, 1,
+ 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15,
+ 7
+ };
+
+ /* The final permutation is the inverse of IP - no table is necessary */
+ private static final byte[] ExpandTr = {
+ /* expansion operation */
+ 32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, 8, 9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17,
+ 16, 17, 18, 19, 20, 21, 20, 21, 22, 23, 24, 25, 24, 25, 26, 27, 28, 29,
+ 28, 29, 30, 31, 32, 1
+ };
+
+ private static final byte[] PC1 = {
+ /* permuted choice table 1 */
+ 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 27, 19, 11,
+ 3, 60, 52, 44, 36,
+
+ 63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21,
+ 13, 5, 28, 20, 12, 4
+ };
+
+ private static final byte[] Rotates = {
+ /* PC1 rotation schedule */
+ 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1
+ };
+
+ private static final byte[] PC2 = {
+ /* permuted choice table 2 */
+ 9, 18, 14, 17, 11, 24, 1, 5, 22, 25, 3, 28, 15, 6, 21, 10, 35, 38, 23, 19, 12, 4, 26, 8,
+ 43, 54, 16, 7, 27, 20, 13, 2,
+
+ 0, 0, 41, 52, 31, 37, 47, 55, 0, 0, 30, 40, 51, 45, 33, 48, 0, 0, 44, 49, 39, 56, 34,
+ 53, 0, 0, 46, 42, 50, 36, 29, 32
+ };
+
+ private static final byte[][] S = {
+ /* 48->32 bit substitution tables */
+ /* S[1] */
+ {
+ 14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 0, 15, 7, 4, 14, 2, 13, 1, 10,
+ 6, 12, 11, 9, 5, 3, 8, 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9,
+ 7, 3, 10, 5, 0, 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13
+ },
+ /* S[2] */
+ {
+ 15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, 3, 13, 4, 7, 15, 2, 8, 14, 12,
+ 0, 1, 10, 6, 9, 11, 5, 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12,
+ 6, 9, 3, 2, 15, 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9
+ },
+ /* S[3] */
+ {
+ 10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, 13, 7, 0, 9, 3, 4, 6, 10, 2,
+ 8, 5, 14, 12, 11, 15, 1, 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2,
+ 12, 5, 10, 14, 7, 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12
+ },
+ /* S[4] */
+ {
+ 7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, 13, 8, 11, 5, 6, 15, 0, 3, 4,
+ 7, 2, 12, 1, 10, 14, 9, 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3,
+ 14, 5, 2, 8, 4, 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14
+ },
+ /* S[5] */
+ {
+ 2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, 14, 11, 2, 12, 4, 7, 13, 1, 5,
+ 0, 15, 10, 3, 9, 8, 6, 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12,
+ 5, 6, 3, 0, 14, 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3
+ },
+ /* S[6] */
+ {
+ 12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, 10, 15, 4, 2, 7, 12, 9, 5, 6,
+ 1, 13, 14, 0, 11, 3, 8, 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4,
+ 10, 1, 13, 11, 6, 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13
+ },
+ /* S[7] */
+ {
+ 4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, 13, 0, 11, 7, 4, 9, 1, 10, 14,
+ 3, 5, 12, 2, 15, 8, 6, 1, 4, 11, 13, 12, 3, 7, 14, 10, 15,
+ 6, 8, 0, 5, 9, 2, 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12
+ },
+ /* S[8] */
+ {
+ 13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 1, 15, 13, 8, 10, 3, 7, 4, 12,
+ 5, 6, 11, 0, 14, 9, 2, 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10,
+ 13, 15, 3, 5, 8, 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11
+ }
+ };
+
+ private static final byte[] P32Tr = {
+ /* 32-bit permutation function */
+ 16, 7, 20, 21, 29, 12, 28, 17, 1, 15, 23, 26, 5, 18, 31, 10, 2, 8, 24, 14, 32, 27, 3,
+ 9, 19, 13, 30, 6, 22, 11, 4, 25
+ };
+
+ private static final byte[] CIFP = {
+ /* compressed/interleaved
+ * permutation
+ */
+ 1, 2, 3, 4, 17, 18, 19, 20, 5, 6, 7, 8, 21, 22, 23, 24, 9, 10, 11, 12, 25, 26, 27, 28,
+ 13, 14, 15, 16, 29, 30, 31, 32,
+
+ 33, 34, 35, 36, 49, 50, 51, 52, 37, 38, 39, 40, 53, 54, 55, 56, 41, 42, 43, 44, 57, 58,
+ 59, 60, 45, 46, 47, 48, 61, 62, 63, 64
+ };
+
+ private static final byte[] ITOA64 = {
+ /* 0..63 => ascii-64 */
+ (byte)'.', (byte)'/', (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4',
+ (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'A',
+ (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', (byte)'H',
+ (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M',
+ (byte)'N', (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T',
+ (byte)'U', (byte)'V', (byte)'W', (byte)'X', (byte)'Y',
+ (byte)'Z', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f',
+ (byte)'g', (byte)'h', (byte)'i', (byte)'j', (byte)'k',
+ (byte)'l', (byte)'m', (byte)'n', (byte)'o', (byte)'p', (byte)'q', (byte)'r',
+ (byte)'s', (byte)'t', (byte)'u', (byte)'v', (byte)'w',
+ (byte)'x', (byte)'y', (byte)'z'
+ };
+
+ /* ===== Tables that are initialized at run time ==================== */
+
+ private static final byte[] A64TOI = new byte[128]; /* ascii-64 => 0..63 */
+
+ /* Initial key schedule permutation */
+ private static final long[][] PC1ROT = new long[16][16];
+
+ /* Subsequent key schedule rotation permutations */
+ private static final long[][][] PC2ROT = new long[2][16][16];
+
+ /* Initial permutation/expansion table */
+ private static final long[][] IE3264 = new long[8][16];
+
+ /* Table that combines the S, P, and E operations. */
+ private static final long[][] SPE = new long[8][64];
+
+ /* compressed/interleaved => final permutation table */
+ private static final long[][] CF6464 = new long[16][16];
+
+ static
+ {
+ byte[] perm = new byte[64];
+ byte[] temp = new byte[64];
+
+ // inverse table.
+ for (int i = 0; i < 64; i++)
+ {
+ A64TOI[ITOA64[i]] = (byte)i;
+ }
+
+ // PC1ROT - bit reverse, then PC1, then Rotate, then PC2
+ for (int i = 0; i < 64; i++)
+ {
+ perm[i] = (byte)0;
+ }
+
+ for (int i = 0; i < 64; i++)
+ {
+ int k;
+ if ((k = PC2[i]) == 0)
+ continue;
+ k += Rotates[0] - 1;
+ if ((k % 28) < Rotates[0])
+ k -= 28;
+ k = PC1[k];
+ if (k > 0)
+ {
+ k--;
+ k = (k | 0x07) - (k & 0x07);
+ k++;
+ }
+ perm[i] = (byte)k;
+ }
+ init_perm(PC1ROT, perm, 8);
+
+ // PC2ROT - PC2 inverse, then Rotate, then PC2
+ for (int j = 0; j < 2; j++)
+ {
+ int k;
+ for (int i = 0; i < 64; i++)
+ {
+ perm[i] = temp[i] = 0;
+ }
+ for (int i = 0; i < 64; i++)
+ {
+ if ((k = PC2[i]) == 0)
+ continue;
+ temp[k - 1] = (byte)(i + 1);
+ }
+ for (int i = 0; i < 64; i++)
+ {
+ if ((k = PC2[i]) == 0)
+ continue;
+ k += j;
+ if ((k % 28) <= j)
+ k -= 28;
+ perm[i] = temp[k];
+ }
+
+ init_perm(PC2ROT[j], perm, 8);
+ }
+
+ // Bit reverse, initial permupation, expantion
+ for (int i = 0; i < 8; i++)
+ {
+ for (int j = 0; j < 8; j++)
+ {
+ int k = (j < 2) ? 0 : IP[ExpandTr[i * 6 + j - 2] - 1];
+ if (k > 32)
+ k -= 32;
+ else if (k > 0)
+ k--;
+ if (k > 0)
+ {
+ k--;
+ k = (k | 0x07) - (k & 0x07);
+ k++;
+ }
+ perm[i * 8 + j] = (byte)k;
+ }
+ }
+
+ init_perm(IE3264, perm, 8);
+
+ // Compression, final permutation, bit reverse
+ for (int i = 0; i < 64; i++)
+ {
+ int k = IP[CIFP[i] - 1];
+ if (k > 0)
+ {
+ k--;
+ k = (k | 0x07) - (k & 0x07);
+ k++;
+ }
+ perm[k - 1] = (byte)(i + 1);
+ }
+
+ init_perm(CF6464, perm, 8);
+
+ // SPE table
+ for (int i = 0; i < 48; i++)
+ {
+ perm[i] = P32Tr[ExpandTr[i] - 1];
+ }
+ for (int t = 0; t < 8; t++)
+ {
+ for (int j = 0; j < 64; j++)
+ {
+ int k = (((j >> 0) & 0x01) << 5) | (((j >> 1) & 0x01) << 3) |
+ (((j >> 2) & 0x01) << 2) |
+ (((j >> 3) & 0x01) << 1) |
+ (((j >> 4) & 0x01) << 0) |
+ (((j >> 5) & 0x01) << 4);
+ k = S[t][k];
+ k = (((k >> 3) & 0x01) << 0) | (((k >> 2) & 0x01) << 1) | (((k >> 1) & 0x01) << 2) | (((k >> 0) & 0x01) << 3);
+ for (int i = 0; i < 32; i++)
+ {
+ temp[i] = 0;
+ }
+ for (int i = 0; i < 4; i++)
+ {
+ temp[4 * t + i] = (byte)((k >> i) & 0x01);
+ }
+ long kk = 0;
+ for (int i = 24; --i >= 0; )
+ {
+ kk = ((kk << 1) | ((long)temp[perm[i] - 1]) << 32 | (temp[perm[i + 24] - 1]));
+ }
+
+ SPE[t][j] = to_six_bit(kk);
+ }
+ }
+ }
+
+ /**
+ * You can't call the constructer.
+ */
+ private UnixCrypt()
+ {
+ }
+
+ // @checkstyle-disable-check : MethodNameCheck
+ // @checkstyle-disable-check : LocalVariableNameCheck
+ // @checkstyle-disable-check : ParameterNameCheck
+
+ /**
+ * Returns the transposed and split code of a 24-bit code into a 4-byte
+ * code, each having 6 bits.
+ */
+ private static int to_six_bit(int num)
+ {
+ return (((num << 26) & 0xfc000000) | ((num << 12) & 0xfc0000) | ((num >> 2) & 0xfc00) | ((num >> 16) & 0xfc));
+ }
+
+ /**
+ * Returns the transposed and split code of two 24-bit code into two 4-byte
+ * code, each having 6 bits.
+ */
+ private static long to_six_bit(long num)
+ {
+ return (((num << 26) & 0xfc000000fc000000L) | ((num << 12) & 0xfc000000fc0000L) | ((num >> 2) & 0xfc000000fc00L) | ((num >> 16) & 0xfc000000fcL));
+ }
+
+ /**
+ * Returns the permutation of the given 64-bit code with the specified
+ * permutataion table.
+ */
+ private static long perm6464(long c, long[][] p)
+ {
+ long out = 0L;
+ for (int i = 8; --i >= 0; )
+ {
+ int t = (int)(0x00ff & c);
+ c >>= 8;
+ long tp = p[i << 1][t & 0x0f];
+ out |= tp;
+ tp = p[(i << 1) + 1][t >> 4];
+ out |= tp;
+ }
+ return out;
+ }
+
+ /**
+ * Returns the permutation of the given 32-bit code with the specified
+ * permutataion table.
+ */
+ private static long perm3264(int c, long[][] p)
+ {
+ long out = 0L;
+ for (int i = 4; --i >= 0; )
+ {
+ int t = (0x00ff & c);
+ c >>= 8;
+ long tp = p[i << 1][t & 0x0f];
+ out |= tp;
+ tp = p[(i << 1) + 1][t >> 4];
+ out |= tp;
+ }
+ return out;
+ }
+
+ /**
+ * Returns the key schedule for the given key.
+ */
+ private static long[] des_setkey(long keyword)
+ {
+ long K = perm6464(keyword, PC1ROT);
+ long[] KS = new long[16];
+ KS[0] = K & ~0x0303030300000000L;
+
+ for (int i = 1; i < 16; i++)
+ {
+ KS[i] = K;
+ K = perm6464(K, PC2ROT[Rotates[i] - 1]);
+
+ KS[i] = K & ~0x0303030300000000L;
+ }
+ return KS;
+ }
+
+ /**
+ * Returns the DES encrypted code of the given word with the specified
+ * environment.
+ */
+ private static long des_cipher(long in, int salt, int num_iter, long[] KS)
+ {
+ salt = to_six_bit(salt);
+ long L = in;
+ long R = L;
+ L &= 0x5555555555555555L;
+ R = (R & 0xaaaaaaaa00000000L) | ((R >> 1) & 0x0000000055555555L);
+ L = ((((L << 1) | (L << 32)) & 0xffffffff00000000L) | ((R | (R >> 32)) & 0x00000000ffffffffL));
+
+ L = perm3264((int)(L >> 32), IE3264);
+ R = perm3264((int)(L & 0xffffffff), IE3264);
+
+ while (--num_iter >= 0)
+ {
+ for (int loop_count = 0; loop_count < 8; loop_count++)
+ {
+ long kp;
+ long B;
+ long k;
+
+ kp = KS[(loop_count << 1)];
+ k = ((R >> 32) ^ R) & salt & 0xffffffffL;
+ k |= (k << 32);
+ B = (k ^ R ^ kp);
+
+ L ^= (SPE[0][(int)((B >> 58) & 0x3f)] ^ SPE[1][(int)((B >> 50) & 0x3f)] ^
+ SPE[2][(int)((B >> 42) & 0x3f)] ^
+ SPE[3][(int)((B >> 34) & 0x3f)] ^
+ SPE[4][(int)((B >> 26) & 0x3f)] ^
+ SPE[5][(int)((B >> 18) & 0x3f)] ^
+ SPE[6][(int)((B >> 10) & 0x3f)] ^ SPE[7][(int)((B >> 2) & 0x3f)]);
+
+ kp = KS[(loop_count << 1) + 1];
+ k = ((L >> 32) ^ L) & salt & 0xffffffffL;
+ k |= (k << 32);
+ B = (k ^ L ^ kp);
+
+ R ^= (SPE[0][(int)((B >> 58) & 0x3f)] ^ SPE[1][(int)((B >> 50) & 0x3f)] ^
+ SPE[2][(int)((B >> 42) & 0x3f)] ^
+ SPE[3][(int)((B >> 34) & 0x3f)] ^
+ SPE[4][(int)((B >> 26) & 0x3f)] ^
+ SPE[5][(int)((B >> 18) & 0x3f)] ^
+ SPE[6][(int)((B >> 10) & 0x3f)] ^ SPE[7][(int)((B >> 2) & 0x3f)]);
+ }
+ // swap L and R
+ L ^= R;
+ R ^= L;
+ L ^= R;
+ }
+ L = ((((L >> 35) & 0x0f0f0f0fL) | (((L & 0xffffffff) << 1) & 0xf0f0f0f0L)) << 32 | (((R >> 35) & 0x0f0f0f0fL) | (((R & 0xffffffff) << 1) & 0xf0f0f0f0L)));
+
+ L = perm6464(L, CF6464);
+
+ return L;
+ }
+
+ /**
+ * Initializes the given permutation table with the mapping table.
+ */
+ private static void init_perm(long[][] perm, byte[] p, int chars_out)
+ {
+ for (int k = 0; k < chars_out * 8; k++)
+ {
+
+ int l = p[k] - 1;
+ if (l < 0)
+ continue;
+ int i = l >> 2;
+ l = 1 << (l & 0x03);
+ for (int j = 0; j < 16; j++)
+ {
+ int s = ((k & 0x07) + ((7 - (k >> 3)) << 3));
+ if ((j & l) != 0x00)
+ perm[i][j] |= (1L << s);
+ }
+ }
+ }
+
+ /**
+ * Encrypts String into crypt (Unix) code.
+ *
+ * @param key the key to be encrypted
+ * @param setting the salt to be used
+ * @return the encrypted String
+ */
+ public static String crypt(String key, String setting)
+ {
+ long constdatablock = 0L; /* encryption constant */
+ byte[] cryptresult = new byte[13]; /* encrypted result */
+ long keyword = 0L;
+ /* invalid parameters! */
+ if (key == null || setting == null)
+ return "*"; // will NOT match under
+ // ANY circumstances!
+
+ int keylen = key.length();
+
+ for (int i = 0; i < 8; i++)
+ {
+ keyword = (keyword << 8) | ((i < keylen) ? 2 * key.charAt(i) : 0);
+ }
+
+ long[] KS = des_setkey(keyword);
+
+ int salt = 0;
+ for (int i = 2; --i >= 0; )
+ {
+ char c = (i < setting.length()) ? setting.charAt(i) : '.';
+ cryptresult[i] = (byte)c;
+ salt = (salt << 6) | (0x00ff & A64TOI[c]);
+ }
+
+ long rsltblock = des_cipher(constdatablock, salt, 25, KS);
+
+ cryptresult[12] = ITOA64[(((int)rsltblock) << 2) & 0x3f];
+ rsltblock >>= 4;
+ for (int i = 12; --i >= 2; )
+ {
+ cryptresult[i] = ITOA64[((int)rsltblock) & 0x3f];
+ rsltblock >>= 6;
+ }
+
+ return new String(cryptresult, 0, 13, StandardCharsets.US_ASCII);
+ }
+
+ public static void main(String[] arg)
+ {
+ if (arg.length != 2)
+ {
+ System.err.println("Usage - java org.eclipse.util.UnixCrypt <key> <salt>");
+ System.exit(1);
+ }
+
+ System.err.println("Crypt=" + crypt(arg[0], arg[1]));
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/package-info.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/package-info.java
new file mode 100644
index 0000000..12244df
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/security/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Util : Common Security Utilities
+ */
+package org.eclipse.jetty.util.security;
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/AliasedX509ExtendedKeyManager.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/AliasedX509ExtendedKeyManager.java
new file mode 100644
index 0000000..dde888d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/AliasedX509ExtendedKeyManager.java
@@ -0,0 +1,154 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.ssl;
+
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedKeyManager;
+
+/**
+ * <p>An {@link X509ExtendedKeyManager} that select a key with desired alias,
+ * delegating other processing to a nested X509ExtendedKeyManager.</p>
+ * <p>Can be used both with server and client sockets.</p>
+ */
+public class AliasedX509ExtendedKeyManager extends X509ExtendedKeyManager
+{
+ private final String _alias;
+ private final X509ExtendedKeyManager _delegate;
+
+ public AliasedX509ExtendedKeyManager(X509ExtendedKeyManager keyManager, String keyAlias)
+ {
+ _alias = keyAlias;
+ _delegate = keyManager;
+ }
+
+ public X509ExtendedKeyManager getDelegate()
+ {
+ return _delegate;
+ }
+
+ @Override
+ public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket)
+ {
+ if (_alias == null)
+ return _delegate.chooseClientAlias(keyType, issuers, socket);
+
+ for (String kt : keyType)
+ {
+ String[] aliases = _delegate.getClientAliases(kt, issuers);
+ if (aliases != null)
+ {
+ for (String a : aliases)
+ {
+ if (_alias.equals(a))
+ return _alias;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket)
+ {
+ if (_alias == null)
+ return _delegate.chooseServerAlias(keyType, issuers, socket);
+
+ String[] aliases = _delegate.getServerAliases(keyType, issuers);
+ if (aliases != null)
+ {
+ for (String a : aliases)
+ {
+ if (_alias.equals(a))
+ return _alias;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public String[] getClientAliases(String keyType, Principal[] issuers)
+ {
+ return _delegate.getClientAliases(keyType, issuers);
+ }
+
+ @Override
+ public String[] getServerAliases(String keyType, Principal[] issuers)
+ {
+ return _delegate.getServerAliases(keyType, issuers);
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias)
+ {
+ return _delegate.getCertificateChain(alias);
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias)
+ {
+ return _delegate.getPrivateKey(alias);
+ }
+
+ @Override
+ public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine)
+ {
+ if (_alias == null)
+ return _delegate.chooseEngineServerAlias(keyType, issuers, engine);
+
+ String[] aliases = _delegate.getServerAliases(keyType, issuers);
+ if (aliases != null)
+ {
+ for (String a : aliases)
+ {
+ if (_alias.equals(a))
+ return _alias;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine)
+ {
+ if (_alias == null)
+ return _delegate.chooseEngineClientAlias(keyType, issuers, engine);
+
+ for (String kt : keyType)
+ {
+ String[] aliases = _delegate.getClientAliases(kt, issuers);
+ if (aliases != null)
+ {
+ for (String a : aliases)
+ {
+ if (_alias.equals(a))
+ return _alias;
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/KeyStoreScanner.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/KeyStoreScanner.java
new file mode 100644
index 0000000..9754ff2
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/KeyStoreScanner.java
@@ -0,0 +1,155 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.ssl;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.function.Consumer;
+
+import org.eclipse.jetty.util.Scanner;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+
+/**
+ * <p>The {@link KeyStoreScanner} is used to monitor the KeyStore file used by the {@link SslContextFactory}.
+ * It will reload the {@link SslContextFactory} if it detects that the KeyStore file has been modified.</p>
+ * <p>If the TrustStore file needs to be changed, then this should be done before touching the KeyStore file,
+ * the {@link SslContextFactory#reload(Consumer)} will only occur after the KeyStore file has been modified.</p>
+ */
+public class KeyStoreScanner extends ContainerLifeCycle implements Scanner.DiscreteListener
+{
+ private static final Logger LOG = Log.getLogger(KeyStoreScanner.class);
+
+ private final SslContextFactory sslContextFactory;
+ private final File keystoreFile;
+ private final Scanner _scanner;
+
+ public KeyStoreScanner(SslContextFactory sslContextFactory)
+ {
+ this.sslContextFactory = sslContextFactory;
+ try
+ {
+ Resource keystoreResource = sslContextFactory.getKeyStoreResource();
+ File monitoredFile = keystoreResource.getFile();
+ if (monitoredFile == null || !monitoredFile.exists())
+ throw new IllegalArgumentException("keystore file does not exist");
+ if (monitoredFile.isDirectory())
+ throw new IllegalArgumentException("expected keystore file not directory");
+
+ if (keystoreResource.getAlias() != null)
+ {
+ // this resource has an alias, use the alias, as that's what's returned in the Scanner
+ monitoredFile = new File(keystoreResource.getAlias());
+ }
+
+ keystoreFile = monitoredFile;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Monitored Keystore File: {}", monitoredFile);
+ }
+ catch (IOException e)
+ {
+ throw new IllegalArgumentException("could not obtain keystore file", e);
+ }
+
+ File parentFile = keystoreFile.getParentFile();
+ if (!parentFile.exists() || !parentFile.isDirectory())
+ throw new IllegalArgumentException("error obtaining keystore dir");
+
+ _scanner = new Scanner();
+ _scanner.setScanDirs(Collections.singletonList(parentFile));
+ _scanner.setScanInterval(1);
+ _scanner.setReportDirs(false);
+ _scanner.setReportExistingFilesOnStartup(false);
+ _scanner.setScanDepth(1);
+ _scanner.addListener(this);
+ addBean(_scanner);
+ }
+
+ @Override
+ public void fileAdded(String filename)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("added {}", filename);
+
+ if (keystoreFile.toPath().toString().equals(filename))
+ reload();
+ }
+
+ @Override
+ public void fileChanged(String filename)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("changed {}", filename);
+
+ if (keystoreFile.toPath().toString().equals(filename))
+ reload();
+ }
+
+ @Override
+ public void fileRemoved(String filename)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("removed {}", filename);
+
+ if (keystoreFile.toPath().toString().equals(filename))
+ reload();
+ }
+
+ @ManagedOperation(value = "Scan for changes in the SSL Keystore", impact = "ACTION")
+ public void scan()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("scanning");
+
+ _scanner.scan();
+ _scanner.scan();
+ }
+
+ @ManagedOperation(value = "Reload the SSL Keystore", impact = "ACTION")
+ public void reload()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("reloading keystore file {}", keystoreFile);
+
+ try
+ {
+ sslContextFactory.reload(scf -> {});
+ }
+ catch (Throwable t)
+ {
+ LOG.warn("Keystore Reload Failed", t);
+ }
+ }
+
+ @ManagedAttribute("scanning interval to detect changes which need reloaded")
+ public int getScanInterval()
+ {
+ return _scanner.getScanInterval();
+ }
+
+ public void setScanInterval(int scanInterval)
+ {
+ _scanner.setScanInterval(scanInterval);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SniX509ExtendedKeyManager.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SniX509ExtendedKeyManager.java
new file mode 100644
index 0000000..dcd742a
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SniX509ExtendedKeyManager.java
@@ -0,0 +1,262 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.ssl;
+
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
+import javax.net.ssl.SNIMatcher;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.X509ExtendedKeyManager;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * <p>A {@link X509ExtendedKeyManager} that selects a key with an alias
+ * retrieved from SNI information, delegating other processing to a nested X509ExtendedKeyManager.</p>
+ * <p>Can only be used on server side.</p>
+ */
+public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
+{
+ public static final String SNI_X509 = "org.eclipse.jetty.util.ssl.snix509";
+ private static final Logger LOG = Log.getLogger(SniX509ExtendedKeyManager.class);
+
+ private final X509ExtendedKeyManager _delegate;
+ private final SslContextFactory.Server _sslContextFactory;
+ private UnaryOperator<String> _aliasMapper = UnaryOperator.identity();
+
+ /**
+ * @deprecated not supported, you must have a {@link SslContextFactory.Server} for this to work.
+ */
+ @Deprecated
+ public SniX509ExtendedKeyManager(X509ExtendedKeyManager keyManager)
+ {
+ this(keyManager, null);
+ }
+
+ public SniX509ExtendedKeyManager(X509ExtendedKeyManager keyManager, SslContextFactory.Server sslContextFactory)
+ {
+ _delegate = keyManager;
+ _sslContextFactory = Objects.requireNonNull(sslContextFactory, "SslContextFactory.Server must be provided");
+ }
+
+ /**
+ * @return the function that transforms the alias
+ * @see #setAliasMapper(UnaryOperator)
+ */
+ public UnaryOperator<String> getAliasMapper()
+ {
+ return _aliasMapper;
+ }
+
+ /**
+ * <p>Sets a function that transforms the alias into a possibly different alias,
+ * invoked when the SNI logic must choose the alias to pick the right certificate.</p>
+ * <p>This function is required when using the
+ * {@link SslContextFactory.Server#setKeyManagerFactoryAlgorithm(String) PKIX KeyManagerFactory algorithm}
+ * which suffers from bug https://bugs.openjdk.java.net/browse/JDK-8246262,
+ * where aliases are returned by the OpenJDK implementation to the application
+ * in the form {@code N.0.alias} where {@code N} is an always increasing number.
+ * Such mangled aliases won't match the aliases in the keystore, so that for
+ * example SNI matching will always fail.</p>
+ * <p>Other implementations such as BouncyCastle have been reported to mangle
+ * the alias in a different way, namely {@code 0.alias.N}.</p>
+ * <p>This function allows to "unmangle" the alias from the implementation
+ * specific mangling back to just {@code alias} so that SNI matching will work
+ * again.</p>
+ *
+ * @param aliasMapper the function that transforms the alias
+ */
+ public void setAliasMapper(UnaryOperator<String> aliasMapper)
+ {
+ _aliasMapper = Objects.requireNonNull(aliasMapper);
+ }
+
+ @Override
+ public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket)
+ {
+ return _delegate.chooseClientAlias(keyType, issuers, socket);
+ }
+
+ @Override
+ public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine)
+ {
+ return _delegate.chooseEngineClientAlias(keyType, issuers, engine);
+ }
+
+ protected String chooseServerAlias(String keyType, Principal[] issuers, Collection<SNIMatcher> matchers, SSLSession session)
+ {
+ // Look for the aliases that are suitable for the keyType and issuers.
+ String[] mangledAliases = _delegate.getServerAliases(keyType, issuers);
+ if (mangledAliases == null || mangledAliases.length == 0)
+ return null;
+
+ // Apply the alias mapping, keeping the alias order.
+ Map<String, String> aliasMap = new LinkedHashMap<>();
+ Arrays.stream(mangledAliases)
+ .forEach(alias -> aliasMap.put(getAliasMapper().apply(alias), alias));
+
+ // Find our SNIMatcher. There should only be one and it always matches (always returns true
+ // from AliasSNIMatcher.matches), but it will capture the SNI Host if one was presented.
+ String host = matchers == null ? null : matchers.stream()
+ .filter(SslContextFactory.AliasSNIMatcher.class::isInstance)
+ .map(SslContextFactory.AliasSNIMatcher.class::cast)
+ .findFirst()
+ .map(SslContextFactory.AliasSNIMatcher::getHost)
+ .orElse(null);
+
+ try
+ {
+ // Filter the certificates by alias.
+ Collection<X509> certificates = aliasMap.keySet().stream()
+ .map(_sslContextFactory::getX509)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+
+ // Delegate the decision to accept to the sniSelector.
+ SniSelector sniSelector = _sslContextFactory.getSNISelector();
+ if (sniSelector == null)
+ sniSelector = _sslContextFactory;
+ String alias = sniSelector.sniSelect(keyType, issuers, session, host, certificates);
+
+ // Check the selected alias.
+ if (alias == null || alias == SniSelector.DELEGATE)
+ return alias;
+
+ // Make sure we got back an alias from the acceptable aliases.
+ X509 x509 = _sslContextFactory.getX509(alias);
+ if (!aliasMap.containsKey(alias) || x509 == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Invalid X509 match for SNI {}: {}", host, alias);
+ return null;
+ }
+
+ if (session != null)
+ session.putValue(SNI_X509, x509);
+
+ // Convert the selected alias back to the original
+ // value before the alias mapping performed above.
+ String mangledAlias = aliasMap.get(alias);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Matched SNI {} with alias {}, certificate {} from aliases {}", host, mangledAlias, x509, aliasMap.keySet());
+ return mangledAlias;
+ }
+ catch (Throwable x)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Failure matching X509 for SNI " + host, x);
+ return null;
+ }
+ }
+
+ @Override
+ public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket)
+ {
+ SSLSocket sslSocket = (SSLSocket)socket;
+ String alias = (socket == null)
+ ? chooseServerAlias(keyType, issuers, Collections.emptyList(), null)
+ : chooseServerAlias(keyType, issuers, sslSocket.getSSLParameters().getSNIMatchers(), sslSocket.getHandshakeSession());
+ boolean delegate = alias == SniSelector.DELEGATE;
+ if (delegate)
+ alias = _delegate.chooseServerAlias(keyType, issuers, socket);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Chose {} alias={} keyType={} on {}", delegate ? "delegate" : "explicit", String.valueOf(alias), keyType, socket);
+ return alias;
+ }
+
+ @Override
+ public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine)
+ {
+ String alias = (engine == null)
+ ? chooseServerAlias(keyType, issuers, Collections.emptyList(), null)
+ : chooseServerAlias(keyType, issuers, engine.getSSLParameters().getSNIMatchers(), engine.getHandshakeSession());
+ boolean delegate = alias == SniSelector.DELEGATE;
+ if (delegate)
+ alias = _delegate.chooseEngineServerAlias(keyType, issuers, engine);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Chose {} alias={} keyType={} on {}", delegate ? "delegate" : "explicit", String.valueOf(alias), keyType, engine);
+ return alias;
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias)
+ {
+ return _delegate.getCertificateChain(alias);
+ }
+
+ @Override
+ public String[] getClientAliases(String keyType, Principal[] issuers)
+ {
+ return _delegate.getClientAliases(keyType, issuers);
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias)
+ {
+ return _delegate.getPrivateKey(alias);
+ }
+
+ @Override
+ public String[] getServerAliases(String keyType, Principal[] issuers)
+ {
+ return _delegate.getServerAliases(keyType, issuers);
+ }
+
+ /**
+ * <p>Selects a certificate based on SNI information.</p>
+ */
+ @FunctionalInterface
+ public interface SniSelector
+ {
+ String DELEGATE = "delegate_no_sni_match";
+
+ /**
+ * <p>Selects a certificate based on SNI information.</p>
+ * <p>This method may be invoked multiple times during the TLS handshake, with different parameters.
+ * For example, the {@code keyType} could be different, and subsequently the collection of certificates
+ * (because they need to match the {@code keyType}).</p>
+ *
+ * @param keyType the key algorithm type name
+ * @param issuers the list of acceptable CA issuer subject names or null if it does not matter which issuers are used
+ * @param session the TLS handshake session or null if not known.
+ * @param sniHost the server name indication sent by the client, or null if the client did not send the server name indication
+ * @param certificates the list of certificates matching {@code keyType} and {@code issuers} known to this SslContextFactory
+ * @return the alias of the certificate to return to the client, from the {@code certificates} list,
+ * or {@link SniSelector#DELEGATE} if the certificate choice should be delegated to the
+ * nested key manager or null for no match.
+ * @throws SSLHandshakeException if the TLS handshake should be aborted
+ */
+ public String sniSelect(String keyType, Principal[] issuers, SSLSession session, String sniHost, Collection<X509> certificates) throws SSLHandshakeException;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java
new file mode 100644
index 0000000..80ff101
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java
@@ -0,0 +1,2569 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.ssl;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.cert.CRL;
+import java.security.cert.CertStore;
+import java.security.cert.CertStoreParameters;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CollectionCertStoreParameters;
+import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.PKIXCertPathChecker;
+import java.security.cert.X509CertSelector;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import javax.net.ssl.CertPathTrustManagerParameters;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIMatcher;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSessionContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.StandardConstants;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509ExtendedTrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.security.CertificateUtils;
+import org.eclipse.jetty.util.security.CertificateValidator;
+import org.eclipse.jetty.util.security.Password;
+
+/**
+ * <p>SslContextFactory is used to configure SSL parameters
+ * to be used by server and client connectors.</p>
+ * <p>Use {@link Server} to configure server-side connectors,
+ * and {@link Client} to configure HTTP or WebSocket clients.</p>
+ */
+@ManagedObject
+public class SslContextFactory extends AbstractLifeCycle implements Dumpable
+{
+ public static final TrustManager[] TRUST_ALL_CERTS = new X509TrustManager[]{new X509ExtendedTrustManagerWrapper(null)};
+ public static final String DEFAULT_KEYMANAGERFACTORY_ALGORITHM = KeyManagerFactory.getDefaultAlgorithm();
+ public static final String DEFAULT_TRUSTMANAGERFACTORY_ALGORITHM = TrustManagerFactory.getDefaultAlgorithm();
+ /**
+ * String name of key password property.
+ */
+ public static final String KEYPASSWORD_PROPERTY = "org.eclipse.jetty.ssl.keypassword";
+ /**
+ * String name of keystore password property.
+ */
+ public static final String PASSWORD_PROPERTY = "org.eclipse.jetty.ssl.password";
+
+ private static final Logger LOG = Log.getLogger(SslContextFactory.class);
+ private static final Logger LOG_CONFIG = LOG.getLogger("config");
+ /**
+ * Default Excluded Protocols List
+ */
+ private static final String[] DEFAULT_EXCLUDED_PROTOCOLS = {"SSL", "SSLv2", "SSLv2Hello", "SSLv3"};
+ /**
+ * Default Excluded Cipher Suite List
+ */
+ private static final String[] DEFAULT_EXCLUDED_CIPHER_SUITES = {
+ // Exclude weak / insecure ciphers
+ "^.*_(MD5|SHA|SHA1)$",
+ // Exclude ciphers that don't support forward secrecy
+ "^TLS_RSA_.*$",
+ // The following exclusions are present to cleanup known bad cipher
+ // suites that may be accidentally included via include patterns.
+ // The default enabled cipher list in Java will not include these
+ // (but they are available in the supported list).
+ "^SSL_.*$",
+ "^.*_NULL_.*$",
+ "^.*_anon_.*$"
+ };
+
+ private final Set<String> _excludeProtocols = new LinkedHashSet<>();
+ private final Set<String> _includeProtocols = new LinkedHashSet<>();
+ private final Set<String> _excludeCipherSuites = new LinkedHashSet<>();
+ private final Set<String> _includeCipherSuites = new LinkedHashSet<>();
+ private final Map<String, X509> _aliasX509 = new HashMap<>();
+ private final Map<String, X509> _certHosts = new HashMap<>();
+ private final Map<String, X509> _certWilds = new HashMap<>();
+ private String[] _selectedProtocols;
+ private boolean _useCipherSuitesOrder = true;
+ private Comparator<String> _cipherComparator;
+ private String[] _selectedCipherSuites;
+ private Resource _keyStoreResource;
+ private String _keyStoreProvider;
+ private String _keyStoreType = "JKS";
+ private String _certAlias;
+ private Resource _trustStoreResource;
+ private String _trustStoreProvider;
+ private String _trustStoreType;
+ private boolean _needClientAuth = false;
+ private boolean _wantClientAuth = false;
+ private Password _keyStorePassword;
+ private Password _keyManagerPassword;
+ private Password _trustStorePassword;
+ private String _sslProvider;
+ private String _sslProtocol = "TLS";
+ private String _secureRandomAlgorithm;
+ private String _keyManagerFactoryAlgorithm = DEFAULT_KEYMANAGERFACTORY_ALGORITHM;
+ private String _trustManagerFactoryAlgorithm = DEFAULT_TRUSTMANAGERFACTORY_ALGORITHM;
+ private boolean _validateCerts;
+ private boolean _validatePeerCerts;
+ private int _maxCertPathLength = -1;
+ private String _crlPath;
+ private boolean _enableCRLDP = false;
+ private boolean _enableOCSP = false;
+ private String _ocspResponderURL;
+ private KeyStore _setKeyStore;
+ private KeyStore _setTrustStore;
+ private boolean _sessionCachingEnabled = true;
+ private int _sslSessionCacheSize = -1;
+ private int _sslSessionTimeout = -1;
+ private SSLContext _setContext;
+ private String _endpointIdentificationAlgorithm = "HTTPS";
+ private boolean _trustAll;
+ private boolean _renegotiationAllowed = true;
+ private int _renegotiationLimit = 5;
+ private Factory _factory;
+ private PKIXCertPathChecker _pkixCertPathChecker;
+ private HostnameVerifier _hostnameVerifier;
+
+ /**
+ * Construct an instance of SslContextFactory with the default configuration.
+ *
+ * @deprecated use {@link Client#Client()} or {@link Server#Server()} instead
+ */
+ @Deprecated
+ public SslContextFactory()
+ {
+ this(false);
+ }
+
+ /**
+ * Construct an instance of SslContextFactory
+ * Default constructor for use in XmlConfiguration files
+ *
+ * @param trustAll whether to blindly trust all certificates
+ * @see #setTrustAll(boolean)
+ * @deprecated use {@link Client#Client(boolean)} instead
+ */
+ @Deprecated
+ public SslContextFactory(boolean trustAll)
+ {
+ this(trustAll, null);
+ }
+
+ /**
+ * Construct an instance of SslContextFactory
+ *
+ * @param keyStorePath default keystore location
+ * @deprecated use {@link #setKeyStorePath(String)} instead
+ */
+ @Deprecated
+ public SslContextFactory(String keyStorePath)
+ {
+ this(false, keyStorePath);
+ }
+
+ private SslContextFactory(boolean trustAll, String keyStorePath)
+ {
+ setTrustAll(trustAll);
+ setExcludeProtocols(DEFAULT_EXCLUDED_PROTOCOLS);
+ setExcludeCipherSuites(DEFAULT_EXCLUDED_CIPHER_SUITES);
+
+ if (keyStorePath != null)
+ setKeyStorePath(keyStorePath);
+ }
+
+ /**
+ * Creates the SSLContext object and starts the lifecycle
+ */
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+ synchronized (this)
+ {
+ load();
+ }
+ checkConfiguration();
+ }
+
+ protected void checkConfiguration()
+ {
+ SSLEngine engine = _factory._context.createSSLEngine();
+ customize(engine);
+ SSLParameters supported = engine.getSSLParameters();
+
+ checkProtocols(supported);
+ checkCiphers(supported);
+ }
+
+ protected void checkTrustAll()
+ {
+ if (isTrustAll())
+ LOG_CONFIG.warn("Trusting all certificates configured for {}", this);
+ }
+
+ protected void checkEndPointIdentificationAlgorithm()
+ {
+ if (getEndpointIdentificationAlgorithm() == null)
+ LOG_CONFIG.warn("No Client EndPointIdentificationAlgorithm configured for {}", this);
+ }
+
+ protected void checkProtocols(SSLParameters supported)
+ {
+ for (String protocol : supported.getProtocols())
+ {
+ for (String excluded : DEFAULT_EXCLUDED_PROTOCOLS)
+ {
+ if (excluded.equals(protocol))
+ LOG_CONFIG.warn("Protocol {} not excluded for {}", protocol, this);
+ }
+ }
+ }
+
+ protected void checkCiphers(SSLParameters supported)
+ {
+ for (String suite : supported.getCipherSuites())
+ {
+ for (String excludedSuiteRegex : DEFAULT_EXCLUDED_CIPHER_SUITES)
+ {
+ if (suite.matches(excludedSuiteRegex))
+ LOG_CONFIG.warn("Weak cipher suite {} enabled for {}", suite, this);
+ }
+ }
+ }
+
+ private void load() throws Exception
+ {
+ SSLContext context = _setContext;
+ KeyStore keyStore = _setKeyStore;
+ KeyStore trustStore = _setTrustStore;
+
+ if (context == null)
+ {
+ // Is this an empty factory?
+ if (keyStore == null && _keyStoreResource == null && trustStore == null && _trustStoreResource == null)
+ {
+ TrustManager[] trustManagers = null;
+
+ if (isTrustAll())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("No keystore or trust store configured. ACCEPTING UNTRUSTED CERTIFICATES!!!!!");
+ // Create a trust manager that does not validate certificate chains
+ trustManagers = TRUST_ALL_CERTS;
+ }
+
+ context = getSSLContextInstance();
+ context.init(null, trustManagers, getSecureRandomInstance());
+ }
+ else
+ {
+ if (keyStore == null)
+ keyStore = loadKeyStore(_keyStoreResource);
+ if (trustStore == null)
+ trustStore = loadTrustStore(_trustStoreResource);
+
+ Collection<? extends CRL> crls = loadCRL(getCrlPath());
+
+ // Look for X.509 certificates to create alias map
+ if (keyStore != null)
+ {
+ for (String alias : Collections.list(keyStore.aliases()))
+ {
+ Certificate certificate = keyStore.getCertificate(alias);
+ if (certificate != null && "X.509".equals(certificate.getType()))
+ {
+ X509Certificate x509C = (X509Certificate)certificate;
+
+ // Exclude certificates with special uses
+ if (X509.isCertSign(x509C))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Skipping " + x509C);
+ continue;
+ }
+ X509 x509 = new X509(alias, x509C);
+ _aliasX509.put(alias, x509);
+
+ if (isValidateCerts())
+ {
+ CertificateValidator validator = new CertificateValidator(trustStore, crls);
+ validator.setMaxCertPathLength(getMaxCertPathLength());
+ validator.setEnableCRLDP(isEnableCRLDP());
+ validator.setEnableOCSP(isEnableOCSP());
+ validator.setOcspResponderURL(getOcspResponderURL());
+ validator.validate(keyStore, x509C); // TODO what about truststore?
+ }
+
+ LOG.info("x509={} for {}", x509, this);
+
+ for (String h : x509.getHosts())
+ {
+ _certHosts.put(h, x509);
+ }
+ for (String w : x509.getWilds())
+ {
+ _certWilds.put(w, x509);
+ }
+ }
+ }
+ }
+
+ // Instantiate key and trust managers
+ KeyManager[] keyManagers = getKeyManagers(keyStore);
+ TrustManager[] trustManagers = getTrustManagers(trustStore, crls);
+
+ // Initialize context
+ context = getSSLContextInstance();
+ context.init(keyManagers, trustManagers, getSecureRandomInstance());
+ }
+ }
+
+ // Initialize cache
+ SSLSessionContext serverContext = context.getServerSessionContext();
+ if (serverContext != null)
+ {
+ if (getSslSessionCacheSize() > -1)
+ serverContext.setSessionCacheSize(getSslSessionCacheSize());
+ if (getSslSessionTimeout() > -1)
+ serverContext.setSessionTimeout(getSslSessionTimeout());
+ }
+
+ // select the protocols and ciphers
+ SSLParameters enabled = context.getDefaultSSLParameters();
+ SSLParameters supported = context.getSupportedSSLParameters();
+ selectCipherSuites(enabled.getCipherSuites(), supported.getCipherSuites());
+ selectProtocols(enabled.getProtocols(), supported.getProtocols());
+
+ _factory = new Factory(keyStore, trustStore, context);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Selected Protocols {} of {}", Arrays.asList(_selectedProtocols), Arrays.asList(supported.getProtocols()));
+ LOG.debug("Selected Ciphers {} of {}", Arrays.asList(_selectedCipherSuites), Arrays.asList(supported.getCipherSuites()));
+ }
+ }
+
+ @Override
+ public String dump()
+ {
+ return Dumpable.dump(this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ try
+ {
+ SSLEngine sslEngine = SSLContext.getDefault().createSSLEngine();
+ Dumpable.dumpObjects(out, indent, this, "trustAll=" + _trustAll,
+ new SslSelectionDump("Protocol",
+ sslEngine.getSupportedProtocols(),
+ sslEngine.getEnabledProtocols(),
+ getExcludeProtocols(),
+ getIncludeProtocols()),
+ new SslSelectionDump("Cipher Suite",
+ sslEngine.getSupportedCipherSuites(),
+ sslEngine.getEnabledCipherSuites(),
+ getExcludeCipherSuites(),
+ getIncludeCipherSuites()));
+ }
+ catch (NoSuchAlgorithmException x)
+ {
+ LOG.ignore(x);
+ }
+ }
+
+ List<SslSelectionDump> selectionDump() throws NoSuchAlgorithmException
+ {
+ /* Use a pristine SSLEngine (not one from this SslContextFactory).
+ * This will allow for proper detection and identification
+ * of JRE/lib/security/java.security level disabled features
+ */
+ SSLEngine sslEngine = SSLContext.getDefault().createSSLEngine();
+
+ List<SslSelectionDump> selections = new ArrayList<>();
+
+ // protocols
+ selections.add(new SslSelectionDump("Protocol",
+ sslEngine.getSupportedProtocols(),
+ sslEngine.getEnabledProtocols(),
+ getExcludeProtocols(),
+ getIncludeProtocols()));
+
+ // ciphers
+ selections.add(new SslSelectionDump("Cipher Suite",
+ sslEngine.getSupportedCipherSuites(),
+ sslEngine.getEnabledCipherSuites(),
+ getExcludeCipherSuites(),
+ getIncludeCipherSuites()));
+
+ return selections;
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ synchronized (this)
+ {
+ unload();
+ }
+ super.doStop();
+ }
+
+ private void unload()
+ {
+ _factory = null;
+ _selectedProtocols = null;
+ _selectedCipherSuites = null;
+ _aliasX509.clear();
+ _certHosts.clear();
+ _certWilds.clear();
+ }
+
+ Map<String, X509> aliasCerts()
+ {
+ return _aliasX509;
+ }
+
+ @ManagedAttribute(value = "The selected TLS protocol versions", readonly = true)
+ public String[] getSelectedProtocols()
+ {
+ return Arrays.copyOf(_selectedProtocols, _selectedProtocols.length);
+ }
+
+ @ManagedAttribute(value = "The selected cipher suites", readonly = true)
+ public String[] getSelectedCipherSuites()
+ {
+ return Arrays.copyOf(_selectedCipherSuites, _selectedCipherSuites.length);
+ }
+
+ public Comparator<String> getCipherComparator()
+ {
+ return _cipherComparator;
+ }
+
+ public void setCipherComparator(Comparator<String> cipherComparator)
+ {
+ if (cipherComparator != null)
+ setUseCipherSuitesOrder(true);
+ _cipherComparator = cipherComparator;
+ }
+
+ public Set<String> getAliases()
+ {
+ return Collections.unmodifiableSet(_aliasX509.keySet());
+ }
+
+ public X509 getX509(String alias)
+ {
+ return _aliasX509.get(alias);
+ }
+
+ /**
+ * @return The array of protocol names to exclude from
+ * {@link SSLEngine#setEnabledProtocols(String[])}
+ */
+ @ManagedAttribute("The excluded TLS protocols")
+ public String[] getExcludeProtocols()
+ {
+ return _excludeProtocols.toArray(new String[0]);
+ }
+
+ /**
+ * You can either use the exact Protocol name or a a regular expression.
+ *
+ * @param protocols The array of protocol names to exclude from
+ * {@link SSLEngine#setEnabledProtocols(String[])}
+ */
+ public void setExcludeProtocols(String... protocols)
+ {
+ _excludeProtocols.clear();
+ _excludeProtocols.addAll(Arrays.asList(protocols));
+ }
+
+ /**
+ * You can either use the exact Protocol name or a a regular expression.
+ *
+ * @param protocol Protocol name patterns to add to {@link SSLEngine#setEnabledProtocols(String[])}
+ */
+ public void addExcludeProtocols(String... protocol)
+ {
+ _excludeProtocols.addAll(Arrays.asList(protocol));
+ }
+
+ /**
+ * @return The array of protocol name patterns to include in
+ * {@link SSLEngine#setEnabledProtocols(String[])}
+ */
+ @ManagedAttribute("The included TLS protocols")
+ public String[] getIncludeProtocols()
+ {
+ return _includeProtocols.toArray(new String[0]);
+ }
+
+ /**
+ * You can either use the exact Protocol name or a a regular expression.
+ *
+ * @param protocols The array of protocol name patterns to include in
+ * {@link SSLEngine#setEnabledProtocols(String[])}
+ */
+ public void setIncludeProtocols(String... protocols)
+ {
+ _includeProtocols.clear();
+ _includeProtocols.addAll(Arrays.asList(protocols));
+ }
+
+ /**
+ * @return The array of cipher suite name patterns to exclude from
+ * {@link SSLEngine#setEnabledCipherSuites(String[])}
+ */
+ @ManagedAttribute("The excluded cipher suites")
+ public String[] getExcludeCipherSuites()
+ {
+ return _excludeCipherSuites.toArray(new String[0]);
+ }
+
+ /**
+ * You can either use the exact Cipher suite name or a a regular expression.
+ *
+ * @param cipherSuites The array of cipher suite names to exclude from
+ * {@link SSLEngine#setEnabledCipherSuites(String[])}
+ */
+ public void setExcludeCipherSuites(String... cipherSuites)
+ {
+ _excludeCipherSuites.clear();
+ _excludeCipherSuites.addAll(Arrays.asList(cipherSuites));
+ }
+
+ /**
+ * You can either use the exact Cipher suite name or a a regular expression.
+ *
+ * @param cipher Cipher names to add to {@link SSLEngine#setEnabledCipherSuites(String[])}
+ */
+ public void addExcludeCipherSuites(String... cipher)
+ {
+ _excludeCipherSuites.addAll(Arrays.asList(cipher));
+ }
+
+ /**
+ * @return The array of Cipher suite names to include in
+ * {@link SSLEngine#setEnabledCipherSuites(String[])}
+ */
+ @ManagedAttribute("The included cipher suites")
+ public String[] getIncludeCipherSuites()
+ {
+ return _includeCipherSuites.toArray(new String[0]);
+ }
+
+ /**
+ * You can either use the exact Cipher suite name or a a regular expression.
+ *
+ * @param cipherSuites The array of cipher suite names to include in
+ * {@link SSLEngine#setEnabledCipherSuites(String[])}
+ */
+ public void setIncludeCipherSuites(String... cipherSuites)
+ {
+ _includeCipherSuites.clear();
+ _includeCipherSuites.addAll(Arrays.asList(cipherSuites));
+ }
+
+ @ManagedAttribute("Whether to respect the cipher suites order")
+ public boolean isUseCipherSuitesOrder()
+ {
+ return _useCipherSuitesOrder;
+ }
+
+ public void setUseCipherSuitesOrder(boolean useCipherSuitesOrder)
+ {
+ _useCipherSuitesOrder = useCipherSuitesOrder;
+ }
+
+ /**
+ * @return The file or URL of the SSL Key store.
+ */
+ @ManagedAttribute("The keyStore path")
+ public String getKeyStorePath()
+ {
+ return Objects.toString(_keyStoreResource, null);
+ }
+
+ /**
+ * @param keyStorePath The file or URL of the SSL Key store.
+ */
+ public void setKeyStorePath(String keyStorePath)
+ {
+ try
+ {
+ _keyStoreResource = Resource.newResource(keyStorePath);
+ }
+ catch (Exception e)
+ {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * @return The provider of the key store
+ */
+ @ManagedAttribute("The keyStore provider name")
+ public String getKeyStoreProvider()
+ {
+ return _keyStoreProvider;
+ }
+
+ /**
+ * @param keyStoreProvider The provider of the key store
+ */
+ public void setKeyStoreProvider(String keyStoreProvider)
+ {
+ _keyStoreProvider = keyStoreProvider;
+ }
+
+ /**
+ * @return The type of the key store (default "JKS")
+ */
+ @ManagedAttribute("The keyStore type")
+ public String getKeyStoreType()
+ {
+ return (_keyStoreType);
+ }
+
+ /**
+ * @param keyStoreType The type of the key store (default "JKS")
+ */
+ public void setKeyStoreType(String keyStoreType)
+ {
+ _keyStoreType = keyStoreType;
+ }
+
+ /**
+ * @return Alias of SSL certificate for the connector
+ */
+ @ManagedAttribute("The certificate alias")
+ public String getCertAlias()
+ {
+ return _certAlias;
+ }
+
+ /**
+ * Set the default certificate Alias.
+ * <p>This can be used if there are multiple non-SNI certificates
+ * to specify the certificate that should be used, or with SNI
+ * certificates to set a certificate to try if no others match
+ * </p>
+ *
+ * @param certAlias Alias of SSL certificate for the connector
+ */
+ public void setCertAlias(String certAlias)
+ {
+ _certAlias = certAlias;
+ }
+
+ @ManagedAttribute("The trustStore path")
+ public String getTrustStorePath()
+ {
+ return Objects.toString(_trustStoreResource, null);
+ }
+
+ /**
+ * @param trustStorePath The file name or URL of the trust store location
+ */
+ public void setTrustStorePath(String trustStorePath)
+ {
+ try
+ {
+ _trustStoreResource = Resource.newResource(trustStorePath);
+ }
+ catch (Exception e)
+ {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * @return The provider of the trust store
+ */
+ @ManagedAttribute("The trustStore provider name")
+ public String getTrustStoreProvider()
+ {
+ return _trustStoreProvider;
+ }
+
+ /**
+ * @param trustStoreProvider The provider of the trust store
+ */
+ public void setTrustStoreProvider(String trustStoreProvider)
+ {
+ _trustStoreProvider = trustStoreProvider;
+ }
+
+ /**
+ * @return The type of the trust store
+ */
+ @ManagedAttribute("The trustStore type")
+ public String getTrustStoreType()
+ {
+ return _trustStoreType;
+ }
+
+ /**
+ * @param trustStoreType The type of the trust store
+ */
+ public void setTrustStoreType(String trustStoreType)
+ {
+ _trustStoreType = trustStoreType;
+ }
+
+ /**
+ * @return True if SSL needs client authentication.
+ * @see SSLEngine#getNeedClientAuth()
+ * @deprecated use {@link Server#getNeedClientAuth()} instead
+ */
+ @ManagedAttribute("Whether client authentication is needed")
+ @Deprecated
+ public boolean getNeedClientAuth()
+ {
+ return _needClientAuth;
+ }
+
+ /**
+ * @param needClientAuth True if SSL needs client authentication.
+ * @see SSLEngine#getNeedClientAuth()
+ * @deprecated use {@link Server#setNeedClientAuth(boolean)} instead
+ */
+ @Deprecated
+ public void setNeedClientAuth(boolean needClientAuth)
+ {
+ _needClientAuth = needClientAuth;
+ }
+
+ /**
+ * @return True if SSL wants client authentication.
+ * @see SSLEngine#getWantClientAuth()
+ * @deprecated use {@link Server#getWantClientAuth()} instead
+ */
+ @ManagedAttribute("Whether client authentication is wanted")
+ @Deprecated
+ public boolean getWantClientAuth()
+ {
+ return _wantClientAuth;
+ }
+
+ /**
+ * @param wantClientAuth True if SSL wants client authentication.
+ * @see SSLEngine#getWantClientAuth()
+ * @deprecated use {@link Server#setWantClientAuth(boolean)} instead
+ */
+ @Deprecated
+ public void setWantClientAuth(boolean wantClientAuth)
+ {
+ _wantClientAuth = wantClientAuth;
+ }
+
+ /**
+ * @return true if SSL certificate has to be validated
+ */
+ @ManagedAttribute("Whether certificates are validated")
+ public boolean isValidateCerts()
+ {
+ return _validateCerts;
+ }
+
+ /**
+ * @param validateCerts true if SSL certificates have to be validated
+ */
+ public void setValidateCerts(boolean validateCerts)
+ {
+ _validateCerts = validateCerts;
+ }
+
+ /**
+ * @return true if SSL certificates of the peer have to be validated
+ */
+ @ManagedAttribute("Whether peer certificates are validated")
+ public boolean isValidatePeerCerts()
+ {
+ return _validatePeerCerts;
+ }
+
+ /**
+ * @param validatePeerCerts true if SSL certificates of the peer have to be validated
+ */
+ public void setValidatePeerCerts(boolean validatePeerCerts)
+ {
+ _validatePeerCerts = validatePeerCerts;
+ }
+
+ /**
+ * @param password The password for the key store. If null is passed and
+ * a keystore is set, then
+ * the {@link #getPassword(String)} is used to
+ * obtain a password either from the {@value #PASSWORD_PROPERTY}
+ * system property or by prompting for manual entry.
+ */
+ public void setKeyStorePassword(String password)
+ {
+ if (password == null)
+ {
+ if (_keyStoreResource != null)
+ _keyStorePassword = getPassword(PASSWORD_PROPERTY);
+ else
+ _keyStorePassword = null;
+ }
+ else
+ {
+ _keyStorePassword = newPassword(password);
+ }
+ }
+
+ /**
+ * @param password The password (if any) for the specific key within the key store.
+ * If null is passed and the {@value #KEYPASSWORD_PROPERTY} system property is set,
+ * then the {@link #getPassword(String)} is used to
+ * obtain a password from the {@value #KEYPASSWORD_PROPERTY} system property.
+ */
+ public void setKeyManagerPassword(String password)
+ {
+ if (password == null)
+ {
+ if (System.getProperty(KEYPASSWORD_PROPERTY) != null)
+ _keyManagerPassword = getPassword(KEYPASSWORD_PROPERTY);
+ else
+ _keyManagerPassword = null;
+ }
+ else
+ {
+ _keyManagerPassword = newPassword(password);
+ }
+ }
+
+ /**
+ * @param password The password for the truststore. If null is passed and a truststore is set
+ * that is different from the keystore, then
+ * the {@link #getPassword(String)} is used to
+ * obtain a password either from the {@value #PASSWORD_PROPERTY}
+ * system property or by prompting for manual entry.
+ */
+ public void setTrustStorePassword(String password)
+ {
+ if (password == null)
+ {
+ if (_trustStoreResource != null && !_trustStoreResource.equals(_keyStoreResource))
+ _trustStorePassword = getPassword(PASSWORD_PROPERTY);
+ else
+ _trustStorePassword = null;
+ }
+ else
+ {
+ _trustStorePassword = newPassword(password);
+ }
+ }
+
+ /**
+ * <p>
+ * Get the optional Security Provider name.
+ * </p>
+ * <p>
+ * Security Provider name used with:
+ * </p>
+ * <ul>
+ * <li>{@link SecureRandom#getInstance(String, String)}</li>
+ * <li>{@link SSLContext#getInstance(String, String)}</li>
+ * <li>{@link TrustManagerFactory#getInstance(String, String)}</li>
+ * <li>{@link KeyManagerFactory#getInstance(String, String)}</li>
+ * <li>{@link CertStore#getInstance(String, CertStoreParameters, String)}</li>
+ * <li>{@link java.security.cert.CertificateFactory#getInstance(String, String)}</li>
+ * </ul>
+ *
+ * @return The optional Security Provider name.
+ */
+ @ManagedAttribute("The provider name")
+ public String getProvider()
+ {
+ return _sslProvider;
+ }
+
+ /**
+ * <p>
+ * Set the optional Security Provider name.
+ * </p>
+ * <p>
+ * Security Provider name used with:
+ * </p>
+ * <ul>
+ * <li>{@link SecureRandom#getInstance(String, String)}</li>
+ * <li>{@link SSLContext#getInstance(String, String)}</li>
+ * <li>{@link TrustManagerFactory#getInstance(String, String)}</li>
+ * <li>{@link KeyManagerFactory#getInstance(String, String)}</li>
+ * <li>{@link CertStore#getInstance(String, CertStoreParameters, String)}</li>
+ * <li>{@link java.security.cert.CertificateFactory#getInstance(String, String)}</li>
+ * </ul>
+ *
+ * @param provider The optional Security Provider name.
+ */
+ public void setProvider(String provider)
+ {
+ _sslProvider = provider;
+ }
+
+ /**
+ * @return The SSL protocol (default "TLS") passed to
+ * {@link SSLContext#getInstance(String, String)}
+ */
+ @ManagedAttribute("The TLS protocol")
+ public String getProtocol()
+ {
+ return _sslProtocol;
+ }
+
+ /**
+ * @param protocol The SSL protocol (default "TLS") passed to
+ * {@link SSLContext#getInstance(String, String)}
+ */
+ public void setProtocol(String protocol)
+ {
+ _sslProtocol = protocol;
+ }
+
+ /**
+ * @return The algorithm name, which if set is passed to
+ * {@link SecureRandom#getInstance(String)} to obtain the {@link SecureRandom} instance passed to
+ * {@link SSLContext#init(javax.net.ssl.KeyManager[], javax.net.ssl.TrustManager[], SecureRandom)}
+ */
+ @ManagedAttribute("The SecureRandom algorithm")
+ public String getSecureRandomAlgorithm()
+ {
+ return _secureRandomAlgorithm;
+ }
+
+ /**
+ * @param algorithm The algorithm name, which if set is passed to
+ * {@link SecureRandom#getInstance(String)} to obtain the {@link SecureRandom} instance passed to
+ * {@link SSLContext#init(javax.net.ssl.KeyManager[], javax.net.ssl.TrustManager[], SecureRandom)}
+ */
+ public void setSecureRandomAlgorithm(String algorithm)
+ {
+ _secureRandomAlgorithm = algorithm;
+ }
+
+ /**
+ * @return The algorithm name (default "SunX509") used by the {@link KeyManagerFactory}
+ */
+ @ManagedAttribute("The KeyManagerFactory algorithm")
+ public String getKeyManagerFactoryAlgorithm()
+ {
+ return _keyManagerFactoryAlgorithm;
+ }
+
+ /**
+ * @param algorithm The algorithm name (default "SunX509") used by the {@link KeyManagerFactory}
+ */
+ public void setKeyManagerFactoryAlgorithm(String algorithm)
+ {
+ _keyManagerFactoryAlgorithm = algorithm;
+ }
+
+ /**
+ * @return The algorithm name (default "SunX509") used by the {@link TrustManagerFactory}
+ */
+ @ManagedAttribute("The TrustManagerFactory algorithm")
+ public String getTrustManagerFactoryAlgorithm()
+ {
+ return _trustManagerFactoryAlgorithm;
+ }
+
+ /**
+ * @return True if all certificates should be trusted if there is no KeyStore or TrustStore
+ */
+ @ManagedAttribute("Whether certificates should be trusted even if they are invalid")
+ public boolean isTrustAll()
+ {
+ return _trustAll;
+ }
+
+ /**
+ * @param trustAll True if all certificates should be trusted if there is no KeyStore or TrustStore
+ */
+ public void setTrustAll(boolean trustAll)
+ {
+ _trustAll = trustAll;
+ if (trustAll)
+ setEndpointIdentificationAlgorithm(null);
+ }
+
+ /**
+ * @param algorithm The algorithm name (default "SunX509") used by the {@link TrustManagerFactory}
+ * Use the string "TrustAll" to install a trust manager that trusts all.
+ */
+ public void setTrustManagerFactoryAlgorithm(String algorithm)
+ {
+ _trustManagerFactoryAlgorithm = algorithm;
+ }
+
+ /**
+ * @return whether TLS renegotiation is allowed (true by default)
+ */
+ @ManagedAttribute("Whether renegotiation is allowed")
+ public boolean isRenegotiationAllowed()
+ {
+ return _renegotiationAllowed;
+ }
+
+ /**
+ * @param renegotiationAllowed whether TLS renegotiation is allowed
+ */
+ public void setRenegotiationAllowed(boolean renegotiationAllowed)
+ {
+ _renegotiationAllowed = renegotiationAllowed;
+ }
+
+ /**
+ * @return The number of renegotiations allowed for this connection. When the limit
+ * is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied.
+ */
+ @ManagedAttribute("The max number of renegotiations allowed")
+ public int getRenegotiationLimit()
+ {
+ return _renegotiationLimit;
+ }
+
+ /**
+ * @param renegotiationLimit The number of renegotions allowed for this connection.
+ * When the limit is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied.
+ * Default 5.
+ */
+ public void setRenegotiationLimit(int renegotiationLimit)
+ {
+ _renegotiationLimit = renegotiationLimit;
+ }
+
+ /**
+ * @return Path to file that contains Certificate Revocation List
+ */
+ @ManagedAttribute("The path to the certificate revocation list file")
+ public String getCrlPath()
+ {
+ return _crlPath;
+ }
+
+ /**
+ * @param crlPath Path to file that contains Certificate Revocation List
+ */
+ public void setCrlPath(String crlPath)
+ {
+ _crlPath = crlPath;
+ }
+
+ /**
+ * @return Maximum number of intermediate certificates in
+ * the certification path (-1 for unlimited)
+ */
+ @ManagedAttribute("The maximum number of intermediate certificates")
+ public int getMaxCertPathLength()
+ {
+ return _maxCertPathLength;
+ }
+
+ /**
+ * @param maxCertPathLength maximum number of intermediate certificates in
+ * the certification path (-1 for unlimited)
+ */
+ public void setMaxCertPathLength(int maxCertPathLength)
+ {
+ _maxCertPathLength = maxCertPathLength;
+ }
+
+ /**
+ * @return The SSLContext
+ */
+ public SSLContext getSslContext()
+ {
+ if (!isStarted())
+ return _setContext;
+
+ synchronized (this)
+ {
+ if (_factory == null)
+ throw new IllegalStateException("SslContextFactory reload failed");
+
+ return _factory._context;
+ }
+ }
+
+ /**
+ * @param sslContext Set a preconfigured SSLContext
+ */
+ public void setSslContext(SSLContext sslContext)
+ {
+ _setContext = sslContext;
+ }
+
+ /**
+ * @return the endpoint identification algorithm
+ */
+ @ManagedAttribute("The endpoint identification algorithm")
+ public String getEndpointIdentificationAlgorithm()
+ {
+ return _endpointIdentificationAlgorithm;
+ }
+
+ /**
+ * When set to "HTTPS" hostname verification will be enabled.
+ * Deployments can be vulnerable to a man-in-the-middle attack if a EndpointIndentificationAlgorithm
+ * is not set.
+ *
+ * @param endpointIdentificationAlgorithm Set the endpointIdentificationAlgorithm
+ * @see #setHostnameVerifier(HostnameVerifier)
+ */
+ public void setEndpointIdentificationAlgorithm(String endpointIdentificationAlgorithm)
+ {
+ _endpointIdentificationAlgorithm = endpointIdentificationAlgorithm;
+ }
+
+ public PKIXCertPathChecker getPkixCertPathChecker()
+ {
+ return _pkixCertPathChecker;
+ }
+
+ public void setPkixCertPathChecker(PKIXCertPathChecker pkixCertPatchChecker)
+ {
+ _pkixCertPathChecker = pkixCertPatchChecker;
+ }
+
+ /**
+ * Override this method to provide alternate way to load a keystore.
+ *
+ * @param resource the resource to load the keystore from
+ * @return the key store instance
+ * @throws Exception if the keystore cannot be loaded
+ */
+ protected KeyStore loadKeyStore(Resource resource) throws Exception
+ {
+ String storePassword = Objects.toString(_keyStorePassword, null);
+ return CertificateUtils.getKeyStore(resource, getKeyStoreType(), getKeyStoreProvider(), storePassword);
+ }
+
+ /**
+ * Override this method to provide alternate way to load a truststore.
+ *
+ * @param resource the resource to load the truststore from
+ * @return the key store instance
+ * @throws Exception if the truststore cannot be loaded
+ */
+ protected KeyStore loadTrustStore(Resource resource) throws Exception
+ {
+ String type = Objects.toString(getTrustStoreType(), getKeyStoreType());
+ String provider = Objects.toString(getTrustStoreProvider(), getKeyStoreProvider());
+ Password passwd = _trustStorePassword;
+ if (resource == null || resource.equals(_keyStoreResource))
+ {
+ resource = _keyStoreResource;
+ if (passwd == null)
+ passwd = _keyStorePassword;
+ }
+ return CertificateUtils.getKeyStore(resource, type, provider, Objects.toString(passwd, null));
+ }
+
+ /**
+ * Loads certificate revocation list (CRL) from a file.
+ * <p>
+ * Required for integrations to be able to override the mechanism used to
+ * load CRL in order to provide their own implementation.
+ *
+ * @param crlPath path of certificate revocation list file
+ * @return Collection of CRL's
+ * @throws Exception if the certificate revocation list cannot be loaded
+ */
+ protected Collection<? extends CRL> loadCRL(String crlPath) throws Exception
+ {
+ return CertificateUtils.loadCRL(crlPath);
+ }
+
+ protected KeyManager[] getKeyManagers(KeyStore keyStore) throws Exception
+ {
+ KeyManager[] managers = null;
+
+ if (keyStore != null)
+ {
+ KeyManagerFactory keyManagerFactory = getKeyManagerFactoryInstance();
+ keyManagerFactory.init(keyStore, _keyManagerPassword == null ? (_keyStorePassword == null ? null : _keyStorePassword.toString().toCharArray()) : _keyManagerPassword.toString().toCharArray());
+ managers = keyManagerFactory.getKeyManagers();
+
+ if (managers != null)
+ {
+ String alias = getCertAlias();
+ if (alias != null)
+ {
+ for (int idx = 0; idx < managers.length; idx++)
+ {
+ if (managers[idx] instanceof X509ExtendedKeyManager)
+ managers[idx] = new AliasedX509ExtendedKeyManager((X509ExtendedKeyManager)managers[idx], alias);
+ }
+ }
+
+ // Is SNI needed to select a certificate?
+ boolean sniRequired = (this instanceof Server) && ((Server)this).isSniRequired();
+ if (sniRequired || !_certWilds.isEmpty() || _certHosts.size() > 1 || (_certHosts.size() == 1 && _aliasX509.size() > 1))
+ {
+ for (int idx = 0; idx < managers.length; idx++)
+ {
+ if (managers[idx] instanceof X509ExtendedKeyManager)
+ managers[idx] = newSniX509ExtendedKeyManager((X509ExtendedKeyManager)managers[idx]);
+ }
+ }
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("managers={} for {}", managers, this);
+
+ return managers;
+ }
+
+ /**
+ * @deprecated use {@link SslContextFactory.Server#newSniX509ExtendedKeyManager(X509ExtendedKeyManager)} instead
+ */
+ @Deprecated
+ protected X509ExtendedKeyManager newSniX509ExtendedKeyManager(X509ExtendedKeyManager keyManager)
+ {
+ throw new IllegalStateException(String.format(
+ "KeyStores with multiple certificates are not supported on the base class %s. (Use %s or %s instead)",
+ SslContextFactory.class.getName(),
+ Server.class.getName(),
+ Client.class.getName()));
+ }
+
+ protected TrustManager[] getTrustManagers(KeyStore trustStore, Collection<? extends CRL> crls) throws Exception
+ {
+ TrustManager[] managers = null;
+ if (trustStore != null)
+ {
+ // Revocation checking is only supported for PKIX algorithm
+ if (isValidatePeerCerts() && "PKIX".equalsIgnoreCase(getTrustManagerFactoryAlgorithm()))
+ {
+ PKIXBuilderParameters pbParams = newPKIXBuilderParameters(trustStore, crls);
+
+ TrustManagerFactory trustManagerFactory = getTrustManagerFactoryInstance();
+ trustManagerFactory.init(new CertPathTrustManagerParameters(pbParams));
+
+ managers = trustManagerFactory.getTrustManagers();
+ }
+ else
+ {
+ TrustManagerFactory trustManagerFactory = getTrustManagerFactoryInstance();
+ trustManagerFactory.init(trustStore);
+
+ managers = trustManagerFactory.getTrustManagers();
+ }
+ }
+
+ return managers;
+ }
+
+ protected PKIXBuilderParameters newPKIXBuilderParameters(KeyStore trustStore, Collection<? extends CRL> crls) throws Exception
+ {
+ PKIXBuilderParameters pbParams = new PKIXBuilderParameters(trustStore, new X509CertSelector());
+
+ // Set maximum certification path length
+ pbParams.setMaxPathLength(_maxCertPathLength);
+
+ // Make sure revocation checking is enabled
+ pbParams.setRevocationEnabled(true);
+
+ if (_pkixCertPathChecker != null)
+ pbParams.addCertPathChecker(_pkixCertPathChecker);
+
+ if (crls != null && !crls.isEmpty())
+ {
+ pbParams.addCertStore(getCertStoreInstance(crls));
+ }
+
+ if (_enableCRLDP)
+ {
+ // Enable Certificate Revocation List Distribution Points (CRLDP) support
+ System.setProperty("com.sun.security.enableCRLDP", "true");
+ }
+
+ if (_enableOCSP)
+ {
+ // Enable On-Line Certificate Status Protocol (OCSP) support
+ Security.setProperty("ocsp.enable", "true");
+
+ if (_ocspResponderURL != null)
+ {
+ // Override location of OCSP Responder
+ Security.setProperty("ocsp.responderURL", _ocspResponderURL);
+ }
+ }
+
+ return pbParams;
+ }
+
+ /**
+ * Select protocols to be used by the connector
+ * based on configured inclusion and exclusion lists
+ * as well as enabled and supported protocols.
+ *
+ * @param enabledProtocols Array of enabled protocols
+ * @param supportedProtocols Array of supported protocols
+ */
+ public void selectProtocols(String[] enabledProtocols, String[] supportedProtocols)
+ {
+ List<String> selectedProtocols = processIncludeExcludePatterns("Protocols", enabledProtocols, supportedProtocols, _includeProtocols, _excludeProtocols);
+
+ if (selectedProtocols.isEmpty())
+ LOG.warn("No selected Protocols from {}", Arrays.asList(supportedProtocols));
+
+ _selectedProtocols = selectedProtocols.toArray(new String[0]);
+ }
+
+ /**
+ * Select cipher suites to be used by the connector
+ * based on configured inclusion and exclusion lists
+ * as well as enabled and supported cipher suite lists.
+ *
+ * @param enabledCipherSuites Array of enabled cipher suites
+ * @param supportedCipherSuites Array of supported cipher suites
+ */
+ protected void selectCipherSuites(String[] enabledCipherSuites, String[] supportedCipherSuites)
+ {
+ List<String> selectedCiphers = processIncludeExcludePatterns("Cipher Suite", enabledCipherSuites, supportedCipherSuites, _includeCipherSuites, _excludeCipherSuites);
+
+ if (selectedCiphers.isEmpty())
+ LOG.warn("No supported Cipher Suite from {}", Arrays.asList(supportedCipherSuites));
+
+ Comparator<String> comparator = getCipherComparator();
+ if (comparator != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Sorting selected ciphers with {}", comparator);
+ selectedCiphers.sort(comparator);
+ }
+
+ _selectedCipherSuites = selectedCiphers.toArray(new String[0]);
+ }
+
+ private List<String> processIncludeExcludePatterns(String type, String[] enabled, String[] supported, Set<String> included, Set<String> excluded)
+ {
+ List<String> selected = new ArrayList<>();
+ // Set the starting list - either from the included or enabled list
+ if (included.isEmpty())
+ {
+ selected.addAll(Arrays.asList(enabled));
+ }
+ else
+ {
+ // process include patterns
+ for (String includedItem : included)
+ {
+ Pattern pattern = Pattern.compile(includedItem);
+ boolean added = false;
+ for (String supportedItem : supported)
+ {
+ if (pattern.matcher(supportedItem).matches())
+ {
+ added = true;
+ selected.add(supportedItem);
+ }
+ }
+ if (!added)
+ LOG.info("No {} matching '{}' is supported", type, includedItem);
+ }
+ }
+
+ // process exclude patterns
+ for (String excludedItem : excluded)
+ {
+ Pattern pattern = Pattern.compile(excludedItem);
+ selected.removeIf(selectedItem -> pattern.matcher(selectedItem).matches());
+ }
+
+ return selected;
+ }
+
+ /**
+ * @deprecated no replacement
+ */
+ @Deprecated
+ protected void processIncludeCipherSuites(String[] supportedCipherSuites, List<String> selectedCiphers)
+ {
+ }
+
+ /**
+ * @deprecated no replacement
+ */
+ @Deprecated
+ protected void removeExcludedCipherSuites(List<String> selectedCiphers)
+ {
+ }
+
+ /**
+ * Check if the lifecycle has been started and throw runtime exception
+ */
+ private void checkIsStarted()
+ {
+ if (!isStarted())
+ throw new IllegalStateException("!STARTED: " + this);
+ }
+
+ /**
+ * @return true if CRL Distribution Points support is enabled
+ */
+ @ManagedAttribute("Whether certificate revocation list distribution points is enabled")
+ public boolean isEnableCRLDP()
+ {
+ return _enableCRLDP;
+ }
+
+ /**
+ * Enables CRL Distribution Points Support
+ *
+ * @param enableCRLDP true - turn on, false - turns off
+ */
+ public void setEnableCRLDP(boolean enableCRLDP)
+ {
+ _enableCRLDP = enableCRLDP;
+ }
+
+ /**
+ * @return true if On-Line Certificate Status Protocol support is enabled
+ */
+ @ManagedAttribute("Whether online certificate status protocol support is enabled")
+ public boolean isEnableOCSP()
+ {
+ return _enableOCSP;
+ }
+
+ /**
+ * Enables On-Line Certificate Status Protocol support
+ *
+ * @param enableOCSP true - turn on, false - turn off
+ */
+ public void setEnableOCSP(boolean enableOCSP)
+ {
+ _enableOCSP = enableOCSP;
+ }
+
+ /**
+ * @return Location of the OCSP Responder
+ */
+ @ManagedAttribute("The online certificate status protocol URL")
+ public String getOcspResponderURL()
+ {
+ return _ocspResponderURL;
+ }
+
+ /**
+ * Set the location of the OCSP Responder.
+ *
+ * @param ocspResponderURL location of the OCSP Responder
+ */
+ public void setOcspResponderURL(String ocspResponderURL)
+ {
+ _ocspResponderURL = ocspResponderURL;
+ }
+
+ /**
+ * Set the key store.
+ *
+ * @param keyStore the key store to set
+ */
+ public void setKeyStore(KeyStore keyStore)
+ {
+ _setKeyStore = keyStore;
+ }
+
+ public KeyStore getKeyStore()
+ {
+ if (!isStarted())
+ return _setKeyStore;
+
+ synchronized (this)
+ {
+ if (_factory == null)
+ throw new IllegalStateException("SslContextFactory reload failed");
+
+ return _factory._keyStore;
+ }
+ }
+
+ /**
+ * Set the trust store.
+ *
+ * @param trustStore the trust store to set
+ */
+ public void setTrustStore(KeyStore trustStore)
+ {
+ _setTrustStore = trustStore;
+ }
+
+ public KeyStore getTrustStore()
+ {
+ if (!isStarted())
+ return _setTrustStore;
+
+ synchronized (this)
+ {
+ if (_factory == null)
+ throw new IllegalStateException("SslContextFactory reload failed");
+
+ return _factory._trustStore;
+ }
+ }
+
+ /**
+ * Set the key store resource.
+ *
+ * @param resource the key store resource to set
+ */
+ public void setKeyStoreResource(Resource resource)
+ {
+ _keyStoreResource = resource;
+ }
+
+ public Resource getKeyStoreResource()
+ {
+ return _keyStoreResource;
+ }
+
+ /**
+ * Set the trust store resource.
+ *
+ * @param resource the trust store resource to set
+ */
+ public void setTrustStoreResource(Resource resource)
+ {
+ _trustStoreResource = resource;
+ }
+
+ public Resource getTrustStoreResource()
+ {
+ return _trustStoreResource;
+ }
+
+ /**
+ * @return true if SSL Session caching is enabled
+ */
+ @ManagedAttribute("Whether TLS session caching is enabled")
+ public boolean isSessionCachingEnabled()
+ {
+ return _sessionCachingEnabled;
+ }
+
+ /**
+ * Set the flag to enable SSL Session caching.
+ * If set to true, then the {@link SSLContext#createSSLEngine(String, int)} method is
+ * used to pass host and port information as a hint for session reuse. Note that
+ * this is only a hint and session may not be reused. Moreover, the hint is typically
+ * only used on client side implementations and setting this to false does not
+ * stop a server from accepting an offered session ID to reuse.
+ *
+ * @param enableSessionCaching the value of the flag
+ */
+ public void setSessionCachingEnabled(boolean enableSessionCaching)
+ {
+ _sessionCachingEnabled = enableSessionCaching;
+ }
+
+ /**
+ * Get SSL session cache size.
+ * Passed directly to {@link SSLSessionContext#setSessionCacheSize(int)}
+ *
+ * @return SSL session cache size
+ */
+ @ManagedAttribute("The maximum TLS session cache size")
+ public int getSslSessionCacheSize()
+ {
+ return _sslSessionCacheSize;
+ }
+
+ /**
+ * Set SSL session cache size.
+ * <p>Set the max cache size to be set on {@link SSLSessionContext#setSessionCacheSize(int)}
+ * when this factory is started.</p>
+ *
+ * @param sslSessionCacheSize SSL session cache size to set. A value of -1 (default) uses
+ * the JVM default, 0 means unlimited and positive number is a max size.
+ */
+ public void setSslSessionCacheSize(int sslSessionCacheSize)
+ {
+ _sslSessionCacheSize = sslSessionCacheSize;
+ }
+
+ /**
+ * Get SSL session timeout.
+ *
+ * @return SSL session timeout
+ */
+ @ManagedAttribute("The TLS session cache timeout, in seconds")
+ public int getSslSessionTimeout()
+ {
+ return _sslSessionTimeout;
+ }
+
+ /**
+ * Set SSL session timeout.
+ * <p>Set the timeout in seconds to be set on {@link SSLSessionContext#setSessionTimeout(int)}
+ * when this factory is started.</p>
+ *
+ * @param sslSessionTimeout SSL session timeout to set in seconds. A value of -1 (default) uses
+ * the JVM default, 0 means unlimited and positive number is a timeout in seconds.
+ */
+ public void setSslSessionTimeout(int sslSessionTimeout)
+ {
+ _sslSessionTimeout = sslSessionTimeout;
+ }
+
+ /**
+ * @return the HostnameVerifier used by a client to verify host names in the server certificate
+ */
+ public HostnameVerifier getHostnameVerifier()
+ {
+ return _hostnameVerifier;
+ }
+
+ /**
+ * <p>Sets a {@code HostnameVerifier} used by a client to verify host names in the server certificate.</p>
+ * <p>The {@code HostnameVerifier} works in conjunction with {@link #setEndpointIdentificationAlgorithm(String)}.</p>
+ * <p>When {@code endpointIdentificationAlgorithm=="HTTPS"} (the default) the JDK TLS implementation
+ * checks that the host name indication set by the client matches the host names in the server certificate.
+ * If this check passes successfully, the {@code HostnameVerifier} is invoked and the application
+ * can perform additional checks and allow/deny the connection to the server.</p>
+ * <p>When {@code endpointIdentificationAlgorithm==null} the JDK TLS implementation will not check
+ * the host names, and any check is therefore performed only by the {@code HostnameVerifier.}</p>
+ *
+ * @param hostnameVerifier the HostnameVerifier used by a client to verify host names in the server certificate
+ */
+ public void setHostnameVerifier(HostnameVerifier hostnameVerifier)
+ {
+ _hostnameVerifier = hostnameVerifier;
+ }
+
+ /**
+ * Returns the password object for the given realm.
+ *
+ * @param realm the realm
+ * @return the Password object
+ */
+ protected Password getPassword(String realm)
+ {
+ return Password.getPassword(realm, null, null);
+ }
+
+ /**
+ * Creates a new Password object.
+ *
+ * @param password the password string
+ * @return the new Password object
+ */
+ public Password newPassword(String password)
+ {
+ return new Password(password);
+ }
+
+ public SSLServerSocket newSslServerSocket(String host, int port, int backlog) throws IOException
+ {
+ checkIsStarted();
+
+ SSLContext context = getSslContext();
+ SSLServerSocketFactory factory = context.getServerSocketFactory();
+ SSLServerSocket socket =
+ (SSLServerSocket)(host == null
+ ? factory.createServerSocket(port, backlog)
+ : factory.createServerSocket(port, backlog, InetAddress.getByName(host)));
+ socket.setSSLParameters(customize(socket.getSSLParameters()));
+
+ return socket;
+ }
+
+ public SSLSocket newSslSocket() throws IOException
+ {
+ checkIsStarted();
+
+ SSLContext context = getSslContext();
+ SSLSocketFactory factory = context.getSocketFactory();
+ SSLSocket socket = (SSLSocket)factory.createSocket();
+ socket.setSSLParameters(customize(socket.getSSLParameters()));
+
+ return socket;
+ }
+
+ protected CertificateFactory getCertificateFactoryInstance(String type) throws CertificateException
+ {
+ String provider = getProvider();
+
+ try
+ {
+ if (provider != null)
+ {
+ return CertificateFactory.getInstance(type, provider);
+ }
+ }
+ catch (Throwable cause)
+ {
+ LOG.info("Unable to get CertificateFactory instance for type [{}] on provider [{}], using default", type, provider);
+ if (LOG.isDebugEnabled())
+ LOG.debug(cause);
+ }
+
+ return CertificateFactory.getInstance(type);
+ }
+
+ protected CertStore getCertStoreInstance(Collection<? extends CRL> crls) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException
+ {
+ String type = "Collection";
+ String provider = getProvider();
+
+ try
+ {
+ if (provider != null)
+ {
+ return CertStore.getInstance(type, new CollectionCertStoreParameters(crls), provider);
+ }
+ }
+ catch (Throwable cause)
+ {
+ LOG.info("Unable to get CertStore instance for type [{}] on provider [{}], using default", type, provider);
+ if (LOG.isDebugEnabled())
+ LOG.debug(cause);
+ }
+
+ return CertStore.getInstance(type, new CollectionCertStoreParameters(crls));
+ }
+
+ protected KeyManagerFactory getKeyManagerFactoryInstance() throws NoSuchAlgorithmException
+ {
+ String algorithm = getKeyManagerFactoryAlgorithm();
+ String provider = getProvider();
+
+ try
+ {
+ if (provider != null)
+ {
+ return KeyManagerFactory.getInstance(algorithm, provider);
+ }
+ }
+ catch (Throwable cause)
+ {
+ // fall back to non-provider option
+ LOG.info("Unable to get KeyManagerFactory instance for algorithm [{}] on provider [{}], using default", algorithm, provider);
+ if (LOG.isDebugEnabled())
+ LOG.debug(cause);
+ }
+
+ return KeyManagerFactory.getInstance(algorithm);
+ }
+
+ protected SecureRandom getSecureRandomInstance() throws NoSuchAlgorithmException
+ {
+ String algorithm = getSecureRandomAlgorithm();
+
+ if (algorithm != null)
+ {
+ String provider = getProvider();
+
+ try
+ {
+ if (provider != null)
+ {
+ return SecureRandom.getInstance(algorithm, provider);
+ }
+ }
+ catch (Throwable cause)
+ {
+ LOG.info("Unable to get SecureRandom instance for algorithm [{}] on provider [{}], using default", algorithm, provider);
+ if (LOG.isDebugEnabled())
+ LOG.debug(cause);
+ }
+
+ return SecureRandom.getInstance(algorithm);
+ }
+
+ return null;
+ }
+
+ protected SSLContext getSSLContextInstance() throws NoSuchAlgorithmException
+ {
+ String protocol = getProtocol();
+ String provider = getProvider();
+
+ try
+ {
+ if (provider != null)
+ {
+ return SSLContext.getInstance(protocol, provider);
+ }
+ }
+ catch (Throwable cause)
+ {
+ LOG.info("Unable to get SSLContext instance for protocol [{}] on provider [{}], using default", protocol, provider);
+ if (LOG.isDebugEnabled())
+ LOG.debug(cause);
+ }
+
+ return SSLContext.getInstance(protocol);
+ }
+
+ protected TrustManagerFactory getTrustManagerFactoryInstance() throws NoSuchAlgorithmException
+ {
+ String algorithm = getTrustManagerFactoryAlgorithm();
+ String provider = getProvider();
+ try
+ {
+ if (provider != null)
+ {
+ return TrustManagerFactory.getInstance(algorithm, provider);
+ }
+ }
+ catch (Throwable cause)
+ {
+ LOG.info("Unable to get TrustManagerFactory instance for algorithm [{}] on provider [{}], using default", algorithm, provider);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug(cause);
+ }
+ }
+
+ return TrustManagerFactory.getInstance(algorithm);
+ }
+
+ /**
+ * Factory method for "scratch" {@link SSLEngine}s, usually only used for retrieving configuration
+ * information such as the application buffer size or the list of protocols/ciphers.
+ * <p>
+ * This method should not be used for creating {@link SSLEngine}s that are used in actual socket
+ * communication.
+ *
+ * @return a new, "scratch" {@link SSLEngine}
+ */
+ public SSLEngine newSSLEngine()
+ {
+ checkIsStarted();
+
+ SSLContext context = getSslContext();
+ SSLEngine sslEngine = context.createSSLEngine();
+ customize(sslEngine);
+
+ return sslEngine;
+ }
+
+ /**
+ * General purpose factory method for creating {@link SSLEngine}s, although creation of
+ * {@link SSLEngine}s on the server-side should prefer {@link #newSSLEngine(InetSocketAddress)}.
+ *
+ * @param host the remote host
+ * @param port the remote port
+ * @return a new {@link SSLEngine}
+ */
+ public SSLEngine newSSLEngine(String host, int port)
+ {
+ checkIsStarted();
+
+ SSLContext context = getSslContext();
+ SSLEngine sslEngine = isSessionCachingEnabled()
+ ? context.createSSLEngine(host, port)
+ : context.createSSLEngine();
+ customize(sslEngine);
+
+ return sslEngine;
+ }
+
+ /**
+ * Server-side only factory method for creating {@link SSLEngine}s.
+ * <p>
+ * If the given {@code address} is null, it is equivalent to {@link #newSSLEngine()}, otherwise
+ * {@link #newSSLEngine(String, int)} is called.
+ * <p>
+ * Clients that wish to create {@link SSLEngine} instances must use {@link #newSSLEngine(String, int)}.
+ *
+ * @param address the remote peer address
+ * @return a new {@link SSLEngine}
+ */
+ public SSLEngine newSSLEngine(InetSocketAddress address)
+ {
+ if (address == null)
+ return newSSLEngine();
+ return newSSLEngine(address.getHostString(), address.getPort());
+ }
+
+ /**
+ * Customize an SslEngine instance with the configuration of this factory,
+ * by calling {@link #customize(SSLParameters)}
+ *
+ * @param sslEngine the SSLEngine to customize
+ */
+ public void customize(SSLEngine sslEngine)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Customize {}", sslEngine);
+
+ sslEngine.setSSLParameters(customize(sslEngine.getSSLParameters()));
+ }
+
+ /**
+ * Customize an SslParameters instance with the configuration of this factory.
+ *
+ * @param sslParams The parameters to customize
+ * @return The passed instance of sslParams (returned as a convenience)
+ */
+ public SSLParameters customize(SSLParameters sslParams)
+ {
+ sslParams.setEndpointIdentificationAlgorithm(getEndpointIdentificationAlgorithm());
+ sslParams.setUseCipherSuitesOrder(isUseCipherSuitesOrder());
+ if (!_certHosts.isEmpty() || !_certWilds.isEmpty())
+ sslParams.setSNIMatchers(Collections.singletonList(new AliasSNIMatcher()));
+ if (_selectedCipherSuites != null)
+ sslParams.setCipherSuites(_selectedCipherSuites);
+ if (_selectedProtocols != null)
+ sslParams.setProtocols(_selectedProtocols);
+ if (!(this instanceof Client))
+ {
+ if (getWantClientAuth())
+ sslParams.setWantClientAuth(true);
+ if (getNeedClientAuth())
+ sslParams.setNeedClientAuth(true);
+ }
+ return sslParams;
+ }
+
+ public void reload(Consumer<SslContextFactory> consumer) throws Exception
+ {
+ synchronized (this)
+ {
+ consumer.accept(this);
+ unload();
+ load();
+ }
+ }
+
+ /**
+ * Obtain the X509 Certificate Chain from the provided SSLSession using this
+ * SslContextFactory's optional Provider specific {@link CertificateFactory}.
+ *
+ * @param sslSession the session to use for active peer certificates
+ * @return the certificate chain
+ */
+ public X509Certificate[] getX509CertChain(SSLSession sslSession)
+ {
+ return getX509CertChain(this, sslSession);
+ }
+
+ /**
+ * Obtain the X509 Certificate Chain from the provided SSLSession using the
+ * default {@link CertificateFactory} behaviors
+ *
+ * @param sslSession the session to use for active peer certificates
+ * @return the certificate chain
+ */
+ public static X509Certificate[] getCertChain(SSLSession sslSession)
+ {
+ return getX509CertChain(null, sslSession);
+ }
+
+ private static X509Certificate[] getX509CertChain(SslContextFactory sslContextFactory, SSLSession sslSession)
+ {
+ try
+ {
+ Certificate[] javaxCerts = sslSession.getPeerCertificates();
+ if (javaxCerts == null || javaxCerts.length == 0)
+ return null;
+
+ int length = javaxCerts.length;
+ X509Certificate[] javaCerts = new X509Certificate[length];
+
+ String type = "X.509";
+ CertificateFactory cf;
+ if (sslContextFactory != null)
+ {
+ cf = sslContextFactory.getCertificateFactoryInstance(type);
+ }
+ else
+ {
+ cf = CertificateFactory.getInstance(type);
+ }
+
+ for (int i = 0; i < length; i++)
+ {
+ byte[] bytes = javaxCerts[i].getEncoded();
+ ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
+ javaCerts[i] = (X509Certificate)cf.generateCertificate(stream);
+ }
+
+ return javaCerts;
+ }
+ catch (SSLPeerUnverifiedException pue)
+ {
+ return null;
+ }
+ catch (Exception e)
+ {
+ LOG.warn(Log.EXCEPTION, e);
+ return null;
+ }
+ }
+
+ /**
+ * Given the name of a TLS/SSL cipher suite, return an int representing it effective stream
+ * cipher key strength. i.e. How much entropy material is in the key material being fed into the
+ * encryption routines.
+ * <p>
+ * This is based on the information on effective key lengths in RFC 2246 - The TLS Protocol
+ * Version 1.0, Appendix C. CipherSuite definitions:
+ * <pre>
+ * Effective
+ * Cipher Type Key Bits
+ *
+ * NULL * Stream 0
+ * IDEA_CBC Block 128
+ * RC2_CBC_40 * Block 40
+ * RC4_40 * Stream 40
+ * RC4_128 Stream 128
+ * DES40_CBC * Block 40
+ * DES_CBC Block 56
+ * 3DES_EDE_CBC Block 168
+ * </pre>
+ *
+ * @param cipherSuite String name of the TLS cipher suite.
+ * @return int indicating the effective key entropy bit-length.
+ */
+ public static int deduceKeyLength(String cipherSuite)
+ {
+ // Roughly ordered from most common to least common.
+ if (cipherSuite == null)
+ return 0;
+ else if (cipherSuite.contains("WITH_AES_256_"))
+ return 256;
+ else if (cipherSuite.contains("WITH_RC4_128_"))
+ return 128;
+ else if (cipherSuite.contains("WITH_AES_128_"))
+ return 128;
+ else if (cipherSuite.contains("WITH_RC4_40_"))
+ return 40;
+ else if (cipherSuite.contains("WITH_3DES_EDE_CBC_"))
+ return 168;
+ else if (cipherSuite.contains("WITH_IDEA_CBC_"))
+ return 128;
+ else if (cipherSuite.contains("WITH_RC2_CBC_40_"))
+ return 40;
+ else if (cipherSuite.contains("WITH_DES40_CBC_"))
+ return 40;
+ else if (cipherSuite.contains("WITH_DES_CBC_"))
+ return 56;
+ else
+ return 0;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x[provider=%s,keyStore=%s,trustStore=%s]",
+ getClass().getSimpleName(),
+ hashCode(),
+ _sslProvider,
+ _keyStoreResource,
+ _trustStoreResource);
+ }
+
+ private static class Factory
+ {
+ private final KeyStore _keyStore;
+ private final KeyStore _trustStore;
+ private final SSLContext _context;
+
+ Factory(KeyStore keyStore, KeyStore trustStore, SSLContext context)
+ {
+ super();
+ _keyStore = keyStore;
+ _trustStore = trustStore;
+ _context = context;
+ }
+ }
+
+ static class AliasSNIMatcher extends SNIMatcher
+ {
+ private String _host;
+
+ AliasSNIMatcher()
+ {
+ super(StandardConstants.SNI_HOST_NAME);
+ }
+
+ @Override
+ public boolean matches(SNIServerName serverName)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("SNI matching for {}", serverName);
+
+ if (serverName instanceof SNIHostName)
+ {
+ _host = StringUtil.asciiToLowerCase(((SNIHostName)serverName).getAsciiName());
+ if (LOG.isDebugEnabled())
+ LOG.debug("SNI host name {}", _host);
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("No SNI host name for {}", serverName);
+ }
+
+ // Return true and allow the KeyManager to accept or reject when choosing a certificate.
+ // If we don't have a SNI host, or didn't see any certificate aliases,
+ // just say true as it will either somehow work or fail elsewhere.
+ return true;
+ }
+
+ public String getHost()
+ {
+ return _host;
+ }
+ }
+
+ public static class Client extends SslContextFactory
+ {
+ private SniProvider sniProvider = (sslEngine, serverNames) -> serverNames;
+
+ public Client()
+ {
+ this(false);
+ }
+
+ public Client(boolean trustAll)
+ {
+ super(trustAll);
+ }
+
+ @Override
+ protected void checkConfiguration()
+ {
+ checkTrustAll();
+ checkEndPointIdentificationAlgorithm();
+ super.checkConfiguration();
+ }
+
+ @Override
+ protected X509ExtendedKeyManager newSniX509ExtendedKeyManager(X509ExtendedKeyManager keyManager)
+ {
+ // Client has no SNI functionality.
+ return keyManager;
+ }
+
+ @Override
+ public void customize(SSLEngine sslEngine)
+ {
+ SSLParameters sslParameters = sslEngine.getSSLParameters();
+ List<SNIServerName> serverNames = sslParameters.getServerNames();
+ if (serverNames == null)
+ serverNames = Collections.emptyList();
+ List<SNIServerName> newServerNames = getSNIProvider().apply(sslEngine, serverNames);
+ if (newServerNames != null && newServerNames != serverNames)
+ {
+ sslParameters.setServerNames(newServerNames);
+ sslEngine.setSSLParameters(sslParameters);
+ }
+ super.customize(sslEngine);
+ }
+
+ /**
+ * @return the SNI provider used to customize the SNI
+ */
+ public SniProvider getSNIProvider()
+ {
+ return sniProvider;
+ }
+
+ /**
+ * @param sniProvider the SNI provider used to customize the SNI
+ */
+ public void setSNIProvider(SniProvider sniProvider)
+ {
+ this.sniProvider = Objects.requireNonNull(sniProvider);
+ }
+
+ /**
+ * <p>A provider for SNI names to send to the server during the TLS handshake.</p>
+ * <p>By default, the OpenJDK TLS implementation does not send SNI names when
+ * they are IP addresses, following what currently specified in
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6066#section-3">TLS 1.3</a>,
+ * or when they are non-domain strings such as {@code "localhost"}.</p>
+ * <p>If you need to send custom SNI, such as a non-domain SNI or an IP address SNI,
+ * you can set your own SNI provider or use {@link #NON_DOMAIN_SNI_PROVIDER}.</p>
+ */
+ @FunctionalInterface
+ public interface SniProvider
+ {
+ /**
+ * <p>An SNI provider that, if the given {@code serverNames} list is empty,
+ * retrieves the host via {@link SSLEngine#getPeerHost()}, converts it to
+ * ASCII bytes, and sends it as SNI.</p>
+ * <p>This allows to send non-domain SNI such as {@code "localhost"} or
+ * IP addresses.</p>
+ */
+ public static final SniProvider NON_DOMAIN_SNI_PROVIDER = Client::getSniServerNames;
+
+ /**
+ * <p>Provides the SNI names to send to the server.</p>
+ * <p>Currently, RFC 6066 allows for different types of server names,
+ * but defines only one of type "host_name".</p>
+ * <p>As such, the input {@code serverNames} list and the list to be returned
+ * contain at most one element.</p>
+ *
+ * @param sslEngine the SSLEngine that processes the TLS handshake
+ * @param serverNames the non-null immutable list of server names computed by implementation
+ * @return either the same {@code serverNames} list passed as parameter, or a new list
+ * containing the server names to send to the server
+ */
+ public List<SNIServerName> apply(SSLEngine sslEngine, List<SNIServerName> serverNames);
+ }
+
+ private static List<SNIServerName> getSniServerNames(SSLEngine sslEngine, List<SNIServerName> serverNames)
+ {
+ if (serverNames.isEmpty())
+ {
+ String host = sslEngine.getPeerHost();
+ if (host != null)
+ {
+ // Must use the byte[] constructor, because the character ':' is forbidden when
+ // using the String constructor (but typically present in IPv6 addresses).
+ // Since Java 17, only letter|digit|hyphen characters are allowed, even by the byte[] constructor.
+ return Collections.singletonList(new SNIHostName(host.getBytes(StandardCharsets.US_ASCII)));
+ }
+ }
+ return serverNames;
+ }
+ }
+
+ @ManagedObject
+ public static class Server extends SslContextFactory implements SniX509ExtendedKeyManager.SniSelector
+ {
+ private boolean _sniRequired;
+ private SniX509ExtendedKeyManager.SniSelector _sniSelector;
+
+ public Server()
+ {
+ setEndpointIdentificationAlgorithm(null);
+ }
+
+ @Override
+ public boolean getWantClientAuth()
+ {
+ return super.getWantClientAuth();
+ }
+
+ public void setWantClientAuth(boolean wantClientAuth)
+ {
+ super.setWantClientAuth(wantClientAuth);
+ }
+
+ @Override
+ public boolean getNeedClientAuth()
+ {
+ return super.getNeedClientAuth();
+ }
+
+ @Override
+ public void setNeedClientAuth(boolean needClientAuth)
+ {
+ super.setNeedClientAuth(needClientAuth);
+ }
+
+ /**
+ * <p>Returns whether an SNI match is required when choosing the alias that
+ * identifies the certificate to send to the client.</p>
+ * <p>The exact logic to choose an alias given the SNI is configurable via
+ * {@link #setSNISelector(SniX509ExtendedKeyManager.SniSelector)}.</p>
+ * <p>The default implementation is {@link #sniSelect(String, Principal[], SSLSession, String, Collection)}
+ * and if SNI is not required it will delegate the TLS implementation to
+ * choose an alias (typically the first alias in the KeyStore).</p>
+ * <p>Note that if a non SNI handshake is accepted, requests may still be rejected
+ * at the HTTP level for incorrect SNI (see SecureRequestCustomizer).</p>
+ *
+ * @return whether an SNI match is required when choosing the alias that identifies the certificate
+ */
+ @ManagedAttribute("Whether the TLS handshake is rejected if there is no SNI host match")
+ public boolean isSniRequired()
+ {
+ return _sniRequired;
+ }
+
+ /**
+ * <p>Sets whether an SNI match is required when choosing the alias that
+ * identifies the certificate to send to the client.</p>
+ * <p>This setting may have no effect if {@link #sniSelect(String, Principal[], SSLSession, String, Collection)} is
+ * overridden or a custom function is passed to {@link #setSNISelector(SniX509ExtendedKeyManager.SniSelector)}.</p>
+ *
+ * @param sniRequired whether an SNI match is required when choosing the alias that identifies the certificate
+ */
+ public void setSniRequired(boolean sniRequired)
+ {
+ _sniRequired = sniRequired;
+ }
+
+ @Override
+ protected KeyManager[] getKeyManagers(KeyStore keyStore) throws Exception
+ {
+ KeyManager[] managers = super.getKeyManagers(keyStore);
+ if (isSniRequired())
+ {
+ if (managers == null || Arrays.stream(managers).noneMatch(SniX509ExtendedKeyManager.class::isInstance))
+ throw new IllegalStateException("No SNI Key managers when SNI is required");
+ }
+ return managers;
+ }
+
+ /**
+ * @return the custom function to select certificates based on SNI information
+ */
+ public SniX509ExtendedKeyManager.SniSelector getSNISelector()
+ {
+ return _sniSelector;
+ }
+
+ /**
+ * <p>Sets a custom function to select certificates based on SNI information.</p>
+ *
+ * @param sniSelector the selection function
+ */
+ public void setSNISelector(SniX509ExtendedKeyManager.SniSelector sniSelector)
+ {
+ _sniSelector = sniSelector;
+ }
+
+ @Override
+ public String sniSelect(String keyType, Principal[] issuers, SSLSession session, String sniHost, Collection<X509> certificates)
+ {
+ boolean sniRequired = isSniRequired();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Selecting alias: keyType={}, sni={}, sniRequired={}, certs={}", keyType, String.valueOf(sniHost), sniRequired, certificates);
+
+ String alias;
+ if (sniHost == null)
+ {
+ // No SNI, so reject or delegate.
+ alias = sniRequired ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE;
+ }
+ else
+ {
+ // Match the SNI host.
+ List<X509> matching = certificates.stream()
+ .filter(x509 -> x509.matches(sniHost))
+ .collect(Collectors.toList());
+
+ if (matching.isEmpty())
+ {
+ // There is no match for this SNI among the certificates valid for
+ // this keyType; check if there is any certificate that matches this
+ // SNI, as we will likely be called again with a different keyType.
+ boolean anyMatching = aliasCerts().values().stream()
+ .anyMatch(x509 -> x509.matches(sniHost));
+ alias = sniRequired || anyMatching ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE;
+ }
+ else
+ {
+ alias = matching.get(0).getAlias();
+ if (matching.size() > 1)
+ {
+ // Prefer strict matches over wildcard matches.
+ alias = matching.stream()
+ .min(Comparator.comparingInt(cert -> cert.getWilds().size()))
+ .map(X509::getAlias)
+ .orElse(alias);
+ }
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Selected alias={}", String.valueOf(alias));
+
+ return alias;
+ }
+
+ @Override
+ protected X509ExtendedKeyManager newSniX509ExtendedKeyManager(X509ExtendedKeyManager keyManager)
+ {
+ return new SniX509ExtendedKeyManager(keyManager, this);
+ }
+ }
+
+ /**
+ * <p>A wrapper that delegates to another (if not {@code null}) X509ExtendedKeyManager.</p>
+ */
+ public static class X509ExtendedKeyManagerWrapper extends X509ExtendedKeyManager
+ {
+ private final X509ExtendedKeyManager keyManager;
+
+ public X509ExtendedKeyManagerWrapper(X509ExtendedKeyManager keyManager)
+ {
+ this.keyManager = keyManager;
+ }
+
+ @Override
+ public String[] getClientAliases(String keyType, Principal[] issuers)
+ {
+ return keyManager == null ? null : keyManager.getClientAliases(keyType, issuers);
+ }
+
+ @Override
+ public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket)
+ {
+ return keyManager == null ? null : keyManager.chooseClientAlias(keyType, issuers, socket);
+ }
+
+ @Override
+ public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine)
+ {
+ return keyManager == null ? null : keyManager.chooseEngineClientAlias(keyType, issuers, engine);
+ }
+
+ @Override
+ public String[] getServerAliases(String keyType, Principal[] issuers)
+ {
+ return keyManager == null ? null : keyManager.getServerAliases(keyType, issuers);
+ }
+
+ @Override
+ public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket)
+ {
+ return keyManager == null ? null : keyManager.chooseServerAlias(keyType, issuers, socket);
+ }
+
+ @Override
+ public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine)
+ {
+ return keyManager == null ? null : keyManager.chooseEngineServerAlias(keyType, issuers, engine);
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias)
+ {
+ return keyManager == null ? null : keyManager.getCertificateChain(alias);
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias)
+ {
+ return keyManager == null ? null : keyManager.getPrivateKey(alias);
+ }
+ }
+
+ /**
+ * <p>A wrapper that delegates to another (if not {@code null}) X509ExtendedTrustManager.</p>
+ */
+ public static class X509ExtendedTrustManagerWrapper extends X509ExtendedTrustManager
+ {
+ private final X509ExtendedTrustManager trustManager;
+
+ public X509ExtendedTrustManagerWrapper(X509ExtendedTrustManager trustManager)
+ {
+ this.trustManager = trustManager;
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers()
+ {
+ return trustManager == null ? new X509Certificate[0] : trustManager.getAcceptedIssuers();
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException
+ {
+ if (trustManager != null)
+ trustManager.checkClientTrusted(certs, authType);
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException
+ {
+ if (trustManager != null)
+ trustManager.checkClientTrusted(chain, authType, socket);
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException
+ {
+ if (trustManager != null)
+ trustManager.checkClientTrusted(chain, authType, engine);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException
+ {
+ if (trustManager != null)
+ trustManager.checkServerTrusted(certs, authType);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException
+ {
+ if (trustManager != null)
+ trustManager.checkServerTrusted(chain, authType, socket);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException
+ {
+ if (trustManager != null)
+ trustManager.checkServerTrusted(chain, authType, engine);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslSelectionDump.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslSelectionDump.java
new file mode 100644
index 0000000..a574ce8
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslSelectionDump.java
@@ -0,0 +1,173 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.ssl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.eclipse.jetty.util.component.Dumpable;
+
+class SslSelectionDump implements Dumpable
+{
+ static class CaptionedList extends ArrayList<String> implements Dumpable
+ {
+ private final String caption;
+
+ public CaptionedList(String caption)
+ {
+ this.caption = caption;
+ }
+
+ @Override
+ public String dump()
+ {
+ return Dumpable.dump(SslSelectionDump.CaptionedList.this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ Object[] array = toArray();
+ Dumpable.dumpObjects(out, indent, caption + " size=" + array.length, array);
+ }
+ }
+
+ final String type;
+ final SslSelectionDump.CaptionedList enabled = new SslSelectionDump.CaptionedList("Enabled");
+ final SslSelectionDump.CaptionedList disabled = new SslSelectionDump.CaptionedList("Disabled");
+
+ public SslSelectionDump(String type,
+ String[] supportedByJVM,
+ String[] enabledByJVM,
+ String[] excludedByConfig,
+ String[] includedByConfig)
+ {
+ this.type = type;
+
+ List<String> jvmEnabled = Arrays.asList(enabledByJVM);
+ List<Pattern> excludedPatterns = Arrays.stream(excludedByConfig)
+ .map((entry) -> Pattern.compile(entry))
+ .collect(Collectors.toList());
+ List<Pattern> includedPatterns = Arrays.stream(includedByConfig)
+ .map((entry) -> Pattern.compile(entry))
+ .collect(Collectors.toList());
+
+ Arrays.stream(supportedByJVM)
+ .sorted(Comparator.naturalOrder())
+ .forEach((entry) ->
+ {
+ boolean isPresent = true;
+
+ StringBuilder s = new StringBuilder();
+ s.append(entry);
+
+ for (Pattern pattern : excludedPatterns)
+ {
+ Matcher m = pattern.matcher(entry);
+ if (m.matches())
+ {
+ if (isPresent)
+ {
+ s.append(" -");
+ isPresent = false;
+ }
+ else
+ {
+ s.append(",");
+ }
+ s.append(" ConfigExcluded:'").append(pattern.pattern()).append('\'');
+ }
+ }
+
+ boolean isIncluded = false;
+
+ if (!includedPatterns.isEmpty())
+ {
+ for (Pattern pattern : includedPatterns)
+ {
+ Matcher m = pattern.matcher(entry);
+ if (m.matches())
+ {
+ isIncluded = true;
+ break;
+ }
+ }
+
+ if (!isIncluded)
+ {
+ if (isPresent)
+ {
+ s.append(" -");
+ isPresent = false;
+ }
+ else
+ {
+ s.append(",");
+ }
+
+ s.append(" ConfigIncluded:NotSelected");
+ }
+ }
+
+ if (!isIncluded && !jvmEnabled.contains(entry))
+ {
+ if (isPresent)
+ {
+ s.append(" -");
+ isPresent = false;
+ }
+
+ s.append(" JVM:disabled");
+ }
+
+ if (isPresent)
+ {
+ enabled.add(s.toString());
+ }
+ else
+ {
+ disabled.add(s.toString());
+ }
+ });
+ }
+
+ @Override
+ public String dump()
+ {
+ return Dumpable.dump(this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ Dumpable.dumpObjects(out, indent, this, enabled, disabled);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s Selections", type);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java
new file mode 100644
index 0000000..9484cf9
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java
@@ -0,0 +1,231 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.ssl;
+
+import java.net.InetAddress;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+import javax.security.auth.x500.X500Principal;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class X509
+{
+ private static final Logger LOG = Log.getLogger(X509.class);
+ /*
+ * @see {@link X509Certificate#getKeyUsage()}
+ */
+ private static final int KEY_USAGE__KEY_CERT_SIGN = 5;
+ /*
+ * @see {@link X509Certificate#getSubjectAlternativeNames()}
+ */
+ private static final int SUBJECT_ALTERNATIVE_NAMES__DNS_NAME = 2;
+ private static final int SUBJECT_ALTERNATIVE_NAMES__IP_ADDRESS = 7;
+
+ public static boolean isCertSign(X509Certificate x509)
+ {
+ boolean[] keyUsage = x509.getKeyUsage();
+ if ((keyUsage == null) || (keyUsage.length <= KEY_USAGE__KEY_CERT_SIGN))
+ {
+ return false;
+ }
+ return keyUsage[KEY_USAGE__KEY_CERT_SIGN];
+ }
+
+ private final X509Certificate _x509;
+ private final String _alias;
+ private final Set<String> _hosts = new LinkedHashSet<>();
+ private final Set<String> _wilds = new LinkedHashSet<>();
+ private final Set<InetAddress> _addresses = new LinkedHashSet<>();
+
+ public X509(String alias, X509Certificate x509)
+ {
+ _alias = alias;
+ _x509 = x509;
+
+ try
+ {
+ // Look for alternative name extensions
+ Collection<List<?>> altNames = x509.getSubjectAlternativeNames();
+ if (altNames != null)
+ {
+ for (List<?> list : altNames)
+ {
+ int nameType = ((Number)list.get(0)).intValue();
+ switch (nameType)
+ {
+ case SUBJECT_ALTERNATIVE_NAMES__DNS_NAME:
+ {
+ String name = list.get(1).toString();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Certificate alias={} SAN dns={} in {}", alias, name, this);
+ addName(name);
+ break;
+ }
+ case SUBJECT_ALTERNATIVE_NAMES__IP_ADDRESS:
+ {
+ String address = list.get(1).toString();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Certificate alias={} SAN ip={} in {}", alias, address, this);
+ addAddress(address);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+
+ // If no names found, look up the CN from the subject
+ LdapName name = new LdapName(x509.getSubjectX500Principal().getName(X500Principal.RFC2253));
+ for (Rdn rdn : name.getRdns())
+ {
+ if (rdn.getType().equalsIgnoreCase("CN"))
+ {
+ String cn = rdn.getValue().toString();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Certificate CN alias={} CN={} in {}", alias, cn, this);
+ addName(cn);
+ }
+ }
+ }
+ catch (Exception x)
+ {
+ throw new IllegalArgumentException(x);
+ }
+ }
+
+ protected void addName(String cn)
+ {
+ if (cn != null)
+ {
+ cn = StringUtil.asciiToLowerCase(cn);
+ if (cn.startsWith("*."))
+ _wilds.add(cn.substring(2));
+ else
+ _hosts.add(cn);
+ }
+ }
+
+ private void addAddress(String host)
+ {
+ // Class InetAddress handles IPV6 brackets and IPv6 short forms, so that [::1]
+ // would match 0:0:0:0:0:0:0:1 as well as 0000:0000:0000:0000:0000:0000:0000:0001.
+ InetAddress address = toInetAddress(host);
+ if (address != null)
+ _addresses.add(address);
+ }
+
+ private InetAddress toInetAddress(String address)
+ {
+ try
+ {
+ return InetAddress.getByName(address);
+ }
+ catch (Throwable x)
+ {
+ LOG.ignore(x);
+ return null;
+ }
+ }
+
+ public String getAlias()
+ {
+ return _alias;
+ }
+
+ public X509Certificate getCertificate()
+ {
+ return _x509;
+ }
+
+ public Set<String> getHosts()
+ {
+ return Collections.unmodifiableSet(_hosts);
+ }
+
+ public Set<String> getWilds()
+ {
+ return Collections.unmodifiableSet(_wilds);
+ }
+
+ public boolean matches(String host)
+ {
+ host = StringUtil.asciiToLowerCase(host);
+ if (_hosts.contains(host) || _wilds.contains(host))
+ return true;
+
+ int dot = host.indexOf('.');
+ if (dot >= 0)
+ {
+ String domain = host.substring(dot + 1);
+ if (_wilds.contains(domain))
+ return true;
+ }
+
+ // Check if the host looks like an IP address to avoid
+ // DNS lookup for host names that did not match.
+ if (seemsIPAddress(host))
+ {
+ InetAddress address = toInetAddress(host);
+ if (address != null)
+ return _addresses.contains(address);
+ }
+
+ return false;
+ }
+
+ private static boolean seemsIPAddress(String host)
+ {
+ // IPv4 is just numbers and dots.
+ String ipv4RegExp = "[0-9\\.]+";
+ // IPv6 is hex and colons and possibly brackets.
+ String ipv6RegExp = "[0-9a-fA-F:\\[\\]]+";
+ return host.matches(ipv4RegExp) ||
+ (host.matches(ipv6RegExp) && containsAtLeastTwoColons(host));
+ }
+
+ private static boolean containsAtLeastTwoColons(String host)
+ {
+ int index = host.indexOf(':');
+ if (index >= 0)
+ index = host.indexOf(':', index + 1);
+ return index > 0;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x(%s,h=%s,a=%s,w=%s)",
+ getClass().getSimpleName(),
+ hashCode(),
+ _alias,
+ _hosts,
+ _addresses,
+ _wilds);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/package-info.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/package-info.java
new file mode 100644
index 0000000..e53c8dc
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Util : Common SSL Utility Classes
+ */
+package org.eclipse.jetty.util.ssl;
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/CounterStatistic.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/CounterStatistic.java
new file mode 100644
index 0000000..80e6b89
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/CounterStatistic.java
@@ -0,0 +1,132 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.statistic;
+
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.LongAccumulator;
+import java.util.concurrent.atomic.LongAdder;
+
+/**
+ * <p>Statistics on a counter value.</p>
+ * <p>This class keeps the total, current and maximum value of a counter
+ * that can be incremented and decremented. The total refers only to increments.</p>
+ */
+public class CounterStatistic
+{
+ private final LongAccumulator _max = new LongAccumulator(Math::max, 0L);
+ private final AtomicLong _current = new AtomicLong();
+ private final LongAdder _total = new LongAdder();
+
+ /**
+ * Resets the max and total to the current value.
+ */
+ public void reset()
+ {
+ _total.reset();
+ _max.reset();
+ long current = _current.get();
+ _total.add(current);
+ _max.accumulate(current);
+ }
+
+ /**
+ * Resets the max, total and current value to the given parameter.
+ *
+ * @param value the new current value
+ */
+ public void reset(final long value)
+ {
+ _current.set(value);
+ _total.reset();
+ _max.reset();
+ if (value > 0)
+ {
+ _total.add(value);
+ _max.accumulate(value);
+ }
+ }
+
+ /**
+ * @param delta the amount to add to the counter
+ * @return the new counter value
+ */
+ public long add(final long delta)
+ {
+ long value = _current.addAndGet(delta);
+ if (delta > 0)
+ {
+ _total.add(delta);
+ _max.accumulate(value);
+ }
+ return value;
+ }
+
+ /**
+ * Increments the value by one.
+ *
+ * @return the new counter value after the increment
+ */
+ public long increment()
+ {
+ long value = _current.incrementAndGet();
+ _total.increment();
+ _max.accumulate(value);
+ return value;
+ }
+
+ /**
+ * Decrements the value by one.
+ *
+ * @return the new counter value after the decrement
+ */
+ public long decrement()
+ {
+ return _current.decrementAndGet();
+ }
+
+ /**
+ * @return max counter value
+ */
+ public long getMax()
+ {
+ return _max.get();
+ }
+
+ /**
+ * @return current counter value
+ */
+ public long getCurrent()
+ {
+ return _current.get();
+ }
+
+ /**
+ * @return total counter value
+ */
+ public long getTotal()
+ {
+ return _total.sum();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{c=%d,m=%d,t=%d}", getClass().getSimpleName(), hashCode(), getCurrent(), getMax(), getTotal());
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/RateCounter.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/RateCounter.java
new file mode 100644
index 0000000..821161d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/RateCounter.java
@@ -0,0 +1,49 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.statistic;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.LongAdder;
+
+/**
+ * Counts the rate that {@link Long}s are added to this from the time of creation or the last call to {@link #reset()}.
+ */
+public class RateCounter
+{
+ private final LongAdder _total = new LongAdder();
+ private final AtomicLong _timeStamp = new AtomicLong(System.nanoTime());
+
+ public void add(long l)
+ {
+ _total.add(l);
+ }
+
+ public long getRate()
+ {
+ long elapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - _timeStamp.get());
+ return elapsed == 0 ? 0 : _total.sum() * 1000 / elapsed;
+ }
+
+ public void reset()
+ {
+ _timeStamp.getAndSet(System.nanoTime());
+ _total.reset();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/RateStatistic.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/RateStatistic.java
new file mode 100644
index 0000000..710252f
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/RateStatistic.java
@@ -0,0 +1,206 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.statistic;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * <p>Statistics on a time sequence rate.</p>
+ * <p>Calculates the rate at which the {@link #record()} method is called
+ * over the configured period, retaining also the total count and maximum
+ * rate achieved.</p>
+ * <p>The implementation keeps a Deque of timestamps for all records for
+ * the last time period, so this method is not suitable for large rates
+ * unless a small time period is used.</p>
+ */
+public class RateStatistic
+{
+ private final Deque<Long> _samples = new ArrayDeque<>();
+ private final long _nanoPeriod;
+ private final TimeUnit _units;
+ private long _max;
+ private long _count;
+
+ public RateStatistic(long period, TimeUnit units)
+ {
+ _nanoPeriod = TimeUnit.NANOSECONDS.convert(period, units);
+ _units = units;
+ }
+
+ public long getPeriod()
+ {
+ return _units.convert(_nanoPeriod, TimeUnit.NANOSECONDS);
+ }
+
+ public TimeUnit getUnits()
+ {
+ return _units;
+ }
+
+ /**
+ * Resets the statistics.
+ */
+ public void reset()
+ {
+ synchronized (this)
+ {
+ _samples.clear();
+ _max = 0;
+ _count = 0;
+ }
+ }
+
+ private void update()
+ {
+ update(System.nanoTime());
+ }
+
+ private void update(long now)
+ {
+ long expire = now - _nanoPeriod;
+ Long head = _samples.peekFirst();
+ while (head != null && head < expire)
+ {
+ _samples.removeFirst();
+ head = _samples.peekFirst();
+ }
+ }
+
+ protected void age(long period, TimeUnit units)
+ {
+ long increment = TimeUnit.NANOSECONDS.convert(period, units);
+ synchronized (this)
+ {
+ int size = _samples.size();
+ for (int i = 0; i < size; i++)
+ {
+ _samples.addLast(_samples.removeFirst() - increment);
+ }
+ update();
+ }
+ }
+
+ /**
+ * Records a sample value.
+ *
+ * @return the number of records in the current period.
+ */
+ public int record()
+ {
+ long now = System.nanoTime();
+ synchronized (this)
+ {
+ _count++;
+ _samples.add(now);
+ update(now);
+ int rate = _samples.size();
+ if (rate > _max)
+ _max = rate;
+ return rate;
+ }
+ }
+
+ /**
+ * @return the number of records in the current period
+ */
+ public int getRate()
+ {
+ synchronized (this)
+ {
+ update();
+ return _samples.size();
+ }
+ }
+
+ /**
+ * @return the max number of samples per period.
+ */
+ public long getMax()
+ {
+ synchronized (this)
+ {
+ return _max;
+ }
+ }
+
+ /**
+ * @param units the units of the return
+ * @return the age of the oldest sample in the requested units
+ */
+ public long getOldest(TimeUnit units)
+ {
+ synchronized (this)
+ {
+ Long head = _samples.peekFirst();
+ if (head == null)
+ return -1;
+ return units.convert(System.nanoTime() - head, TimeUnit.NANOSECONDS);
+ }
+ }
+
+ /**
+ * @return the number of samples recorded
+ */
+ public long getCount()
+ {
+ synchronized (this)
+ {
+ return _count;
+ }
+ }
+
+ public String dump()
+ {
+ return dump(TimeUnit.MINUTES);
+ }
+
+ public String dump(TimeUnit units)
+ {
+ long now = System.nanoTime();
+ synchronized (this)
+ {
+ String samples = _samples.stream()
+ .mapToLong(t -> units.convert(now - t, TimeUnit.NANOSECONDS))
+ .mapToObj(Long::toString)
+ .collect(Collectors.joining(System.lineSeparator()));
+ return String.format("%s%n%s", toString(now), samples);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return toString(System.nanoTime());
+ }
+
+ private String toString(long nanoTime)
+ {
+ synchronized (this)
+ {
+ update(nanoTime);
+ return String.format("%s@%x{count=%d,max=%d,rate=%d per %d %s}",
+ getClass().getSimpleName(), hashCode(),
+ _count, _max, _samples.size(),
+ _units.convert(_nanoPeriod, TimeUnit.NANOSECONDS), _units);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/SampleStatistic.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/SampleStatistic.java
new file mode 100644
index 0000000..7db5516
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/SampleStatistic.java
@@ -0,0 +1,139 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.statistic;
+
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.LongAccumulator;
+import java.util.concurrent.atomic.LongAdder;
+
+/**
+ * <p>Statistics on a sampled value.</p>
+ * <p>Provides max, total, mean, count, variance, and standard deviation of continuous sequence of samples.</p>
+ * <p>Calculates estimates of mean, variance, and standard deviation characteristics of a sample using a non synchronized
+ * approximation of the on-line algorithm presented in <cite>Donald Knuth's Art of Computer Programming, Volume 2,
+ * Semi numerical Algorithms, 3rd edition, page 232, Boston: Addison-Wesley</cite>. That cites a 1962 paper by B.P. Welford:
+ * <a href="http://www.jstor.org/pss/1266577">Note on a Method for Calculating Corrected Sums of Squares and Products</a></p>
+ * <p>This algorithm is also described in Wikipedia in the section "Online algorithm":
+ * <a href="https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance">Algorithms for calculating variance</a>.</p>
+ */
+public class SampleStatistic
+{
+ private final LongAccumulator _max = new LongAccumulator(Math::max, 0L);
+ private final AtomicLong _total = new AtomicLong();
+ private final AtomicLong _count = new AtomicLong();
+ private final LongAdder _totalVariance100 = new LongAdder();
+
+ /**
+ * Resets the statistics.
+ */
+ public void reset()
+ {
+ _max.reset();
+ _total.set(0);
+ _count.set(0);
+ _totalVariance100.reset();
+ }
+
+ /**
+ * Records a sample value.
+ *
+ * @param sample the value to record.
+ */
+ public void record(long sample)
+ {
+ long total = _total.addAndGet(sample);
+ long count = _count.incrementAndGet();
+
+ if (count > 1)
+ {
+ long mean10 = total * 10 / count;
+ long delta10 = sample * 10 - mean10;
+ _totalVariance100.add(delta10 * delta10);
+ }
+
+ _max.accumulate(sample);
+ }
+
+ /**
+ * @param sample the value to record.
+ * @deprecated use {@link #record(long)} instead
+ */
+ @Deprecated
+ public void set(long sample)
+ {
+ record(sample);
+ }
+
+ /**
+ * @return the max value of the recorded samples
+ */
+ public long getMax()
+ {
+ return _max.get();
+ }
+
+ /**
+ * @return the sum of all the recorded samples
+ */
+ public long getTotal()
+ {
+ return _total.get();
+ }
+
+ /**
+ * @return the number of samples recorded
+ */
+ public long getCount()
+ {
+ return _count.get();
+ }
+
+ /**
+ * @return the average value of the samples recorded, or zero if there are no samples
+ */
+ public double getMean()
+ {
+ long count = getCount();
+ return count > 0 ? (double)_total.get() / _count.get() : 0.0D;
+ }
+
+ /**
+ * @return the variance of the samples recorded, or zero if there are less than 2 samples
+ */
+ public double getVariance()
+ {
+ long variance100 = _totalVariance100.sum();
+ long count = getCount();
+ return count > 1 ? variance100 / 100.0D / (count - 1) : 0.0D;
+ }
+
+ /**
+ * @return the standard deviation of the samples recorded
+ */
+ public double getStdDev()
+ {
+ return Math.sqrt(getVariance());
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{count=%d,mean=%d,total=%d,stddev=%f}", getClass().getSimpleName(), hashCode(), getCount(), getMax(), getTotal(), getStdDev());
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/package-info.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/package-info.java
new file mode 100644
index 0000000..b480fe3
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Util : Common Statistics Utility classes
+ */
+package org.eclipse.jetty.util.statistic;
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutionStrategy.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutionStrategy.java
new file mode 100644
index 0000000..79c936d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutionStrategy.java
@@ -0,0 +1,67 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+/**
+ * <p>An {@link ExecutionStrategy} executes {@link Runnable} tasks produced by a {@link Producer}.
+ * The strategy to execute the task may vary depending on the implementation; the task may be
+ * run in the calling thread, or in a new thread, etc.</p>
+ * <p>The strategy delegates the production of tasks to a {@link Producer}, and continues to
+ * execute tasks until the producer continues to produce them.</p>
+ */
+public interface ExecutionStrategy
+{
+ /**
+ * <p>Initiates (or resumes) the task production and consumption.</p>
+ * <p>This method guarantees that the task is never run by the
+ * thread that called this method.</p>
+ *
+ * TODO review the need for this (only used by HTTP2 push)
+ *
+ * @see #produce()
+ */
+ void dispatch();
+
+ /**
+ * <p>Initiates (or resumes) the task production and consumption.</p>
+ * <p>The produced task may be run by the same thread that called
+ * this method.</p>
+ *
+ * @see #dispatch()
+ */
+ void produce();
+
+ /**
+ * <p>A producer of {@link Runnable} tasks to run.</p>
+ * <p>The {@link ExecutionStrategy} will repeatedly invoke {@link #produce()} until
+ * the producer returns null, indicating that it has nothing more to produce.</p>
+ * <p>When no more tasks can be produced, implementations should arrange for the
+ * {@link ExecutionStrategy} to be invoked again in case an external event resumes
+ * the tasks production.</p>
+ */
+ interface Producer
+ {
+ /**
+ * <p>Produces a task to be executed.</p>
+ *
+ * @return a task to executed or null if there are no more tasks to execute
+ */
+ Runnable produce();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutorSizedThreadPool.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutorSizedThreadPool.java
new file mode 100644
index 0000000..196d626
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutorSizedThreadPool.java
@@ -0,0 +1,27 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+/**
+ * @deprecated Use {@link ExecutorThreadPool}
+ */
+@Deprecated
+public class ExecutorSizedThreadPool extends ExecutorThreadPool
+{
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutorThreadPool.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutorThreadPool.java
new file mode 100644
index 0000000..fa36df1
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutorThreadPool.java
@@ -0,0 +1,416 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import org.eclipse.jetty.util.ProcessorUtils;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.component.DumpableCollection;
+
+/**
+ * A {@link org.eclipse.jetty.util.thread.ThreadPool.SizedThreadPool} wrapper around {@link ThreadPoolExecutor}.
+ */
+@ManagedObject("A thread pool")
+public class ExecutorThreadPool extends ContainerLifeCycle implements ThreadPool.SizedThreadPool, TryExecutor
+{
+ private final ThreadPoolExecutor _executor;
+ private final ThreadPoolBudget _budget;
+ private final ThreadGroup _group;
+ private String _name = "etp" + hashCode();
+ private int _minThreads;
+ private int _reservedThreads = -1;
+ private TryExecutor _tryExecutor = TryExecutor.NO_TRY;
+ private int _priority = Thread.NORM_PRIORITY;
+ private boolean _daemon;
+ private boolean _detailedDump;
+
+ public ExecutorThreadPool()
+ {
+ this(200, 8);
+ }
+
+ public ExecutorThreadPool(int maxThreads)
+ {
+ this(maxThreads, Math.min(8, maxThreads));
+ }
+
+ public ExecutorThreadPool(int maxThreads, int minThreads)
+ {
+ this(maxThreads, minThreads, new LinkedBlockingQueue<>());
+ }
+
+ public ExecutorThreadPool(int maxThreads, int minThreads, BlockingQueue<Runnable> queue)
+ {
+ this(new ThreadPoolExecutor(maxThreads, maxThreads, 60, TimeUnit.SECONDS, queue), minThreads, -1, null);
+ }
+
+ public ExecutorThreadPool(ThreadPoolExecutor executor)
+ {
+ this(executor, -1);
+ }
+
+ public ExecutorThreadPool(ThreadPoolExecutor executor, int reservedThreads)
+ {
+ this(executor, reservedThreads, null);
+ }
+
+ public ExecutorThreadPool(ThreadPoolExecutor executor, int reservedThreads, ThreadGroup group)
+ {
+ this(executor, Math.min(ProcessorUtils.availableProcessors(), executor.getCorePoolSize()), reservedThreads, group);
+ }
+
+ private ExecutorThreadPool(ThreadPoolExecutor executor, int minThreads, int reservedThreads, ThreadGroup group)
+ {
+ int maxThreads = executor.getMaximumPoolSize();
+ if (maxThreads < minThreads)
+ {
+ executor.shutdownNow();
+ throw new IllegalArgumentException("max threads (" + maxThreads + ") cannot be less than min threads (" + minThreads + ")");
+ }
+ _executor = executor;
+ _executor.setThreadFactory(this::newThread);
+ _group = group;
+ _minThreads = minThreads;
+ _reservedThreads = reservedThreads;
+ _budget = new ThreadPoolBudget(this);
+ }
+
+ /**
+ * @return the name of the this thread pool
+ */
+ @ManagedAttribute("name of this thread pool")
+ public String getName()
+ {
+ return _name;
+ }
+
+ /**
+ * @param name the name of this thread pool, used to name threads
+ */
+ public void setName(String name)
+ {
+ if (isRunning())
+ throw new IllegalStateException(getState());
+ _name = name;
+ }
+
+ @Override
+ @ManagedAttribute("minimum number of threads in the pool")
+ public int getMinThreads()
+ {
+ return _minThreads;
+ }
+
+ @Override
+ public void setMinThreads(int threads)
+ {
+ _minThreads = threads;
+ }
+
+ @Override
+ @ManagedAttribute("maximum number of threads in the pool")
+ public int getMaxThreads()
+ {
+ return _executor.getMaximumPoolSize();
+ }
+
+ @Override
+ public void setMaxThreads(int threads)
+ {
+ if (_budget != null)
+ _budget.check(threads);
+ _executor.setCorePoolSize(threads);
+ _executor.setMaximumPoolSize(threads);
+ }
+
+ /**
+ * @return the maximum thread idle time in ms.
+ * @see #setIdleTimeout(int)
+ */
+ @ManagedAttribute("maximum time a thread may be idle in ms")
+ public int getIdleTimeout()
+ {
+ return (int)_executor.getKeepAliveTime(TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * <p>Sets the maximum thread idle time in ms.</p>
+ * <p>Threads that are idle for longer than this
+ * period may be stopped.</p>
+ *
+ * @param idleTimeout the maximum thread idle time in ms.
+ * @see #getIdleTimeout()
+ */
+ public void setIdleTimeout(int idleTimeout)
+ {
+ _executor.setKeepAliveTime(idleTimeout, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * @return number of reserved threads or -1 to indicate that the number is heuristically determined
+ * @see #setReservedThreads(int)
+ */
+ @ManagedAttribute("the number of reserved threads in the pool")
+ public int getReservedThreads()
+ {
+ if (isStarted())
+ return getBean(ReservedThreadExecutor.class).getCapacity();
+ return _reservedThreads;
+ }
+
+ /**
+ * Sets the number of reserved threads.
+ *
+ * @param reservedThreads number of reserved threads or -1 to determine the number heuristically
+ * @see #getReservedThreads()
+ */
+ public void setReservedThreads(int reservedThreads)
+ {
+ if (isRunning())
+ throw new IllegalStateException(getState());
+ _reservedThreads = reservedThreads;
+ }
+
+ public void setThreadsPriority(int priority)
+ {
+ _priority = priority;
+ }
+
+ public int getThreadsPriority()
+ {
+ return _priority;
+ }
+
+ /**
+ * @return whether this thread pool uses daemon threads
+ * @see #setDaemon(boolean)
+ */
+ @ManagedAttribute("whether this thread pool uses daemon threads")
+ public boolean isDaemon()
+ {
+ return _daemon;
+ }
+
+ /**
+ * @param daemon whether this thread pool uses daemon threads
+ * @see Thread#setDaemon(boolean)
+ */
+ public void setDaemon(boolean daemon)
+ {
+ _daemon = daemon;
+ }
+
+ @ManagedAttribute("reports additional details in the dump")
+ public boolean isDetailedDump()
+ {
+ return _detailedDump;
+ }
+
+ public void setDetailedDump(boolean detailedDump)
+ {
+ _detailedDump = detailedDump;
+ }
+
+ @Override
+ @ManagedAttribute("number of threads in the pool")
+ public int getThreads()
+ {
+ return _executor.getPoolSize();
+ }
+
+ @Override
+ @ManagedAttribute("number of idle threads in the pool")
+ public int getIdleThreads()
+ {
+ return _executor.getPoolSize() - _executor.getActiveCount();
+ }
+
+ @Override
+ public void execute(Runnable command)
+ {
+ _executor.execute(command);
+ }
+
+ @Override
+ public boolean tryExecute(Runnable task)
+ {
+ TryExecutor tryExecutor = _tryExecutor;
+ return tryExecutor != null && tryExecutor.tryExecute(task);
+ }
+
+ @Override
+ @ManagedAttribute(value = "thread pool is low on threads", readonly = true)
+ public boolean isLowOnThreads()
+ {
+ return getThreads() == getMaxThreads() && _executor.getQueue().size() >= getIdleThreads();
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (_executor.isShutdown())
+ throw new IllegalStateException("This thread pool is not restartable");
+ for (int i = 0; i < _minThreads; ++i)
+ {
+ _executor.prestartCoreThread();
+ }
+
+ _tryExecutor = new ReservedThreadExecutor(this, _reservedThreads);
+ addBean(_tryExecutor);
+
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ super.doStop();
+ removeBean(_tryExecutor);
+ _tryExecutor = TryExecutor.NO_TRY;
+ _executor.shutdownNow();
+ _budget.reset();
+ }
+
+ @Override
+ public void join() throws InterruptedException
+ {
+ _executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public ThreadPoolBudget getThreadPoolBudget()
+ {
+ return _budget;
+ }
+
+ protected Thread newThread(Runnable job)
+ {
+ Thread thread = new Thread(_group, job);
+ thread.setDaemon(isDaemon());
+ thread.setPriority(getThreadsPriority());
+ thread.setName(getName() + "-" + thread.getId());
+ return thread;
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ String prefix = getName() + "-";
+ List<Dumpable> threads = Thread.getAllStackTraces().entrySet().stream()
+ .filter(entry -> entry.getKey().getName().startsWith(prefix))
+ .map(entry ->
+ {
+ Thread thread = entry.getKey();
+ StackTraceElement[] frames = entry.getValue();
+ String knownMethod = null;
+ for (StackTraceElement frame : frames)
+ {
+ if ("getTask".equals(frame.getMethodName()) && frame.getClassName().endsWith("ThreadPoolExecutor"))
+ {
+ knownMethod = "IDLE ";
+ break;
+ }
+ else if ("reservedWait".equals(frame.getMethodName()) && frame.getClassName().endsWith("ReservedThread"))
+ {
+ knownMethod = "RESERVED ";
+ break;
+ }
+ else if ("select".equals(frame.getMethodName()) && frame.getClassName().endsWith("SelectorProducer"))
+ {
+ knownMethod = "SELECTING ";
+ break;
+ }
+ else if ("accept".equals(frame.getMethodName()) && frame.getClassName().contains("ServerConnector"))
+ {
+ knownMethod = "ACCEPTING ";
+ break;
+ }
+ }
+ String known = knownMethod == null ? "" : knownMethod;
+ return new Dumpable()
+ {
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ StringBuilder b = new StringBuilder();
+ b.append(thread.getId())
+ .append(" ")
+ .append(thread.getName())
+ .append(" p=").append(thread.getPriority())
+ .append(" ")
+ .append(known)
+ .append(thread.getState().toString());
+
+ if (isDetailedDump())
+ {
+ if (known.isEmpty())
+ Dumpable.dumpObjects(out, indent, b.toString(), (Object[])frames);
+ else
+ Dumpable.dumpObject(out, b.toString());
+ }
+ else
+ {
+ b.append(" @ ").append(frames.length > 0 ? String.valueOf(frames[0]) : "<no_stack_frames>");
+ Dumpable.dumpObject(out, b.toString());
+ }
+ }
+
+ @Override
+ public String dump()
+ {
+ return null;
+ }
+ };
+ })
+ .collect(Collectors.toList());
+
+ List<Runnable> jobs = Collections.emptyList();
+ if (isDetailedDump())
+ jobs = new ArrayList<>(_executor.getQueue());
+ dumpObjects(out, indent, threads, new DumpableCollection("jobs", jobs));
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[%s]@%x{%s,%d<=%d<=%d,i=%d,q=%d,%s}",
+ getClass().getSimpleName(),
+ getName(),
+ hashCode(),
+ getState(),
+ getMinThreads(),
+ getThreads(),
+ getMaxThreads(),
+ getIdleThreads(),
+ _executor.getQueue().size(),
+ _tryExecutor);
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Invocable.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Invocable.java
new file mode 100644
index 0000000..b2dd8cc
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Invocable.java
@@ -0,0 +1,112 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.concurrent.Callable;
+
+/**
+ * <p>A task (typically either a {@link Runnable} or {@link Callable}
+ * that declares how it will behave when invoked:</p>
+ * <ul>
+ * <li>blocking, the invocation will certainly block (e.g. performs blocking I/O)</li>
+ * <li>non-blocking, the invocation will certainly <strong>not</strong> block</li>
+ * <li>either, the invocation <em>may</em> block</li>
+ * </ul>
+ *
+ * <p>
+ * Static methods and are provided that allow the current thread to be tagged
+ * with a {@link ThreadLocal} to indicate if it has a blocking invocation type.
+ * </p>
+ */
+public interface Invocable
+{
+ enum InvocationType
+ {
+ BLOCKING, NON_BLOCKING, EITHER
+ }
+
+ ThreadLocal<Boolean> __nonBlocking = new ThreadLocal<>();
+
+ /**
+ * Test if the current thread has been tagged as non blocking
+ *
+ * @return True if the task the current thread is running has
+ * indicated that it will not block.
+ */
+ static boolean isNonBlockingInvocation()
+ {
+ return Boolean.TRUE.equals(__nonBlocking.get());
+ }
+
+ /**
+ * Invoke a task with the calling thread, tagged to indicate
+ * that it will not block.
+ *
+ * @param task The task to invoke.
+ */
+ static void invokeNonBlocking(Runnable task)
+ {
+ Boolean wasNonBlocking = __nonBlocking.get();
+ try
+ {
+ __nonBlocking.set(Boolean.TRUE);
+ task.run();
+ }
+ finally
+ {
+ __nonBlocking.set(wasNonBlocking);
+ }
+ }
+
+ static InvocationType combine(InvocationType it1, InvocationType it2)
+ {
+ if (it1 != null && it2 != null)
+ {
+ if (it1 == it2)
+ return it1;
+ if (it1 == InvocationType.EITHER)
+ return it2;
+ if (it2 == InvocationType.EITHER)
+ return it1;
+ }
+ return InvocationType.BLOCKING;
+ }
+
+ /**
+ * Get the invocation type of an Object.
+ *
+ * @param o The object to check the invocation type of.
+ * @return If the object is an Invocable, it is coerced and the {@link #getInvocationType()}
+ * used, otherwise {@link InvocationType#BLOCKING} is returned.
+ */
+ static InvocationType getInvocationType(Object o)
+ {
+ if (o instanceof Invocable)
+ return ((Invocable)o).getInvocationType();
+ return InvocationType.BLOCKING;
+ }
+
+ /**
+ * @return The InvocationType of this object
+ */
+ default InvocationType getInvocationType()
+ {
+ return InvocationType.BLOCKING;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Locker.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Locker.java
new file mode 100644
index 0000000..59e51a6
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Locker.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * <p>Convenience auto closeable {@link java.util.concurrent.locks.ReentrantLock} wrapper.</p>
+ *
+ * <pre>
+ * try (Locker.Lock lock = locker.lock())
+ * {
+ * // something
+ * }
+ * </pre>
+ */
+public class Locker
+{
+ private final ReentrantLock _lock = new ReentrantLock();
+ private final Lock _unlock = new Lock();
+
+ /**
+ * <p>Acquires the lock.</p>
+ *
+ * @return the lock to unlock
+ */
+ public Lock lock()
+ {
+ _lock.lock();
+ return _unlock;
+ }
+
+ /**
+ * @return the lock to unlock
+ * @deprecated use {@link #lock()} instead
+ */
+ @Deprecated
+ public Lock lockIfNotHeld()
+ {
+ return lock();
+ }
+
+ /**
+ * @return whether this lock has been acquired
+ */
+ public boolean isLocked()
+ {
+ return _lock.isLocked();
+ }
+
+ /**
+ * @return a {@link Condition} associated with this lock
+ */
+ public Condition newCondition()
+ {
+ return _lock.newCondition();
+ }
+
+ /**
+ * <p>The unlocker object that unlocks when it is closed.</p>
+ */
+ public class Lock implements AutoCloseable
+ {
+ @Override
+ public void close()
+ {
+ _lock.unlock();
+ }
+ }
+
+ @Deprecated
+ public class UnLock extends Lock
+ {
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/MonitoredQueuedThreadPool.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/MonitoredQueuedThreadPool.java
new file mode 100644
index 0000000..c34db0f
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/MonitoredQueuedThreadPool.java
@@ -0,0 +1,169 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.concurrent.BlockingQueue;
+
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.statistic.CounterStatistic;
+import org.eclipse.jetty.util.statistic.SampleStatistic;
+
+/**
+ * <p>A {@link QueuedThreadPool} subclass that monitors its own activity by recording queue and task statistics.</p>
+ */
+@ManagedObject
+public class MonitoredQueuedThreadPool extends QueuedThreadPool
+{
+ private final CounterStatistic queueStats = new CounterStatistic();
+ private final SampleStatistic queueLatencyStats = new SampleStatistic();
+ private final SampleStatistic taskLatencyStats = new SampleStatistic();
+ private final CounterStatistic threadStats = new CounterStatistic();
+
+ public MonitoredQueuedThreadPool()
+ {
+ this(256);
+ }
+
+ public MonitoredQueuedThreadPool(int maxThreads)
+ {
+ this(maxThreads, maxThreads, 24 * 3600 * 1000, new BlockingArrayQueue<>(maxThreads, 256));
+ }
+
+ public MonitoredQueuedThreadPool(int maxThreads, int minThreads, int idleTimeOut, BlockingQueue<Runnable> queue)
+ {
+ super(maxThreads, minThreads, idleTimeOut, queue);
+ addBean(queueStats);
+ addBean(queueLatencyStats);
+ addBean(taskLatencyStats);
+ addBean(threadStats);
+ }
+
+ @Override
+ public void execute(final Runnable job)
+ {
+ queueStats.increment();
+ long begin = System.nanoTime();
+ super.execute(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ long queueLatency = System.nanoTime() - begin;
+ queueStats.decrement();
+ threadStats.increment();
+ queueLatencyStats.record(queueLatency);
+ long start = System.nanoTime();
+ try
+ {
+ job.run();
+ }
+ finally
+ {
+ long taskLatency = System.nanoTime() - start;
+ threadStats.decrement();
+ taskLatencyStats.record(taskLatency);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return job.toString();
+ }
+ });
+ }
+
+ /**
+ * Resets the statistics.
+ */
+ @ManagedOperation(value = "resets the statistics", impact = "ACTION")
+ public void reset()
+ {
+ queueStats.reset();
+ queueLatencyStats.reset();
+ taskLatencyStats.reset();
+ threadStats.reset(0);
+ }
+
+ /**
+ * @return the number of tasks executed
+ */
+ @ManagedAttribute("the number of tasks executed")
+ public long getTasks()
+ {
+ return taskLatencyStats.getCount();
+ }
+
+ /**
+ * @return the maximum number of busy threads
+ */
+ @ManagedAttribute("the maximum number of busy threads")
+ public int getMaxBusyThreads()
+ {
+ return (int)threadStats.getMax();
+ }
+
+ /**
+ * @return the maximum task queue size
+ */
+ @ManagedAttribute("the maximum task queue size")
+ public int getMaxQueueSize()
+ {
+ return (int)queueStats.getMax();
+ }
+
+ /**
+ * @return the average time a task remains in the queue, in nanoseconds
+ */
+ @ManagedAttribute("the average time a task remains in the queue, in nanoseconds")
+ public long getAverageQueueLatency()
+ {
+ return (long)queueLatencyStats.getMean();
+ }
+
+ /**
+ * @return the maximum time a task remains in the queue, in nanoseconds
+ */
+ @ManagedAttribute("the maximum time a task remains in the queue, in nanoseconds")
+ public long getMaxQueueLatency()
+ {
+ return queueLatencyStats.getMax();
+ }
+
+ /**
+ * @return the average task execution time, in nanoseconds
+ */
+ @ManagedAttribute("the average task execution time, in nanoseconds")
+ public long getAverageTaskLatency()
+ {
+ return (long)taskLatencyStats.getMean();
+ }
+
+ /**
+ * @return the maximum task execution time, in nanoseconds
+ */
+ @ManagedAttribute("the maximum task execution time, in nanoseconds")
+ public long getMaxTaskLatency()
+ {
+ return taskLatencyStats.getMax();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/PrivilegedThreadFactory.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/PrivilegedThreadFactory.java
new file mode 100644
index 0000000..bc6ad33
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/PrivilegedThreadFactory.java
@@ -0,0 +1,56 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.function.Supplier;
+
+/**
+ * Convenience class to ensure that a new Thread is created
+ * inside a privileged block.
+ *
+ * This prevents the Thread constructor
+ * from pinning the caller's context classloader. This happens
+ * when the Thread constructor takes a snapshot of the current
+ * calling context - which contains ProtectionDomains that may
+ * reference the context classloader - and remembers it for the
+ * lifetime of the Thread.
+ */
+class PrivilegedThreadFactory
+{
+ /**
+ * Use a Supplier to make a new thread, calling it within
+ * a privileged block to prevent classloader pinning.
+ *
+ * @param newThreadSupplier a Supplier to create a fresh thread
+ * @return a new thread, protected from classloader pinning.
+ */
+ static <T extends Thread> T newThread(Supplier<T> newThreadSupplier)
+ {
+ return AccessController.doPrivileged(new PrivilegedAction<T>()
+ {
+ @Override
+ public T run()
+ {
+ return newThreadSupplier.get();
+ }
+ });
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java
new file mode 100644
index 0000000..7af0a7e
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java
@@ -0,0 +1,1071 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.eclipse.jetty.util.AtomicBiInteger;
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.annotation.Name;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.component.DumpableCollection;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.ThreadPool.SizedThreadPool;
+
+/**
+ * <p>A thread pool with a queue of jobs to execute.</p>
+ * <p>Jetty components that need threads (such as network acceptors and selector) may lease threads
+ * from this thread pool using a {@link ThreadPoolBudget}; these threads are "active" from the point
+ * of view of the thread pool, but not available to run <em>transient</em> jobs such as processing
+ * an HTTP request or a WebSocket frame.</p>
+ * <p>QueuedThreadPool has a {@link ReservedThreadExecutor} which leases threads from this pool,
+ * but makes them available as if they are "idle" threads.</p>
+ * <p>QueuedThreadPool therefore has the following <em>fundamental</em> values:</p>
+ * <ul>
+ * <li>{@link #getThreads() threads}: the current number of threads. These threads may execute
+ * a job (either internal or transient), or may be ready to run (either idle or reserved).
+ * This number may grow or shrink as the thread pool grows or shrinks.</li>
+ * <li>{@link #getReadyThreads() readyThreads}: the current number of threads that are ready to
+ * run transient jobs.
+ * This number may grow or shrink as the thread pool grows or shrinks.</li>
+ * <li>{@link #getLeasedThreads() leasedThreads}: the number of threads that run internal jobs.
+ * This number is typically constant after this thread pool is {@link #start() started}.</li>
+ * </ul>
+ * <p>Given the definitions above, the most interesting definitions are:</p>
+ * <ul>
+ * <li>{@link #getThreads() threads} = {@link #getReadyThreads() readyThreads} + {@link #getLeasedThreads() leasedThreads} + {@link #getUtilizedThreads() utilizedThreads}</li>
+ * <li>readyThreads = {@link #getIdleThreads() idleThreads} + {@link #getAvailableReservedThreads() availableReservedThreads}</li>
+ * <li>{@link #getMaxAvailableThreads() maxAvailableThreads} = {@link #getMaxThreads() maxThreads} - leasedThreads</li>
+ * <li>{@link #getUtilizationRate() utilizationRate} = utilizedThreads / maxAvailableThreads</li>
+ * </ul>
+ * <p>Other definitions, typically less interesting because they take into account threads that
+ * execute internal jobs, or because they don't take into account available reserved threads
+ * (that are essentially ready to execute transient jobs), are:</p>
+ * <ul>
+ * <li>{@link #getBusyThreads() busyThreads} = utilizedThreads + leasedThreads</li>
+ * <li>{@link #getIdleThreads()} idleThreads} = readyThreads - availableReservedThreads</li>
+ * </ul>
+ */
+@ManagedObject("A thread pool")
+public class QueuedThreadPool extends ContainerLifeCycle implements ThreadFactory, SizedThreadPool, Dumpable, TryExecutor
+{
+ private static final Logger LOG = Log.getLogger(QueuedThreadPool.class);
+ private static final Runnable NOOP = () ->
+ {
+ };
+
+ /**
+ * Encodes thread counts:
+ * <dl>
+ * <dt>Hi</dt><dd>Total thread count or Integer.MIN_VALUE if the pool is stopping</dd>
+ * <dt>Lo</dt><dd>Net idle threads == idle threads - job queue size. Essentially if positive,
+ * this represents the effective number of idle threads, and if negative it represents the
+ * demand for more threads</dd>
+ * </dl>
+ */
+ private final AtomicBiInteger _counts = new AtomicBiInteger(Integer.MIN_VALUE, 0);
+ private final AtomicLong _lastShrink = new AtomicLong();
+ private final Set<Thread> _threads = ConcurrentHashMap.newKeySet();
+ private final Object _joinLock = new Object();
+ private final BlockingQueue<Runnable> _jobs;
+ private final ThreadGroup _threadGroup;
+ private final ThreadFactory _threadFactory;
+ private String _name = "qtp" + hashCode();
+ private int _idleTimeout;
+ private int _maxThreads;
+ private int _minThreads;
+ private int _reservedThreads = -1;
+ private TryExecutor _tryExecutor = TryExecutor.NO_TRY;
+ private int _priority = Thread.NORM_PRIORITY;
+ private boolean _daemon = false;
+ private boolean _detailedDump = false;
+ private int _lowThreadsThreshold = 1;
+ private ThreadPoolBudget _budget;
+
+ public QueuedThreadPool()
+ {
+ this(200);
+ }
+
+ public QueuedThreadPool(@Name("maxThreads") int maxThreads)
+ {
+ this(maxThreads, Math.min(8, maxThreads));
+ }
+
+ public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads)
+ {
+ this(maxThreads, minThreads, 60000);
+ }
+
+ public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads, @Name("queue") BlockingQueue<Runnable> queue)
+ {
+ this(maxThreads, minThreads, 60000, -1, queue, null);
+ }
+
+ public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads, @Name("idleTimeout") int idleTimeout)
+ {
+ this(maxThreads, minThreads, idleTimeout, null);
+ }
+
+ public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads, @Name("idleTimeout") int idleTimeout, @Name("queue") BlockingQueue<Runnable> queue)
+ {
+ this(maxThreads, minThreads, idleTimeout, queue, null);
+ }
+
+ public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads, @Name("idleTimeout") int idleTimeout, @Name("queue") BlockingQueue<Runnable> queue, @Name("threadGroup") ThreadGroup threadGroup)
+ {
+ this(maxThreads, minThreads, idleTimeout, -1, queue, threadGroup);
+ }
+
+ public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads,
+ @Name("idleTimeout") int idleTimeout, @Name("reservedThreads") int reservedThreads,
+ @Name("queue") BlockingQueue<Runnable> queue, @Name("threadGroup") ThreadGroup threadGroup)
+ {
+ this(maxThreads, minThreads, idleTimeout, reservedThreads, queue, threadGroup, null);
+ }
+
+ public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads,
+ @Name("idleTimeout") int idleTimeout, @Name("reservedThreads") int reservedThreads,
+ @Name("queue") BlockingQueue<Runnable> queue, @Name("threadGroup") ThreadGroup threadGroup,
+ @Name("threadFactory") ThreadFactory threadFactory)
+ {
+ if (maxThreads < minThreads)
+ throw new IllegalArgumentException("max threads (" + maxThreads + ") less than min threads (" + minThreads + ")");
+ setMinThreads(minThreads);
+ setMaxThreads(maxThreads);
+ setIdleTimeout(idleTimeout);
+ setStopTimeout(5000);
+ setReservedThreads(reservedThreads);
+ if (queue == null)
+ {
+ int capacity = Math.max(_minThreads, 8) * 1024;
+ queue = new BlockingArrayQueue<>(capacity, capacity);
+ }
+ _jobs = queue;
+ _threadGroup = threadGroup;
+ setThreadPoolBudget(new ThreadPoolBudget(this));
+ _threadFactory = threadFactory == null ? this : threadFactory;
+ }
+
+ @Override
+ public ThreadPoolBudget getThreadPoolBudget()
+ {
+ return _budget;
+ }
+
+ public void setThreadPoolBudget(ThreadPoolBudget budget)
+ {
+ if (budget != null && budget.getSizedThreadPool() != this)
+ throw new IllegalArgumentException();
+ updateBean(_budget, budget);
+ _budget = budget;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (_reservedThreads == 0)
+ {
+ _tryExecutor = NO_TRY;
+ }
+ else
+ {
+ ReservedThreadExecutor reserved = new ReservedThreadExecutor(this, _reservedThreads);
+ reserved.setIdleTimeout(_idleTimeout, TimeUnit.MILLISECONDS);
+ _tryExecutor = reserved;
+ }
+ addBean(_tryExecutor);
+
+ _lastShrink.set(System.nanoTime());
+
+ super.doStart();
+ // The threads count set to MIN_VALUE is used to signal to Runners that the pool is stopped.
+ _counts.set(0, 0); // threads, idle
+ ensureThreads();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Stopping {}", this);
+
+ super.doStop();
+
+ removeBean(_tryExecutor);
+ _tryExecutor = TryExecutor.NO_TRY;
+
+ // Signal the Runner threads that we are stopping
+ int threads = _counts.getAndSetHi(Integer.MIN_VALUE);
+
+ // If stop timeout try to gracefully stop
+ long timeout = getStopTimeout();
+ BlockingQueue<Runnable> jobs = getQueue();
+
+ // Fill the job queue with noop jobs to wakeup idle threads.
+ for (int i = 0; i < threads; ++i)
+ jobs.offer(NOOP);
+
+ // If we have a timeout, try to let jobs complete naturally for half our stop time
+ if (timeout > 0)
+ joinThreads(System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout) / 2);
+ Thread.yield();
+
+ // If we still have threads running, get a bit more aggressive
+ // interrupt remaining threads
+ for (Thread thread : _threads)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Interrupting {}", thread);
+ thread.interrupt();
+ }
+
+ // If we have a timeout, wait again for the other half of our stop time
+ if (timeout > 0)
+ {
+ joinThreads(System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout) / 2);
+ Thread.yield();
+ if (LOG.isDebugEnabled())
+ {
+ for (Thread unstopped : _threads)
+ {
+ StringBuilder dmp = new StringBuilder();
+ for (StackTraceElement element : unstopped.getStackTrace())
+ {
+ dmp.append(System.lineSeparator()).append("\tat ").append(element);
+ }
+ LOG.warn("Couldn't stop {}{}", unstopped, dmp.toString());
+ }
+ }
+ else
+ {
+ for (Thread unstopped : _threads)
+ {
+ LOG.warn("{} Couldn't stop {}", this, unstopped);
+ }
+ }
+ }
+
+ // Close any un-executed jobs
+ while (!_jobs.isEmpty())
+ {
+ Runnable job = _jobs.poll();
+ if (job instanceof Closeable)
+ {
+ try
+ {
+ ((Closeable)job).close();
+ }
+ catch (Throwable t)
+ {
+ LOG.warn(t);
+ }
+ }
+ else if (job != NOOP)
+ {
+ LOG.warn("Stopped without executing or closing {}", job);
+ }
+ }
+
+ if (_budget != null)
+ _budget.reset();
+
+ synchronized (_joinLock)
+ {
+ _joinLock.notifyAll();
+ }
+ }
+
+ private void joinThreads(long stopByNanos) throws InterruptedException
+ {
+ for (Thread thread : _threads)
+ {
+ long canWait = TimeUnit.NANOSECONDS.toMillis(stopByNanos - System.nanoTime());
+ if (LOG.isDebugEnabled())
+ LOG.debug("Waiting for {} for {}", thread, canWait);
+ if (canWait > 0)
+ thread.join(canWait);
+ }
+ }
+
+ /**
+ * @return the maximum thread idle time in ms
+ */
+ @ManagedAttribute("maximum time a thread may be idle in ms")
+ public int getIdleTimeout()
+ {
+ return _idleTimeout;
+ }
+
+ /**
+ * <p>Set the maximum thread idle time in ms.</p>
+ * <p>Threads that are idle for longer than this period may be stopped.</p>
+ *
+ * @param idleTimeout the maximum thread idle time in ms
+ */
+ public void setIdleTimeout(int idleTimeout)
+ {
+ _idleTimeout = idleTimeout;
+ ReservedThreadExecutor reserved = getBean(ReservedThreadExecutor.class);
+ if (reserved != null)
+ reserved.setIdleTimeout(idleTimeout, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * @return the maximum number of threads
+ */
+ @Override
+ @ManagedAttribute("maximum number of threads in the pool")
+ public int getMaxThreads()
+ {
+ return _maxThreads;
+ }
+
+ /**
+ * @param maxThreads the maximum number of threads
+ */
+ @Override
+ public void setMaxThreads(int maxThreads)
+ {
+ if (_budget != null)
+ _budget.check(maxThreads);
+ _maxThreads = maxThreads;
+ if (_minThreads > _maxThreads)
+ _minThreads = _maxThreads;
+ }
+
+ /**
+ * @return the minimum number of threads
+ */
+ @Override
+ @ManagedAttribute("minimum number of threads in the pool")
+ public int getMinThreads()
+ {
+ return _minThreads;
+ }
+
+ /**
+ * @param minThreads minimum number of threads
+ */
+ @Override
+ public void setMinThreads(int minThreads)
+ {
+ _minThreads = minThreads;
+
+ if (_minThreads > _maxThreads)
+ _maxThreads = _minThreads;
+
+ if (isStarted())
+ ensureThreads();
+ }
+
+ /**
+ * @return number of reserved threads or -1 for heuristically determined
+ */
+ @ManagedAttribute("number of configured reserved threads or -1 for heuristic")
+ public int getReservedThreads()
+ {
+ return _reservedThreads;
+ }
+
+ /**
+ * @param reservedThreads number of reserved threads or -1 for heuristically determined
+ */
+ public void setReservedThreads(int reservedThreads)
+ {
+ if (isRunning())
+ throw new IllegalStateException(getState());
+ _reservedThreads = reservedThreads;
+ }
+
+ /**
+ * @return the name of the this thread pool
+ */
+ @ManagedAttribute("name of the thread pool")
+ public String getName()
+ {
+ return _name;
+ }
+
+ /**
+ * <p>Sets the name of this thread pool, used as a prefix for the thread names.</p>
+ *
+ * @param name the name of the this thread pool
+ */
+ public void setName(String name)
+ {
+ if (isRunning())
+ throw new IllegalStateException(getState());
+ _name = name;
+ }
+
+ /**
+ * @return the priority of the pool threads
+ */
+ @ManagedAttribute("priority of threads in the pool")
+ public int getThreadsPriority()
+ {
+ return _priority;
+ }
+
+ /**
+ * @param priority the priority of the pool threads
+ */
+ public void setThreadsPriority(int priority)
+ {
+ _priority = priority;
+ }
+
+ /**
+ * @return whether to use daemon threads
+ * @see Thread#isDaemon()
+ */
+ @ManagedAttribute("thread pool uses daemon threads")
+ public boolean isDaemon()
+ {
+ return _daemon;
+ }
+
+ /**
+ * @param daemon whether to use daemon threads
+ * @see Thread#setDaemon(boolean)
+ */
+ public void setDaemon(boolean daemon)
+ {
+ _daemon = daemon;
+ }
+
+ @ManagedAttribute("reports additional details in the dump")
+ public boolean isDetailedDump()
+ {
+ return _detailedDump;
+ }
+
+ public void setDetailedDump(boolean detailedDump)
+ {
+ _detailedDump = detailedDump;
+ }
+
+ @ManagedAttribute("threshold at which the pool is low on threads")
+ public int getLowThreadsThreshold()
+ {
+ return _lowThreadsThreshold;
+ }
+
+ public void setLowThreadsThreshold(int lowThreadsThreshold)
+ {
+ _lowThreadsThreshold = lowThreadsThreshold;
+ }
+
+ /**
+ * @return the number of jobs in the queue waiting for a thread
+ */
+ @ManagedAttribute("size of the job queue")
+ public int getQueueSize()
+ {
+ // The idle counter encodes demand, which is the effective queue size
+ int idle = _counts.getLo();
+ return Math.max(0, -idle);
+ }
+
+ /**
+ * @return the maximum number (capacity) of reserved threads
+ * @see ReservedThreadExecutor#getCapacity()
+ */
+ @ManagedAttribute("maximum number (capacity) of reserved threads")
+ public int getMaxReservedThreads()
+ {
+ TryExecutor tryExecutor = _tryExecutor;
+ if (tryExecutor instanceof ReservedThreadExecutor)
+ {
+ ReservedThreadExecutor reservedThreadExecutor = (ReservedThreadExecutor)tryExecutor;
+ return reservedThreadExecutor.getCapacity();
+ }
+ return 0;
+ }
+
+ /**
+ * @return the number of available reserved threads
+ * @see ReservedThreadExecutor#getAvailable()
+ */
+ @ManagedAttribute("number of available reserved threads")
+ public int getAvailableReservedThreads()
+ {
+ TryExecutor tryExecutor = _tryExecutor;
+ if (tryExecutor instanceof ReservedThreadExecutor)
+ {
+ ReservedThreadExecutor reservedThreadExecutor = (ReservedThreadExecutor)tryExecutor;
+ return reservedThreadExecutor.getAvailable();
+ }
+ return 0;
+ }
+
+ /**
+ * <p>The <em>fundamental</em> value that represents the number of threads currently known by this thread pool.</p>
+ * <p>This value includes threads that have been leased to internal components, idle threads, reserved threads
+ * and threads that are executing transient jobs.</p>
+ *
+ * @return the number of threads currently known to the pool
+ * @see #getReadyThreads()
+ * @see #getLeasedThreads()
+ */
+ @Override
+ @ManagedAttribute("number of threads in the pool")
+ public int getThreads()
+ {
+ int threads = _counts.getHi();
+ return Math.max(0, threads);
+ }
+
+ /**
+ * <p>The <em>fundamental</em> value that represents the number of threads ready to execute transient jobs.</p>
+ *
+ * @return the number of threads ready to execute transient jobs
+ * @see #getThreads()
+ * @see #getLeasedThreads()
+ * @see #getUtilizedThreads()
+ */
+ @ManagedAttribute("number of threads ready to execute transient jobs")
+ public int getReadyThreads()
+ {
+ return getIdleThreads() + getAvailableReservedThreads();
+ }
+
+ /**
+ * <p>The <em>fundamental</em> value that represents the number of threads that are leased
+ * to internal components, and therefore cannot be used to execute transient jobs.</p>
+ *
+ * @return the number of threads currently used by internal components
+ * @see #getThreads()
+ * @see #getReadyThreads()
+ */
+ @ManagedAttribute("number of threads used by internal components")
+ public int getLeasedThreads()
+ {
+ return getMaxLeasedThreads() - getMaxReservedThreads();
+ }
+
+ /**
+ * <p>The maximum number of threads that are leased to internal components,
+ * as some component may allocate its threads lazily.</p>
+ *
+ * @return the maximum number of threads leased by internal components
+ * @see #getLeasedThreads()
+ */
+ @ManagedAttribute("maximum number of threads leased to internal components")
+ public int getMaxLeasedThreads()
+ {
+ ThreadPoolBudget budget = _budget;
+ return budget == null ? 0 : budget.getLeasedThreads();
+ }
+
+ /**
+ * <p>The number of idle threads, but without including reserved threads.</p>
+ * <p>Prefer {@link #getReadyThreads()} for a better representation of
+ * "threads ready to execute transient jobs".</p>
+ *
+ * @return the number of idle threads but not reserved
+ * @see #getReadyThreads()
+ */
+ @Override
+ @ManagedAttribute("number of idle threads but not reserved")
+ public int getIdleThreads()
+ {
+ int idle = _counts.getLo();
+ return Math.max(0, idle);
+ }
+
+ /**
+ * <p>The number of threads executing internal and transient jobs.</p>
+ * <p>Prefer {@link #getUtilizedThreads()} for a better representation of
+ * "threads executing transient jobs".</p>
+ *
+ * @return the number of threads executing internal and transient jobs
+ * @see #getUtilizedThreads()
+ */
+ @ManagedAttribute("number of threads executing internal and transient jobs")
+ public int getBusyThreads()
+ {
+ return getThreads() - getReadyThreads();
+ }
+
+ /**
+ * <p>The number of threads executing transient jobs.</p>
+ *
+ * @return the number of threads executing transient jobs
+ * @see #getReadyThreads()
+ */
+ @ManagedAttribute("number of threads executing transient jobs")
+ public int getUtilizedThreads()
+ {
+ return getThreads() - getLeasedThreads() - getReadyThreads();
+ }
+
+ /**
+ * <p>The maximum number of threads available to run transient jobs.</p>
+ *
+ * @return the maximum number of threads available to run transient jobs
+ */
+ @ManagedAttribute("maximum number of threads available to run transient jobs")
+ public int getMaxAvailableThreads()
+ {
+ return getMaxThreads() - getLeasedThreads();
+ }
+
+ /**
+ * <p>The rate between the number of {@link #getUtilizedThreads() utilized threads}
+ * and the maximum number of {@link #getMaxAvailableThreads() utilizable threads}.</p>
+ * <p>A value of {@code 0.0D} means that the thread pool is not utilized, while a
+ * value of {@code 1.0D} means that the thread pool is fully utilized to execute
+ * transient jobs.</p>
+ *
+ * @return the utilization rate of threads executing transient jobs
+ */
+ @ManagedAttribute("utilization rate of threads executing transient jobs")
+ public double getUtilizationRate()
+ {
+ return (double)getUtilizedThreads() / getMaxAvailableThreads();
+ }
+
+ /**
+ * <p>Returns whether this thread pool is low on threads.</p>
+ * <p>The current formula is:</p>
+ * <pre>
+ * maxThreads - threads + readyThreads - queueSize <= lowThreadsThreshold
+ * </pre>
+ *
+ * @return whether the pool is low on threads
+ * @see #getLowThreadsThreshold()
+ */
+ @Override
+ @ManagedAttribute(value = "thread pool is low on threads", readonly = true)
+ public boolean isLowOnThreads()
+ {
+ return getMaxThreads() - getThreads() + getReadyThreads() - getQueueSize() <= getLowThreadsThreshold();
+ }
+
+ @Override
+ public void execute(Runnable job)
+ {
+ // Determine if we need to start a thread, use and idle thread or just queue this job
+ int startThread;
+ while (true)
+ {
+ // Get the atomic counts
+ long counts = _counts.get();
+
+ // Get the number of threads started (might not yet be running)
+ int threads = AtomicBiInteger.getHi(counts);
+ if (threads == Integer.MIN_VALUE)
+ throw new RejectedExecutionException(job.toString());
+
+ // Get the number of truly idle threads. This count is reduced by the
+ // job queue size so that any threads that are idle but are about to take
+ // a job from the queue are not counted.
+ int idle = AtomicBiInteger.getLo(counts);
+
+ // Start a thread if we have insufficient idle threads to meet demand
+ // and we are not at max threads.
+ startThread = (idle <= 0 && threads < _maxThreads) ? 1 : 0;
+
+ // The job will be run by an idle thread when available
+ if (!_counts.compareAndSet(counts, threads + startThread, idle + startThread - 1))
+ continue;
+
+ break;
+ }
+
+ if (!_jobs.offer(job))
+ {
+ // reverse our changes to _counts.
+ if (addCounts(-startThread, 1 - startThread))
+ LOG.warn("{} rejected {}", this, job);
+ throw new RejectedExecutionException(job.toString());
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("queue {} startThread={}", job, startThread);
+
+ // Start a thread if one was needed
+ while (startThread-- > 0)
+ startThread();
+ }
+
+ @Override
+ public boolean tryExecute(Runnable task)
+ {
+ TryExecutor tryExecutor = _tryExecutor;
+ return tryExecutor != null && tryExecutor.tryExecute(task);
+ }
+
+ /**
+ * Blocks until the thread pool is {@link LifeCycle#stop stopped}.
+ */
+ @Override
+ public void join() throws InterruptedException
+ {
+ synchronized (_joinLock)
+ {
+ while (isRunning())
+ {
+ _joinLock.wait();
+ }
+ }
+
+ while (isStopping())
+ {
+ Thread.sleep(1);
+ }
+ }
+
+ private void ensureThreads()
+ {
+ while (true)
+ {
+ long counts = _counts.get();
+ int threads = AtomicBiInteger.getHi(counts);
+ if (threads == Integer.MIN_VALUE)
+ break;
+
+ // If we have less than min threads
+ // OR insufficient idle threads to meet demand
+ int idle = AtomicBiInteger.getLo(counts);
+ if (threads < _minThreads || (idle < 0 && threads < _maxThreads))
+ {
+ // Then try to start a thread.
+ if (_counts.compareAndSet(counts, threads + 1, idle + 1))
+ startThread();
+ // Otherwise continue to check state again.
+ continue;
+ }
+ break;
+ }
+ }
+
+ protected void startThread()
+ {
+ boolean started = false;
+ try
+ {
+ Thread thread = _threadFactory.newThread(_runnable);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Starting {}", thread);
+ _threads.add(thread);
+ _lastShrink.set(System.nanoTime());
+ thread.start();
+ started = true;
+ }
+ finally
+ {
+ if (!started)
+ addCounts(-1, -1); // threads, idle
+ }
+ }
+
+ private boolean addCounts(int deltaThreads, int deltaIdle)
+ {
+ while (true)
+ {
+ long encoded = _counts.get();
+ int threads = AtomicBiInteger.getHi(encoded);
+ int idle = AtomicBiInteger.getLo(encoded);
+ if (threads == Integer.MIN_VALUE) // This is a marker that the pool is stopped.
+ return false;
+ long update = AtomicBiInteger.encode(threads + deltaThreads, idle + deltaIdle);
+ if (_counts.compareAndSet(encoded, update))
+ return true;
+ }
+ }
+
+ @Override
+ public Thread newThread(Runnable runnable)
+ {
+ return PrivilegedThreadFactory.newThread(() ->
+ {
+ Thread thread = new Thread(_threadGroup, runnable);
+ thread.setDaemon(isDaemon());
+ thread.setPriority(getThreadsPriority());
+ thread.setName(_name + "-" + thread.getId());
+ thread.setContextClassLoader(getClass().getClassLoader());
+ return thread;
+ });
+ }
+
+ protected void removeThread(Thread thread)
+ {
+ _threads.remove(thread);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ List<Object> threads = new ArrayList<>(getMaxThreads());
+ for (Thread thread : _threads)
+ {
+ StackTraceElement[] trace = thread.getStackTrace();
+ String stackTag = getCompressedStackTag(trace);
+ String baseThreadInfo = String.format("%s %s tid=%d prio=%d", thread.getName(), thread.getState(), thread.getId(), thread.getPriority());
+
+ if (!StringUtil.isBlank(stackTag))
+ threads.add(baseThreadInfo + " " + stackTag);
+ else if (isDetailedDump())
+ threads.add((Dumpable)(o, i) -> Dumpable.dumpObjects(o, i, baseThreadInfo, (Object[])trace));
+ else
+ threads.add(baseThreadInfo + " @ " + (trace.length > 0 ? trace[0].toString() : "???"));
+ }
+
+ DumpableCollection threadsDump = new DumpableCollection("threads", threads);
+ if (isDetailedDump())
+ dumpObjects(out, indent, threadsDump, new DumpableCollection("jobs", new ArrayList<>(getQueue())));
+ else
+ dumpObjects(out, indent, threadsDump);
+ }
+
+ private String getCompressedStackTag(StackTraceElement[] trace)
+ {
+ for (StackTraceElement t : trace)
+ {
+ if ("idleJobPoll".equals(t.getMethodName()) && t.getClassName().equals(Runner.class.getName()))
+ return "IDLE";
+ if ("reservedWait".equals(t.getMethodName()) && t.getClassName().endsWith("ReservedThread"))
+ return "RESERVED";
+ if ("select".equals(t.getMethodName()) && t.getClassName().endsWith("SelectorProducer"))
+ return "SELECTING";
+ if ("accept".equals(t.getMethodName()) && t.getClassName().contains("ServerConnector"))
+ return "ACCEPTING";
+ }
+ return "";
+ }
+
+ private final Runnable _runnable = new Runner();
+
+ /**
+ * <p>Runs the given job in the {@link Thread#currentThread() current thread}.</p>
+ * <p>Subclasses may override to perform pre/post actions before/after the job is run.</p>
+ *
+ * @param job the job to run
+ */
+ protected void runJob(Runnable job)
+ {
+ job.run();
+ }
+
+ /**
+ * @return the job queue
+ */
+ protected BlockingQueue<Runnable> getQueue()
+ {
+ return _jobs;
+ }
+
+ /**
+ * @param queue the job queue
+ * @deprecated pass the queue to the constructor instead
+ */
+ @Deprecated
+ public void setQueue(BlockingQueue<Runnable> queue)
+ {
+ throw new UnsupportedOperationException("Use constructor injection");
+ }
+
+ /**
+ * @param id the thread ID to interrupt.
+ * @return true if the thread was found and interrupted.
+ */
+ @ManagedOperation("interrupts a pool thread")
+ public boolean interruptThread(@Name("id") long id)
+ {
+ for (Thread thread : _threads)
+ {
+ if (thread.getId() == id)
+ {
+ thread.interrupt();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param id the thread ID to interrupt.
+ * @return the stack frames dump
+ */
+ @ManagedOperation("dumps a pool thread stack")
+ public String dumpThread(@Name("id") long id)
+ {
+ for (Thread thread : _threads)
+ {
+ if (thread.getId() == id)
+ {
+ StringBuilder buf = new StringBuilder();
+ buf.append(thread.getId()).append(" ").append(thread.getName()).append(" ");
+ buf.append(thread.getState()).append(":").append(System.lineSeparator());
+ for (StackTraceElement element : thread.getStackTrace())
+ {
+ buf.append(" at ").append(element.toString()).append(System.lineSeparator());
+ }
+ return buf.toString();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String toString()
+ {
+ long count = _counts.get();
+ int threads = Math.max(0, AtomicBiInteger.getHi(count));
+ int idle = Math.max(0, AtomicBiInteger.getLo(count));
+ int queue = getQueueSize();
+
+ return String.format("%s[%s]@%x{%s,%d<=%d<=%d,i=%d,r=%d,q=%d}[%s]",
+ getClass().getSimpleName(),
+ _name,
+ hashCode(),
+ getState(),
+ getMinThreads(),
+ threads,
+ getMaxThreads(),
+ idle,
+ getReservedThreads(),
+ queue,
+ _tryExecutor);
+ }
+
+ private class Runner implements Runnable
+ {
+ private Runnable idleJobPoll(long idleTimeout) throws InterruptedException
+ {
+ if (idleTimeout <= 0)
+ return _jobs.take();
+ return _jobs.poll(idleTimeout, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void run()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Runner started for {}", QueuedThreadPool.this);
+
+ // We always start idle so the idle count is not adjust when we enter the loop below
+ // with no job. The actual thread and idle counts have already been adjusted accounting
+ // for this newly started thread in #execute(Runnable)
+ boolean idle = true;
+ try
+ {
+ Runnable job = null;
+ while (true)
+ {
+ // Adjust the idle count and check we are still running
+ if (!addCounts(0, job == null ? 0 : 1))
+ break;
+ idle = true;
+
+ try
+ {
+ // Look for an immediately available job
+ job = _jobs.poll();
+ if (job == null)
+ {
+ // No job immediately available maybe we should shrink?
+ long idleTimeout = getIdleTimeout();
+ if (idleTimeout > 0 && getThreads() > _minThreads)
+ {
+ long last = _lastShrink.get();
+ long now = System.nanoTime();
+ if ((now - last) > TimeUnit.MILLISECONDS.toNanos(idleTimeout) && _lastShrink.compareAndSet(last, now))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("shrinking {}", QueuedThreadPool.this);
+ break;
+ }
+ }
+
+ // Wait for a job, only after we have checked if we should shrink
+ job = idleJobPoll(idleTimeout);
+
+ // If still no job?
+ if (job == null)
+ // continue to try again
+ continue;
+ }
+
+ // The actual idle count was already adjusted in #execute(Runnable)
+ // Here we just remember we are not idle so that we don't adjust again
+ // in finally below if we exit loop after running this job
+ idle = false;
+
+ // run job
+ if (LOG.isDebugEnabled())
+ LOG.debug("run {} in {}", job, QueuedThreadPool.this);
+ runJob(job);
+ if (LOG.isDebugEnabled())
+ LOG.debug("ran {} in {}", job, QueuedThreadPool.this);
+ }
+ catch (InterruptedException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("interrupted {} in {}", job, QueuedThreadPool.this);
+ LOG.ignore(e);
+ }
+ catch (Throwable e)
+ {
+ LOG.warn(e);
+ }
+ finally
+ {
+ // Clear any interrupted status
+ Thread.interrupted();
+ }
+ }
+ }
+ finally
+ {
+ Thread thread = Thread.currentThread();
+ removeThread(thread);
+
+ // Decrement the total thread count and the idle count if we had no job
+ addCounts(-1, idle ? -1 : 0);
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} exited for {}", thread, QueuedThreadPool.this);
+
+ // There is a chance that we shrunk just as a job was queued for us, so
+ // check again if we have sufficient threads to meet demand
+ ensureThreads();
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ReservedThreadExecutor.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ReservedThreadExecutor.java
new file mode 100644
index 0000000..3685a3a
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ReservedThreadExecutor.java
@@ -0,0 +1,436 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+import org.eclipse.jetty.util.AtomicBiInteger;
+import org.eclipse.jetty.util.ProcessorUtils;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.component.DumpableCollection;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static org.eclipse.jetty.util.AtomicBiInteger.getHi;
+import static org.eclipse.jetty.util.AtomicBiInteger.getLo;
+
+/**
+ * <p>A TryExecutor using pre-allocated/reserved threads from an external Executor.</p>
+ * <p>Calls to {@link #tryExecute(Runnable)} on ReservedThreadExecutor will either
+ * succeed with a reserved thread immediately being assigned the task, or fail if
+ * no reserved thread is available.</p>
+ * <p>Threads are reserved lazily, with new reserved threads being allocated from the external
+ * {@link Executor} passed to the constructor. Whenever 1 or more reserved threads have been
+ * idle for more than {@link #getIdleTimeoutMs()} then one reserved thread will return to
+ * the external Executor.</p>
+ */
+@ManagedObject("A pool for reserved threads")
+public class ReservedThreadExecutor extends AbstractLifeCycle implements TryExecutor, Dumpable
+{
+ private static final Logger LOG = Log.getLogger(ReservedThreadExecutor.class);
+ private static final long DEFAULT_IDLE_TIMEOUT = TimeUnit.MINUTES.toNanos(1);
+ private static final Runnable STOP = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ }
+
+ @Override
+ public String toString()
+ {
+ return "STOP";
+ }
+ };
+
+ private final Executor _executor;
+ private final int _capacity;
+ private final Set<ReservedThread> _threads = ConcurrentHashMap.newKeySet();
+ private final SynchronousQueue<Runnable> _queue = new SynchronousQueue<>(false);
+ private final AtomicBiInteger _count = new AtomicBiInteger(); // hi=pending; lo=size;
+ private final AtomicLong _lastEmptyTime = new AtomicLong(System.nanoTime());
+ private ThreadPoolBudget.Lease _lease;
+ private long _idleTimeNanos = DEFAULT_IDLE_TIMEOUT;
+
+ /**
+ * @param executor The executor to use to obtain threads
+ * @param capacity The number of threads to preallocate. If less than 0 then capacity
+ * is calculated based on a heuristic from the number of available processors and
+ * thread pool size.
+ */
+ public ReservedThreadExecutor(Executor executor, int capacity)
+ {
+ _executor = executor;
+ _capacity = reservedThreads(executor, capacity);
+ if (LOG.isDebugEnabled())
+ LOG.debug("{}", this);
+ }
+
+ /**
+ * @param executor The executor to use to obtain threads
+ * @param capacity The number of threads to preallocate, If less than 0 then capacity
+ * is calculated based on a heuristic from the number of available processors and
+ * thread pool size.
+ * @return the number of reserved threads that would be used by a ReservedThreadExecutor
+ * constructed with these arguments.
+ */
+ private static int reservedThreads(Executor executor, int capacity)
+ {
+ if (capacity >= 0)
+ return capacity;
+ int cpus = ProcessorUtils.availableProcessors();
+ if (executor instanceof ThreadPool.SizedThreadPool)
+ {
+ int threads = ((ThreadPool.SizedThreadPool)executor).getMaxThreads();
+ return Math.max(1, Math.min(cpus, threads / 10));
+ }
+ return cpus;
+ }
+
+ public Executor getExecutor()
+ {
+ return _executor;
+ }
+
+ /**
+ * @return the maximum number of reserved threads
+ */
+ @ManagedAttribute(value = "max number of reserved threads", readonly = true)
+ public int getCapacity()
+ {
+ return _capacity;
+ }
+
+ /**
+ * @return the number of threads available to {@link #tryExecute(Runnable)}
+ */
+ @ManagedAttribute(value = "available reserved threads", readonly = true)
+ public int getAvailable()
+ {
+ return _count.getLo();
+ }
+
+ @ManagedAttribute(value = "pending reserved threads", readonly = true)
+ public int getPending()
+ {
+ return _count.getHi();
+ }
+
+ @ManagedAttribute(value = "idle timeout in ms", readonly = true)
+ public long getIdleTimeoutMs()
+ {
+ return NANOSECONDS.toMillis(_idleTimeNanos);
+ }
+
+ /**
+ * Set the idle timeout for shrinking the reserved thread pool
+ *
+ * @param idleTime Time to wait before shrinking, or 0 for default timeout.
+ * @param idleTimeUnit Time units for idle timeout
+ */
+ public void setIdleTimeout(long idleTime, TimeUnit idleTimeUnit)
+ {
+ if (isRunning())
+ throw new IllegalStateException();
+ _idleTimeNanos = (idleTime <= 0 || idleTimeUnit == null) ? DEFAULT_IDLE_TIMEOUT : idleTimeUnit.toNanos(idleTime);
+ }
+
+ @Override
+ public void doStart() throws Exception
+ {
+ _lease = ThreadPoolBudget.leaseFrom(getExecutor(), this, _capacity);
+ _count.set(0, 0);
+ super.doStart();
+ }
+
+ @Override
+ public void doStop() throws Exception
+ {
+ if (_lease != null)
+ _lease.close();
+
+ super.doStop();
+
+ // Mark this instance as stopped.
+ int size = _count.getAndSetLo(-1);
+
+ // Offer the STOP task to all waiting reserved threads.
+ for (int i = 0; i < size; ++i)
+ {
+ // Yield to wait for any reserved threads that
+ // have incremented the size but not yet polled.
+ Thread.yield();
+ _queue.offer(STOP);
+ }
+
+ // Interrupt any reserved thread missed the offer,
+ // so they do not wait for the whole idle timeout.
+ _threads.stream()
+ .filter(ReservedThread::isReserved)
+ .map(t -> t._thread)
+ .filter(Objects::nonNull)
+ .forEach(Thread::interrupt);
+ _threads.clear();
+ _count.getAndSetHi(0);
+ }
+
+ @Override
+ public void execute(Runnable task) throws RejectedExecutionException
+ {
+ _executor.execute(task);
+ }
+
+ /**
+ * <p>Executes the given task if and only if a reserved thread is available.</p>
+ *
+ * @param task the task to run
+ * @return true if and only if a reserved thread was available and has been assigned the task to run.
+ */
+ @Override
+ public boolean tryExecute(Runnable task)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} tryExecute {}", this, task);
+ if (task == null)
+ return false;
+
+ // Offer will only succeed if there is a reserved thread waiting
+ boolean offered = _queue.offer(task);
+
+ // If the offer succeeded we need to reduce the size, unless it is set to -1 in the meantime
+ int size = _count.getLo();
+ while (offered && size > 0 && !_count.compareAndSetLo(size, --size))
+ size = _count.getLo();
+
+ // If size is 0 and we are not stopping, start a new reserved thread
+ if (size == 0 && task != STOP)
+ startReservedThread();
+
+ return offered;
+ }
+
+ private void startReservedThread()
+ {
+ while (true)
+ {
+ long count = _count.get();
+ int pending = getHi(count);
+ int size = getLo(count);
+ if (size < 0 || pending + size >= _capacity)
+ return;
+ if (size == 0)
+ _lastEmptyTime.set(System.nanoTime());
+ if (!_count.compareAndSet(count, pending + 1, size))
+ continue;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} startReservedThread p={}", this, pending + 1);
+ try
+ {
+ ReservedThread thread = new ReservedThread();
+ _threads.add(thread);
+ _executor.execute(thread);
+ }
+ catch (Throwable e)
+ {
+ _count.add(-1, 0);
+ LOG.ignore(e);
+ }
+ return;
+ }
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ Dumpable.dumpObjects(out, indent, this,
+ new DumpableCollection("threads",
+ _threads.stream()
+ .filter(ReservedThread::isReserved)
+ .collect(Collectors.toList())));
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{reserved=%d/%d,pending=%d}",
+ getClass().getSimpleName(),
+ hashCode(),
+ _count.getLo(),
+ _capacity,
+ _count.getHi());
+ }
+
+ private enum State
+ {
+ PENDING,
+ RESERVED,
+ RUNNING,
+ IDLE,
+ STOPPED
+ }
+
+ private class ReservedThread implements Runnable
+ {
+ // The state and thread are kept only for dumping
+ private volatile State _state = State.PENDING;
+ private volatile Thread _thread;
+
+ private boolean isReserved()
+ {
+ return _state == State.RESERVED;
+ }
+
+ private Runnable reservedWait()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} waiting {}", this, ReservedThreadExecutor.this);
+
+ // Keep waiting until stopped, tasked or idle
+ while (_count.getLo() >= 0)
+ {
+ try
+ {
+ // Always poll at some period as safety to ensure we don't poll forever.
+ Runnable task = _queue.poll(_idleTimeNanos, NANOSECONDS);
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} task={} {}", this, task, ReservedThreadExecutor.this);
+ if (task != null)
+ return task;
+
+ // we have idled out
+ int size = _count.getLo();
+ // decrement size if we have not also been stopped.
+ while (size > 0)
+ {
+ if (_count.compareAndSetLo(size, --size))
+ break;
+ size = _count.getLo();
+ }
+ _state = size >= 0 ? State.IDLE : State.STOPPED;
+ return STOP;
+ }
+ catch (InterruptedException e)
+ {
+ LOG.ignore(e);
+ }
+ }
+ _state = State.STOPPED;
+ return STOP;
+ }
+
+ @Override
+ public void run()
+ {
+ _thread = Thread.currentThread();
+ try
+ {
+ while (true)
+ {
+ long count = _count.get();
+
+ // reduce pending if this thread was pending
+ int pending = getHi(count) - (_state == State.PENDING ? 1 : 0);
+ int size = getLo(count);
+
+ State next;
+ if (size < 0 || size >= _capacity)
+ {
+ // The executor has stopped or this thread is excess to capacity
+ next = State.STOPPED;
+ }
+ else
+ {
+ long now = System.nanoTime();
+ long lastEmpty = _lastEmptyTime.get();
+ if (size > 0 && _idleTimeNanos < (now - lastEmpty) && _lastEmptyTime.compareAndSet(lastEmpty, now))
+ {
+ // it has been too long since we hit zero reserved threads, so are "busy" idle
+ next = State.IDLE;
+ }
+ else
+ {
+ // We will become a reserved thread if we can update the count below.
+ next = State.RESERVED;
+ size++;
+ }
+ }
+
+ // Update count for pending and size
+ if (!_count.compareAndSet(count, pending, size))
+ continue;
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} was={} next={} size={}+{} capacity={}", this, _state, next, pending, size, _capacity);
+ _state = next;
+ if (next != State.RESERVED)
+ break;
+
+ // We are reserved whilst we are waiting for an offered _task.
+ Runnable task = reservedWait();
+
+ // Is the task the STOP poison pill?
+ if (task == STOP)
+ break;
+
+ // Run the task
+ try
+ {
+ _state = State.RUNNING;
+ task.run();
+ }
+ catch (Throwable e)
+ {
+ LOG.warn("Unable to run task", e);
+ }
+ }
+ }
+ finally
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} exited {}", this, ReservedThreadExecutor.this);
+ _threads.remove(this);
+ _thread = null;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{%s,thread=%s}",
+ getClass().getSimpleName(),
+ hashCode(),
+ _state,
+ _thread);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ScheduledExecutorScheduler.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ScheduledExecutorScheduler.java
new file mode 100644
index 0000000..6d15336
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ScheduledExecutorScheduler.java
@@ -0,0 +1,178 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.io.IOException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.Name;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+
+/**
+ * Implementation of {@link Scheduler} based on JDK's {@link ScheduledThreadPoolExecutor}.
+ * <p>
+ * While use of {@link ScheduledThreadPoolExecutor} creates futures that will not be used,
+ * it has the advantage of allowing to set a property to remove cancelled tasks from its
+ * queue even if the task did not fire, which provides a huge benefit in the performance
+ * of garbage collection in young generation.
+ */
+@ManagedObject
+public class ScheduledExecutorScheduler extends AbstractLifeCycle implements Scheduler, Dumpable
+{
+ private final String name;
+ private final boolean daemon;
+ private final ClassLoader classloader;
+ private final ThreadGroup threadGroup;
+ private final int threads;
+ private final AtomicInteger count = new AtomicInteger();
+ private volatile ScheduledThreadPoolExecutor scheduler;
+ private volatile Thread thread;
+
+ public ScheduledExecutorScheduler()
+ {
+ this(null, false);
+ }
+
+ public ScheduledExecutorScheduler(String name, boolean daemon)
+ {
+ this(name, daemon, null);
+ }
+
+ public ScheduledExecutorScheduler(@Name("name") String name, @Name("daemon") boolean daemon, @Name("threads") int threads)
+ {
+ this(name, daemon, null, null, threads);
+ }
+
+ public ScheduledExecutorScheduler(String name, boolean daemon, ClassLoader classLoader)
+ {
+ this(name, daemon, classLoader, null);
+ }
+
+ public ScheduledExecutorScheduler(String name, boolean daemon, ClassLoader classLoader, ThreadGroup threadGroup)
+ {
+ this(name, daemon, classLoader, threadGroup, -1);
+ }
+
+ /**
+ * @param name The name of the scheduler threads or null for automatic name
+ * @param daemon True if scheduler threads should be daemon
+ * @param classLoader The classloader to run the threads with or null to use the current thread context classloader
+ * @param threadGroup The threadgroup to use or null for no thread group
+ * @param threads The number of threads to pass to the the core {@link ScheduledThreadPoolExecutor} or -1 for a
+ * heuristic determined number of threads.
+ */
+ public ScheduledExecutorScheduler(@Name("name") String name, @Name("daemon") boolean daemon, @Name("classLoader") ClassLoader classLoader, @Name("threadGroup") ThreadGroup threadGroup, @Name("threads") int threads)
+ {
+ this.name = StringUtil.isBlank(name) ? "Scheduler-" + hashCode() : name;
+ this.daemon = daemon;
+ this.classloader = classLoader == null ? Thread.currentThread().getContextClassLoader() : classLoader;
+ this.threadGroup = threadGroup;
+ this.threads = threads;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ int size = threads > 0 ? threads : 1;
+ scheduler = new ScheduledThreadPoolExecutor(size, r ->
+ {
+ Thread thread = ScheduledExecutorScheduler.this.thread = new Thread(threadGroup, r, name + "-" + count.incrementAndGet());
+ thread.setDaemon(daemon);
+ thread.setContextClassLoader(classloader);
+ return thread;
+ });
+ scheduler.setRemoveOnCancelPolicy(true);
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ scheduler.shutdownNow();
+ super.doStop();
+ scheduler = null;
+ }
+
+ @Override
+ public Task schedule(Runnable task, long delay, TimeUnit unit)
+ {
+ ScheduledThreadPoolExecutor s = scheduler;
+ if (s == null)
+ return () -> false;
+ ScheduledFuture<?> result = s.schedule(task, delay, unit);
+ return new ScheduledFutureTask(result);
+ }
+
+ @Override
+ public String dump()
+ {
+ return Dumpable.dump(this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ Thread thread = this.thread;
+ if (thread == null)
+ Dumpable.dumpObject(out, this);
+ else
+ Dumpable.dumpObjects(out, indent, this, (Object[])thread.getStackTrace());
+ }
+
+ private static class ScheduledFutureTask implements Task
+ {
+ private final ScheduledFuture<?> scheduledFuture;
+
+ ScheduledFutureTask(ScheduledFuture<?> scheduledFuture)
+ {
+ this.scheduledFuture = scheduledFuture;
+ }
+
+ @Override
+ public boolean cancel()
+ {
+ return scheduledFuture.cancel(false);
+ }
+ }
+
+ @ManagedAttribute("The name of the scheduler")
+ public String getName()
+ {
+ return name;
+ }
+
+ @ManagedAttribute("Whether the scheduler uses daemon threads")
+ public boolean isDaemon()
+ {
+ return daemon;
+ }
+
+ @ManagedAttribute("The number of scheduler threads")
+ public int getThreads()
+ {
+ return threads;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Scheduler.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Scheduler.java
new file mode 100644
index 0000000..37aed26
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Scheduler.java
@@ -0,0 +1,33 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.util.component.LifeCycle;
+
+public interface Scheduler extends LifeCycle
+{
+ interface Task
+ {
+ boolean cancel();
+ }
+
+ Task schedule(Runnable task, long delay, TimeUnit units);
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/SerializedExecutor.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/SerializedExecutor.java
new file mode 100644
index 0000000..999d76b
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/SerializedExecutor.java
@@ -0,0 +1,111 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+import org.eclipse.jetty.util.log.Log;
+
+/**
+ * An executor than ensurers serial execution of submitted tasks.
+ * <p>
+ * Callers of this execute will never block in the executor, but they may
+ * be required to either execute the task they submit or tasks submitted
+ * by other threads whilst they are executing tasks.
+ * </p>
+ * <p>
+ * This class was inspired by the public domain class
+ * <a href="https://github.com/jroper/reactive-streams-servlet/blob/master/reactive-streams-servlet/src/main/java/org/reactivestreams/servlet/NonBlockingMutexExecutor.java">NonBlockingMutexExecutor</a>
+ * </p>
+ */
+public class SerializedExecutor implements Executor
+{
+ private final AtomicReference<Link> _tail = new AtomicReference<>();
+
+ @Override
+ public void execute(Runnable task)
+ {
+ Link link = new Link(task);
+ Link lastButOne = _tail.getAndSet(link);
+ if (lastButOne == null)
+ run(link);
+ else
+ lastButOne._next.lazySet(link);
+ }
+
+ protected void onError(Runnable task, Throwable t)
+ {
+ if (task instanceof ErrorHandlingTask)
+ ((ErrorHandlingTask)task).accept(t);
+ Log.getLogger(task.getClass()).warn(t);
+ }
+
+ private void run(Link link)
+ {
+ while (link != null)
+ {
+ try
+ {
+ link._task.run();
+ }
+ catch (Throwable t)
+ {
+ onError(link._task, t);
+ }
+ finally
+ {
+ // Are we the current the last Link?
+ if (_tail.compareAndSet(link, null))
+ link = null;
+ else
+ {
+ // not the last task, so its next link will eventually be set
+ Link next = link._next.get();
+ while (next == null)
+ {
+ Thread.yield(); // Thread.onSpinWait();
+ next = link._next.get();
+ }
+ link = next;
+ }
+ }
+ }
+ }
+
+ private class Link
+ {
+ private final Runnable _task;
+ private final AtomicReference<Link> _next = new AtomicReference<>();
+
+ public Link(Runnable task)
+ {
+ _task = task;
+ }
+ }
+
+ /**
+ * Error handling task
+ * <p>If a submitted task implements this interface, it will be passed
+ * any exceptions thrown when running the task.</p>
+ */
+ public interface ErrorHandlingTask extends Runnable, Consumer<Throwable>
+ {}
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ShutdownThread.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ShutdownThread.java
new file mode 100644
index 0000000..c992d53
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ShutdownThread.java
@@ -0,0 +1,149 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.jetty.util.component.Destroyable;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * ShutdownThread is a shutdown hook thread implemented as
+ * singleton that maintains a list of lifecycle instances
+ * that are registered with it and provides ability to stop
+ * these lifecycles upon shutdown of the Java Virtual Machine
+ */
+public class ShutdownThread extends Thread
+{
+ private static final Logger LOG = Log.getLogger(ShutdownThread.class);
+ private static final ShutdownThread _thread = PrivilegedThreadFactory.newThread(() ->
+ {
+ return new ShutdownThread();
+ });
+
+ private boolean _hooked;
+ private final List<LifeCycle> _lifeCycles = new CopyOnWriteArrayList<LifeCycle>();
+
+ /**
+ * Default constructor for the singleton
+ *
+ * Registers the instance as shutdown hook with the Java Runtime
+ */
+ private ShutdownThread()
+ {
+ super("JettyShutdownThread");
+ }
+
+ private synchronized void hook()
+ {
+ try
+ {
+ if (!_hooked)
+ Runtime.getRuntime().addShutdownHook(this);
+ _hooked = true;
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ LOG.info("shutdown already commenced");
+ }
+ }
+
+ private synchronized void unhook()
+ {
+ try
+ {
+ _hooked = false;
+ Runtime.getRuntime().removeShutdownHook(this);
+ }
+ catch (Exception e)
+ {
+ LOG.ignore(e);
+ LOG.debug("shutdown already commenced");
+ }
+ }
+
+ /**
+ * Returns the instance of the singleton
+ *
+ * @return the singleton instance of the {@link ShutdownThread}
+ */
+ public static ShutdownThread getInstance()
+ {
+ return _thread;
+ }
+
+ public static synchronized void register(LifeCycle... lifeCycles)
+ {
+ _thread._lifeCycles.addAll(Arrays.asList(lifeCycles));
+ if (_thread._lifeCycles.size() > 0)
+ _thread.hook();
+ }
+
+ public static synchronized void register(int index, LifeCycle... lifeCycles)
+ {
+ _thread._lifeCycles.addAll(index, Arrays.asList(lifeCycles));
+ if (_thread._lifeCycles.size() > 0)
+ _thread.hook();
+ }
+
+ public static synchronized void deregister(LifeCycle lifeCycle)
+ {
+ _thread._lifeCycles.remove(lifeCycle);
+ if (_thread._lifeCycles.size() == 0)
+ _thread.unhook();
+ }
+
+ public static synchronized boolean isRegistered(LifeCycle lifeCycle)
+ {
+ return _thread._lifeCycles.contains(lifeCycle);
+ }
+
+ @Override
+ public void run()
+ {
+ for (LifeCycle lifeCycle : _thread._lifeCycles)
+ {
+ try
+ {
+ if (lifeCycle.isStarted())
+ {
+ lifeCycle.stop();
+ LOG.debug("Stopped {}", lifeCycle);
+ }
+
+ if (lifeCycle instanceof Destroyable)
+ {
+ ((Destroyable)lifeCycle).destroy();
+ LOG.debug("Destroyed {}", lifeCycle);
+ }
+ }
+ catch (Exception ex)
+ {
+ LOG.debug(ex);
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Sweeper.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Sweeper.java
new file mode 100644
index 0000000..9bdd91d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Sweeper.java
@@ -0,0 +1,194 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * <p>A utility class to perform periodic sweeping of resources.</p>
+ * <p>{@link Sweepable} resources may be added to or removed from a
+ * {@link Sweeper} and the resource implementation decides whether
+ * it should be swept or not.</p>
+ * <p>If a {@link Sweepable} resources is itself a container of
+ * other sweepable resources, it will forward the sweep operation
+ * to children resources, and so on recursively.</p>
+ * <p>Typical usage is to add {@link Sweeper} as a bean to an existing
+ * container:</p>
+ * <pre>
+ * Server server = new Server();
+ * server.addBean(new Sweeper(), true);
+ * server.start();
+ * </pre>
+ * Code that knows it has sweepable resources can then lookup the
+ * {@link Sweeper} and offer the sweepable resources to it:
+ * <pre>
+ * class MyComponent implements Sweeper.Sweepable
+ * {
+ * private final long creation;
+ * private volatile destroyed;
+ *
+ * MyComponent(Server server)
+ * {
+ * this.creation = System.nanoTime();
+ * Sweeper sweeper = server.getBean(Sweeper.class);
+ * sweeper.offer(this);
+ * }
+ *
+ * void destroy()
+ * {
+ * destroyed = true;
+ * }
+ *
+ * @Override
+ * public boolean sweep()
+ * {
+ * return destroyed;
+ * }
+ * }
+ * </pre>
+ */
+public class Sweeper extends AbstractLifeCycle implements Runnable
+{
+ private static final Logger LOG = Log.getLogger(Sweeper.class);
+
+ private final AtomicReference<List<Sweepable>> items = new AtomicReference<>();
+ private final AtomicReference<Scheduler.Task> task = new AtomicReference<>();
+ private final Scheduler scheduler;
+ private final long period;
+
+ public Sweeper(Scheduler scheduler, long period)
+ {
+ this.scheduler = scheduler;
+ this.period = period;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+ items.set(new CopyOnWriteArrayList<Sweepable>());
+ activate();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ deactivate();
+ items.set(null);
+ super.doStop();
+ }
+
+ public int getSize()
+ {
+ List<Sweepable> refs = items.get();
+ return refs == null ? 0 : refs.size();
+ }
+
+ public boolean offer(Sweepable sweepable)
+ {
+ List<Sweepable> refs = items.get();
+ if (refs == null)
+ return false;
+ refs.add(sweepable);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Resource offered {}", sweepable);
+ return true;
+ }
+
+ public boolean remove(Sweepable sweepable)
+ {
+ List<Sweepable> refs = items.get();
+ return refs != null && refs.remove(sweepable);
+ }
+
+ @Override
+ public void run()
+ {
+ List<Sweepable> refs = items.get();
+ if (refs == null)
+ return;
+ for (Sweepable sweepable : refs)
+ {
+ try
+ {
+ if (sweepable.sweep())
+ {
+ refs.remove(sweepable);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Resource swept {}", sweepable);
+ }
+ }
+ catch (Throwable x)
+ {
+ LOG.info("Exception while sweeping " + sweepable, x);
+ }
+ }
+ activate();
+ }
+
+ private void activate()
+ {
+ if (isRunning())
+ {
+ Scheduler.Task t = scheduler.schedule(this, period, TimeUnit.MILLISECONDS);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Scheduled in {} ms sweep task {}", period, t);
+ task.set(t);
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Skipping sweep task scheduling");
+ }
+ }
+
+ private void deactivate()
+ {
+ Scheduler.Task t = task.getAndSet(null);
+ if (t != null)
+ {
+ boolean cancelled = t.cancel();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Cancelled ({}) sweep task {}", cancelled, t);
+ }
+ }
+
+ /**
+ * <p>A {@link Sweepable} resource implements this interface to
+ * signal to a {@link Sweeper} or to a parent container if it
+ * needs to be swept or not.</p>
+ * <p>Typical implementations will check their own internal state
+ * and return true or false from {@link #sweep()} to indicate
+ * whether they should be swept.</p>
+ */
+ public interface Sweepable
+ {
+ /**
+ * @return whether this resource should be swept
+ */
+ boolean sweep();
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadClassLoaderScope.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadClassLoaderScope.java
new file mode 100644
index 0000000..3a772e9
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadClassLoaderScope.java
@@ -0,0 +1,45 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.io.Closeable;
+
+public class ThreadClassLoaderScope implements Closeable
+{
+ private final ClassLoader old;
+ private final ClassLoader scopedClassLoader;
+
+ public ThreadClassLoaderScope(ClassLoader cl)
+ {
+ old = Thread.currentThread().getContextClassLoader();
+ scopedClassLoader = cl;
+ Thread.currentThread().setContextClassLoader(scopedClassLoader);
+ }
+
+ @Override
+ public void close()
+ {
+ Thread.currentThread().setContextClassLoader(old);
+ }
+
+ public ClassLoader getScopedClassLoader()
+ {
+ return scopedClassLoader;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPool.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPool.java
new file mode 100644
index 0000000..fa3a06b
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPool.java
@@ -0,0 +1,95 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.concurrent.Executor;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.LifeCycle;
+
+/**
+ * <p>A pool for threads.</p>
+ * <p>A specialization of Executor interface that provides reporting methods (eg {@link #getThreads()})
+ * and the option of configuration methods (e.g. @link {@link SizedThreadPool#setMaxThreads(int)}).</p>
+ */
+@ManagedObject("Pool of Threads")
+public interface ThreadPool extends Executor
+{
+ /**
+ * Blocks until the thread pool is {@link LifeCycle#stop stopped}.
+ *
+ * @throws InterruptedException if thread was interrupted
+ */
+ void join() throws InterruptedException;
+
+ /**
+ * @return The total number of threads currently in the pool
+ */
+ @ManagedAttribute("number of threads in pool")
+ int getThreads();
+
+ /**
+ * @return The number of idle threads in the pool
+ */
+ @ManagedAttribute("number of idle threads in pool")
+ int getIdleThreads();
+
+ /**
+ * @return True if the pool is low on threads
+ */
+ @ManagedAttribute("indicates the pool is low on available threads")
+ boolean isLowOnThreads();
+
+ /**
+ * <p>Specialized sub-interface of ThreadPool that allows to get/set
+ * the minimum and maximum number of threads of the pool.</p>
+ */
+ interface SizedThreadPool extends ThreadPool
+ {
+ /**
+ * @return the minimum number of threads
+ */
+ int getMinThreads();
+
+ /**
+ * @return the maximum number of threads
+ */
+ int getMaxThreads();
+
+ /**
+ * @param threads the minimum number of threads
+ */
+ void setMinThreads(int threads);
+
+ /**
+ * @param threads the maximum number of threads
+ */
+ void setMaxThreads(int threads);
+
+ /**
+ * @return a ThreadPoolBudget for this sized thread pool,
+ * or null of no ThreadPoolBudget can be returned
+ */
+ default ThreadPoolBudget getThreadPoolBudget()
+ {
+ return null;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPoolBudget.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPoolBudget.java
new file mode 100644
index 0000000..39a3064
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPoolBudget.java
@@ -0,0 +1,195 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.io.Closeable;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * <p>A budget of required thread usage, used to warn or error for insufficient configured threads.</p>
+ *
+ * @see ThreadPool.SizedThreadPool#getThreadPoolBudget()
+ */
+@ManagedObject
+public class ThreadPoolBudget
+{
+ private static final Logger LOG = Log.getLogger(ThreadPoolBudget.class);
+
+ public interface Lease extends Closeable
+ {
+ int getThreads();
+ }
+
+ /**
+ * An allocation of threads
+ */
+ public class Leased implements Lease
+ {
+ private final Object leasee;
+ private final int threads;
+
+ private Leased(Object leasee, int threads)
+ {
+ this.leasee = leasee;
+ this.threads = threads;
+ }
+
+ @Override
+ public int getThreads()
+ {
+ return threads;
+ }
+
+ @Override
+ public void close()
+ {
+ leases.remove(this);
+ warned.set(false);
+ }
+ }
+
+ private static final Lease NOOP_LEASE = new Lease()
+ {
+ @Override
+ public void close()
+ {
+ }
+
+ @Override
+ public int getThreads()
+ {
+ return 0;
+ }
+ };
+
+ private final Set<Leased> leases = new CopyOnWriteArraySet<>();
+ private final AtomicBoolean warned = new AtomicBoolean();
+ private final ThreadPool.SizedThreadPool pool;
+ private final int warnAt;
+
+ /**
+ * Construct a budget for a SizedThreadPool.
+ *
+ * @param pool The pool to budget thread allocation for.
+ */
+ public ThreadPoolBudget(ThreadPool.SizedThreadPool pool)
+ {
+ this.pool = pool;
+ this.warnAt = -1;
+ }
+
+ /**
+ * @param pool The pool to budget thread allocation for.
+ * @param warnAt The level of free threads at which a warning is generated.
+ */
+ @Deprecated
+ public ThreadPoolBudget(ThreadPool.SizedThreadPool pool, int warnAt)
+ {
+ this.pool = pool;
+ this.warnAt = warnAt;
+ }
+
+ public ThreadPool.SizedThreadPool getSizedThreadPool()
+ {
+ return pool;
+ }
+
+ @ManagedAttribute("the number of threads leased to components")
+ public int getLeasedThreads()
+ {
+ return leases.stream()
+ .mapToInt(Lease::getThreads)
+ .sum();
+ }
+
+ public void reset()
+ {
+ leases.clear();
+ warned.set(false);
+ }
+
+ public Lease leaseTo(Object leasee, int threads)
+ {
+ Leased lease = new Leased(leasee, threads);
+ leases.add(lease);
+ try
+ {
+ check(pool.getMaxThreads());
+ return lease;
+ }
+ catch (IllegalStateException e)
+ {
+ lease.close();
+ throw e;
+ }
+ }
+
+ /**
+ * <p>Checks leases against the given number of {@code maxThreads}.</p>
+ *
+ * @param maxThreads A proposed change to the maximum threads to check.
+ * @return true if passes check, false if otherwise (see logs for details)
+ * @throws IllegalStateException if insufficient threads are configured.
+ */
+ public boolean check(int maxThreads) throws IllegalStateException
+ {
+ int required = getLeasedThreads();
+ int left = maxThreads - required;
+ if (left <= 0)
+ {
+ printInfoOnLeases();
+ throw new IllegalStateException(String.format("Insufficient configured threads: required=%d < max=%d for %s", required, maxThreads, pool));
+ }
+
+ if (left < warnAt)
+ {
+ if (warned.compareAndSet(false, true))
+ {
+ printInfoOnLeases();
+ LOG.info("Low configured threads: (max={} - required={})={} < warnAt={} for {}", maxThreads, required, left, warnAt, pool);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private void printInfoOnLeases()
+ {
+ leases.forEach(lease -> LOG.info("{} requires {} threads from {}", lease.leasee, lease.getThreads(), pool));
+ }
+
+ public static Lease leaseFrom(Executor executor, Object leasee, int threads)
+ {
+ if (executor instanceof ThreadPool.SizedThreadPool)
+ {
+ ThreadPoolBudget budget = ((ThreadPool.SizedThreadPool)executor).getThreadPoolBudget();
+ if (budget != null)
+ return budget.leaseTo(leasee, threads);
+ }
+ return NOOP_LEASE;
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TimerScheduler.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TimerScheduler.java
new file mode 100644
index 0000000..7e75139
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TimerScheduler.java
@@ -0,0 +1,128 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * A scheduler based on the the JVM Timer class
+ */
+public class TimerScheduler extends AbstractLifeCycle implements Scheduler, Runnable
+{
+ private static final Logger LOG = Log.getLogger(TimerScheduler.class);
+
+ /*
+ * This class uses the Timer class rather than an ScheduledExecutionService because
+ * it uses the same algorithm internally and the signature is cheaper to use as there are no
+ * Futures involved (which we do not need).
+ * However, Timer is still locking and a concurrent queue would be better.
+ */
+
+ private final String _name;
+ private final boolean _daemon;
+ private Timer _timer;
+
+ public TimerScheduler()
+ {
+ this(null, false);
+ }
+
+ public TimerScheduler(String name, boolean daemon)
+ {
+ _name = name;
+ _daemon = daemon;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ _timer = _name == null ? new Timer() : new Timer(_name, _daemon);
+ run();
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ _timer.cancel();
+ super.doStop();
+ _timer = null;
+ }
+
+ @Override
+ public Task schedule(final Runnable task, final long delay, final TimeUnit units)
+ {
+ Timer timer = _timer;
+ if (timer == null)
+ throw new RejectedExecutionException("STOPPED: " + this);
+ SimpleTask t = new SimpleTask(task);
+ timer.schedule(t, units.toMillis(delay));
+ return t;
+ }
+
+ @Override
+ public void run()
+ {
+ Timer timer = _timer;
+ if (timer != null)
+ {
+ timer.purge();
+ schedule(this, 1, TimeUnit.SECONDS);
+ }
+ }
+
+ private static class SimpleTask extends TimerTask implements Task
+ {
+ private final Runnable _task;
+
+ private SimpleTask(Runnable runnable)
+ {
+ _task = runnable;
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ _task.run();
+ }
+ catch (Throwable x)
+ {
+ LOG.warn("Exception while executing task " + _task, x);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s.%s@%x",
+ TimerScheduler.class.getSimpleName(),
+ SimpleTask.class.getSimpleName(),
+ hashCode());
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TryExecutor.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TryExecutor.java
new file mode 100644
index 0000000..8c4d2d5
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TryExecutor.java
@@ -0,0 +1,93 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * A variation of Executor that can confirm if a thread is available immediately
+ */
+public interface TryExecutor extends Executor
+{
+ /**
+ * Attempt to execute a task.
+ *
+ * @param task The task to be executed
+ * @return True IFF the task has been given directly to a thread to execute. The task cannot be queued pending the later availability of a Thread.
+ */
+ boolean tryExecute(Runnable task);
+
+ @Override
+ default void execute(Runnable task)
+ {
+ if (!tryExecute(task))
+ throw new RejectedExecutionException();
+ }
+
+ static TryExecutor asTryExecutor(Executor executor)
+ {
+ if (executor instanceof TryExecutor)
+ return (TryExecutor)executor;
+ return new NoTryExecutor(executor);
+ }
+
+ class NoTryExecutor implements TryExecutor
+ {
+ private final Executor executor;
+
+ public NoTryExecutor(Executor executor)
+ {
+ this.executor = executor;
+ }
+
+ @Override
+ public void execute(Runnable task)
+ {
+ executor.execute(task);
+ }
+
+ @Override
+ public boolean tryExecute(Runnable task)
+ {
+ return false;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), executor);
+ }
+ }
+
+ TryExecutor NO_TRY = new TryExecutor()
+ {
+ @Override
+ public boolean tryExecute(Runnable task)
+ {
+ return false;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "NO_TRY";
+ }
+ };
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/package-info.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/package-info.java
new file mode 100644
index 0000000..b33776a
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty Util : Common ThreadPool Utilities
+ */
+package org.eclipse.jetty.util.thread;
+
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/EatWhatYouKill.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/EatWhatYouKill.java
new file mode 100644
index 0000000..f60720d
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/EatWhatYouKill.java
@@ -0,0 +1,486 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread.strategy;
+
+import java.io.Closeable;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.LongAdder;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.ExecutionStrategy;
+import org.eclipse.jetty.util.thread.Invocable;
+import org.eclipse.jetty.util.thread.TryExecutor;
+
+/**
+ * <p>A strategy where the thread that produces will run the resulting task if it
+ * is possible to do so without thread starvation.</p>
+ *
+ * <p>This strategy preemptively dispatches a thread as a pending producer, so that
+ * when a thread produces a task it can immediately run the task and let the pending
+ * producer thread take over production. When operating in this way, the sub-strategy
+ * is called Execute Produce Consume (EPC).</p>
+ * <p>However, if the task produced uses the {@link Invocable} API to indicate that
+ * it will not block, then the strategy will run it directly, regardless of the
+ * presence of a pending producer thread and then resume production after the
+ * task has completed. When operating in this pattern, the sub-strategy is called
+ * ProduceConsume (PC).</p>
+ * <p>If there is no pending producer thread available and if the task has not
+ * indicated it is non-blocking, then this strategy will dispatch the execution of
+ * the task and immediately continue production. When operating in this pattern, the
+ * sub-strategy is called ProduceExecuteConsume (PEC).</p>
+ * <p>The EatWhatYouKill strategy is named after a hunting proverb, in the
+ * sense that one should kill(produce) only to eat(consume).</p>
+ */
+@ManagedObject("eat what you kill execution strategy")
+public class EatWhatYouKill extends ContainerLifeCycle implements ExecutionStrategy, Runnable
+{
+ private static final Logger LOG = Log.getLogger(EatWhatYouKill.class);
+
+ private enum State
+ {
+ IDLE, PRODUCING, REPRODUCING
+ }
+
+ /* The modes this strategy can work in */
+ private enum Mode
+ {
+ PRODUCE_CONSUME,
+ PRODUCE_INVOKE_CONSUME, // This is PRODUCE_CONSUME an EITHER task with NON_BLOCKING invocation
+ PRODUCE_EXECUTE_CONSUME,
+ EXECUTE_PRODUCE_CONSUME // Eat What You Kill!
+ }
+
+ private final LongAdder _pcMode = new LongAdder();
+ private final LongAdder _picMode = new LongAdder();
+ private final LongAdder _pecMode = new LongAdder();
+ private final LongAdder _epcMode = new LongAdder();
+ private final Producer _producer;
+ private final Executor _executor;
+ private final TryExecutor _tryExecutor;
+ private State _state = State.IDLE;
+ private boolean _pending;
+
+ public EatWhatYouKill(Producer producer, Executor executor)
+ {
+ _producer = producer;
+ _executor = executor;
+ _tryExecutor = TryExecutor.asTryExecutor(executor);
+ addBean(_producer);
+ addBean(_tryExecutor);
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} created", this);
+ }
+
+ @Override
+ public void dispatch()
+ {
+ boolean execute = false;
+ synchronized (this)
+ {
+ switch (_state)
+ {
+ case IDLE:
+ if (!_pending)
+ {
+ _pending = true;
+ execute = true;
+ }
+ break;
+
+ case PRODUCING:
+ _state = State.REPRODUCING;
+ break;
+
+ default:
+ break;
+ }
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} dispatch {}", this, execute);
+ if (execute)
+ _executor.execute(this);
+ }
+
+ @Override
+ public void run()
+ {
+ tryProduce(true);
+ }
+
+ @Override
+ public void produce()
+ {
+ tryProduce(false);
+ }
+
+ private void tryProduce(boolean wasPending)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} tryProduce {}", this, wasPending);
+
+ synchronized (this)
+ {
+ if (wasPending)
+ _pending = false;
+
+ switch (_state)
+ {
+ case IDLE:
+ // Enter PRODUCING
+ _state = State.PRODUCING;
+ break;
+
+ case PRODUCING:
+ // Keep other Thread producing
+ _state = State.REPRODUCING;
+ return;
+
+ default:
+ return;
+ }
+ }
+
+ boolean nonBlocking = Invocable.isNonBlockingInvocation();
+
+ while (isRunning())
+ {
+ try
+ {
+ if (doProduce(nonBlocking))
+ continue;
+ return;
+ }
+ catch (Throwable ex)
+ {
+ LOG.warn(ex);
+ }
+ }
+ }
+
+ private boolean doProduce(boolean nonBlocking)
+ {
+ Runnable task = produceTask();
+
+ if (task == null)
+ {
+ synchronized (this)
+ {
+ // Could another task just have been queued with a produce call?
+ switch (_state)
+ {
+ case PRODUCING:
+ _state = State.IDLE;
+ return false;
+
+ case REPRODUCING:
+ _state = State.PRODUCING;
+ return true;
+
+ default:
+ throw new IllegalStateException(toStringLocked());
+ }
+ }
+ }
+
+ Mode mode;
+ if (nonBlocking)
+ {
+ // The calling thread cannot block, so we only have a choice between PC and PEC modes,
+ // based on the invocation type of the task
+ switch (Invocable.getInvocationType(task))
+ {
+ case NON_BLOCKING:
+ mode = Mode.PRODUCE_CONSUME;
+ break;
+
+ case EITHER:
+ mode = Mode.PRODUCE_INVOKE_CONSUME;
+ break;
+
+ default:
+ mode = Mode.PRODUCE_EXECUTE_CONSUME;
+ break;
+ }
+ }
+ else
+ {
+ // The calling thread can block, so we can choose between PC, PEC and EPC modes,
+ // based on the invocation type of the task and if a reserved thread is available
+ switch (Invocable.getInvocationType(task))
+ {
+ case NON_BLOCKING:
+ mode = Mode.PRODUCE_CONSUME;
+ break;
+
+ case BLOCKING:
+ // The task is blocking, so PC is not an option. Thus we choose
+ // between EPC and PEC based on the availability of a reserved thread.
+ synchronized (this)
+ {
+ if (_pending)
+ {
+ _state = State.IDLE;
+ mode = Mode.EXECUTE_PRODUCE_CONSUME;
+ }
+ else if (_tryExecutor.tryExecute(this))
+ {
+ _pending = true;
+ _state = State.IDLE;
+ mode = Mode.EXECUTE_PRODUCE_CONSUME;
+ }
+ else
+ {
+ mode = Mode.PRODUCE_EXECUTE_CONSUME;
+ }
+ }
+ break;
+
+ case EITHER:
+ // The task may be non blocking, so PC is an option. Thus we choose
+ // between EPC and PC based on the availability of a reserved thread.
+ synchronized (this)
+ {
+ if (_pending)
+ {
+ _state = State.IDLE;
+ mode = Mode.EXECUTE_PRODUCE_CONSUME;
+ }
+ else if (_tryExecutor.tryExecute(this))
+ {
+ _pending = true;
+ _state = State.IDLE;
+ mode = Mode.EXECUTE_PRODUCE_CONSUME;
+ }
+ else
+ {
+ // PC mode, but we must consume with non-blocking invocation
+ // as we may be the last thread and we cannot block
+ mode = Mode.PRODUCE_INVOKE_CONSUME;
+ }
+ }
+ break;
+
+ default:
+ throw new IllegalStateException(toString());
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} m={} t={}/{}", this, mode, task, Invocable.getInvocationType(task));
+
+ // Consume or execute task
+ switch (mode)
+ {
+ case PRODUCE_CONSUME:
+ _pcMode.increment();
+ runTask(task);
+ return true;
+
+ case PRODUCE_INVOKE_CONSUME:
+ _picMode.increment();
+ invokeTask(task);
+ return true;
+
+ case PRODUCE_EXECUTE_CONSUME:
+ _pecMode.increment();
+ execute(task);
+ return true;
+
+ case EXECUTE_PRODUCE_CONSUME:
+ _epcMode.increment();
+ runTask(task);
+
+ // Try to produce again?
+ synchronized (this)
+ {
+ if (_state == State.IDLE)
+ {
+ // We beat the pending producer, so we will become the producer instead
+ _state = State.PRODUCING;
+ return true;
+ }
+ }
+ return false;
+
+ default:
+ throw new IllegalStateException(toString());
+ }
+ }
+
+ private void runTask(Runnable task)
+ {
+ try
+ {
+ task.run();
+ }
+ catch (Throwable x)
+ {
+ LOG.warn(x);
+ }
+ }
+
+ private void invokeTask(Runnable task)
+ {
+ try
+ {
+ Invocable.invokeNonBlocking(task);
+ }
+ catch (Throwable x)
+ {
+ LOG.warn(x);
+ }
+ }
+
+ private Runnable produceTask()
+ {
+ try
+ {
+ return _producer.produce();
+ }
+ catch (Throwable e)
+ {
+ LOG.warn(e);
+ return null;
+ }
+ }
+
+ private void execute(Runnable task)
+ {
+ try
+ {
+ _executor.execute(task);
+ }
+ catch (RejectedExecutionException e)
+ {
+ if (isRunning())
+ LOG.warn(e);
+ else
+ LOG.ignore(e);
+
+ if (task instanceof Closeable)
+ {
+ try
+ {
+ ((Closeable)task).close();
+ }
+ catch (Throwable ex2)
+ {
+ LOG.ignore(ex2);
+ }
+ }
+ }
+ }
+
+ @ManagedAttribute(value = "number of tasks consumed with PC mode", readonly = true)
+ public long getPCTasksConsumed()
+ {
+ return _pcMode.longValue();
+ }
+
+ @ManagedAttribute(value = "number of tasks executed with PIC mode", readonly = true)
+ public long getPICTasksExecuted()
+ {
+ return _picMode.longValue();
+ }
+
+ @ManagedAttribute(value = "number of tasks executed with PEC mode", readonly = true)
+ public long getPECTasksExecuted()
+ {
+ return _pecMode.longValue();
+ }
+
+ @ManagedAttribute(value = "number of tasks consumed with EPC mode", readonly = true)
+ public long getEPCTasksConsumed()
+ {
+ return _epcMode.longValue();
+ }
+
+ @ManagedAttribute(value = "whether this execution strategy is idle", readonly = true)
+ public boolean isIdle()
+ {
+ synchronized (this)
+ {
+ return _state == State.IDLE;
+ }
+ }
+
+ @ManagedOperation(value = "resets the task counts", impact = "ACTION")
+ public void reset()
+ {
+ _pcMode.reset();
+ _epcMode.reset();
+ _pecMode.reset();
+ _picMode.reset();
+ }
+
+ @Override
+ public String toString()
+ {
+ synchronized (this)
+ {
+ return toStringLocked();
+ }
+ }
+
+ public String toStringLocked()
+ {
+ StringBuilder builder = new StringBuilder();
+ getString(builder);
+ getState(builder);
+ return builder.toString();
+ }
+
+ private void getString(StringBuilder builder)
+ {
+ builder.append(getClass().getSimpleName());
+ builder.append('@');
+ builder.append(Integer.toHexString(hashCode()));
+ builder.append('/');
+ builder.append(_producer);
+ builder.append('/');
+ }
+
+ private void getState(StringBuilder builder)
+ {
+ builder.append(_state);
+ builder.append("/p=");
+ builder.append(_pending);
+ builder.append('/');
+ builder.append(_tryExecutor);
+ builder.append("[pc=");
+ builder.append(getPCTasksConsumed());
+ builder.append(",pic=");
+ builder.append(getPICTasksExecuted());
+ builder.append(",pec=");
+ builder.append(getPECTasksExecuted());
+ builder.append(",epc=");
+ builder.append(getEPCTasksConsumed());
+ builder.append("]");
+ builder.append("@");
+ builder.append(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now()));
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ExecuteProduceConsume.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ExecuteProduceConsume.java
new file mode 100644
index 0000000..97f0a3e
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ExecuteProduceConsume.java
@@ -0,0 +1,245 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread.strategy;
+
+import java.util.concurrent.Executor;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.ExecutionStrategy;
+import org.eclipse.jetty.util.thread.Invocable;
+import org.eclipse.jetty.util.thread.Invocable.InvocationType;
+import org.eclipse.jetty.util.thread.Locker;
+import org.eclipse.jetty.util.thread.Locker.Lock;
+
+/**
+ * <p>A strategy where the thread that produces will always run the resulting task.</p>
+ * <p>The strategy may then dispatch another thread to continue production.</p>
+ * <p>The strategy is also known by the nickname 'eat what you kill', which comes from
+ * the hunting ethic that says a person should not kill anything he or she does not
+ * plan on eating. In this case, the phrase is used to mean that a thread should
+ * not produce a task that it does not intend to run. By making producers run the
+ * task that they have just produced avoids execution delays and avoids parallel slow
+ * down by running the task in the same core, with good chances of having a hot CPU
+ * cache. It also avoids the creation of a queue of produced tasks that the system
+ * does not yet have capacity to consume, which can save memory and exert back
+ * pressure on producers.</p>
+ */
+public class ExecuteProduceConsume implements ExecutionStrategy, Runnable
+{
+ private static final Logger LOG = Log.getLogger(ExecuteProduceConsume.class);
+
+ private final Locker _locker = new Locker();
+ private final Runnable _runProduce = new RunProduce();
+ private final Producer _producer;
+ private final Executor _executor;
+ private boolean _idle = true;
+ private boolean _execute;
+ private boolean _producing;
+ private boolean _pending;
+
+ public ExecuteProduceConsume(Producer producer, Executor executor)
+ {
+ this._producer = producer;
+ _executor = executor;
+ }
+
+ @Override
+ public void produce()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} execute", this);
+
+ boolean produce = false;
+ try (Lock locked = _locker.lock())
+ {
+ // If we are idle and a thread is not producing
+ if (_idle)
+ {
+ if (_producing)
+ throw new IllegalStateException();
+
+ // Then this thread will do the producing
+ produce = _producing = true;
+ // and we are no longer idle
+ _idle = false;
+ }
+ else
+ {
+ // Otherwise, lets tell the producing thread
+ // that it should call produce again before going idle
+ _execute = true;
+ }
+ }
+
+ if (produce)
+ produceConsume();
+ }
+
+ @Override
+ public void dispatch()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} spawning", this);
+ boolean dispatch = false;
+ try (Lock locked = _locker.lock())
+ {
+ if (_idle)
+ dispatch = true;
+ else
+ _execute = true;
+ }
+ if (dispatch)
+ _executor.execute(_runProduce);
+ }
+
+ @Override
+ public void run()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} run", this);
+ boolean produce = false;
+ try (Lock locked = _locker.lock())
+ {
+ _pending = false;
+ if (!_idle && !_producing)
+ {
+ produce = _producing = true;
+ }
+ }
+
+ if (produce)
+ produceConsume();
+ }
+
+ private void produceConsume()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} produce enter", this);
+
+ while (true)
+ {
+ // If we got here, then we are the thread that is producing.
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} producing", this);
+
+ Runnable task = _producer.produce();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} produced {}", this, task);
+
+ boolean dispatch = false;
+ try (Lock locked = _locker.lock())
+ {
+ // Finished producing
+ _producing = false;
+
+ // Did we produced a task?
+ if (task == null)
+ {
+ // There is no task.
+ // Could another one just have been queued with an execute?
+ if (_execute)
+ {
+ _idle = false;
+ _producing = true;
+ _execute = false;
+ continue;
+ }
+
+ // ... and no additional calls to execute, so we are idle
+ _idle = true;
+ break;
+ }
+
+ // We have a task, which we will run ourselves,
+ // so if we don't have another thread pending
+ if (!_pending)
+ {
+ // dispatch one
+ dispatch = _pending = Invocable.getInvocationType(task) != InvocationType.NON_BLOCKING;
+ }
+
+ _execute = false;
+ }
+
+ // If we became pending
+ if (dispatch)
+ {
+ // Spawn a new thread to continue production by running the produce loop.
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} dispatch", this);
+ _executor.execute(this);
+ }
+
+ // Run the task.
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} run {}", this, task);
+ if (task != null)
+ task.run();
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} ran {}", this, task);
+
+ // Once we have run the task, we can try producing again.
+ try (Lock locked = _locker.lock())
+ {
+ // Is another thread already producing or we are now idle?
+ if (_producing || _idle)
+ break;
+ _producing = true;
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} produce exit", this);
+ }
+
+ public Boolean isIdle()
+ {
+ try (Lock locked = _locker.lock())
+ {
+ return _idle;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append("EPC ");
+ try (Lock locked = _locker.lock())
+ {
+ builder.append(_idle ? "Idle/" : "");
+ builder.append(_producing ? "Prod/" : "");
+ builder.append(_pending ? "Pend/" : "");
+ builder.append(_execute ? "Exec/" : "");
+ }
+ builder.append(_producer);
+ return builder.toString();
+ }
+
+ private class RunProduce implements Runnable
+ {
+ @Override
+ public void run()
+ {
+ produce();
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceConsume.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceConsume.java
new file mode 100644
index 0000000..2c401dc
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceConsume.java
@@ -0,0 +1,112 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread.strategy;
+
+import java.util.concurrent.Executor;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.ExecutionStrategy;
+import org.eclipse.jetty.util.thread.Locker;
+
+/**
+ * <p>A strategy where the caller thread iterates over task production, submitting each
+ * task to an {@link Executor} for execution.</p>
+ */
+public class ProduceConsume implements ExecutionStrategy, Runnable
+{
+ private static final Logger LOG = Log.getLogger(ExecuteProduceConsume.class);
+
+ private final Locker _locker = new Locker();
+ private final Producer _producer;
+ private final Executor _executor;
+ private State _state = State.IDLE;
+
+ public ProduceConsume(Producer producer, Executor executor)
+ {
+ this._producer = producer;
+ this._executor = executor;
+ }
+
+ @Override
+ public void produce()
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ switch (_state)
+ {
+ case IDLE:
+ _state = State.PRODUCE;
+ break;
+
+ case PRODUCE:
+ case EXECUTE:
+ _state = State.EXECUTE;
+ return;
+ }
+ }
+
+ // Iterate until we are complete.
+ while (true)
+ {
+ // Produce a task.
+ Runnable task = _producer.produce();
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} produced {}", _producer, task);
+
+ if (task == null)
+ {
+ try (Locker.Lock lock = _locker.lock())
+ {
+ switch (_state)
+ {
+ case IDLE:
+ throw new IllegalStateException();
+ case PRODUCE:
+ _state = State.IDLE;
+ return;
+ case EXECUTE:
+ _state = State.PRODUCE;
+ continue;
+ }
+ }
+ }
+
+ // Run the task.
+ task.run();
+ }
+ }
+
+ @Override
+ public void dispatch()
+ {
+ _executor.execute(this);
+ }
+
+ @Override
+ public void run()
+ {
+ produce();
+ }
+
+ private enum State
+ {
+ IDLE, PRODUCE, EXECUTE
+ }
+}
diff --git a/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceExecuteConsume.java b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceExecuteConsume.java
new file mode 100644
index 0000000..d052250
--- /dev/null
+++ b/third_party/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceExecuteConsume.java
@@ -0,0 +1,112 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread.strategy;
+
+import java.util.concurrent.Executor;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.ExecutionStrategy;
+import org.eclipse.jetty.util.thread.Invocable;
+import org.eclipse.jetty.util.thread.Invocable.InvocationType;
+import org.eclipse.jetty.util.thread.Locker;
+import org.eclipse.jetty.util.thread.Locker.Lock;
+
+/**
+ * <p>A strategy where the caller thread iterates over task production, submitting each
+ * task to an {@link Executor} for execution.</p>
+ */
+public class ProduceExecuteConsume implements ExecutionStrategy
+{
+ private static final Logger LOG = Log.getLogger(ProduceExecuteConsume.class);
+
+ private final Locker _locker = new Locker();
+ private final Producer _producer;
+ private final Executor _executor;
+ private State _state = State.IDLE;
+
+ public ProduceExecuteConsume(Producer producer, Executor executor)
+ {
+ _producer = producer;
+ _executor = executor;
+ }
+
+ @Override
+ public void produce()
+ {
+ try (Lock locked = _locker.lock())
+ {
+ switch (_state)
+ {
+ case IDLE:
+ _state = State.PRODUCE;
+ break;
+
+ case PRODUCE:
+ case EXECUTE:
+ _state = State.EXECUTE;
+ return;
+ }
+ }
+
+ // Produce until we no task is found.
+ while (true)
+ {
+ // Produce a task.
+ Runnable task = _producer.produce();
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} produced {}", _producer, task);
+
+ if (task == null)
+ {
+ try (Lock locked = _locker.lock())
+ {
+ switch (_state)
+ {
+ case IDLE:
+ throw new IllegalStateException();
+ case PRODUCE:
+ _state = State.IDLE;
+ return;
+ case EXECUTE:
+ _state = State.PRODUCE;
+ continue;
+ }
+ }
+ }
+
+ // Execute the task.
+ if (Invocable.getInvocationType(task) == InvocationType.NON_BLOCKING)
+ task.run();
+ else
+ _executor.execute(task);
+ }
+ }
+
+ @Override
+ public void dispatch()
+ {
+ _executor.execute(() -> produce());
+ }
+
+ private enum State
+ {
+ IDLE, PRODUCE, EXECUTE
+ }
+}
diff --git a/third_party/jetty-util/src/main/resources/org/eclipse/jetty/version/build.properties b/third_party/jetty-util/src/main/resources/org/eclipse/jetty/version/build.properties
new file mode 100644
index 0000000..c9d2022
--- /dev/null
+++ b/third_party/jetty-util/src/main/resources/org/eclipse/jetty/version/build.properties
@@ -0,0 +1,4 @@
+buildNumber=${buildNumber}
+timestamp=${timestamp}
+version=${project.version}
+scmUrl=${project.scm.connection}
\ No newline at end of file
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ArrayUtilTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ArrayUtilTest.java
new file mode 100644
index 0000000..157442f
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ArrayUtilTest.java
@@ -0,0 +1,102 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Unit tests for class {@link ArrayUtil}.
+ *
+ * @see ArrayUtil
+ */
+public class ArrayUtilTest
+{
+
+ @Test
+ public void testAddToArrayWithEmptyArray()
+ {
+ String[] stringArray = new String[0];
+ String[] resultArray = ArrayUtil.addToArray(stringArray, "Ca?", Object.class);
+
+ assertEquals(0, stringArray.length);
+ assertEquals(1, resultArray.length);
+
+ assertNotSame(stringArray, resultArray);
+ assertNotSame(resultArray, stringArray);
+
+ assertFalse(resultArray.equals(stringArray));
+ assertEquals(String.class, resultArray[0].getClass());
+ }
+
+ @Test
+ public void testAddUsingNull()
+ {
+ String[] stringArray = new String[7];
+ String[] stringArrayTwo = ArrayUtil.add(stringArray, null);
+
+ assertEquals(7, stringArray.length);
+ assertEquals(7, stringArrayTwo.length);
+
+ assertSame(stringArray, stringArrayTwo);
+ assertSame(stringArrayTwo, stringArray);
+ }
+
+ @Test
+ public void testAddWithNonEmptyArray()
+ {
+ Object[] objectArray = new Object[3];
+ Object[] objectArrayTwo = ArrayUtil.add(objectArray, objectArray);
+
+ assertEquals(3, objectArray.length);
+ assertEquals(6, objectArrayTwo.length);
+
+ assertNotSame(objectArray, objectArrayTwo);
+ assertNotSame(objectArrayTwo, objectArray);
+
+ assertFalse(objectArrayTwo.equals(objectArray));
+ }
+
+ @Test
+ public void testRemoveFromNullArrayReturningNull()
+ {
+ assertNull(ArrayUtil.removeFromArray((Integer[])null, new Object()));
+ }
+
+ @Test
+ public void testRemoveNulls()
+ {
+ Object[] objectArray = new Object[2];
+ objectArray[0] = new Object();
+ Object[] resultArray = ArrayUtil.removeNulls(objectArray);
+
+ assertEquals(2, objectArray.length);
+ assertEquals(1, resultArray.length);
+
+ assertNotSame(objectArray, resultArray);
+ assertNotSame(resultArray, objectArray);
+
+ assertFalse(resultArray.equals(objectArray));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/AtomicBiIntegerTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/AtomicBiIntegerTest.java
new file mode 100644
index 0000000..7ab8678
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/AtomicBiIntegerTest.java
@@ -0,0 +1,106 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class AtomicBiIntegerTest
+{
+
+ @Test
+ public void testBitOperations()
+ {
+ long encoded;
+
+ encoded = AtomicBiInteger.encode(0, 0);
+ assertThat(AtomicBiInteger.getHi(encoded), is(0));
+ assertThat(AtomicBiInteger.getLo(encoded), is(0));
+
+ encoded = AtomicBiInteger.encode(1, 2);
+ assertThat(AtomicBiInteger.getHi(encoded), is(1));
+ assertThat(AtomicBiInteger.getLo(encoded), is(2));
+
+ encoded = AtomicBiInteger.encode(Integer.MAX_VALUE, -1);
+ assertThat(AtomicBiInteger.getHi(encoded), is(Integer.MAX_VALUE));
+ assertThat(AtomicBiInteger.getLo(encoded), is(-1));
+ encoded = AtomicBiInteger.encodeLo(encoded, 42);
+ assertThat(AtomicBiInteger.getHi(encoded), is(Integer.MAX_VALUE));
+ assertThat(AtomicBiInteger.getLo(encoded), is(42));
+
+ encoded = AtomicBiInteger.encode(-1, Integer.MAX_VALUE);
+ assertThat(AtomicBiInteger.getHi(encoded), is(-1));
+ assertThat(AtomicBiInteger.getLo(encoded), is(Integer.MAX_VALUE));
+ encoded = AtomicBiInteger.encodeHi(encoded, 42);
+ assertThat(AtomicBiInteger.getHi(encoded), is(42));
+ assertThat(AtomicBiInteger.getLo(encoded), is(Integer.MAX_VALUE));
+
+ encoded = AtomicBiInteger.encode(Integer.MIN_VALUE, 1);
+ assertThat(AtomicBiInteger.getHi(encoded), is(Integer.MIN_VALUE));
+ assertThat(AtomicBiInteger.getLo(encoded), is(1));
+ encoded = AtomicBiInteger.encodeLo(encoded, Integer.MAX_VALUE);
+ assertThat(AtomicBiInteger.getHi(encoded), is(Integer.MIN_VALUE));
+ assertThat(AtomicBiInteger.getLo(encoded), is(Integer.MAX_VALUE));
+
+ encoded = AtomicBiInteger.encode(1, Integer.MIN_VALUE);
+ assertThat(AtomicBiInteger.getHi(encoded), is(1));
+ assertThat(AtomicBiInteger.getLo(encoded), is(Integer.MIN_VALUE));
+ encoded = AtomicBiInteger.encodeHi(encoded, Integer.MAX_VALUE);
+ assertThat(AtomicBiInteger.getHi(encoded), is(Integer.MAX_VALUE));
+ assertThat(AtomicBiInteger.getLo(encoded), is(Integer.MIN_VALUE));
+ }
+
+ @Test
+ public void testSet()
+ {
+ AtomicBiInteger abi = new AtomicBiInteger();
+ assertThat(abi.getHi(), is(0));
+ assertThat(abi.getLo(), is(0));
+
+ abi.getAndSetHi(Integer.MAX_VALUE);
+ assertThat(abi.getHi(), is(Integer.MAX_VALUE));
+ assertThat(abi.getLo(), is(0));
+
+ abi.getAndSetLo(Integer.MIN_VALUE);
+ assertThat(abi.getHi(), is(Integer.MAX_VALUE));
+ assertThat(abi.getLo(), is(Integer.MIN_VALUE));
+ }
+
+ @Test
+ public void testCompareAndSet()
+ {
+ AtomicBiInteger abi = new AtomicBiInteger();
+ assertThat(abi.getHi(), is(0));
+ assertThat(abi.getLo(), is(0));
+
+ assertFalse(abi.compareAndSetHi(1, 42));
+ assertTrue(abi.compareAndSetHi(0, 42));
+ assertThat(abi.getHi(), is(42));
+ assertThat(abi.getLo(), is(0));
+
+ assertFalse(abi.compareAndSetLo(1, -42));
+ assertTrue(abi.compareAndSetLo(0, -42));
+ assertThat(abi.getHi(), is(42));
+ assertThat(abi.getLo(), is(-42));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/B64CodeTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/B64CodeTest.java
new file mode 100644
index 0000000..17f8539
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/B64CodeTest.java
@@ -0,0 +1,138 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.Base64;
+
+import org.junit.jupiter.api.Test;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class B64CodeTest
+{
+ String text = "Man is distinguished, not only by his reason, but by this singular passion " +
+ "from other animals, which is a lust of the mind, that by a perseverance of delight in " +
+ "the continued and indefatigable generation of knowledge, exceeds the short vehemence " +
+ "of any carnal pleasure.";
+
+ @Test
+ public void testEncodeRFC1421()
+ {
+ String expected = "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz" +
+ "IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg" +
+ "dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu" +
+ "dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo" +
+ "ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=";
+
+ // Default Encode
+ String b64 = B64Code.encode(text, ISO_8859_1);
+ assertThat("B64Code.encode(String)", b64, is(expected));
+
+ // Specified RFC Encode
+ byte[] rawInputBytes = text.getBytes(ISO_8859_1);
+ char[] chars = B64Code.encode(rawInputBytes, false);
+ b64 = new String(chars, 0, chars.length);
+ assertThat("B64Code.encode(byte[], false)", b64, is(expected));
+
+ // Standard Java Encode
+ String javaBase64 = Base64.getEncoder().encodeToString(rawInputBytes);
+ assertThat("Base64.getEncoder().encodeToString((byte[])", javaBase64, is(expected));
+ }
+
+ @Test
+ public void testEncodeRFC2045()
+ {
+ byte[] rawInputBytes = text.getBytes(ISO_8859_1);
+
+ // Old Jetty way
+ char[] chars = B64Code.encode(rawInputBytes, true);
+ String b64 = new String(chars, 0, chars.length);
+
+ String expected = "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz\r\n" +
+ "IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg\r\n" +
+ "dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu\r\n" +
+ "dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo\r\n" +
+ "ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=\r\n";
+
+ assertThat(b64, is(expected));
+
+ // Standard Java way
+ String javaBase64 = Base64.getMimeEncoder().encodeToString(rawInputBytes);
+ // NOTE: MIME standard for encoding should not include final "\r\n"
+ assertThat(javaBase64 + "\r\n", is(expected));
+ }
+
+ @Test
+ public void testInteger() throws Exception
+ {
+ byte[] bytes = text.getBytes(ISO_8859_1);
+ int value = (bytes[0] << 24) +
+ (bytes[1] << 16) +
+ (bytes[2] << 8) +
+ (bytes[3]);
+
+ String expected = "TWFuIA";
+
+ // Old Jetty way
+ StringBuilder b = new StringBuilder();
+ B64Code.encode(value, b);
+ assertThat("Old Jetty B64Code", b.toString(), is(expected));
+
+ // Standard Java technique
+ byte[] intBytes = new byte[Integer.BYTES];
+ for (int i = Integer.BYTES - 1; i >= 0; i--)
+ {
+ intBytes[i] = (byte)(value & 0xFF);
+ value >>= 8;
+ }
+ assertThat("Standard Java Base64", Base64.getEncoder().withoutPadding().encodeToString(intBytes), is(expected));
+ }
+
+ @Test
+ public void testLong() throws Exception
+ {
+ byte[] bytes = text.getBytes(ISO_8859_1);
+ long value = ((0xffL & bytes[0]) << 56) +
+ ((0xffL & bytes[1]) << 48) +
+ ((0xffL & bytes[2]) << 40) +
+ ((0xffL & bytes[3]) << 32) +
+ ((0xffL & bytes[4]) << 24) +
+ ((0xffL & bytes[5]) << 16) +
+ ((0xffL & bytes[6]) << 8) +
+ (0xffL & bytes[7]);
+
+ String expected = "TWFuIGlzIGQ";
+
+ // Old Jetty way
+ StringBuilder b = new StringBuilder();
+ B64Code.encode(value, b);
+ assertThat("Old Jetty B64Code", b.toString(), is(expected));
+
+ // Standard Java technique
+ byte[] longBytes = new byte[Long.BYTES];
+ for (int i = Long.BYTES - 1; i >= 0; i--)
+ {
+ longBytes[i] = (byte)(value & 0xFF);
+ value >>= 8;
+ }
+ assertThat("Standard Java Base64", Base64.getEncoder().withoutPadding().encodeToString(longBytes), is(expected));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/BlockingArrayQueueTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/BlockingArrayQueueTest.java
new file mode 100644
index 0000000..4c470cc
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/BlockingArrayQueueTest.java
@@ -0,0 +1,533 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class BlockingArrayQueueTest
+{
+ @Test
+ public void testWrap() throws Exception
+ {
+ BlockingArrayQueue<String> queue = new BlockingArrayQueue<>(3);
+
+ assertEquals(0, queue.size());
+
+ for (int i = 0; i < queue.getMaxCapacity(); i++)
+ {
+ queue.offer("one");
+ assertEquals(1, queue.size());
+
+ queue.offer("two");
+ assertEquals(2, queue.size());
+
+ queue.offer("three");
+ assertEquals(3, queue.size());
+
+ assertEquals("one", queue.get(0));
+ assertEquals("two", queue.get(1));
+ assertEquals("three", queue.get(2));
+
+ assertEquals("[one, two, three]", queue.toString());
+
+ assertEquals("one", queue.poll());
+ assertEquals(2, queue.size());
+
+ assertEquals("two", queue.poll());
+ assertEquals(1, queue.size());
+
+ assertEquals("three", queue.poll());
+ assertEquals(0, queue.size());
+
+ queue.offer("xxx");
+ assertEquals(1, queue.size());
+ assertEquals("xxx", queue.poll());
+ assertEquals(0, queue.size());
+ }
+ }
+
+ @Test
+ public void testRemove() throws Exception
+ {
+ BlockingArrayQueue<String> queue = new BlockingArrayQueue<>(3, 3);
+
+ queue.add("0");
+ queue.add("x");
+
+ for (int i = 1; i < 100; i++)
+ {
+ queue.add("" + i);
+ queue.add("x");
+ queue.remove(queue.size() - 3);
+ queue.set(queue.size() - 3, queue.get(queue.size() - 3) + "!");
+ }
+
+ for (int i = 0; i < 99; i++)
+ {
+ assertEquals(i + "!", queue.get(i));
+ }
+ }
+
+ @Test
+ public void testLimit() throws Exception
+ {
+ BlockingArrayQueue<String> queue = new BlockingArrayQueue<>(1, 0, 1);
+
+ String element = "0";
+ assertTrue(queue.add(element));
+ assertFalse(queue.offer("1"));
+
+ assertEquals(element, queue.poll());
+ assertTrue(queue.add(element));
+ }
+
+ @Test
+ public void testGrow() throws Exception
+ {
+ BlockingArrayQueue<String> queue = new BlockingArrayQueue<>(3, 2);
+ assertEquals(3, queue.getCapacity());
+
+ queue.add("a");
+ queue.add("a");
+ assertEquals(2, queue.size());
+ assertEquals(3, queue.getCapacity());
+ queue.add("a");
+ queue.add("a");
+ assertEquals(4, queue.size());
+ assertEquals(5, queue.getCapacity());
+
+ int s = 5;
+ int c = 5;
+ queue.add("a");
+
+ for (int t = 0; t < 100; t++)
+ {
+ assertEquals(s, queue.size());
+ assertEquals(c, queue.getCapacity());
+
+ for (int i = queue.size(); i-- > 0; )
+ {
+ queue.poll();
+ }
+ assertEquals(0, queue.size());
+ assertEquals(c, queue.getCapacity());
+
+ for (int i = queue.getCapacity(); i-- > 0; )
+ {
+ queue.add("a");
+ }
+ queue.add("a");
+ assertEquals(s + 1, queue.size());
+ assertEquals(c + 2, queue.getCapacity());
+
+ queue.poll();
+ queue.add("a");
+ queue.add("a");
+ assertEquals(s + 2, queue.size());
+ assertEquals(c + 2, queue.getCapacity());
+
+ s += 2;
+ c += 2;
+ }
+ }
+
+ @Test
+ public void testTake() throws Exception
+ {
+ final String[] data = new String[4];
+
+ final BlockingArrayQueue<String> queue = new BlockingArrayQueue<>();
+ CyclicBarrier barrier = new CyclicBarrier(2);
+
+ Thread thread = new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ data[0] = queue.take();
+ data[1] = queue.take();
+ barrier.await(5, TimeUnit.SECONDS); // Wait until the main thread already called offer().
+ data[2] = queue.take();
+ data[3] = queue.poll(100, TimeUnit.MILLISECONDS);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ fail("Should not had failed");
+ }
+ }
+ };
+
+ thread.start();
+
+ // Wait until the spawned thread is blocked in queue.take().
+ await().atMost(5, TimeUnit.SECONDS).until(() -> thread.getState() == Thread.State.WAITING);
+
+ queue.offer("zero");
+ queue.offer("one");
+ queue.offer("two");
+ barrier.await(5, TimeUnit.SECONDS); // Notify the spawned thread that offer() was called.
+ thread.join();
+
+ assertEquals("zero", data[0]);
+ assertEquals("one", data[1]);
+ assertEquals("two", data[2]);
+ assertNull(data[3]);
+ }
+
+ @Test
+ public void testConcurrentAccess() throws Exception
+ {
+ final int THREADS = 32;
+ final int LOOPS = 1000;
+
+ BlockingArrayQueue<Integer> queue = new BlockingArrayQueue<>(1 + THREADS * LOOPS);
+
+ Set<Integer> produced = ConcurrentHashMap.newKeySet();
+ Set<Integer> consumed = ConcurrentHashMap.newKeySet();
+
+ AtomicBoolean consumersRunning = new AtomicBoolean(true);
+
+ // start consumers
+ CyclicBarrier consumersBarrier = new CyclicBarrier(THREADS + 1);
+ for (int i = 0; i < THREADS; i++)
+ {
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ setPriority(getPriority() - 1);
+ try
+ {
+ while (consumersRunning.get())
+ {
+ int r = 1 + ThreadLocalRandom.current().nextInt(10);
+ if (r % 2 == 0)
+ {
+ Integer msg = queue.poll();
+ if (msg == null)
+ {
+ Thread.sleep(ThreadLocalRandom.current().nextInt(2));
+ continue;
+ }
+ consumed.add(msg);
+ }
+ else
+ {
+ Integer msg = queue.poll(r, TimeUnit.MILLISECONDS);
+ if (msg != null)
+ consumed.add(msg);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ finally
+ {
+ try
+ {
+ consumersBarrier.await();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+ }.start();
+ }
+
+ // start producers
+ CyclicBarrier producersBarrier = new CyclicBarrier(THREADS + 1);
+ for (int i = 0; i < THREADS; i++)
+ {
+ final int id = i;
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ for (int j = 0; j < LOOPS; j++)
+ {
+ Integer msg = ThreadLocalRandom.current().nextInt();
+ produced.add(msg);
+ if (!queue.offer(msg))
+ throw new Exception(id + " FULL! " + queue.size());
+ Thread.sleep(ThreadLocalRandom.current().nextInt(2));
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ finally
+ {
+ try
+ {
+ producersBarrier.await();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+ }.start();
+ }
+
+ producersBarrier.await();
+
+ AtomicInteger last = new AtomicInteger(queue.size() - 1);
+ await().atMost(5, TimeUnit.SECONDS).until(() ->
+ {
+ int size = queue.size();
+ if (size == 0 && last.get() == size)
+ return true;
+ last.set(size);
+ return false;
+ });
+
+ consumersRunning.set(false);
+ consumersBarrier.await();
+
+ assertEquals(produced, consumed);
+ }
+
+ @Test
+ public void testRemoveObjectFromEmptyQueue()
+ {
+ BlockingArrayQueue<String> queue = new BlockingArrayQueue<>(4, 0, 4);
+ assertFalse(queue.remove("SOMETHING"));
+ }
+
+ @Test
+ public void testRemoveObjectWithWrappedTail() throws Exception
+ {
+ BlockingArrayQueue<String> queue = new BlockingArrayQueue<>(6);
+ // Wrap the tail
+ for (int i = 0; i < queue.getMaxCapacity(); ++i)
+ {
+ queue.offer("" + i);
+ }
+ // Advance the head
+ queue.poll();
+ // Remove from the middle
+ assertTrue(queue.remove("2"));
+
+ // Advance the tail
+ assertTrue(queue.offer("A"));
+ assertTrue(queue.offer("B"));
+ queue.poll();
+ // Remove from the middle
+ assertTrue(queue.remove("3"));
+ }
+
+ @Test
+ public void testRemoveObject() throws Exception
+ {
+ BlockingArrayQueue<String> queue = new BlockingArrayQueue<>(4, 0, 4);
+
+ String element1 = "A";
+ assertTrue(queue.offer(element1));
+ assertTrue(queue.remove(element1));
+
+ for (int i = 0; i < queue.getMaxCapacity() - 1; ++i)
+ {
+ queue.offer("" + i);
+ queue.poll();
+ }
+ String element2 = "B";
+ assertTrue(queue.offer(element2));
+ assertTrue(queue.offer(element1));
+ assertTrue(queue.remove(element1));
+
+ assertFalse(queue.remove("NOT_PRESENT"));
+
+ assertTrue(queue.remove(element2));
+ assertFalse(queue.remove("NOT_PRESENT"));
+
+ queue.clear();
+
+ for (int i = 0; i < queue.getMaxCapacity(); ++i)
+ {
+ queue.offer("" + i);
+ }
+
+ assertTrue(queue.remove("" + (queue.getMaxCapacity() - 1)));
+ }
+
+ @Test
+ public void testRemoveWithMaxCapacityOne() throws Exception
+ {
+ BlockingArrayQueue<String> queue = new BlockingArrayQueue<>(1);
+
+ String element = "A";
+ assertTrue(queue.offer(element));
+ assertTrue(queue.remove(element));
+
+ assertTrue(queue.offer(element));
+ assertEquals(element, queue.remove(0));
+ }
+
+ @Test
+ public void testIteratorWithModification() throws Exception
+ {
+ BlockingArrayQueue<String> queue = new BlockingArrayQueue<>(4, 0, 4);
+ int count = queue.getMaxCapacity() - 1;
+ for (int i = 0; i < count; ++i)
+ {
+ queue.offer("" + i);
+ }
+
+ int sum = 0;
+ for (String element : queue)
+ {
+ ++sum;
+ // Concurrent modification, must not change the iterator
+ queue.remove(element);
+ }
+
+ assertEquals(count, sum);
+ assertTrue(queue.isEmpty());
+ }
+
+ @Test
+ public void testListIterator() throws Exception
+ {
+ BlockingArrayQueue<String> queue = new BlockingArrayQueue<>(4, 0, 4);
+ String element1 = "A";
+ String element2 = "B";
+ queue.offer(element1);
+ queue.offer(element2);
+
+ ListIterator<String> iterator = queue.listIterator();
+ assertTrue(iterator.hasNext());
+ assertFalse(iterator.hasPrevious());
+
+ String element = iterator.next();
+ assertEquals(element1, element);
+ assertTrue(iterator.hasNext());
+ assertTrue(iterator.hasPrevious());
+
+ element = iterator.next();
+ assertEquals(element2, element);
+ assertFalse(iterator.hasNext());
+ assertTrue(iterator.hasPrevious());
+
+ element = iterator.previous();
+ assertEquals(element2, element);
+ assertTrue(iterator.hasNext());
+ assertTrue(iterator.hasPrevious());
+
+ element = iterator.previous();
+ assertEquals(element1, element);
+ assertTrue(iterator.hasNext());
+ assertFalse(iterator.hasPrevious());
+ }
+
+ @Test
+ public void testListIteratorWithWrappedHead() throws Exception
+ {
+ BlockingArrayQueue<String> queue = new BlockingArrayQueue<>(4, 0, 4);
+ // This sequence of offers and polls wraps the head around the array
+ queue.offer("0");
+ queue.offer("1");
+ queue.offer("2");
+ queue.offer("3");
+ queue.poll();
+ queue.poll();
+
+ String element1 = queue.get(0);
+ String element2 = queue.get(1);
+
+ ListIterator<String> iterator = queue.listIterator();
+ assertTrue(iterator.hasNext());
+ assertFalse(iterator.hasPrevious());
+
+ String element = iterator.next();
+ assertEquals(element1, element);
+ assertTrue(iterator.hasNext());
+ assertTrue(iterator.hasPrevious());
+
+ element = iterator.next();
+ assertEquals(element2, element);
+ assertFalse(iterator.hasNext());
+ assertTrue(iterator.hasPrevious());
+
+ element = iterator.previous();
+ assertEquals(element2, element);
+ assertTrue(iterator.hasNext());
+ assertTrue(iterator.hasPrevious());
+
+ element = iterator.previous();
+ assertEquals(element1, element);
+ assertTrue(iterator.hasNext());
+ assertFalse(iterator.hasPrevious());
+ }
+
+ @Test
+ public void testDrainTo() throws Exception
+ {
+ BlockingArrayQueue<String> queue = new BlockingArrayQueue<>();
+ queue.add("one");
+ queue.add("two");
+ queue.add("three");
+ queue.add("four");
+ queue.add("five");
+ queue.add("six");
+
+ List<String> to = new ArrayList<>();
+ queue.drainTo(to, 3);
+ assertThat(to, Matchers.contains("one", "two", "three"));
+ assertThat(queue.size(), Matchers.is(3));
+ assertThat(queue, Matchers.contains("four", "five", "six"));
+
+ queue.drainTo(to);
+ assertThat(to, Matchers.contains("one", "two", "three", "four", "five", "six"));
+ assertThat(queue.size(), Matchers.is(0));
+ assertThat(queue, Matchers.empty());
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/BufferUtilTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/BufferUtilTest.java
new file mode 100644
index 0000000..6174a95
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/BufferUtilTest.java
@@ -0,0 +1,343 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class BufferUtilTest
+{
+ @Test
+ public void testToInt() throws Exception
+ {
+ ByteBuffer[] buf =
+ {
+ BufferUtil.toBuffer("0"),
+ BufferUtil.toBuffer(" 42 "),
+ BufferUtil.toBuffer(" 43abc"),
+ BufferUtil.toBuffer("-44"),
+ BufferUtil.toBuffer(" - 45;"),
+ BufferUtil.toBuffer("-2147483648"),
+ BufferUtil.toBuffer("2147483647"),
+ };
+
+ int[] val =
+ {
+ 0, 42, 43, -44, -45, -2147483648, 2147483647
+ };
+
+ for (int i = 0; i < buf.length; i++)
+ {
+ assertEquals(val[i], BufferUtil.toInt(buf[i]), "t" + i);
+ }
+ }
+
+ @Test
+ public void testPutInt() throws Exception
+ {
+ int[] val =
+ {
+ 0, 42, 43, -44, -45, Integer.MIN_VALUE, Integer.MAX_VALUE
+ };
+
+ String[] str =
+ {
+ "0", "42", "43", "-44", "-45", "" + Integer.MIN_VALUE, "" + Integer.MAX_VALUE
+ };
+
+ ByteBuffer buffer = ByteBuffer.allocate(24);
+
+ for (int i = 0; i < val.length; i++)
+ {
+ BufferUtil.clearToFill(buffer);
+ BufferUtil.putDecInt(buffer, val[i]);
+ BufferUtil.flipToFlush(buffer, 0);
+ assertEquals(str[i], BufferUtil.toString(buffer), "t" + i);
+ }
+ }
+
+ @Test
+ public void testPutLong() throws Exception
+ {
+ long[] val =
+ {
+ 0L, 42L, 43L, -44L, -45L, Long.MIN_VALUE, Long.MAX_VALUE
+ };
+
+ String[] str =
+ {
+ "0", "42", "43", "-44", "-45", "" + Long.MIN_VALUE, "" + Long.MAX_VALUE
+ };
+
+ ByteBuffer buffer = ByteBuffer.allocate(50);
+
+ for (int i = 0; i < val.length; i++)
+ {
+ BufferUtil.clearToFill(buffer);
+ BufferUtil.putDecLong(buffer, val[i]);
+ BufferUtil.flipToFlush(buffer, 0);
+ assertEquals(str[i], BufferUtil.toString(buffer), "t" + i);
+ }
+ }
+
+ @Test
+ public void testPutHexInt() throws Exception
+ {
+ int[] val =
+ {
+ 0, 42, 43, -44, -45, -2147483648, 2147483647
+ };
+
+ String[] str =
+ {
+ "0", "2A", "2B", "-2C", "-2D", "-80000000", "7FFFFFFF"
+ };
+
+ ByteBuffer buffer = ByteBuffer.allocate(50);
+
+ for (int i = 0; i < val.length; i++)
+ {
+ BufferUtil.clearToFill(buffer);
+ BufferUtil.putHexInt(buffer, val[i]);
+ BufferUtil.flipToFlush(buffer, 0);
+ assertEquals(str[i], BufferUtil.toString(buffer), "t" + i);
+ }
+ }
+
+ @Test
+ public void testPut() throws Exception
+ {
+ ByteBuffer to = BufferUtil.allocate(10);
+ ByteBuffer from = BufferUtil.toBuffer("12345");
+
+ BufferUtil.clear(to);
+ assertEquals(5, BufferUtil.append(to, from));
+ assertTrue(BufferUtil.isEmpty(from));
+ assertEquals("12345", BufferUtil.toString(to));
+
+ from = BufferUtil.toBuffer("XX67890ZZ");
+ from.position(2);
+
+ assertEquals(5, BufferUtil.append(to, from));
+ assertEquals(2, from.remaining());
+ assertEquals("1234567890", BufferUtil.toString(to));
+ }
+
+ @Test
+ public void testAppend() throws Exception
+ {
+ ByteBuffer to = BufferUtil.allocate(8);
+ ByteBuffer from = BufferUtil.toBuffer("12345");
+
+ BufferUtil.append(to, from.array(), 0, 3);
+ assertEquals("123", BufferUtil.toString(to));
+ BufferUtil.append(to, from.array(), 3, 2);
+ assertEquals("12345", BufferUtil.toString(to));
+
+ assertThrows(BufferOverflowException.class, () ->
+ {
+ BufferUtil.append(to, from.array(), 0, 5);
+ });
+ }
+
+ @Test
+ public void testPutDirect() throws Exception
+ {
+ ByteBuffer to = BufferUtil.allocateDirect(10);
+ ByteBuffer from = BufferUtil.toBuffer("12345");
+
+ BufferUtil.clear(to);
+ assertEquals(5, BufferUtil.append(to, from));
+ assertTrue(BufferUtil.isEmpty(from));
+ assertEquals("12345", BufferUtil.toString(to));
+
+ from = BufferUtil.toBuffer("XX67890ZZ");
+ from.position(2);
+
+ assertEquals(5, BufferUtil.append(to, from));
+ assertEquals(2, from.remaining());
+ assertEquals("1234567890", BufferUtil.toString(to));
+ }
+
+ @Test
+ public void testToBufferArray()
+ {
+ byte[] arr = new byte[128];
+ Arrays.fill(arr, (byte)0x44);
+ ByteBuffer buf = BufferUtil.toBuffer(arr);
+
+ int count = 0;
+ while (buf.remaining() > 0)
+ {
+ byte b = buf.get();
+ assertEquals(b, 0x44);
+ count++;
+ }
+
+ assertEquals(arr.length, count, "Count of bytes");
+ }
+
+ @Test
+ public void testToBufferArrayOffsetLength()
+ {
+ byte[] arr = new byte[128];
+ Arrays.fill(arr, (byte)0xFF); // fill whole thing with FF
+ int offset = 10;
+ int length = 100;
+ Arrays.fill(arr, offset, offset + length, (byte)0x77); // fill partial with 0x77
+ ByteBuffer buf = BufferUtil.toBuffer(arr, offset, length);
+
+ int count = 0;
+ while (buf.remaining() > 0)
+ {
+ byte b = buf.get();
+ assertEquals(b, 0x77);
+ count++;
+ }
+
+ assertEquals(length, count, "Count of bytes");
+ }
+
+ private static final Logger LOG = Log.getLogger(BufferUtilTest.class);
+
+ @Test
+ @Disabled("Very simple microbenchmark to compare different writeTo implementations. Only for development thus " +
+ "ignored.")
+ public void testWriteToMicrobenchmark() throws IOException
+ {
+ int capacity = 1024 * 128;
+ int iterations = 100;
+ int testRuns = 10;
+ byte[] bytes = new byte[capacity];
+ ThreadLocalRandom.current().nextBytes(bytes);
+ ByteBuffer buffer = BufferUtil.allocate(capacity);
+ BufferUtil.append(buffer, bytes, 0, capacity);
+ long startTest = System.nanoTime();
+ for (int i = 0; i < testRuns; i++)
+ {
+ long start = System.nanoTime();
+ for (int j = 0; j < iterations; j++)
+ {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ long startRun = System.nanoTime();
+ BufferUtil.writeTo(buffer.asReadOnlyBuffer(), out);
+ long elapsedRun = System.nanoTime() - startRun;
+// LOG.warn("run elapsed={}ms", elapsedRun / 1000);
+ assertThat("Bytes in out equal bytes in buffer", Arrays.equals(bytes, out.toByteArray()), is(true));
+ }
+ long elapsed = System.nanoTime() - start;
+ LOG.warn("elapsed={}ms average={}ms", elapsed / 1000, elapsed / iterations / 1000);
+ }
+ LOG.warn("overall average: {}ms", (System.nanoTime() - startTest) / testRuns / iterations / 1000);
+ }
+
+ @Test
+ public void testWriteToWithBufferThatDoesNotExposeArrayAndSmallContent() throws IOException
+ {
+ int capacity = BufferUtil.TEMP_BUFFER_SIZE / 4;
+ testWriteToWithBufferThatDoesNotExposeArray(capacity);
+ }
+
+ @Test
+ public void testWriteToWithBufferThatDoesNotExposeArrayAndContentLengthMatchingTempBufferSize() throws IOException
+ {
+ int capacity = BufferUtil.TEMP_BUFFER_SIZE;
+ testWriteToWithBufferThatDoesNotExposeArray(capacity);
+ }
+
+ @Test
+ public void testWriteToWithBufferThatDoesNotExposeArrayAndContentSlightlyBiggerThanTwoTimesTempBufferSize()
+ throws
+ IOException
+ {
+ int capacity = BufferUtil.TEMP_BUFFER_SIZE * 2 + 1024;
+ testWriteToWithBufferThatDoesNotExposeArray(capacity);
+ }
+
+ @Test
+ @SuppressWarnings("ReferenceEquality")
+ public void testEnsureCapacity() throws Exception
+ {
+ ByteBuffer b = BufferUtil.toBuffer("Goodbye Cruel World");
+ assertTrue(b == BufferUtil.ensureCapacity(b, 0));
+ assertTrue(b == BufferUtil.ensureCapacity(b, 10));
+ assertTrue(b == BufferUtil.ensureCapacity(b, b.capacity()));
+
+ ByteBuffer b1 = BufferUtil.ensureCapacity(b, 64);
+ assertTrue(b != b1);
+ assertEquals(64, b1.capacity());
+ assertEquals("Goodbye Cruel World", BufferUtil.toString(b1));
+
+ b1.position(8);
+ b1.limit(13);
+ assertEquals("Cruel", BufferUtil.toString(b1));
+ ByteBuffer b2 = b1.slice();
+ assertEquals("Cruel", BufferUtil.toString(b2));
+ System.err.println(BufferUtil.toDetailString(b2));
+ assertEquals(8, b2.arrayOffset());
+ assertEquals(5, b2.capacity());
+
+ assertTrue(b2 == BufferUtil.ensureCapacity(b2, 5));
+
+ ByteBuffer b3 = BufferUtil.ensureCapacity(b2, 64);
+ assertTrue(b2 != b3);
+ assertEquals(64, b3.capacity());
+ assertEquals("Cruel", BufferUtil.toString(b3));
+ assertEquals(0, b3.arrayOffset());
+ }
+
+ @Test
+ public void testToDetailWithDEL()
+ {
+ ByteBuffer b = ByteBuffer.allocate(40);
+ b.putChar('a').putChar('b').putChar('c');
+ b.put((byte)0x7F);
+ b.putChar('x').putChar('y').putChar('z');
+ b.flip();
+ String result = BufferUtil.toDetailString(b);
+ assertThat("result", result, containsString("\\x7f"));
+ }
+
+ private void testWriteToWithBufferThatDoesNotExposeArray(int capacity) throws IOException
+ {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] bytes = new byte[capacity];
+ ThreadLocalRandom.current().nextBytes(bytes);
+ ByteBuffer buffer = BufferUtil.allocate(capacity);
+ BufferUtil.append(buffer, bytes, 0, capacity);
+ BufferUtil.writeTo(buffer.asReadOnlyBuffer(), out);
+ assertThat("Bytes in out equal bytes in buffer", Arrays.equals(bytes, out.toByteArray()), is(true));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/CollectionAssert.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/CollectionAssert.java
new file mode 100644
index 0000000..057c37b
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/CollectionAssert.java
@@ -0,0 +1,141 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @deprecated use {@link org.eclipse.jetty.toolchain.test.ExtraMatchers#ordered(List)} instead
+ */
+@Deprecated
+public class CollectionAssert
+{
+ public static void assertContainsUnordered(String msg, Collection<String> expectedSet, Collection<String> actualSet)
+ {
+ // same size?
+ boolean mismatch = expectedSet.size() != actualSet.size();
+
+ // test content
+ Set<String> missing = new HashSet<>();
+ for (String expected : expectedSet)
+ {
+ if (!actualSet.contains(expected))
+ {
+ missing.add(expected);
+ }
+ }
+
+ if (mismatch || missing.size() > 0)
+ {
+ // build up detailed error message
+ StringWriter message = new StringWriter();
+ PrintWriter err = new PrintWriter(message);
+
+ err.printf("%s: Assert Contains (Unordered)", msg);
+ if (mismatch)
+ {
+ err.print(" [size mismatch]");
+ }
+ if (missing.size() >= 0)
+ {
+ err.printf(" [%d entries missing]", missing.size());
+ }
+ err.println();
+ err.printf("Actual Entries (size: %d)%n", actualSet.size());
+ for (String actual : actualSet)
+ {
+ char indicator = expectedSet.contains(actual) ? ' ' : '>';
+ err.printf("%s| %s%n", indicator, actual);
+ }
+ err.printf("Expected Entries (size: %d)%n", expectedSet.size());
+ for (String expected : expectedSet)
+ {
+ char indicator = actualSet.contains(expected) ? ' ' : '>';
+ err.printf("%s| %s%n", indicator, expected);
+ }
+ err.flush();
+ fail(message.toString());
+ }
+ }
+
+ public static void assertOrdered(String msg, List<String> expectedList, List<String> actualList)
+ {
+ // same size?
+ boolean mismatch = expectedList.size() != actualList.size();
+
+ // test content
+ List<Integer> badEntries = new ArrayList<>();
+ int min = Math.min(expectedList.size(), actualList.size());
+ int max = Math.max(expectedList.size(), actualList.size());
+ for (int i = 0; i < min; i++)
+ {
+ if (!expectedList.get(i).equals(actualList.get(i)))
+ {
+ badEntries.add(i);
+ }
+ }
+ for (int i = min; i < max; i++)
+ {
+ badEntries.add(i);
+ }
+
+ if (mismatch || badEntries.size() > 0)
+ {
+ // build up detailed error message
+ StringWriter message = new StringWriter();
+ PrintWriter err = new PrintWriter(message);
+
+ err.printf("%s: Assert Contains (Unordered)", msg);
+ if (mismatch)
+ {
+ err.print(" [size mismatch]");
+ }
+ if (badEntries.size() >= 0)
+ {
+ err.printf(" [%d entries not matched]", badEntries.size());
+ }
+ err.println();
+ err.printf("Actual Entries (size: %d)%n", actualList.size());
+ for (int i = 0; i < actualList.size(); i++)
+ {
+ String actual = actualList.get(i);
+ char indicator = badEntries.contains(i) ? '>' : ' ';
+ err.printf("%s[%d] %s%n", indicator, i, actual);
+ }
+
+ err.printf("Expected Entries (size: %d)%n", expectedList.size());
+ for (int i = 0; i < expectedList.size(); i++)
+ {
+ String expected = expectedList.get(i);
+ char indicator = badEntries.contains(i) ? '>' : ' ';
+ err.printf("%s[%d] %s%n", indicator, i, expected);
+ }
+ err.flush();
+ fail(message.toString());
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/DateCacheTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/DateCacheTest.java
new file mode 100644
index 0000000..7ef1ef2
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/DateCacheTest.java
@@ -0,0 +1,101 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Util meta Tests.
+ */
+public class DateCacheTest
+{
+
+ @Test
+ @SuppressWarnings("ReferenceEquality")
+ public void testDateCache() throws Exception
+ {
+ //@WAS: Test t = new Test("org.eclipse.jetty.util.DateCache");
+ // 012345678901234567890123456789
+ DateCache dc = new DateCache("EEE, dd MMM yyyy HH:mm:ss zzz ZZZ", Locale.US, TimeZone.getTimeZone("GMT"));
+
+ Thread.sleep(2000);
+
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ long end = now + 3000;
+ String f = dc.formatNow(now);
+ String last = f;
+
+ int hits = 0;
+ int misses = 0;
+
+ while (now < end)
+ {
+ last = f;
+ f = dc.formatNow(now);
+ // System.err.printf("%s %s%n",f,last==f);
+ if (last == f)
+ hits++;
+ else
+ misses++;
+
+ TimeUnit.MILLISECONDS.sleep(100);
+ now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ }
+ assertThat(hits, Matchers.greaterThan(misses));
+ }
+
+ @Test
+ public void testAllMethods()
+ {
+ // we simply check we do not have any exception
+ DateCache dateCache = new DateCache();
+ assertNotNull(dateCache.formatNow(System.currentTimeMillis()));
+ assertNotNull(dateCache.formatNow(new Date().getTime()));
+ assertNotNull(dateCache.formatNow(Instant.now().toEpochMilli()));
+
+ assertNotNull(dateCache.format(new Date()));
+ assertNotNull(dateCache.format(new Date(System.currentTimeMillis())));
+
+ assertNotNull(dateCache.format(System.currentTimeMillis()));
+ assertNotNull(dateCache.format(new Date().getTime()));
+ assertNotNull(dateCache.format(Instant.now().toEpochMilli()));
+
+ assertNotNull(dateCache.formatTick(System.currentTimeMillis()));
+ assertNotNull(dateCache.formatTick(new Date().getTime()));
+ assertNotNull(dateCache.formatTick(Instant.now().toEpochMilli()));
+
+ assertNotNull(dateCache.getFormatString());
+
+ assertNotNull(dateCache.getTimeZone());
+
+ assertNotNull(dateCache.now());
+
+ assertNotNull(dateCache.tick());
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/FutureCallbackTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/FutureCallbackTest.java
new file mode 100644
index 0000000..b47d249
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/FutureCallbackTest.java
@@ -0,0 +1,192 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class FutureCallbackTest
+{
+ @Test
+ public void testNotDone()
+ {
+ FutureCallback fcb = new FutureCallback();
+ assertFalse(fcb.isDone());
+ assertFalse(fcb.isCancelled());
+ }
+
+ @Test
+ public void testGetNotDone() throws Exception
+ {
+ FutureCallback fcb = new FutureCallback();
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ assertThrows(TimeoutException.class, () -> fcb.get(500, TimeUnit.MILLISECONDS));
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, Matchers.greaterThan(50L));
+ }
+
+ @Test
+ public void testDone() throws Exception
+ {
+ FutureCallback fcb = new FutureCallback();
+ fcb.succeeded();
+ assertTrue(fcb.isDone());
+ assertFalse(fcb.isCancelled());
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ assertEquals(null, fcb.get());
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, Matchers.lessThan(500L));
+ }
+
+ @Test
+ public void testGetDone() throws Exception
+ {
+ final FutureCallback fcb = new FutureCallback();
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ new Thread(() ->
+ {
+ latch.countDown();
+ try
+ {
+ TimeUnit.MILLISECONDS.sleep(100);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ fcb.succeeded();
+ }).start();
+
+ latch.await();
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ assertEquals(null, fcb.get(10000, TimeUnit.MILLISECONDS));
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, Matchers.greaterThan(10L));
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, Matchers.lessThan(1000L));
+
+ assertTrue(fcb.isDone());
+ assertFalse(fcb.isCancelled());
+ }
+
+ @Test
+ public void testFailed() throws Exception
+ {
+ FutureCallback fcb = new FutureCallback();
+ Exception ex = new Exception("FAILED");
+ fcb.failed(ex);
+ assertTrue(fcb.isDone());
+ assertFalse(fcb.isCancelled());
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ ExecutionException e = assertThrows(ExecutionException.class, () -> fcb.get());
+ assertEquals(ex, e.getCause());
+
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, Matchers.lessThan(100L));
+ }
+
+ @Test
+ public void testGetFailed() throws Exception
+ {
+ final FutureCallback fcb = new FutureCallback();
+ final Exception ex = new Exception("FAILED");
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ new Thread(() ->
+ {
+ latch.countDown();
+ try
+ {
+ TimeUnit.MILLISECONDS.sleep(100);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ fcb.failed(ex);
+ }).start();
+
+ latch.await();
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ ExecutionException e = assertThrows(ExecutionException.class, () -> fcb.get(10000, TimeUnit.MILLISECONDS));
+ assertEquals(ex, e.getCause());
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, Matchers.greaterThan(10L));
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, Matchers.lessThan(5000L));
+
+ assertTrue(fcb.isDone());
+ assertFalse(fcb.isCancelled());
+ }
+
+ @Test
+ public void testCancelled() throws Exception
+ {
+ FutureCallback fcb = new FutureCallback();
+ fcb.cancel(true);
+ assertTrue(fcb.isDone());
+ assertTrue(fcb.isCancelled());
+
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ CancellationException e = assertThrows(CancellationException.class, () -> fcb.get());
+ assertThat(e.getCause(), Matchers.instanceOf(CancellationException.class));
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, Matchers.lessThan(100L));
+ }
+
+ @Test
+ public void testGetCancelled() throws Exception
+ {
+ final FutureCallback fcb = new FutureCallback();
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ new Thread(() ->
+ {
+ latch.countDown();
+ try
+ {
+ TimeUnit.MILLISECONDS.sleep(100);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ fcb.cancel(true);
+ }).start();
+
+ latch.await();
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ CancellationException e = assertThrows(CancellationException.class, () -> fcb.get(10000, TimeUnit.MILLISECONDS));
+ assertThat(e.getCause(), Matchers.instanceOf(CancellationException.class));
+
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, Matchers.greaterThan(10L));
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, Matchers.lessThan(1000L));
+
+ assertTrue(fcb.isDone());
+ assertTrue(fcb.isCancelled());
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java
new file mode 100644
index 0000000..d7aff1d
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java
@@ -0,0 +1,120 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class HostPortTest
+{
+ private static Stream<Arguments> validAuthorityProvider()
+ {
+ return Stream.of(
+ Arguments.of("", "", null),
+ Arguments.of(":80", "", "80"),
+ Arguments.of("host", "host", null),
+ Arguments.of("host:80", "host", "80"),
+ Arguments.of("10.10.10.1", "10.10.10.1", null),
+ Arguments.of("10.10.10.1:80", "10.10.10.1", "80"),
+ Arguments.of("[0::0::0::1]", "[0::0::0::1]", null),
+ Arguments.of("[0::0::0::1]:80", "[0::0::0::1]", "80"),
+ Arguments.of("0:1:2:3:4:5:6", "[0:1:2:3:4:5:6]", null),
+ Arguments.of("127.0.0.1:65535", "127.0.0.1", "65535"),
+ // Localhost tests
+ Arguments.of("localhost:80", "localhost", "80"),
+ Arguments.of("127.0.0.1:80", "127.0.0.1", "80"),
+ Arguments.of("::1", "[::1]", null),
+ Arguments.of("[::1]:443", "[::1]", "443"),
+ // Examples from https://tools.ietf.org/html/rfc2732#section-2
+ Arguments.of("[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80", "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]", "80"),
+ Arguments.of("[1080:0:0:0:8:800:200C:417A]", "[1080:0:0:0:8:800:200C:417A]", null),
+ Arguments.of("[3ffe:2a00:100:7031::1]", "[3ffe:2a00:100:7031::1]", null),
+ Arguments.of("[1080::8:800:200C:417A]", "[1080::8:800:200C:417A]", null),
+ Arguments.of("[::192.9.5.5]", "[::192.9.5.5]", null),
+ Arguments.of("[::FFFF:129.144.52.38]:80", "[::FFFF:129.144.52.38]", "80"),
+ Arguments.of("[2010:836B:4179::836B:4179]", "[2010:836B:4179::836B:4179]", null),
+ // Modified Examples from above, not using square brackets (valid, but should never have a port)
+ Arguments.of("FEDC:BA98:7654:3210:FEDC:BA98:7654:3210", "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]", null),
+ Arguments.of("1080:0:0:0:8:800:200C:417A", "[1080:0:0:0:8:800:200C:417A]", null),
+ Arguments.of("3ffe:2a00:100:7031::1", "[3ffe:2a00:100:7031::1]", null),
+ Arguments.of("1080::8:800:200C:417A", "[1080::8:800:200C:417A]", null),
+ Arguments.of("::192.9.5.5", "[::192.9.5.5]", null),
+ Arguments.of("::FFFF:129.144.52.38", "[::FFFF:129.144.52.38]", null),
+ Arguments.of("2010:836B:4179::836B:4179", "[2010:836B:4179::836B:4179]", null)
+ );
+ }
+
+ private static Stream<Arguments> invalidAuthorityProvider()
+ {
+ return Stream.of(
+ null,
+ "host:",
+ "127.0.0.1:",
+ "[0::0::0::0::1]:",
+ "host:xxx",
+ "127.0.0.1:xxx",
+ "[0::0::0::0::1]:xxx",
+ "host:-80",
+ "127.0.0.1:-80",
+ "[0::0::0::0::1]:-80",
+ "127.0.0.1:65536"
+ )
+ .map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("validAuthorityProvider")
+ public void testValidAuthority(String authority, String expectedHost, Integer expectedPort)
+ {
+ try
+ {
+ HostPort hostPort = new HostPort(authority);
+ assertThat(authority, hostPort.getHost(), is(expectedHost));
+
+ if (expectedPort == null)
+ assertThat(authority, hostPort.getPort(), is(0));
+ else
+ assertThat(authority, hostPort.getPort(), is(expectedPort));
+ }
+ catch (Exception e)
+ {
+ if (expectedHost != null)
+ e.printStackTrace();
+ assertNull(authority, expectedHost);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("invalidAuthorityProvider")
+ public void testInvalidAuthority(String authority)
+ {
+ assertThrows(IllegalArgumentException.class, () ->
+ {
+ new HostPort(authority);
+ });
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IPAddressMapTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IPAddressMapTest.java
new file mode 100644
index 0000000..1566aae
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IPAddressMapTest.java
@@ -0,0 +1,170 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class IPAddressMapTest
+{
+ @Test
+ public void testOneAddress()
+ {
+ IPAddressMap<String> map = new IPAddressMap<>();
+
+ map.put("10.5.2.1", "1");
+
+ assertNotNull(map.match("10.5.2.1"));
+
+ assertNull(map.match("101.5.2.1"));
+ assertNull(map.match("10.15.2.1"));
+ assertNull(map.match("10.5.22.1"));
+ assertNull(map.match("10.5.2.0"));
+ }
+
+ @Test
+ public void testOneRange()
+ {
+ IPAddressMap<String> map = new IPAddressMap<>();
+
+ map.put("1-15.16-31.32-63.64-127", "1");
+
+ assertNotNull(map.match("7.23.39.71"));
+ assertNotNull(map.match("1.16.32.64"));
+ assertNotNull(map.match("15.31.63.127"));
+
+ assertNull(map.match("16.32.64.128"));
+ assertNull(map.match("1.16.32.63"));
+ assertNull(map.match("1.16.31.64"));
+ assertNull(map.match("1.15.32.64"));
+ assertNull(map.match("0.16.32.64"));
+ }
+
+ @Test
+ public void testOneMissing()
+ {
+ IPAddressMap<String> map = new IPAddressMap<>();
+
+ map.put("10.5.2.", "1");
+
+ assertNotNull(map.match("10.5.2.0"));
+ assertNotNull(map.match("10.5.2.128"));
+ assertNotNull(map.match("10.5.2.255"));
+ }
+
+ @Test
+ public void testTwoMissing()
+ {
+ IPAddressMap<String> map = new IPAddressMap<>();
+
+ map.put("10.5.", "1");
+
+ assertNotNull(map.match("10.5.2.0"));
+ assertNotNull(map.match("10.5.2.128"));
+ assertNotNull(map.match("10.5.2.255"));
+ assertNotNull(map.match("10.5.0.1"));
+ assertNotNull(map.match("10.5.128.1"));
+ assertNotNull(map.match("10.5.255.1"));
+ }
+
+ @Test
+ public void testThreeMissing()
+ {
+ IPAddressMap<String> map = new IPAddressMap<>();
+
+ map.put("10.", "1");
+
+ assertNotNull(map.match("10.5.2.0"));
+ assertNotNull(map.match("10.5.2.128"));
+ assertNotNull(map.match("10.5.2.255"));
+ assertNotNull(map.match("10.5.0.1"));
+ assertNotNull(map.match("10.5.128.1"));
+ assertNotNull(map.match("10.5.255.1"));
+ assertNotNull(map.match("10.0.1.1"));
+ assertNotNull(map.match("10.128.1.1"));
+ assertNotNull(map.match("10.255.1.1"));
+ }
+
+ @Test
+ public void testOneMixed()
+ {
+ IPAddressMap<String> map = new IPAddressMap<>();
+
+ map.put("0-15,21.10,16-31.0-15,32-63.-95,128-", "1");
+
+ assertNotNull(map.match("7.23.39.46"));
+ assertNotNull(map.match("10.20.10.150"));
+ assertNotNull(map.match("21.10.32.255"));
+ assertNotNull(map.match("21.10.15.0"));
+
+ assertNull(map.match("16.15.20.100"));
+ assertNull(map.match("15.10.63.100"));
+ assertNull(map.match("15.10.64.128"));
+ assertNull(map.match("15.11.32.95"));
+ assertNull(map.match("16.31.63.128"));
+ }
+
+ @Test
+ public void testManyMixed()
+ {
+ IPAddressMap<String> map = new IPAddressMap<>();
+
+ map.put("10.5.2.1", "1");
+ map.put("1-15.16-31.32-63.64-127", "2");
+ map.put("1-15,21.10,16-31.0-15,32-63.-55,195-", "3");
+ map.put("44.99.99.", "4");
+ map.put("55.99.", "5");
+ map.put("66.", "6");
+
+ assertEquals("1", map.match("10.5.2.1"));
+
+ assertEquals("2", map.match("7.23.39.71"));
+ assertEquals("2", map.match("1.16.32.64"));
+ assertEquals("2", map.match("15.31.63.127"));
+
+ assertEquals("3", map.match("7.23.39.46"));
+ assertEquals("3", map.match("10.20.10.200"));
+ assertEquals("3", map.match("21.10.32.255"));
+ assertEquals("3", map.match("21.10.15.0"));
+
+ assertEquals("4", map.match("44.99.99.0"));
+ assertEquals("5", map.match("55.99.128.1"));
+ assertEquals("6", map.match("66.255.1.1"));
+
+ assertNull(map.match("101.5.2.1"));
+ assertNull(map.match("10.15.2.1"));
+ assertNull(map.match("10.5.22.1"));
+ assertNull(map.match("10.5.2.0"));
+
+ assertNull(map.match("16.32.64.96"));
+ assertNull(map.match("1.16.32.194"));
+ assertNull(map.match("1.16.31.64"));
+ assertNull(map.match("1.15.32.64"));
+ assertNull(map.match("0.16.32.64"));
+
+ assertNull(map.match("16.15.20.100"));
+ assertNull(map.match("15.10.63.100"));
+ assertNull(map.match("15.10.64.128"));
+ assertNull(map.match("15.11.32.95"));
+ assertNull(map.match("16.31.63.128"));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IncludeExcludeSetTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IncludeExcludeSetTest.java
new file mode 100644
index 0000000..e40f0ff
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IncludeExcludeSetTest.java
@@ -0,0 +1,57 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.net.InetAddress;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class IncludeExcludeSetTest
+{
+ @Test
+ public void testWithInetAddressSet() throws Exception
+ {
+ IncludeExcludeSet<String, InetAddress> set = new IncludeExcludeSet<>(InetAddressSet.class);
+ assertTrue(set.test(InetAddress.getByName("192.168.0.1")));
+
+ set.include("10.10.0.0/16");
+ assertFalse(set.test(InetAddress.getByName("192.168.0.1")));
+ assertTrue(set.test(InetAddress.getByName("10.10.128.1")));
+
+ set.exclude("[::ffff:10.10.128.1]");
+ assertFalse(set.test(InetAddress.getByName("10.10.128.1")));
+
+ set.include("[ffff:ff00::]/24");
+ assertTrue(set.test(InetAddress.getByName("ffff:ff00::1")));
+ assertTrue(set.test(InetAddress.getByName("ffff:ff00::42")));
+
+ set.exclude("[ffff:ff00::42]");
+ assertTrue(set.test(InetAddress.getByName("ffff:ff00::41")));
+ assertFalse(set.test(InetAddress.getByName("ffff:ff00::42")));
+ assertTrue(set.test(InetAddress.getByName("ffff:ff00::43")));
+
+ set.include("192.168.0.0-192.168.255.128");
+ assertTrue(set.test(InetAddress.getByName("192.168.0.1")));
+ assertTrue(set.test(InetAddress.getByName("192.168.254.255")));
+ assertFalse(set.test(InetAddress.getByName("192.168.255.255")));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IncludeExcludeTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IncludeExcludeTest.java
new file mode 100644
index 0000000..1a48afe
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IncludeExcludeTest.java
@@ -0,0 +1,150 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class IncludeExcludeTest
+{
+ @Test
+ public void testEmpty()
+ {
+ IncludeExclude<String> ie = new IncludeExclude<>();
+
+ assertThat("Empty IncludeExclude", ie.size(), is(0));
+ assertThat("Matches 'foo'", ie.test("foo"), is(true));
+ }
+
+ @Test
+ public void testIncludeOnly()
+ {
+ IncludeExclude<String> ie = new IncludeExclude<>();
+ ie.include("foo");
+ ie.include("bar");
+
+ assertThat("IncludeExclude.size", ie.size(), is(2));
+ assertEquals(false, ie.test(""));
+ assertEquals(true, ie.test("foo"));
+ assertEquals(true, ie.test("bar"));
+ assertEquals(false, ie.test("foobar"));
+ }
+
+ @Test
+ public void testExcludeOnly()
+ {
+ IncludeExclude<String> ie = new IncludeExclude<>();
+ ie.exclude("foo");
+ ie.exclude("bar");
+
+ assertEquals(2, ie.size());
+
+ assertEquals(false, ie.test("foo"));
+ assertEquals(false, ie.test("bar"));
+ assertEquals(true, ie.test(""));
+ assertEquals(true, ie.test("foobar"));
+ assertEquals(true, ie.test("wibble"));
+ }
+
+ @Test
+ public void testIncludeExclude()
+ {
+ IncludeExclude<String> ie = new IncludeExclude<>();
+ ie.include("foo");
+ ie.include("bar");
+ ie.exclude("bar");
+ ie.exclude("xxx");
+
+ assertEquals(4, ie.size());
+
+ assertEquals(true, ie.test("foo"));
+ assertEquals(false, ie.test("bar"));
+ assertEquals(false, ie.test(""));
+ assertEquals(false, ie.test("foobar"));
+ assertEquals(false, ie.test("xxx"));
+ }
+
+ @Test
+ public void testEmptyRegex()
+ {
+ IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class);
+
+ assertEquals(0, ie.size());
+ assertEquals(true, ie.test("foo"));
+ }
+
+ @Test
+ public void testIncludeRegex()
+ {
+ IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class);
+ ie.include("f..");
+ ie.include("b((ar)|(oo))");
+
+ assertEquals(2, ie.size());
+ assertEquals(false, ie.test(""));
+ assertEquals(true, ie.test("foo"));
+ assertEquals(true, ie.test("far"));
+ assertEquals(true, ie.test("bar"));
+ assertEquals(true, ie.test("boo"));
+ assertEquals(false, ie.test("foobar"));
+ assertEquals(false, ie.test("xxx"));
+ }
+
+ @Test
+ public void testExcludeRegex()
+ {
+ IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class);
+ ie.exclude("f..");
+ ie.exclude("b((ar)|(oo))");
+
+ assertEquals(2, ie.size());
+
+ assertEquals(false, ie.test("foo"));
+ assertEquals(false, ie.test("far"));
+ assertEquals(false, ie.test("bar"));
+ assertEquals(false, ie.test("boo"));
+ assertEquals(true, ie.test(""));
+ assertEquals(true, ie.test("foobar"));
+ assertEquals(true, ie.test("xxx"));
+ }
+
+ @Test
+ public void testIncludeExcludeRegex()
+ {
+ IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class);
+ ie.include(".*[aeiou].*");
+ ie.include("[AEIOU].*");
+ ie.exclude("f..");
+ ie.exclude("b((ar)|(oo))");
+
+ assertEquals(4, ie.size());
+ assertEquals(false, ie.test("foo"));
+ assertEquals(false, ie.test("far"));
+ assertEquals(false, ie.test("bar"));
+ assertEquals(false, ie.test("boo"));
+ assertEquals(false, ie.test(""));
+ assertEquals(false, ie.test("xxx"));
+
+ assertEquals(true, ie.test("foobar"));
+ assertEquals(true, ie.test("Ant"));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/InetAddressSetTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/InetAddressSetTest.java
new file mode 100644
index 0000000..4ffd604
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/InetAddressSetTest.java
@@ -0,0 +1,362 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.toolchain.test.Net;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class InetAddressSetTest
+{
+ public static Stream<String> loopbacks()
+ {
+ List<String> loopbacks = new ArrayList<>();
+
+ loopbacks.add("127.0.0.1");
+ loopbacks.add("127.0.0.2");
+
+ if (Net.isIpv6InterfaceAvailable())
+ {
+ loopbacks.add("::1");
+ loopbacks.add("::0.0.0.1");
+ loopbacks.add("[::1]");
+ loopbacks.add("[::0.0.0.1]");
+ loopbacks.add("[::ffff:127.0.0.1]");
+ }
+
+ return loopbacks.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("loopbacks")
+ public void testInetAddressLoopback(String addr) throws Exception
+ {
+ InetAddress inetAddress = InetAddress.getByName(addr);
+ assertNotNull(inetAddress);
+ assertTrue(inetAddress.isLoopbackAddress());
+ }
+
+ @Test
+ public void testSingleton() throws Exception
+ {
+ InetAddressSet set = new InetAddressSet();
+
+ set.add("webtide.com");
+ set.add("1.2.3.4");
+ set.add("::abcd");
+
+ assertTrue(set.test(InetAddress.getByName("webtide.com")));
+ assertTrue(set.test(InetAddress.getByName(InetAddress.getByName("webtide.com").getHostAddress())));
+ assertTrue(set.test(InetAddress.getByName("1.2.3.4")));
+ assertTrue(set.test(InetAddress.getByAddress(new byte[]{(byte)1, (byte)2, (byte)3, (byte)4})));
+ assertTrue(set.test(InetAddress.getByAddress("hostname", new byte[]{(byte)1, (byte)2, (byte)3, (byte)4})));
+ assertTrue(set.test(InetAddress.getByName("::0:0:abcd")));
+ assertTrue(set.test(InetAddress.getByName("::abcd")));
+ assertTrue(set.test(InetAddress.getByName("[::abcd]")));
+ assertTrue(set.test(InetAddress.getByName("::ffff:1.2.3.4")));
+
+ assertFalse(set.test(InetAddress.getByName("www.google.com")));
+ assertFalse(set.test(InetAddress.getByName("1.2.3.5")));
+ assertFalse(set.test(InetAddress.getByAddress(new byte[]{(byte)1, (byte)2, (byte)3, (byte)5})));
+ assertFalse(set.test(InetAddress.getByAddress("webtide.com", new byte[]{(byte)1, (byte)2, (byte)3, (byte)5})));
+ assertFalse(set.test(InetAddress.getByName("::1.2.3.4")));
+ assertFalse(set.test(InetAddress.getByName("::1234")));
+ assertFalse(set.test(InetAddress.getByName("::abce")));
+ assertFalse(set.test(InetAddress.getByName("1::abcd")));
+ }
+
+ public static Stream<String> badsingletons()
+ {
+ List<String> bad = new ArrayList<>();
+
+ bad.add("intentionally invalid hostname");
+ bad.add("nonexistentdomain.tld");
+ bad.add("1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16");
+ bad.add("a.b.c.d");
+
+ bad.add("[::1"); // incomplete
+ bad.add("[xxx]"); // not valid octets
+ bad.add("[:::1]"); // too many colons
+
+ return bad.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("badsingletons")
+ public void testBadSingleton(final String badAddr)
+ {
+ try
+ {
+ InetAddress inetAddress = InetAddress.getByName(badAddr);
+ Assumptions.assumeTrue(inetAddress == null);
+ }
+ catch (UnknownHostException expected)
+ {
+ //noinspection MismatchedQueryAndUpdateOfCollection
+ InetAddressSet inetAddressSet = new InetAddressSet();
+ IllegalArgumentException cause = assertThrows(IllegalArgumentException.class, () -> inetAddressSet.add(badAddr));
+ assertThat(cause.getMessage(), containsString(badAddr));
+ }
+ }
+
+ @Test
+ public void testCIDR() throws Exception
+ {
+ InetAddressSet set = new InetAddressSet();
+
+ set.add("10.10.0.0/16");
+ set.add("192.0.80.0/22");
+ set.add("168.0.0.80/30");
+ set.add("abcd:ef00::/24");
+
+ assertTrue(set.test(InetAddress.getByName("10.10.0.0")));
+ assertTrue(set.test(InetAddress.getByName("10.10.0.1")));
+ assertTrue(set.test(InetAddress.getByName("10.10.255.255")));
+ assertTrue(set.test(InetAddress.getByName("::ffff:10.10.0.1")));
+ assertTrue(set.test(InetAddress.getByName("192.0.80.0")));
+ assertTrue(set.test(InetAddress.getByName("192.0.83.1")));
+ assertTrue(set.test(InetAddress.getByName("168.0.0.80")));
+ assertTrue(set.test(InetAddress.getByName("168.0.0.83")));
+ assertTrue(set.test(InetAddress.getByName("abcd:ef00::1")));
+ assertTrue(set.test(InetAddress.getByName("abcd:efff::ffff")));
+
+ assertFalse(set.test(InetAddress.getByName("10.11.0.0")));
+ assertFalse(set.test(InetAddress.getByName("1.2.3.5")));
+ assertFalse(set.test(InetAddress.getByName("192.0.84.1")));
+ assertFalse(set.test(InetAddress.getByName("168.0.0.84")));
+ assertFalse(set.test(InetAddress.getByName("::10.10.0.1")));
+ assertFalse(set.test(InetAddress.getByName("abcd:eeff::1")));
+ assertFalse(set.test(InetAddress.getByName("abcd:f000::")));
+
+ set.add("255.255.255.255/32");
+ assertTrue(set.test(InetAddress.getByName("255.255.255.255")));
+ assertFalse(set.test(InetAddress.getByName("10.11.0.0")));
+
+ set.add("0.0.0.0/0");
+ assertTrue(set.test(InetAddress.getByName("10.11.0.0")));
+
+ // test #1664
+ set.add("2.144.0.0/14");
+ set.add("2.176.0.0/12");
+ set.add("5.22.0.0/17");
+ set.add("5.22.192.0/19");
+ assertTrue(set.test(InetAddress.getByName("2.144.0.1")));
+ assertTrue(set.test(InetAddress.getByName("2.176.0.1")));
+ assertTrue(set.test(InetAddress.getByName("5.22.0.1")));
+ assertTrue(set.test(InetAddress.getByName("5.22.192.1")));
+ }
+
+ public static Stream<String> badCidrs()
+ {
+ List<String> bad = new ArrayList<>();
+ bad.add("intentionally invalid hostname/8");
+ bad.add("nonexistentdomain.tld/8");
+ bad.add("1.2.3.4/-1");
+ bad.add("1.2.3.4/xxx");
+ bad.add("1.2.3.4/33");
+ bad.add("255.255.8.0/16");
+ bad.add("255.255.8.1/17");
+
+ if (Net.isIpv6InterfaceAvailable())
+ {
+ bad.add("[::1]/129");
+ }
+
+ return bad.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("badCidrs")
+ public void testBadCIDR(String cidr)
+ {
+ //noinspection MismatchedQueryAndUpdateOfCollection
+ InetAddressSet inetAddressSet = new InetAddressSet();
+
+ IllegalArgumentException cause = assertThrows(IllegalArgumentException.class, () -> inetAddressSet.add(cidr));
+ assertThat(cause.getMessage(), containsString(cidr));
+ }
+
+ @Test
+ public void testMinMax() throws Exception
+ {
+ InetAddressSet set = new InetAddressSet();
+
+ set.add("10.0.0.4-10.0.0.6");
+ set.add("10.1.0.254-10.1.1.1");
+
+ if (Net.isIpv6InterfaceAvailable())
+ {
+ set.add("[abcd:ef::fffe]-[abcd:ef::1:1]");
+ }
+
+ assertFalse(set.test(InetAddress.getByName("10.0.0.3")));
+ assertTrue(set.test(InetAddress.getByName("10.0.0.4")));
+ assertTrue(set.test(InetAddress.getByName("10.0.0.5")));
+ assertTrue(set.test(InetAddress.getByName("10.0.0.6")));
+ assertFalse(set.test(InetAddress.getByName("10.0.0.7")));
+
+ assertFalse(set.test(InetAddress.getByName("10.1.0.253")));
+ assertTrue(set.test(InetAddress.getByName("10.1.0.254")));
+ assertTrue(set.test(InetAddress.getByName("10.1.0.255")));
+ assertTrue(set.test(InetAddress.getByName("10.1.1.0")));
+ assertTrue(set.test(InetAddress.getByName("10.1.1.1")));
+ assertFalse(set.test(InetAddress.getByName("10.1.1.2")));
+
+ if (Net.isIpv6InterfaceAvailable())
+ {
+ assertFalse(set.test(InetAddress.getByName("ABCD:EF::FFFD")));
+ assertTrue(set.test(InetAddress.getByName("ABCD:EF::FFFE")));
+ assertTrue(set.test(InetAddress.getByName("ABCD:EF::FFFF")));
+ assertTrue(set.test(InetAddress.getByName("ABCD:EF::1:0")));
+ assertTrue(set.test(InetAddress.getByName("ABCD:EF::1:1")));
+ assertFalse(set.test(InetAddress.getByName("ABCD:EF::1:2")));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "10.0.0.0-9.0.0.0",
+ "9.0.0.0-[::10.0.0.0]"
+ })
+ public void testBadMinMax(String bad)
+ {
+ //noinspection MismatchedQueryAndUpdateOfCollection
+ InetAddressSet inetAddressSet = new InetAddressSet();
+ IllegalArgumentException cause = assertThrows(IllegalArgumentException.class, () -> inetAddressSet.add(bad));
+ assertThat(cause.getMessage(), containsString(bad));
+ }
+
+ @Test
+ public void testLegacy() throws Exception
+ {
+ InetAddressSet set = new InetAddressSet();
+
+ set.add("10.-.245-.-2");
+ set.add("11.11.11.127-129");
+
+ assertFalse(set.test(InetAddress.getByName("9.0.245.0")));
+
+ assertTrue(set.test(InetAddress.getByName("10.0.245.0")));
+ assertTrue(set.test(InetAddress.getByName("10.0.245.1")));
+ assertTrue(set.test(InetAddress.getByName("10.0.245.2")));
+ assertFalse(set.test(InetAddress.getByName("10.0.245.3")));
+
+ assertTrue(set.test(InetAddress.getByName("10.255.255.0")));
+ assertTrue(set.test(InetAddress.getByName("10.255.255.1")));
+ assertTrue(set.test(InetAddress.getByName("10.255.255.2")));
+ assertFalse(set.test(InetAddress.getByName("10.255.255.3")));
+
+ assertFalse(set.test(InetAddress.getByName("10.0.244.0")));
+ assertFalse(set.test(InetAddress.getByName("10.0.244.1")));
+ assertFalse(set.test(InetAddress.getByName("10.0.244.2")));
+ assertFalse(set.test(InetAddress.getByName("10.0.244.3")));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "9.0-10.0",
+ "10.0.0--1.1",
+ "10.0.0-256.1"
+ })
+ public void testBadLegacy(String bad)
+ {
+ //noinspection MismatchedQueryAndUpdateOfCollection
+ InetAddressSet inetAddressSet = new InetAddressSet();
+ IllegalArgumentException cause = assertThrows(IllegalArgumentException.class, () -> inetAddressSet.add(bad));
+ assertThat(cause.getMessage(), containsString(bad));
+ }
+
+ @Test
+ public void testRemove() throws Exception
+ {
+ InetAddressSet set = new InetAddressSet();
+
+ set.add("webtide.com");
+ set.add("1.2.3.4");
+ set.add("::abcd");
+ set.add("10.0.0.4-10.0.0.6");
+
+ assertTrue(set.test(InetAddress.getByName("webtide.com")));
+ assertTrue(set.test(InetAddress.getByName(InetAddress.getByName("webtide.com").getHostAddress())));
+ assertTrue(set.test(InetAddress.getByName("1.2.3.4")));
+ assertTrue(set.test(InetAddress.getByAddress(new byte[]{(byte)1, (byte)2, (byte)3, (byte)4})));
+ assertTrue(set.test(InetAddress.getByAddress("hostname", new byte[]{(byte)1, (byte)2, (byte)3, (byte)4})));
+ if (Net.isIpv6InterfaceAvailable())
+ {
+ assertTrue(set.test(InetAddress.getByName("::0:0:abcd")));
+ assertTrue(set.test(InetAddress.getByName("::abcd")));
+ assertTrue(set.test(InetAddress.getByName("[::abcd]")));
+ assertTrue(set.test(InetAddress.getByName("::ffff:1.2.3.4")));
+ }
+ assertTrue(set.test(InetAddress.getByName("10.0.0.4")));
+ assertTrue(set.test(InetAddress.getByName("10.0.0.5")));
+ assertTrue(set.test(InetAddress.getByName("10.0.0.6")));
+
+ set.remove("1.2.3.4");
+ assertTrue(set.test(InetAddress.getByName("webtide.com")));
+ assertTrue(set.test(InetAddress.getByName(InetAddress.getByName("webtide.com").getHostAddress())));
+ assertFalse(set.test(InetAddress.getByName("1.2.3.4")));
+ assertFalse(set.test(InetAddress.getByAddress(new byte[]{(byte)1, (byte)2, (byte)3, (byte)4})));
+ assertFalse(set.test(InetAddress.getByAddress("hostname", new byte[]{(byte)1, (byte)2, (byte)3, (byte)4})));
+ if (Net.isIpv6InterfaceAvailable())
+ {
+ assertTrue(set.test(InetAddress.getByName("::0:0:abcd")));
+ assertTrue(set.test(InetAddress.getByName("::abcd")));
+ assertTrue(set.test(InetAddress.getByName("[::abcd]")));
+ assertFalse(set.test(InetAddress.getByName("::ffff:1.2.3.4")));
+ }
+ assertTrue(set.test(InetAddress.getByName("10.0.0.4")));
+ assertTrue(set.test(InetAddress.getByName("10.0.0.5")));
+ assertTrue(set.test(InetAddress.getByName("10.0.0.6")));
+
+ set.removeIf("::abcd"::equals);
+
+ assertTrue(set.test(InetAddress.getByName("webtide.com")));
+ assertTrue(set.test(InetAddress.getByName(InetAddress.getByName("webtide.com").getHostAddress())));
+ assertFalse(set.test(InetAddress.getByName("1.2.3.4")));
+ assertFalse(set.test(InetAddress.getByAddress(new byte[]{(byte)1, (byte)2, (byte)3, (byte)4})));
+ assertFalse(set.test(InetAddress.getByAddress("hostname", new byte[]{(byte)1, (byte)2, (byte)3, (byte)4})));
+ if (Net.isIpv6InterfaceAvailable())
+ {
+ assertFalse(set.test(InetAddress.getByName("::0:0:abcd")));
+ assertFalse(set.test(InetAddress.getByName("::abcd")));
+ assertFalse(set.test(InetAddress.getByName("[::abcd]")));
+ assertFalse(set.test(InetAddress.getByName("::ffff:1.2.3.4")));
+ }
+ assertTrue(set.test(InetAddress.getByName("10.0.0.4")));
+ assertTrue(set.test(InetAddress.getByName("10.0.0.5")));
+ assertTrue(set.test(InetAddress.getByName("10.0.0.6")));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IntrospectionUtilTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IntrospectionUtilTest.java
new file mode 100644
index 0000000..927514b
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IntrospectionUtilTest.java
@@ -0,0 +1,75 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.lang.reflect.Array;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for class {@link IntrospectionUtil}.
+ *
+ * @see IntrospectionUtil
+ */
+public class IntrospectionUtilTest
+{
+
+ @Test
+ public void testIsTypeCompatibleWithTwoTimesString()
+ {
+ assertTrue(IntrospectionUtil.isTypeCompatible(String.class, String.class, true));
+ }
+
+ @Test
+ public void testIsSameSignatureWithNull()
+ {
+ assertFalse(IntrospectionUtil.isSameSignature(null, null));
+ }
+
+ @Test
+ public void testFindMethodWithEmptyString()
+ {
+ assertThrows(NoSuchMethodException.class,
+ () -> IntrospectionUtil.findMethod(Integer.class, "", null, false, false));
+ }
+
+ @Test
+ public void testFindMethodWithNullMethodParameter()
+ {
+ assertThrows(NoSuchMethodException.class,
+ () -> IntrospectionUtil.findMethod(String.class, null, (Class<Integer>[])Array.newInstance(Class.class, 3), true, true));
+ }
+
+ @Test
+ public void testFindMethodWithNullClassParameter() throws NoSuchMethodException
+ {
+ assertThrows(NoSuchMethodException.class,
+ () -> IntrospectionUtil.findMethod(null, "subSequence", (Class<Object>[])Array.newInstance(Class.class, 9), false, false));
+ }
+
+ @Test
+ public void testIsJavaBeanCompliantSetterWithNull()
+ {
+ assertFalse(IntrospectionUtil.isJavaBeanCompliantSetter(null));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IteratingCallbackTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IteratingCallbackTest.java
new file mode 100644
index 0000000..906983f
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/IteratingCallbackTest.java
@@ -0,0 +1,330 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class IteratingCallbackTest
+{
+ private Scheduler scheduler;
+
+ @BeforeEach
+ public void prepare() throws Exception
+ {
+ scheduler = new ScheduledExecutorScheduler();
+ scheduler.start();
+ }
+
+ @AfterEach
+ public void dispose() throws Exception
+ {
+ scheduler.stop();
+ }
+
+ @Test
+ public void testNonWaitingProcess() throws Exception
+ {
+ TestCB cb = new TestCB()
+ {
+ int i = 10;
+
+ @Override
+ protected Action process() throws Exception
+ {
+ processed++;
+ if (i-- > 1)
+ {
+ succeeded(); // fake a completed IO operation
+ return Action.SCHEDULED;
+ }
+ return Action.SUCCEEDED;
+ }
+ };
+
+ cb.iterate();
+ assertTrue(cb.waitForComplete());
+ assertEquals(10, cb.processed);
+ }
+
+ @Test
+ public void testWaitingProcess() throws Exception
+ {
+ TestCB cb = new TestCB()
+ {
+ int i = 4;
+
+ @Override
+ protected Action process() throws Exception
+ {
+ processed++;
+ if (i-- > 1)
+ {
+ scheduler.schedule(successTask, 50, TimeUnit.MILLISECONDS);
+ return Action.SCHEDULED;
+ }
+ return Action.SUCCEEDED;
+ }
+ };
+
+ cb.iterate();
+
+ assertTrue(cb.waitForComplete());
+
+ assertEquals(4, cb.processed);
+ }
+
+ @Test
+ public void testWaitingProcessSpuriousIterate() throws Exception
+ {
+ final TestCB cb = new TestCB()
+ {
+ int i = 4;
+
+ @Override
+ protected Action process() throws Exception
+ {
+ processed++;
+ if (i-- > 1)
+ {
+ scheduler.schedule(successTask, 50, TimeUnit.MILLISECONDS);
+ return Action.SCHEDULED;
+ }
+ return Action.SUCCEEDED;
+ }
+ };
+
+ cb.iterate();
+ scheduler.schedule(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ cb.iterate();
+ if (!cb.isSucceeded())
+ scheduler.schedule(this, 50, TimeUnit.MILLISECONDS);
+ }
+ }, 49, TimeUnit.MILLISECONDS);
+
+ assertTrue(cb.waitForComplete());
+
+ assertEquals(4, cb.processed);
+ }
+
+ @Test
+ public void testNonWaitingProcessFailure() throws Exception
+ {
+ TestCB cb = new TestCB()
+ {
+ int i = 10;
+
+ @Override
+ protected Action process() throws Exception
+ {
+ processed++;
+ if (i-- > 1)
+ {
+ if (i > 5)
+ succeeded(); // fake a completed IO operation
+ else
+ failed(new Exception("testing"));
+ return Action.SCHEDULED;
+ }
+ return Action.SUCCEEDED;
+ }
+ };
+
+ cb.iterate();
+ assertFalse(cb.waitForComplete());
+ assertEquals(5, cb.processed);
+ }
+
+ @Test
+ public void testWaitingProcessFailure() throws Exception
+ {
+ TestCB cb = new TestCB()
+ {
+ int i = 4;
+
+ @Override
+ protected Action process() throws Exception
+ {
+ processed++;
+ if (i-- > 1)
+ {
+ scheduler.schedule(i > 2 ? successTask : failTask, 50, TimeUnit.MILLISECONDS);
+ return Action.SCHEDULED;
+ }
+ return Action.SUCCEEDED;
+ }
+ };
+
+ cb.iterate();
+
+ assertFalse(cb.waitForComplete());
+ assertEquals(2, cb.processed);
+ }
+
+ @Test
+ public void testIdleWaiting() throws Exception
+ {
+ final CountDownLatch idle = new CountDownLatch(1);
+
+ TestCB cb = new TestCB()
+ {
+ int i = 5;
+
+ @Override
+ protected Action process()
+ {
+ processed++;
+
+ switch (i--)
+ {
+ case 5:
+ succeeded();
+ return Action.SCHEDULED;
+
+ case 4:
+ scheduler.schedule(successTask, 5, TimeUnit.MILLISECONDS);
+ return Action.SCHEDULED;
+
+ case 3:
+ scheduler.schedule(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ idle.countDown();
+ }
+ }, 5, TimeUnit.MILLISECONDS);
+ return Action.IDLE;
+
+ case 2:
+ succeeded();
+ return Action.SCHEDULED;
+
+ case 1:
+ scheduler.schedule(successTask, 5, TimeUnit.MILLISECONDS);
+ return Action.SCHEDULED;
+
+ case 0:
+ return Action.SUCCEEDED;
+
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ };
+
+ cb.iterate();
+ idle.await(10, TimeUnit.SECONDS);
+ assertTrue(cb.isIdle());
+
+ cb.iterate();
+ assertTrue(cb.waitForComplete());
+ assertEquals(6, cb.processed);
+ }
+
+ @Test
+ public void testCloseDuringProcessingReturningScheduled() throws Exception
+ {
+ testCloseDuringProcessing(IteratingCallback.Action.SCHEDULED);
+ }
+
+ @Test
+ public void testCloseDuringProcessingReturningSucceeded() throws Exception
+ {
+ testCloseDuringProcessing(IteratingCallback.Action.SUCCEEDED);
+ }
+
+ private void testCloseDuringProcessing(final IteratingCallback.Action action) throws Exception
+ {
+ final CountDownLatch failureLatch = new CountDownLatch(1);
+ IteratingCallback callback = new IteratingCallback()
+ {
+ @Override
+ protected Action process() throws Exception
+ {
+ close();
+ return action;
+ }
+
+ @Override
+ protected void onCompleteFailure(Throwable cause)
+ {
+ failureLatch.countDown();
+ }
+ };
+
+ callback.iterate();
+
+ assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
+ }
+
+ private abstract static class TestCB extends IteratingCallback
+ {
+ protected Runnable successTask = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ succeeded();
+ }
+ };
+ protected Runnable failTask = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ failed(new Exception("testing failure"));
+ }
+ };
+ protected CountDownLatch completed = new CountDownLatch(1);
+ protected int processed = 0;
+
+ @Override
+ protected void onCompleteSuccess()
+ {
+ completed.countDown();
+ }
+
+ @Override
+ public void onCompleteFailure(Throwable x)
+ {
+ completed.countDown();
+ }
+
+ boolean waitForComplete() throws InterruptedException
+ {
+ completed.await(10, TimeUnit.SECONDS);
+ return isSucceeded();
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/JavaVersionTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/JavaVersionTest.java
new file mode 100644
index 0000000..8e8eeea
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/JavaVersionTest.java
@@ -0,0 +1,162 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Tests for LazyList utility class.
+ */
+public class JavaVersionTest
+{
+ @Test
+ public void testAndroid()
+ {
+ JavaVersion version = JavaVersion.parse("0.9");
+ assertThat(version.toString(), is("0.9"));
+ assertThat(version.getPlatform(), is(9));
+ assertThat(version.getMajor(), is(0));
+ assertThat(version.getMinor(), is(9));
+ assertThat(version.getMicro(), is(0));
+ }
+
+ @Test
+ public void test9()
+ {
+ JavaVersion version = JavaVersion.parse("9.0.1");
+ assertThat(version.toString(), is("9.0.1"));
+ assertThat(version.getPlatform(), is(9));
+ assertThat(version.getMajor(), is(9));
+ assertThat(version.getMinor(), is(0));
+ assertThat(version.getMicro(), is(1));
+ }
+
+ @Test
+ public void test9nano()
+ {
+ JavaVersion version = JavaVersion.parse("9.0.1.3");
+ assertThat(version.toString(), is("9.0.1.3"));
+ assertThat(version.getPlatform(), is(9));
+ assertThat(version.getMajor(), is(9));
+ assertThat(version.getMinor(), is(0));
+ assertThat(version.getMicro(), is(1));
+ }
+
+ @Test
+ public void test9build()
+ {
+ JavaVersion version = JavaVersion.parse("9.0.1+11");
+ assertThat(version.toString(), is("9.0.1+11"));
+ assertThat(version.getPlatform(), is(9));
+ assertThat(version.getMajor(), is(9));
+ assertThat(version.getMinor(), is(0));
+ assertThat(version.getMicro(), is(1));
+ }
+
+ @Test
+ public void test9all()
+ {
+ JavaVersion version = JavaVersion.parse("9.0.1-ea+11-b01");
+ assertThat(version.toString(), is("9.0.1-ea+11-b01"));
+ assertThat(version.getPlatform(), is(9));
+ assertThat(version.getMajor(), is(9));
+ assertThat(version.getMinor(), is(0));
+ assertThat(version.getMicro(), is(1));
+ }
+
+ @Test
+ public void test9yuck()
+ {
+ JavaVersion version = JavaVersion.parse("9.0.1.2.3-ea+11-b01");
+ assertThat(version.toString(), is("9.0.1.2.3-ea+11-b01"));
+ assertThat(version.getPlatform(), is(9));
+ assertThat(version.getMajor(), is(9));
+ assertThat(version.getMinor(), is(0));
+ assertThat(version.getMicro(), is(1));
+ }
+
+ @Test
+ public void test10ea()
+ {
+ JavaVersion version = JavaVersion.parse("10-ea");
+ assertThat(version.toString(), is("10-ea"));
+ assertThat(version.getPlatform(), is(10));
+ assertThat(version.getMajor(), is(10));
+ assertThat(version.getMinor(), is(0));
+ assertThat(version.getMicro(), is(0));
+ }
+
+ @Test
+ public void test8()
+ {
+ JavaVersion version = JavaVersion.parse("1.8.0_152");
+ assertThat(version.toString(), is("1.8.0_152"));
+ assertThat(version.getPlatform(), is(8));
+ assertThat(version.getMajor(), is(1));
+ assertThat(version.getMinor(), is(8));
+ assertThat(version.getMicro(), is(0));
+ }
+
+ @Test
+ public void test8ea()
+ {
+ JavaVersion version = JavaVersion.parse("1.8.1_03-ea");
+ assertThat(version.toString(), is("1.8.1_03-ea"));
+ assertThat(version.getPlatform(), is(8));
+ assertThat(version.getMajor(), is(1));
+ assertThat(version.getMinor(), is(8));
+ assertThat(version.getMicro(), is(1));
+ }
+
+ @Test
+ public void test3eaBuild()
+ {
+ JavaVersion version = JavaVersion.parse("1.3.1_05-ea-b01");
+ assertThat(version.toString(), is("1.3.1_05-ea-b01"));
+ assertThat(version.getPlatform(), is(3));
+ assertThat(version.getMajor(), is(1));
+ assertThat(version.getMinor(), is(3));
+ assertThat(version.getMicro(), is(1));
+ }
+
+ @Test
+ public void testUbuntu()
+ {
+ JavaVersion version = JavaVersion.parse("9-Ubuntu+0-9b181-4");
+ assertThat(version.toString(), is("9-Ubuntu+0-9b181-4"));
+ assertThat(version.getPlatform(), is(9));
+ assertThat(version.getMajor(), is(9));
+ assertThat(version.getMinor(), is(0));
+ assertThat(version.getMicro(), is(0));
+ }
+
+ @Test
+ public void testUbuntu8()
+ {
+ JavaVersion version = JavaVersion.parse("1.8.0_151-8u151-b12-1~deb9u1-b12");
+ assertThat(version.toString(), is("1.8.0_151-8u151-b12-1~deb9u1-b12"));
+ assertThat(version.getPlatform(), is(8));
+ assertThat(version.getMajor(), is(1));
+ assertThat(version.getMinor(), is(8));
+ assertThat(version.getMicro(), is(0));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/LazyListTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/LazyListTest.java
new file mode 100644
index 0000000..435e40b
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/LazyListTest.java
@@ -0,0 +1,2137 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+/**
+ * Tests for LazyList utility class.
+ */
+public class LazyListTest
+{
+ public static final boolean STRICT = false;
+
+ /**
+ * Tests for {@link LazyList#add(Object, Object)}
+ */
+ @Test
+ public void testAddObjectObjectNullInputNullItem()
+ {
+ Object list = LazyList.add(null, null);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(1, LazyList.size(list));
+ }
+
+ /**
+ * Tests for {@link LazyList#add(Object, Object)}
+ */
+ @Test
+ public void testAddObjectObjectNullInputNonListItem()
+ {
+ String item = "a";
+ Object list = LazyList.add(null, item);
+ assertNotNull(list);
+ if (STRICT)
+ {
+ assertTrue(list instanceof List);
+ }
+ assertEquals(1, LazyList.size(list));
+ }
+
+ /**
+ * Tests for {@link LazyList#add(Object, Object)}
+ */
+ @Test
+ public void testAddObjectObjectNullInputLazyListItem()
+ {
+ Object item = LazyList.add(null, "x");
+ item = LazyList.add(item, "y");
+ item = LazyList.add(item, "z");
+
+ Object list = LazyList.add(null, item);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(1, LazyList.size(list));
+
+ Object val = LazyList.get(list, 0);
+ assertTrue(val instanceof List);
+ }
+
+ /**
+ * Tests for {@link LazyList#add(Object, Object)}
+ */
+ @Test
+ public void testAddObjectObjectNonListInput()
+ {
+ String input = "a";
+
+ Object list = LazyList.add(input, "b");
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(2, LazyList.size(list));
+ }
+
+ /**
+ * Tests for {@link LazyList#add(Object, Object)}
+ */
+ @Test
+ public void testAddObjectObjectLazyListInput()
+ {
+ Object input = LazyList.add(null, "a");
+
+ Object list = LazyList.add(input, "b");
+ assertEquals(2, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+
+ list = LazyList.add(list, "c");
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "c");
+ }
+
+ /**
+ * Tests for {@link LazyList#add(Object, Object)}
+ */
+ @Test
+ public void testAddObjectObjectGenericListInput()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("a");
+
+ Object list = LazyList.add(input, "b");
+ assertEquals(2, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+
+ list = LazyList.add(list, "c");
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "c");
+ }
+
+ /**
+ * Tests for {@link LazyList#add(Object, Object)}
+ */
+ @Test
+ public void testAddObjectObjectAddNull()
+ {
+ Object list = null;
+ list = LazyList.add(list, null);
+ assertEquals(1, LazyList.size(list));
+ assertNull(LazyList.get(list, 0));
+
+ list = "a";
+ list = LazyList.add(list, null);
+ assertEquals(2, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertNull(LazyList.get(list, 1));
+
+ list = LazyList.add(list, null);
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertNull(LazyList.get(list, 1));
+ assertNull(LazyList.get(list, 2));
+ }
+
+ /**
+ * Test for {@link LazyList#add(Object, int, Object)}
+ */
+ @Test
+ public void testAddObjectIntObjectNullInputNullItem()
+ {
+ Object list = LazyList.add(null, 0, null);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(1, LazyList.size(list));
+ }
+
+ /**
+ * Test for {@link LazyList#add(Object, int, Object)}
+ */
+ @Test
+ public void testAddObjectIntObjectNullInputNonListItem()
+ {
+ String item = "a";
+ Object list = LazyList.add(null, 0, item);
+ assertNotNull(list);
+ if (STRICT)
+ {
+ assertTrue(list instanceof List);
+ }
+ assertEquals(1, LazyList.size(list));
+ }
+
+ /**
+ * Test for {@link LazyList#add(Object, int, Object)}
+ */
+ @Test
+ public void testAddObjectIntObjectNullInputNonListItem2()
+ {
+ assumeTrue(STRICT); // Only run in STRICT mode.
+
+ String item = "a";
+ // Test branch of logic "index>0"
+ Object list = LazyList.add(null, 1, item); // Always throws exception?
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(1, LazyList.size(list));
+ }
+
+ /**
+ * Test for {@link LazyList#add(Object, int, Object)}
+ */
+ @Test
+ public void testAddObjectIntObjectNullInputLazyListItem()
+ {
+ Object item = LazyList.add(null, "x");
+ item = LazyList.add(item, "y");
+ item = LazyList.add(item, "z");
+
+ Object list = LazyList.add(null, 0, item);
+ assertNotNull(list);
+ assertEquals(1, LazyList.size(list));
+
+ Object val = LazyList.get(list, 0);
+ assertTrue(val instanceof List);
+ }
+
+ /**
+ * Test for {@link LazyList#add(Object, int, Object)}
+ */
+ @Test
+ public void testAddObjectIntObjectNullInputGenericListItem()
+ {
+ List<String> item = new ArrayList<String>();
+ item.add("a");
+
+ Object list = LazyList.add(null, 0, item);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(1, LazyList.size(list));
+ }
+
+ /**
+ * Test for {@link LazyList#add(Object, int, Object)}
+ */
+ @Test
+ public void testAddObjectIntObjectNonListInputNullItem()
+ {
+ String input = "a";
+
+ Object list = LazyList.add(input, 0, null);
+ assertNotNull(list);
+ assertEquals(2, LazyList.size(list));
+ assertNull(LazyList.get(list, 0));
+ assertEquals(LazyList.get(list, 1), "a");
+ }
+
+ /**
+ * Test for {@link LazyList#add(Object, int, Object)}
+ */
+ @Test
+ public void testAddObjectIntObjectNonListInputNonListItem()
+ {
+ String input = "a";
+ String item = "b";
+
+ Object list = LazyList.add(input, 0, item);
+ assertNotNull(list);
+ assertEquals(2, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "b");
+ assertEquals(LazyList.get(list, 1), "a");
+ }
+
+ /**
+ * Test for {@link LazyList#add(Object, int, Object)}
+ */
+ @Test
+ public void testAddObjectIntObjectLazyListInput()
+ {
+ Object list = LazyList.add(null, "c"); // [c]
+ list = LazyList.add(list, 0, "a"); // [a, c]
+ list = LazyList.add(list, 1, "b"); // [a, b, c]
+ list = LazyList.add(list, 3, "d"); // [a, b, c, d]
+
+ assertEquals(4, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "c");
+ assertEquals(LazyList.get(list, 3), "d");
+ }
+
+ /**
+ * Test for {@link LazyList#addCollection(Object, java.util.Collection)}
+ */
+ @Test
+ public void testAddCollectionNullInput()
+ {
+ Collection<?> coll = Arrays.asList("a", "b", "c");
+
+ Object list = LazyList.addCollection(null, coll);
+ assertTrue(list instanceof List);
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "c");
+ }
+
+ /**
+ * Test for {@link LazyList#addCollection(Object, java.util.Collection)}
+ */
+ @Test
+ public void testAddCollectionNonListInput()
+ {
+ Collection<?> coll = Arrays.asList("a", "b", "c");
+ String input = "z";
+
+ Object list = LazyList.addCollection(input, coll);
+ assertTrue(list instanceof List);
+ assertEquals(4, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "z");
+ assertEquals(LazyList.get(list, 1), "a");
+ assertEquals(LazyList.get(list, 2), "b");
+ assertEquals(LazyList.get(list, 3), "c");
+ }
+
+ /**
+ * Test for {@link LazyList#addCollection(Object, java.util.Collection)}
+ */
+ @Test
+ public void testAddCollectionLazyListInput()
+ {
+ Collection<?> coll = Arrays.asList("a", "b", "c");
+
+ Object input = LazyList.add(null, "x");
+ input = LazyList.add(input, "y");
+ input = LazyList.add(input, "z");
+
+ Object list = LazyList.addCollection(input, coll);
+ assertTrue(list instanceof List);
+ assertEquals(6, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "x");
+ assertEquals(LazyList.get(list, 1), "y");
+ assertEquals(LazyList.get(list, 2), "z");
+ assertEquals(LazyList.get(list, 3), "a");
+ assertEquals(LazyList.get(list, 4), "b");
+ assertEquals(LazyList.get(list, 5), "c");
+ }
+
+ /**
+ * Test for {@link LazyList#addCollection(Object, java.util.Collection)}
+ */
+ @Test
+ public void testAddCollectionGenericListInput()
+ {
+ Collection<?> coll = Arrays.asList("a", "b", "c");
+
+ List<String> input = new ArrayList<String>();
+ input.add("x");
+ input.add("y");
+ input.add("z");
+
+ Object list = LazyList.addCollection(input, coll);
+ assertTrue(list instanceof List);
+ assertEquals(6, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "x");
+ assertEquals(LazyList.get(list, 1), "y");
+ assertEquals(LazyList.get(list, 2), "z");
+ assertEquals(LazyList.get(list, 3), "a");
+ assertEquals(LazyList.get(list, 4), "b");
+ assertEquals(LazyList.get(list, 5), "c");
+ }
+
+ /**
+ * Test for {@link LazyList#addCollection(Object, java.util.Collection)}
+ */
+ @Test
+ public void testAddCollectionSequential()
+ {
+ Collection<?> coll = Arrays.asList("a", "b");
+
+ Object list = null;
+ list = LazyList.addCollection(list, coll);
+ list = LazyList.addCollection(list, coll);
+
+ assertEquals(4, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "a");
+ assertEquals(LazyList.get(list, 3), "b");
+ }
+
+ /**
+ * Test for {@link LazyList#addCollection(Object, java.util.Collection)}
+ */
+ @Test
+ public void testAddCollectionGenericListInput2()
+ {
+ List<String> l = new ArrayList<String>();
+ l.add("a");
+ l.add("b");
+
+ Object list = null;
+ list = LazyList.addCollection(list, l);
+ list = LazyList.addCollection(list, l);
+
+ assertEquals(4, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "a");
+ assertEquals(LazyList.get(list, 3), "b");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayNullInputNullArray()
+ {
+ String[] arr = null;
+ Object list = LazyList.addArray(null, arr);
+ assertNull(list);
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayNullInputEmptyArray()
+ {
+ String[] arr = new String[0];
+ Object list = LazyList.addArray(null, arr);
+ if (STRICT)
+ {
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ }
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayNullInputSingleArray()
+ {
+ String[] arr = new String[]{"a"};
+ Object list = LazyList.addArray(null, arr);
+ assertNotNull(list);
+ if (STRICT)
+ {
+ assertTrue(list instanceof List);
+ }
+ assertEquals(1, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayNullInputArray()
+ {
+ String[] arr = new String[]{"a", "b", "c"};
+ Object list = LazyList.addArray(null, arr);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "c");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayNonListInputNullArray()
+ {
+ String input = "z";
+ String[] arr = null;
+ Object list = LazyList.addArray(input, arr);
+ assertNotNull(list);
+ if (STRICT)
+ {
+ assertTrue(list instanceof List);
+ }
+ assertEquals(1, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "z");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayNonListInputEmptyArray()
+ {
+ String input = "z";
+ String[] arr = new String[0];
+ Object list = LazyList.addArray(input, arr);
+ assertNotNull(list);
+ if (STRICT)
+ {
+ assertTrue(list instanceof List);
+ }
+ assertEquals(1, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "z");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayNonListInputSingleArray()
+ {
+ String input = "z";
+ String[] arr = new String[]{"a"};
+ Object list = LazyList.addArray(input, arr);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(2, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "z");
+ assertEquals(LazyList.get(list, 1), "a");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayNonListInputArray()
+ {
+ String input = "z";
+ String[] arr = new String[]{"a", "b", "c"};
+ Object list = LazyList.addArray(input, arr);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(4, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "z");
+ assertEquals(LazyList.get(list, 1), "a");
+ assertEquals(LazyList.get(list, 2), "b");
+ assertEquals(LazyList.get(list, 3), "c");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayLazyListInputNullArray()
+ {
+ Object input = LazyList.add(null, "x");
+ input = LazyList.add(input, "y");
+ input = LazyList.add(input, "z");
+
+ String[] arr = null;
+ Object list = LazyList.addArray(input, arr);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "x");
+ assertEquals(LazyList.get(list, 1), "y");
+ assertEquals(LazyList.get(list, 2), "z");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayLazyListInputEmptyArray()
+ {
+ Object input = LazyList.add(null, "x");
+ input = LazyList.add(input, "y");
+ input = LazyList.add(input, "z");
+
+ String[] arr = new String[0];
+ Object list = LazyList.addArray(input, arr);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "x");
+ assertEquals(LazyList.get(list, 1), "y");
+ assertEquals(LazyList.get(list, 2), "z");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayLazyListInputSingleArray()
+ {
+ Object input = LazyList.add(null, "x");
+ input = LazyList.add(input, "y");
+ input = LazyList.add(input, "z");
+
+ String[] arr = new String[]{"a"};
+ Object list = LazyList.addArray(input, arr);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(4, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "x");
+ assertEquals(LazyList.get(list, 1), "y");
+ assertEquals(LazyList.get(list, 2), "z");
+ assertEquals(LazyList.get(list, 3), "a");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayLazyListInputArray()
+ {
+ Object input = LazyList.add(null, "x");
+ input = LazyList.add(input, "y");
+ input = LazyList.add(input, "z");
+
+ String[] arr = new String[]{"a", "b", "c"};
+ Object list = LazyList.addArray(input, arr);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(6, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "x");
+ assertEquals(LazyList.get(list, 1), "y");
+ assertEquals(LazyList.get(list, 2), "z");
+ assertEquals(LazyList.get(list, 3), "a");
+ assertEquals(LazyList.get(list, 4), "b");
+ assertEquals(LazyList.get(list, 5), "c");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayGenericListInputNullArray()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("x");
+ input.add("y");
+ input.add("z");
+
+ String[] arr = null;
+ Object list = LazyList.addArray(input, arr);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "x");
+ assertEquals(LazyList.get(list, 1), "y");
+ assertEquals(LazyList.get(list, 2), "z");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayGenericListInputEmptyArray()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("x");
+ input.add("y");
+ input.add("z");
+
+ String[] arr = new String[0];
+ Object list = LazyList.addArray(input, arr);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "x");
+ assertEquals(LazyList.get(list, 1), "y");
+ assertEquals(LazyList.get(list, 2), "z");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayGenericListInputSingleArray()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("x");
+ input.add("y");
+ input.add("z");
+
+ String[] arr = new String[]{"a"};
+ Object list = LazyList.addArray(input, arr);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(4, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "x");
+ assertEquals(LazyList.get(list, 1), "y");
+ assertEquals(LazyList.get(list, 2), "z");
+ assertEquals(LazyList.get(list, 3), "a");
+ }
+
+ /**
+ * Tests for {@link LazyList#addArray(Object, Object[])}
+ */
+ @Test
+ public void testAddArrayGenericListInputArray()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("x");
+ input.add("y");
+ input.add("z");
+
+ String[] arr = new String[]{"a", "b", "c"};
+ Object list = LazyList.addArray(input, arr);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(6, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "x");
+ assertEquals(LazyList.get(list, 1), "y");
+ assertEquals(LazyList.get(list, 2), "z");
+ assertEquals(LazyList.get(list, 3), "a");
+ assertEquals(LazyList.get(list, 4), "b");
+ assertEquals(LazyList.get(list, 5), "c");
+ }
+
+ /**
+ * Tests for {@link LazyList#ensureSize(Object, int)}
+ */
+ @Test
+ public void testEnsureSizeNullInput()
+ {
+ Object list = LazyList.ensureSize(null, 10);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ // Not possible to test for List capacity value.
+ }
+
+ /**
+ * Tests for {@link LazyList#ensureSize(Object, int)}
+ */
+ @Test
+ public void testEnsureSizeNonListInput()
+ {
+ String input = "a";
+ Object list = LazyList.ensureSize(input, 10);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ // Not possible to test for List capacity value.
+ assertEquals(1, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ }
+
+ /**
+ * Tests for {@link LazyList#ensureSize(Object, int)}
+ */
+ @Test
+ public void testEnsureSizeLazyListInput()
+ {
+ Object input = LazyList.add(null, "a");
+ input = LazyList.add(input, "b");
+
+ Object list = LazyList.ensureSize(input, 10);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ // Not possible to test for List capacity value.
+ assertEquals(2, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ }
+
+ /**
+ * Tests for {@link LazyList#ensureSize(Object, int)}
+ */
+ @Test
+ public void testEnsureSizeGenericListInput()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("a");
+ input.add("b");
+
+ Object list = LazyList.ensureSize(input, 10);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ // Not possible to test for List capacity value.
+ assertEquals(2, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ }
+
+ /**
+ * Tests for {@link LazyList#ensureSize(Object, int)}
+ */
+ @Test
+ public void testEnsureSizeGenericListInputLinkedList()
+ {
+ assumeTrue(STRICT); // Only run in STRICT mode.
+
+ // Using LinkedList concrete type as LazyList internal
+ // implementation does not look for this specifically.
+ List<String> input = new LinkedList<String>();
+ input.add("a");
+ input.add("b");
+
+ Object list = LazyList.ensureSize(input, 10);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ // Not possible to test for List capacity value.
+ assertEquals(2, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ }
+
+ /**
+ * Tests for {@link LazyList#ensureSize(Object, int)}
+ */
+ @Test
+ public void testEnsureSizeGrowth()
+ {
+ List<String> l = new ArrayList<String>();
+ l.add("a");
+ l.add("b");
+ l.add("c");
+
+ // NOTE: Testing for object equality might be viewed as
+ // fragile by most developers, however, for this
+ // specific implementation, we don't want the
+ // provided list to change if the size requirements
+ // have been met.
+
+ // Trigger growth
+ Object ret = LazyList.ensureSize(l, 10);
+ assertTrue(ret != l, "Should have returned a new list object");
+
+ // Growth not neeed.
+ ret = LazyList.ensureSize(l, 1);
+ assertTrue(ret == l, "Should have returned same list object");
+ }
+
+ /**
+ * Tests for {@link LazyList#ensureSize(Object, int)}
+ */
+ @Test
+ public void testEnsureSizeGrowthLinkedList()
+ {
+ assumeTrue(STRICT); // Only run in STRICT mode.
+
+ // Using LinkedList concrete type as LazyList internal
+ // implementation has not historically looked for this
+ // specifically.
+ List<String> l = new LinkedList<String>();
+ l.add("a");
+ l.add("b");
+ l.add("c");
+
+ // NOTE: Testing for object equality might be viewed as
+ // fragile by most developers, however, for this
+ // specific implementation, we don't want the
+ // provided list to change if the size requirements
+ // have been met.
+
+ // Trigger growth
+ Object ret = LazyList.ensureSize(l, 10);
+ assertTrue(ret != l, "Should have returned a new list object");
+
+ // Growth not neeed.
+ ret = LazyList.ensureSize(l, 1);
+ assertTrue(ret == l, "Should have returned same list object");
+ }
+
+ /**
+ * Test for {@link LazyList#remove(Object, Object)}
+ */
+ @Test
+ public void testRemoveObjectObjectNullInput()
+ {
+ Object input = null;
+
+ assertNull(LazyList.remove(input, null));
+ assertNull(LazyList.remove(input, "a"));
+ assertNull(LazyList.remove(input, new ArrayList<Object>()));
+ assertNull(LazyList.remove(input, Integer.valueOf(42)));
+ }
+
+ /**
+ * Test for {@link LazyList#remove(Object, Object)}
+ */
+ @Test
+ public void testRemoveObjectObjectNonListInput()
+ {
+ String input = "a";
+
+ // Remove null item
+ Object list = LazyList.remove(input, null);
+ assertNotNull(list);
+ if (STRICT)
+ {
+ assertTrue(list instanceof List);
+ }
+ assertEquals(1, LazyList.size(list));
+
+ // Remove item that doesn't exist
+ list = LazyList.remove(input, "b");
+ assertNotNull(list);
+ if (STRICT)
+ {
+ assertTrue(list instanceof List);
+ }
+ assertEquals(1, LazyList.size(list));
+
+ // Remove item that exists
+ list = LazyList.remove(input, "a");
+ // TODO: should this be null? or an empty list?
+ assertNull(list); // nothing left in list
+ assertEquals(0, LazyList.size(list));
+ }
+
+ /**
+ * Test for {@link LazyList#remove(Object, Object)}
+ */
+ @Test
+ public void testRemoveObjectObjectLazyListInput()
+ {
+ Object input = LazyList.add(null, "a");
+ input = LazyList.add(input, "b");
+ input = LazyList.add(input, "c");
+
+ // Remove null item
+ Object list = LazyList.remove(input, null);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(3, LazyList.size(list));
+
+ // Attempt to remove something that doesn't exist
+ list = LazyList.remove(input, "z");
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(3, LazyList.size(list));
+
+ // Remove something that exists in input
+ list = LazyList.remove(input, "b");
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(2, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "c");
+ }
+
+ /**
+ * Test for {@link LazyList#remove(Object, Object)}
+ */
+ @Test
+ public void testRemoveObjectObjectGenericListInput()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("a");
+ input.add("b");
+ input.add("c");
+
+ // Remove null item
+ Object list = LazyList.remove(input, null);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertTrue(input == list, "Should not have recreated list obj");
+ assertEquals(3, LazyList.size(list));
+
+ // Attempt to remove something that doesn't exist
+ list = LazyList.remove(input, "z");
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertTrue(input == list, "Should not have recreated list obj");
+ assertEquals(3, LazyList.size(list));
+
+ // Remove something that exists in input
+ list = LazyList.remove(input, "b");
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertTrue(input == list, "Should not have recreated list obj");
+ assertEquals(2, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "c");
+
+ // Try to remove the rest.
+ list = LazyList.remove(list, "a");
+ list = LazyList.remove(list, "c");
+ assertNull(list);
+ }
+
+ /**
+ * Test for {@link LazyList#remove(Object, Object)}
+ */
+ @Test
+ public void testRemoveObjectObjectLinkedListInput()
+ {
+ // Should be able to use any collection object.
+ List<String> input = new LinkedList<String>();
+ input.add("a");
+ input.add("b");
+ input.add("c");
+
+ // Remove null item
+ Object list = LazyList.remove(input, null);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertTrue(input == list, "Should not have recreated list obj");
+ assertEquals(3, LazyList.size(list));
+
+ // Attempt to remove something that doesn't exist
+ list = LazyList.remove(input, "z");
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertTrue(input == list, "Should not have recreated list obj");
+ assertEquals(3, LazyList.size(list));
+
+ // Remove something that exists in input
+ list = LazyList.remove(input, "b");
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertTrue(input == list, "Should not have recreated list obj");
+ assertEquals(2, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "c");
+ }
+
+ /**
+ * Tests for {@link LazyList#remove(Object, int)}
+ */
+ @Test
+ public void testRemoveObjectIntNullInput()
+ {
+ Object input = null;
+
+ assertNull(LazyList.remove(input, 0));
+ assertNull(LazyList.remove(input, 2));
+ assertNull(LazyList.remove(input, -2));
+ }
+
+ /**
+ * Tests for {@link LazyList#remove(Object, int)}
+ */
+ @Test
+ public void testRemoveObjectIntNonListInput()
+ {
+ String input = "a";
+
+ // Invalid index
+ Object list = LazyList.remove(input, 1);
+ assertNotNull(list);
+ if (STRICT)
+ {
+ assertTrue(list instanceof List);
+ }
+ assertEquals(1, LazyList.size(list));
+
+ // Valid index
+ list = LazyList.remove(input, 0);
+ // TODO: should this be null? or an empty list?
+ assertNull(list); // nothing left in list
+ assertEquals(0, LazyList.size(list));
+ }
+
+ /**
+ * Tests for {@link LazyList#remove(Object, int)}
+ */
+ @Test
+ public void testRemoveObjectIntLazyListInput()
+ {
+ Object input = LazyList.add(null, "a");
+ input = LazyList.add(input, "b");
+ input = LazyList.add(input, "c");
+
+ Object list = null;
+
+ if (STRICT)
+ {
+ // Invalid index
+ // Shouldn't cause a IndexOutOfBoundsException as this is not the
+ // same behavior you experience in testRemoveObjectInt_NonListInput and
+ // testRemoveObjectInt_NullInput when using invalid indexes.
+ list = LazyList.remove(input, 5);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(3, LazyList.size(list));
+ }
+
+ // Valid index
+ list = LazyList.remove(input, 1); // remove the 'b'
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(2, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "c");
+ }
+
+ /**
+ * Tests for {@link LazyList#remove(Object, int)}
+ */
+ @Test
+ public void testRemoveObjectIntGenericListInput()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("a");
+ input.add("b");
+ input.add("c");
+
+ Object list = null;
+
+ if (STRICT)
+ {
+ // Invalid index
+ // Shouldn't cause a IndexOutOfBoundsException as this is not the
+ // same behavior you experience in testRemoveObjectInt_NonListInput and
+ // testRemoveObjectInt_NullInput when using invalid indexes.
+ list = LazyList.remove(input, 5);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(3, LazyList.size(list));
+ }
+
+ // Valid index
+ list = LazyList.remove(input, 1); // remove the 'b'
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(2, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "c");
+
+ // Remove the rest
+ list = LazyList.remove(list, 0); // the 'a'
+ list = LazyList.remove(list, 0); // the 'c'
+ assertNull(list);
+ }
+
+ /**
+ * Test for {@link LazyList#getList(Object)}
+ */
+ @Test
+ public void testGetListObjectNullInput()
+ {
+ Object input = null;
+
+ Object list = LazyList.getList(input);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(0, LazyList.size(list));
+ }
+
+ /**
+ * Test for {@link LazyList#getList(Object)}
+ */
+ @Test
+ public void testGetListObjectNonListInput()
+ {
+ String input = "a";
+
+ Object list = LazyList.getList(input);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(1, LazyList.size(list));
+ }
+
+ /**
+ * Test for {@link LazyList#getList(Object)}
+ */
+ @Test
+ public void testGetListObjectLazyListInput()
+ {
+ Object input = LazyList.add(null, "a");
+ input = LazyList.add(input, "b");
+ input = LazyList.add(input, "c");
+
+ Object list = LazyList.getList(input);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "c");
+ }
+
+ /**
+ * Test for {@link LazyList#getList(Object)}
+ */
+ @Test
+ public void testGetListObjectGenericListInput()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("a");
+ input.add("b");
+ input.add("c");
+
+ Object list = LazyList.getList(input);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "c");
+ }
+
+ /**
+ * Test for {@link LazyList#getList(Object)}
+ */
+ @Test
+ public void testGetListObjectLinkedListInput()
+ {
+ List<String> input = new LinkedList<String>();
+ input.add("a");
+ input.add("b");
+ input.add("c");
+
+ Object list = LazyList.getList(input);
+ assertNotNull(list);
+ assertTrue(list instanceof List);
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "c");
+ }
+
+ /**
+ * Test for {@link LazyList#getList(Object)}
+ */
+ @Test
+ public void testGetListObjectNullForEmpty()
+ {
+ assertNull(LazyList.getList(null, true));
+ assertNotNull(LazyList.getList(null, false));
+ }
+
+ /**
+ * Tests for {@link LazyList#toStringArray(Object)}
+ */
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testToStringArray()
+ {
+ assertEquals(0, LazyList.toStringArray(null).length);
+
+ assertEquals(1, LazyList.toStringArray("a").length);
+ assertEquals("a", LazyList.toStringArray("a")[0]);
+
+ @SuppressWarnings("rawtypes")
+ ArrayList l = new ArrayList();
+ l.add("a");
+ l.add(null);
+ l.add(new Integer(2));
+ String[] a = LazyList.toStringArray(l);
+
+ assertEquals(3, a.length);
+ assertEquals("a", a[0]);
+ assertEquals(null, a[1]);
+ assertEquals("2", a[2]);
+ }
+
+ /**
+ * Tests for {@link LazyList#toArray(Object, Class)}
+ */
+ @Test
+ public void testToArrayNullInputObject()
+ {
+ Object input = null;
+
+ Object arr = LazyList.toArray(input, Object.class);
+ assertNotNull(arr);
+ assertTrue(arr.getClass().isArray());
+ }
+
+ /**
+ * Tests for {@link LazyList#toArray(Object, Class)}
+ */
+ @Test
+ public void testToArrayNullInputString()
+ {
+ String input = null;
+
+ Object arr = LazyList.toArray(input, String.class);
+ assertNotNull(arr);
+ assertTrue(arr.getClass().isArray());
+ assertTrue(arr instanceof String[]);
+ }
+
+ /**
+ * Tests for {@link LazyList#toArray(Object, Class)}
+ */
+ @Test
+ public void testToArrayNonListInput()
+ {
+ String input = "a";
+
+ Object arr = LazyList.toArray(input, String.class);
+ assertNotNull(arr);
+ assertTrue(arr.getClass().isArray());
+ assertTrue(arr instanceof String[]);
+
+ String[] strs = (String[])arr;
+ assertEquals(1, strs.length);
+ assertEquals("a", strs[0]);
+ }
+
+ /**
+ * Tests for {@link LazyList#toArray(Object, Class)}
+ */
+ @Test
+ public void testToArrayLazyListInput()
+ {
+ Object input = LazyList.add(null, "a");
+ input = LazyList.add(input, "b");
+ input = LazyList.add(input, "c");
+
+ Object arr = LazyList.toArray(input, String.class);
+ assertNotNull(arr);
+ assertTrue(arr.getClass().isArray());
+ assertTrue(arr instanceof String[]);
+
+ String[] strs = (String[])arr;
+ assertEquals(3, strs.length);
+ assertEquals("a", strs[0]);
+ assertEquals("b", strs[1]);
+ assertEquals("c", strs[2]);
+ }
+
+ /**
+ * Tests for {@link LazyList#toArray(Object, Class)}
+ */
+ @Test
+ public void testToArrayLazyListInputPrimitives()
+ {
+ Object input = LazyList.add(null, 22);
+ input = LazyList.add(input, 333);
+ input = LazyList.add(input, 4444);
+ input = LazyList.add(input, 55555);
+
+ Object arr = LazyList.toArray(input, int.class);
+ assertNotNull(arr);
+ assertTrue(arr.getClass().isArray());
+ assertTrue(arr instanceof int[]);
+
+ int[] nums = (int[])arr;
+ assertEquals(4, nums.length);
+ assertEquals(22, nums[0]);
+ assertEquals(333, nums[1]);
+ assertEquals(4444, nums[2]);
+ assertEquals(55555, nums[3]);
+ }
+
+ /**
+ * Tests for {@link LazyList#toArray(Object, Class)}
+ */
+ @Test
+ public void testToArrayGenericListInput()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("a");
+ input.add("b");
+ input.add("c");
+
+ Object arr = LazyList.toArray(input, String.class);
+ assertNotNull(arr);
+ assertTrue(arr.getClass().isArray());
+ assertTrue(arr instanceof String[]);
+
+ String[] strs = (String[])arr;
+ assertEquals(3, strs.length);
+ assertEquals("a", strs[0]);
+ assertEquals("b", strs[1]);
+ assertEquals("c", strs[2]);
+ }
+
+ /**
+ * Tests for {@link LazyList#size(Object)}
+ */
+ @Test
+ public void testSizeNullInput()
+ {
+ assertEquals(0, LazyList.size(null));
+ }
+
+ /**
+ * Tests for {@link LazyList#size(Object)}
+ */
+ @Test
+ public void testSizeNonListInput()
+ {
+ String input = "a";
+ assertEquals(1, LazyList.size(input));
+ }
+
+ /**
+ * Tests for {@link LazyList#size(Object)}
+ */
+ @Test
+ public void testSizeLazyListInput()
+ {
+ Object input = LazyList.add(null, "a");
+ input = LazyList.add(input, "b");
+
+ assertEquals(2, LazyList.size(input));
+
+ input = LazyList.add(input, "c");
+
+ assertEquals(3, LazyList.size(input));
+ }
+
+ /**
+ * Tests for {@link LazyList#size(Object)}
+ */
+ @Test
+ public void testSizeGenericListInput()
+ {
+ List<String> input = new ArrayList<String>();
+
+ assertEquals(0, LazyList.size(input));
+
+ input.add("a");
+ input.add("b");
+
+ assertEquals(2, LazyList.size(input));
+
+ input.add("c");
+
+ assertEquals(3, LazyList.size(input));
+ }
+
+ /**
+ * Tests for bad input on {@link LazyList#get(Object, int)}
+ */
+ @Test
+ public void testGetOutOfBoundsNullInput()
+ {
+ assertThrows(IndexOutOfBoundsException.class, () ->
+ {
+ LazyList.get(null, 0); // Should Fail due to null input
+ }
+ );
+ }
+
+ /**
+ * Tests for bad input on {@link LazyList#get(Object, int)}
+ */
+ @Test
+ public void testGetOutOfBoundsNonListInput()
+ {
+ assertThrows(IndexOutOfBoundsException.class, () ->
+ {
+ String input = "a";
+ LazyList.get(input, 1); // Should Fail
+ });
+ }
+
+ /**
+ * Tests for bad input on {@link LazyList#get(Object, int)}
+ */
+ @Test
+ public void testGetOutOfBoundsLazyListInput()
+ {
+ assertThrows(IndexOutOfBoundsException.class, () ->
+ {
+ Object input = LazyList.add(null, "a");
+ LazyList.get(input, 1); // Should Fail
+ });
+ }
+
+ /**
+ * Tests for bad input on {@link LazyList#get(Object, int)}
+ */
+ @Test
+ public void testGetOutOfBoundsGenericListInput()
+ {
+ assertThrows(IndexOutOfBoundsException.class, () ->
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("a");
+ LazyList.get(input, 1); // Should Fail
+ });
+ }
+
+ /**
+ * Tests for non-list input on {@link LazyList#get(Object, int)}
+ */
+ @Test
+ public void testGetNonListInput()
+ {
+ String input = "a";
+ assertEquals(LazyList.get(input, 0), "a");
+ }
+
+ /**
+ * Tests for list input on {@link LazyList#get(Object, int)}
+ */
+ @Test
+ public void testGetLazyListInput()
+ {
+ Object input = LazyList.add(null, "a");
+ assertEquals(LazyList.get(input, 0), "a");
+ }
+
+ /**
+ * Tests for list input on {@link LazyList#get(Object, int)}
+ */
+ @Test
+ public void testGetGenericListInput()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("a");
+ assertEquals(LazyList.get(input, 0), "a");
+
+ List<URI> uris = new ArrayList<URI>();
+ uris.add(URI.create("http://www.mortbay.org/"));
+ uris.add(URI.create("http://jetty.codehaus.org/jetty/"));
+ uris.add(URI.create("http://www.intalio.com/jetty/"));
+ uris.add(URI.create("https://www.eclipse.org/jetty/"));
+
+ // Make sure that Generics pass through the 'get' routine safely.
+ // We should be able to call this without casting the result to URI
+ URI eclipseUri = LazyList.get(uris, 3);
+ assertEquals("https://www.eclipse.org/jetty/", eclipseUri.toASCIIString());
+ }
+
+ /**
+ * Tests for {@link LazyList#contains(Object, Object)}
+ */
+ @Test
+ public void testContainsNullInput()
+ {
+ assertFalse(LazyList.contains(null, "z"));
+ }
+
+ /**
+ * Tests for {@link LazyList#contains(Object, Object)}
+ */
+ @Test
+ public void testContainsNonListInput()
+ {
+ String input = "a";
+ assertFalse(LazyList.contains(input, "z"));
+ assertTrue(LazyList.contains(input, "a"));
+ }
+
+ /**
+ * Tests for {@link LazyList#contains(Object, Object)}
+ */
+ @Test
+ public void testContainsLazyListInput()
+ {
+ Object input = LazyList.add(null, "a");
+ input = LazyList.add(input, "b");
+ input = LazyList.add(input, "c");
+
+ assertFalse(LazyList.contains(input, "z"));
+ assertTrue(LazyList.contains(input, "a"));
+ assertTrue(LazyList.contains(input, "b"));
+ }
+
+ /**
+ * Tests for {@link LazyList#contains(Object, Object)}
+ */
+ @Test
+ public void testContainsGenericListInput()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("a");
+ input.add("b");
+ input.add("c");
+
+ assertFalse(LazyList.contains(input, "z"));
+ assertTrue(LazyList.contains(input, "a"));
+ assertTrue(LazyList.contains(input, "b"));
+ }
+
+ /**
+ * Tests for {@link LazyList#clone(Object)}
+ */
+ @Test
+ public void testCloneNullInput()
+ {
+ Object input = null;
+
+ Object list = LazyList.clone(input);
+ assertNull(list);
+ }
+
+ /**
+ * Tests for {@link LazyList#clone(Object)}
+ */
+ @Test
+ public void testCloneNonListInput()
+ {
+ String input = "a";
+
+ Object list = LazyList.clone(input);
+ assertNotNull(list);
+ assertTrue(input == list, "Should be the same object");
+ }
+
+ /**
+ * Tests for {@link LazyList#clone(Object)}
+ */
+ @Test
+ public void testCloneLazyListInput()
+ {
+ Object input = LazyList.add(null, "a");
+ input = LazyList.add(input, "b");
+ input = LazyList.add(input, "c");
+
+ Object list = LazyList.clone(input);
+ assertNotNull(list);
+ assertTrue(list instanceof List, "Should be a List object");
+ assertFalse(input == list, "Should NOT be the same object");
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "c");
+ }
+
+ /**
+ * Tests for {@link LazyList#clone(Object)}
+ */
+ @Test
+ public void testCloneGenericListInput()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("a");
+ input.add("b");
+ input.add("c");
+
+ // TODO: decorate the .clone(Object) method to return
+ // the same generic object element type
+ Object list = LazyList.clone(input);
+ assertNotNull(list);
+ assertTrue(list instanceof List, "Should be a List object");
+ assertFalse(input == list, "Should NOT be the same object");
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "c");
+ }
+
+ /**
+ * Tests for {@link LazyList#toString(Object)}
+ */
+ @Test
+ public void testToStringNullInput()
+ {
+ Object input = null;
+ assertEquals("[]", LazyList.toString(input));
+ }
+
+ /**
+ * Tests for {@link LazyList#toString(Object)}
+ */
+ @Test
+ public void testToStringNonListInput()
+ {
+ String input = "a";
+ assertEquals("[a]", LazyList.toString(input));
+ }
+
+ /**
+ * Tests for {@link LazyList#toString(Object)}
+ */
+ @Test
+ public void testToStringLazyListInput()
+ {
+ Object input = LazyList.add(null, "a");
+
+ assertEquals("[a]", LazyList.toString(input));
+
+ input = LazyList.add(input, "b");
+ input = LazyList.add(input, "c");
+
+ assertEquals("[a, b, c]", LazyList.toString(input));
+ }
+
+ /**
+ * Tests for {@link LazyList#toString(Object)}
+ */
+ @Test
+ public void testToStringGenericListInput()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("a");
+
+ assertEquals("[a]", LazyList.toString(input));
+
+ input.add("b");
+ input.add("c");
+
+ assertEquals("[a, b, c]", LazyList.toString(input));
+ }
+
+ /**
+ * Tests for {@link LazyList#iterator(Object)}
+ */
+ @Test
+ public void testIteratorNullInput()
+ {
+ Iterator<?> iter = LazyList.iterator(null);
+ assertNotNull(iter);
+ assertFalse(iter.hasNext());
+ }
+
+ /**
+ * Tests for {@link LazyList#iterator(Object)}
+ */
+ @Test
+ public void testIteratorNonListInput()
+ {
+ String input = "a";
+
+ Iterator<?> iter = LazyList.iterator(input);
+ assertNotNull(iter);
+ assertTrue(iter.hasNext());
+ assertEquals("a", iter.next());
+ assertFalse(iter.hasNext());
+ }
+
+ /**
+ * Tests for {@link LazyList#iterator(Object)}
+ */
+ @Test
+ public void testIteratorLazyListInput()
+ {
+ Object input = LazyList.add(null, "a");
+ input = LazyList.add(input, "b");
+ input = LazyList.add(input, "c");
+
+ Iterator<?> iter = LazyList.iterator(input);
+ assertNotNull(iter);
+ assertTrue(iter.hasNext());
+ assertEquals("a", iter.next());
+ assertEquals("b", iter.next());
+ assertEquals("c", iter.next());
+ assertFalse(iter.hasNext());
+ }
+
+ /**
+ * Tests for {@link LazyList#iterator(Object)}
+ */
+ @Test
+ public void testIteratorGenericListInput()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("a");
+ input.add("b");
+ input.add("c");
+
+ Iterator<String> iter = LazyList.iterator(input);
+ assertNotNull(iter);
+ assertTrue(iter.hasNext());
+ assertEquals("a", iter.next());
+ assertEquals("b", iter.next());
+ assertEquals("c", iter.next());
+ assertFalse(iter.hasNext());
+ }
+
+ /**
+ * Tests for {@link LazyList#listIterator(Object)}
+ */
+ @Test
+ public void testListIteratorNullInput()
+ {
+ ListIterator<?> iter = LazyList.listIterator(null);
+ assertNotNull(iter);
+ assertFalse(iter.hasNext());
+ assertFalse(iter.hasPrevious());
+ }
+
+ /**
+ * Tests for {@link LazyList#listIterator(Object)}
+ */
+ @Test
+ public void testListIteratorNonListInput()
+ {
+ String input = "a";
+
+ ListIterator<?> iter = LazyList.listIterator(input);
+ assertNotNull(iter);
+ assertTrue(iter.hasNext());
+ assertFalse(iter.hasPrevious());
+ assertEquals("a", iter.next());
+ assertFalse(iter.hasNext());
+ assertTrue(iter.hasPrevious());
+ }
+
+ /**
+ * Tests for {@link LazyList#listIterator(Object)}
+ */
+ @Test
+ public void testListIteratorLazyListInput()
+ {
+ Object input = LazyList.add(null, "a");
+ input = LazyList.add(input, "b");
+ input = LazyList.add(input, "c");
+
+ ListIterator<?> iter = LazyList.listIterator(input);
+ assertNotNull(iter);
+ assertTrue(iter.hasNext());
+ assertFalse(iter.hasPrevious());
+ assertEquals("a", iter.next());
+ assertEquals("b", iter.next());
+ assertEquals("c", iter.next());
+ assertFalse(iter.hasNext());
+ assertTrue(iter.hasPrevious());
+ assertEquals("c", iter.previous());
+ assertEquals("b", iter.previous());
+ assertEquals("a", iter.previous());
+ assertFalse(iter.hasPrevious());
+ }
+
+ /**
+ * Tests for {@link LazyList#listIterator(Object)}
+ */
+ @Test
+ public void testListIteratorGenericListInput()
+ {
+ List<String> input = new ArrayList<String>();
+ input.add("a");
+ input.add("b");
+ input.add("c");
+
+ ListIterator<?> iter = LazyList.listIterator(input);
+ assertNotNull(iter);
+ assertTrue(iter.hasNext());
+ assertFalse(iter.hasPrevious());
+ assertEquals("a", iter.next());
+ assertEquals("b", iter.next());
+ assertEquals("c", iter.next());
+ assertFalse(iter.hasNext());
+ assertTrue(iter.hasPrevious());
+ assertEquals("c", iter.previous());
+ assertEquals("b", iter.previous());
+ assertEquals("a", iter.previous());
+ assertFalse(iter.hasPrevious());
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#asMutableList(Object[])}
+ */
+ @Test
+ public void testArray2ListNullInput()
+ {
+ Object[] input = null;
+
+ Object list = ArrayUtil.asMutableList(input);
+ assertNotNull(list);
+ assertTrue(list instanceof List, "Should be a List object");
+ assertEquals(0, LazyList.size(list));
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#asMutableList(Object[])}
+ */
+ @Test
+ public void testArray2ListEmptyInput()
+ {
+ String[] input = new String[0];
+
+ Object list = ArrayUtil.asMutableList(input);
+ assertNotNull(list);
+ assertTrue(list instanceof List, "Should be a List object");
+ assertEquals(0, LazyList.size(list));
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#asMutableList(Object[])}
+ */
+ @Test
+ public void testArray2ListSingleInput()
+ {
+ String[] input = new String[]{"a"};
+
+ Object list = ArrayUtil.asMutableList(input);
+ assertNotNull(list);
+ assertTrue(list instanceof List, "Should be a List object");
+ assertEquals(1, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#asMutableList(Object[])}
+ */
+ @Test
+ public void testArray2ListMultiInput()
+ {
+ String[] input = new String[]{"a", "b", "c"};
+
+ Object list = ArrayUtil.asMutableList(input);
+ assertNotNull(list);
+ assertTrue(list instanceof List, "Should be a List object");
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "c");
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#asMutableList(Object[])}
+ */
+ @Test
+ public void testArray2ListGenericsInput()
+ {
+ String[] input = new String[]{"a", "b", "c"};
+
+ // Test the Generics definitions for array2List
+ List<String> list = ArrayUtil.asMutableList(input);
+ assertNotNull(list);
+ assertTrue(list instanceof List, "Should be a List object");
+ assertEquals(3, LazyList.size(list));
+ assertEquals(LazyList.get(list, 0), "a");
+ assertEquals(LazyList.get(list, 1), "b");
+ assertEquals(LazyList.get(list, 2), "c");
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#addToArray(Object[], Object, Class)}
+ */
+ @Test
+ public void testAddToArrayNullInputNullItem()
+ {
+ Object[] input = null;
+
+ Object[] arr = ArrayUtil.addToArray(input, null, Object.class);
+ assertNotNull(arr);
+ if (STRICT)
+ {
+ // Adding null item to array should result in nothing added?
+ assertEquals(0, arr.length);
+ }
+ else
+ {
+ assertEquals(1, arr.length);
+ }
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#addToArray(Object[], Object, Class)}
+ */
+ @Test
+ public void testAddToArrayNullNullNull()
+ {
+ // NPE if item && type are both null.
+ assumeTrue(STRICT);
+
+ // Harsh test case.
+ Object[] input = null;
+
+ Object[] arr = ArrayUtil.addToArray(input, null, null);
+ assertNotNull(arr);
+ if (STRICT)
+ {
+ // Adding null item to array should result in nothing added?
+ assertEquals(0, arr.length);
+ }
+ else
+ {
+ assertEquals(1, arr.length);
+ }
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#addToArray(Object[], Object, Class)}
+ */
+ @Test
+ public void testAddToArrayNullInputSimpleItem()
+ {
+ Object[] input = null;
+
+ Object[] arr = ArrayUtil.addToArray(input, "a", String.class);
+ assertNotNull(arr);
+ assertEquals(1, arr.length);
+ assertEquals("a", arr[0]);
+
+ // Same test, but with an undefined type
+ arr = ArrayUtil.addToArray(input, "b", null);
+ assertNotNull(arr);
+ assertEquals(1, arr.length);
+ assertEquals("b", arr[0]);
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#addToArray(Object[], Object, Class)}
+ */
+ @Test
+ public void testAddToArrayEmptyInputNullItem()
+ {
+ String[] input = new String[0];
+
+ String[] arr = ArrayUtil.addToArray(input, null, Object.class);
+ assertNotNull(arr);
+ if (STRICT)
+ {
+ // Adding null item to array should result in nothing added?
+ assertEquals(0, arr.length);
+ }
+ else
+ {
+ assertEquals(1, arr.length);
+ }
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#addToArray(Object[], Object, Class)}
+ */
+ @Test
+ public void testAddToArrayEmptyInputSimpleItem()
+ {
+ String[] input = new String[0];
+
+ String[] arr = ArrayUtil.addToArray(input, "a", String.class);
+ assertNotNull(arr);
+ assertEquals(1, arr.length);
+ assertEquals("a", arr[0]);
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#addToArray(Object[], Object, Class)}
+ */
+ @Test
+ public void testAddToArraySingleInputNullItem()
+ {
+ String[] input = new String[]{"z"};
+
+ String[] arr = ArrayUtil.addToArray(input, null, Object.class);
+ assertNotNull(arr);
+ if (STRICT)
+ {
+ // Should a null item be added to an array?
+ assertEquals(1, arr.length);
+ }
+ else
+ {
+ assertEquals(2, arr.length);
+ assertEquals("z", arr[0]);
+ assertEquals(null, arr[1]);
+ }
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#addToArray(Object[], Object, Class)}
+ */
+ @Test
+ public void testAddToArraySingleInputSimpleItem()
+ {
+ String[] input = new String[]{"z"};
+
+ String[] arr = ArrayUtil.addToArray(input, "a", String.class);
+ assertNotNull(arr);
+ assertEquals(2, arr.length);
+ assertEquals("z", arr[0]);
+ assertEquals("a", arr[1]);
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#removeFromArray(Object[], Object)}
+ */
+ @Test
+ public void testRemoveFromArrayNullInputNullItem()
+ {
+ Object[] input = null;
+
+ Object[] arr = ArrayUtil.removeFromArray(input, null);
+ assertNull(arr);
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#removeFromArray(Object[], Object)}
+ */
+ @Test
+ public void testRemoveFromArrayNullInputSimpleItem()
+ {
+ Object[] input = null;
+
+ Object[] arr = ArrayUtil.removeFromArray(input, "a");
+ assertNull(arr);
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#removeFromArray(Object[], Object)}
+ */
+ @Test
+ public void testRemoveFromArrayEmptyInputNullItem()
+ {
+ String[] input = new String[0];
+
+ String[] arr = ArrayUtil.removeFromArray(input, null);
+ assertNotNull(arr, "Should not be null");
+ assertEquals(0, arr.length);
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#removeFromArray(Object[], Object)}
+ */
+ @Test
+ public void testRemoveFromArrayEmptyInputSimpleItem()
+ {
+ String[] input = new String[0];
+
+ String[] arr = ArrayUtil.removeFromArray(input, "a");
+ assertNotNull(arr, "Should not be null");
+ assertEquals(0, arr.length);
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#removeFromArray(Object[], Object)}
+ */
+ @Test
+ public void testRemoveFromArraySingleInput()
+ {
+ String[] input = new String[]{"a"};
+
+ String[] arr = ArrayUtil.removeFromArray(input, null);
+ assertNotNull(arr, "Should not be null");
+ assertEquals(1, arr.length);
+ assertEquals("a", arr[0]);
+
+ // Remove actual item
+ arr = ArrayUtil.removeFromArray(input, "a");
+ assertNotNull(arr, "Should not be null");
+ assertEquals(0, arr.length);
+ }
+
+ /**
+ * Tests for {@link ArrayUtil#removeFromArray(Object[], Object)}
+ */
+ @Test
+ public void testRemoveFromArrayMultiInput()
+ {
+ String[] input = new String[]{"a", "b", "c"};
+
+ String[] arr = ArrayUtil.removeFromArray(input, null);
+ assertNotNull(arr, "Should not be null");
+ assertEquals(3, arr.length);
+ assertEquals("a", arr[0]);
+ assertEquals("b", arr[1]);
+ assertEquals("c", arr[2]);
+
+ // Remove an actual item
+ arr = ArrayUtil.removeFromArray(input, "b");
+ assertNotNull(arr, "Should not be null");
+ assertEquals(2, arr.length);
+ assertEquals("a", arr[0]);
+ assertEquals("c", arr[1]);
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/LeakDetectorTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/LeakDetectorTest.java
new file mode 100644
index 0000000..8718bc8
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/LeakDetectorTest.java
@@ -0,0 +1,96 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class LeakDetectorTest
+{
+ private LeakDetector<Object> leakDetector;
+
+ public void prepare(LeakDetector<Object> leakDetector) throws Exception
+ {
+ this.leakDetector = leakDetector;
+ leakDetector.start();
+ }
+
+ public void dispose() throws Exception
+ {
+ leakDetector.stop();
+ }
+
+ private void gc()
+ {
+ for (int i = 0; i < 3; ++i)
+ {
+ System.gc();
+ }
+ }
+
+ @Test
+ public void testResourceAcquiredAndReleased() throws Exception
+ {
+ final CountDownLatch latch = new CountDownLatch(1);
+ prepare(new LeakDetector<Object>()
+ {
+ @Override
+ protected void leaked(LeakInfo leakInfo)
+ {
+ latch.countDown();
+ }
+ });
+
+ // Block to make sure "resource" goes out of scope
+ {
+ Object resource = new Object();
+ leakDetector.acquired(resource);
+ leakDetector.released(resource);
+ }
+
+ gc();
+
+ assertFalse(latch.await(1, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void testResourceAcquiredAndNotReleased() throws Exception
+ {
+ final CountDownLatch latch = new CountDownLatch(1);
+ prepare(new LeakDetector<Object>()
+ {
+ @Override
+ protected void leaked(LeakInfo leakInfo)
+ {
+ latch.countDown();
+ }
+ });
+
+ leakDetector.acquired(new Object());
+
+ gc();
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/LoaderTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/LoaderTest.java
new file mode 100644
index 0000000..8b33b9f
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/LoaderTest.java
@@ -0,0 +1,54 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.Locale;
+import java.util.MissingResourceException;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Unit tests for class {@link Loader}.
+ *
+ * @see Loader
+ */
+public class LoaderTest
+{
+
+ @Test
+ public void testGetResourceBundleThrowsMissingResourceException()
+ {
+ assertThrows(MissingResourceException.class, () -> Loader.getResourceBundle("nothing", true, Locale.ITALIAN));
+ }
+
+ @Test
+ public void testLoadClassThrowsClassNotFoundException()
+ {
+ assertThrows(ClassNotFoundException.class, () -> Loader.loadClass(Object.class, "String"));
+ }
+
+ @Test
+ public void testLoadClassSucceeds() throws ClassNotFoundException
+ {
+ assertEquals(LazyList.class, Loader.loadClass(Object.class, "org.eclipse.jetty.util.LazyList"));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/MultiExceptionTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/MultiExceptionTest.java
new file mode 100644
index 0000000..c2f4fff
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/MultiExceptionTest.java
@@ -0,0 +1,225 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class MultiExceptionTest
+{
+ @Test
+ public void testEmpty() throws Exception
+ {
+ MultiException me = new MultiException();
+
+ assertEquals(0, me.size());
+ me.ifExceptionThrow();
+ me.ifExceptionThrowMulti();
+ me.ifExceptionThrowRuntime();
+ me.ifExceptionThrowSuppressed();
+
+ assertEquals(0, me.getStackTrace().length, "Stack trace should not be filled out");
+ }
+
+ @Test
+ public void testOne() throws Exception
+ {
+ MultiException me = new MultiException();
+ IOException io = new IOException("one");
+ me.add(io);
+
+ assertEquals(1, me.size());
+
+ // TODO: convert to assertThrows chain
+ try
+ {
+ me.ifExceptionThrow();
+ assertTrue(false);
+ }
+ catch (IOException e)
+ {
+ assertTrue(e == io);
+ }
+
+ try
+ {
+ me.ifExceptionThrowMulti();
+ assertTrue(false);
+ }
+ catch (MultiException e)
+ {
+ assertTrue(e instanceof MultiException);
+ }
+
+ try
+ {
+ me.ifExceptionThrowRuntime();
+ assertTrue(false);
+ }
+ catch (RuntimeException e)
+ {
+ assertTrue(e.getCause() == io);
+ }
+
+ try
+ {
+ me.ifExceptionThrowSuppressed();
+ assertTrue(false);
+ }
+ catch (IOException e)
+ {
+ assertTrue(e == io);
+ }
+
+ me = new MultiException();
+ RuntimeException run = new RuntimeException("one");
+ me.add(run);
+
+ try
+ {
+ me.ifExceptionThrowRuntime();
+ assertTrue(false);
+ }
+ catch (RuntimeException e)
+ {
+ assertTrue(run == e);
+ }
+
+ assertEquals(0, me.getStackTrace().length, "Stack trace should not be filled out");
+ }
+
+ private MultiException multiExceptionWithIoRt()
+ {
+ MultiException me = new MultiException();
+ IOException io = new IOException("one");
+ RuntimeException run = new RuntimeException("two");
+ me.add(io);
+ me.add(run);
+ assertEquals(2, me.size());
+
+ assertEquals(0, me.getStackTrace().length, "Stack trace should not be filled out");
+ return me;
+ }
+
+ private MultiException multiExceptionWithRtIo()
+ {
+ MultiException me = new MultiException();
+ RuntimeException run = new RuntimeException("one");
+ IOException io = new IOException("two");
+ me.add(run);
+ me.add(io);
+ assertEquals(2, me.size());
+
+ assertEquals(0, me.getStackTrace().length, "Stack trace should not be filled out");
+ return me;
+ }
+
+ @Test
+ public void testTwo() throws Exception
+ {
+ MultiException me = multiExceptionWithIoRt();
+ try
+ {
+ me.ifExceptionThrow();
+ assertTrue(false);
+ }
+ catch (MultiException e)
+ {
+ assertTrue(e instanceof MultiException);
+ assertTrue(e.getStackTrace().length > 0);
+ }
+
+ me = multiExceptionWithIoRt();
+ try
+ {
+ me.ifExceptionThrowMulti();
+ assertTrue(false);
+ }
+ catch (MultiException e)
+ {
+ assertTrue(e instanceof MultiException);
+ assertTrue(e.getStackTrace().length > 0);
+ }
+
+ me = multiExceptionWithIoRt();
+ try
+ {
+ me.ifExceptionThrowRuntime();
+ assertTrue(false);
+ }
+ catch (RuntimeException e)
+ {
+ assertTrue(e.getCause() instanceof MultiException);
+ assertTrue(e.getStackTrace().length > 0);
+ }
+
+ me = multiExceptionWithRtIo();
+ try
+ {
+ me.ifExceptionThrowRuntime();
+ assertTrue(false);
+ }
+ catch (RuntimeException e)
+ {
+ assertThat(e.getCause(), instanceOf(MultiException.class));
+ assertTrue(e.getStackTrace().length > 0);
+ }
+
+ me = multiExceptionWithRtIo();
+ try
+ {
+ me.ifExceptionThrowSuppressed();
+ assertTrue(false);
+ }
+ catch (RuntimeException e)
+ {
+ assertThat(e.getCause(), is(nullValue()));
+ assertEquals(1, e.getSuppressed().length, 1);
+ assertEquals(IOException.class, e.getSuppressed()[0].getClass());
+ }
+ }
+
+ @Test
+ public void testCause() throws Exception
+ {
+ MultiException me = new MultiException();
+ IOException io = new IOException("one");
+ RuntimeException run = new RuntimeException("two");
+ me.add(io);
+ me.add(run);
+
+ try
+ {
+ me.ifExceptionThrow();
+ }
+ catch (MultiException e)
+ {
+ assertEquals(io, e.getCause());
+ assertEquals(2, e.size());
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/MultiMapTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/MultiMapTest.java
new file mode 100644
index 0000000..c9f514a
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/MultiMapTest.java
@@ -0,0 +1,617 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class MultiMapTest
+{
+ /**
+ * Tests {@link MultiMap#put(String, Object)}
+ */
+ @Test
+ public void testPut()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+
+ mm.put(key, "gzip");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip");
+ }
+
+ /**
+ * Tests {@link MultiMap#put(String, Object)}
+ */
+ @Test
+ public void testPutNullString()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+ String val = null;
+
+ mm.put(key, val);
+ assertMapSize(mm, 1);
+ assertNullValues(mm, key);
+ }
+
+ /**
+ * Tests {@link MultiMap#put(String, Object)}
+ */
+ @Test
+ public void testPutNullList()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+ List<String> vals = null;
+
+ mm.put(key, vals);
+ assertMapSize(mm, 1);
+ assertNullValues(mm, key);
+ }
+
+ /**
+ * Tests {@link MultiMap#put(String, Object)}
+ */
+ @Test
+ public void testPutReplace()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+ Object ret;
+
+ ret = mm.put(key, "gzip");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip");
+ assertNull(ret, "Should not have replaced anything");
+ Object orig = mm.get(key);
+
+ // Now replace it
+ ret = mm.put(key, "jar");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "jar");
+ assertEquals(orig, ret, "Should have replaced original");
+ }
+
+ /**
+ * Tests {@link MultiMap#putValues(String, List)}
+ */
+ @Test
+ public void testPutValuesList()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+
+ List<String> input = new ArrayList<String>();
+ input.add("gzip");
+ input.add("jar");
+ input.add("pack200");
+
+ mm.putValues(key, input);
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip", "jar", "pack200");
+ }
+
+ /**
+ * Tests {@link MultiMap#putValues(String, Object[])}
+ */
+ @Test
+ public void testPutValuesStringArray()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+
+ String[] input = {"gzip", "jar", "pack200"};
+ mm.putValues(key, input);
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip", "jar", "pack200");
+ }
+
+ /**
+ * Tests {@link MultiMap#putValues(String, List)}
+ */
+ @Test
+ public void testPutValuesVarArgs()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+
+ mm.putValues(key, "gzip", "jar", "pack200");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip", "jar", "pack200");
+ }
+
+ /**
+ * Tests {@link MultiMap#add(String, Object)}
+ */
+ @Test
+ public void testAdd()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+
+ // Setup the key
+ mm.put(key, "gzip");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip");
+
+ // Add to the key
+ mm.add(key, "jar");
+ mm.add(key, "pack200");
+
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip", "jar", "pack200");
+ }
+
+ /**
+ * Tests {@link MultiMap#addValues(String, List)}
+ */
+ @Test
+ public void testAddValuesList()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+
+ // Setup the key
+ mm.put(key, "gzip");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip");
+
+ // Add to the key
+ List<String> extras = new ArrayList<String>();
+ extras.add("jar");
+ extras.add("pack200");
+ extras.add("zip");
+ mm.addValues(key, extras);
+
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip", "jar", "pack200", "zip");
+ }
+
+ /**
+ * Tests {@link MultiMap#addValues(String, List)}
+ */
+ @Test
+ public void testAddValuesListEmpty()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+
+ // Setup the key
+ mm.put(key, "gzip");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip");
+
+ // Add to the key
+ List<String> extras = new ArrayList<String>();
+ mm.addValues(key, extras);
+
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip");
+ }
+
+ /**
+ * Tests {@link MultiMap#addValues(String, Object[])}
+ */
+ @Test
+ public void testAddValuesStringArray()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+
+ // Setup the key
+ mm.put(key, "gzip");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip");
+
+ // Add to the key
+ String[] extras = {"jar", "pack200", "zip"};
+ mm.addValues(key, extras);
+
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip", "jar", "pack200", "zip");
+ }
+
+ /**
+ * Tests {@link MultiMap#addValues(String, Object[])}
+ */
+ @Test
+ public void testAddValuesStringArrayEmpty()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+
+ // Setup the key
+ mm.put(key, "gzip");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip");
+
+ // Add to the key
+ String[] extras = new String[0];
+ mm.addValues(key, extras);
+
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip");
+ }
+
+ /**
+ * Tests {@link MultiMap#removeValue(String, Object)}
+ */
+ @Test
+ public void testRemoveValue()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+
+ // Setup the key
+ mm.putValues(key, "gzip", "jar", "pack200");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip", "jar", "pack200");
+
+ // Remove a value
+ mm.removeValue(key, "jar");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip", "pack200");
+ }
+
+ /**
+ * Tests {@link MultiMap#removeValue(String, Object)}
+ */
+ @Test
+ public void testRemoveValueInvalidItem()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+
+ // Setup the key
+ mm.putValues(key, "gzip", "jar", "pack200");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip", "jar", "pack200");
+
+ // Remove a value that isn't there
+ mm.removeValue(key, "msi");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip", "jar", "pack200");
+ }
+
+ /**
+ * Tests {@link MultiMap#removeValue(String, Object)}
+ */
+ @Test
+ public void testRemoveValueAllItems()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+
+ // Setup the key
+ mm.putValues(key, "gzip", "jar", "pack200");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip", "jar", "pack200");
+
+ // Remove a value
+ mm.removeValue(key, "jar");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "gzip", "pack200");
+
+ // Remove another value
+ mm.removeValue(key, "gzip");
+ assertMapSize(mm, 1);
+ assertValues(mm, key, "pack200");
+
+ // Remove last value
+ mm.removeValue(key, "pack200");
+ assertMapSize(mm, 0); // should be empty now
+ }
+
+ /**
+ * Tests {@link MultiMap#removeValue(String, Object)}
+ */
+ @Test
+ public void testRemoveValueFromEmpty()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ String key = "formats";
+
+ // Setup the key
+ mm.putValues(key, new String[0]);
+ assertMapSize(mm, 1);
+ assertEmptyValues(mm, key);
+
+ // Remove a value that isn't in the underlying values
+ mm.removeValue(key, "jar");
+ assertMapSize(mm, 1);
+ assertEmptyValues(mm, key);
+ }
+
+ /**
+ * Tests {@link MultiMap#putAll(java.util.Map)}
+ */
+ @Test
+ public void testPutAllMap()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ assertMapSize(mm, 0); // Shouldn't have anything yet.
+
+ Map<String, String> input = new HashMap<String, String>();
+ input.put("food", "apple");
+ input.put("color", "red");
+ input.put("amount", "bushel");
+
+ mm.putAllValues(input);
+
+ assertMapSize(mm, 3);
+ assertValues(mm, "food", "apple");
+ assertValues(mm, "color", "red");
+ assertValues(mm, "amount", "bushel");
+ }
+
+ /**
+ * Tests {@link MultiMap#putAll(java.util.Map)}
+ */
+ @Test
+ public void testPutAllMultiMapSimple()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ assertMapSize(mm, 0); // Shouldn't have anything yet.
+
+ MultiMap<String> input = new MultiMap<>();
+ input.put("food", "apple");
+ input.put("color", "red");
+ input.put("amount", "bushel");
+
+ mm.putAll(input);
+
+ assertMapSize(mm, 3);
+ assertValues(mm, "food", "apple");
+ assertValues(mm, "color", "red");
+ assertValues(mm, "amount", "bushel");
+ }
+
+ /**
+ * Tests {@link MultiMap#putAll(java.util.Map)}
+ */
+ @Test
+ public void testPutAllMultiMapComplex()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+
+ assertMapSize(mm, 0); // Shouldn't have anything yet.
+
+ MultiMap<String> input = new MultiMap<>();
+ input.putValues("food", "apple", "cherry", "raspberry");
+ input.put("color", "red");
+ input.putValues("amount", "bushel", "pint");
+
+ mm.putAll(input);
+
+ assertMapSize(mm, 3);
+ assertValues(mm, "food", "apple", "cherry", "raspberry");
+ assertValues(mm, "color", "red");
+ assertValues(mm, "amount", "bushel", "pint");
+ }
+
+ /**
+ * Tests {@link MultiMap#toStringArrayMap()}
+ */
+ @Test
+ public void testToStringArrayMap()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+ mm.putValues("food", "apple", "cherry", "raspberry");
+ mm.put("color", "red");
+ mm.putValues("amount", "bushel", "pint");
+
+ assertMapSize(mm, 3);
+
+ Map<String, String[]> sam = mm.toStringArrayMap();
+ assertEquals(3, sam.size(), "String Array Map.size");
+
+ assertArray("toStringArrayMap(food)", sam.get("food"), "apple", "cherry", "raspberry");
+ assertArray("toStringArrayMap(color)", sam.get("color"), "red");
+ assertArray("toStringArrayMap(amount)", sam.get("amount"), "bushel", "pint");
+ }
+
+ /**
+ * Tests {@link MultiMap#toString()}
+ */
+ @Test
+ public void testToString()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+ mm.put("color", "red");
+
+ assertEquals("{color=red}", mm.toString());
+
+ mm.putValues("food", "apple", "cherry", "raspberry");
+
+ String expected1 = "{color=red, food=[apple, cherry, raspberry]}";
+ String expected2 = "{food=[apple, cherry, raspberry], color=red}";
+ String actual = mm.toString();
+ assertTrue(actual.equals(expected1) || actual.equals(expected2));
+ }
+
+ /**
+ * Tests {@link MultiMap#clear()}
+ */
+ @Test
+ public void testClear()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+ mm.putValues("food", "apple", "cherry", "raspberry");
+ mm.put("color", "red");
+ mm.putValues("amount", "bushel", "pint");
+
+ assertMapSize(mm, 3);
+
+ mm.clear();
+
+ assertMapSize(mm, 0);
+ }
+
+ /**
+ * Tests {@link MultiMap#containsKey(Object)}
+ */
+ @Test
+ public void testContainsKey()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+ mm.putValues("food", "apple", "cherry", "raspberry");
+ mm.put("color", "red");
+ mm.putValues("amount", "bushel", "pint");
+
+ assertTrue(mm.containsKey("color"), "Contains Key [color]");
+ assertFalse(mm.containsKey("nutrition"), "Contains Key [nutrition]");
+ }
+
+ /**
+ * Tests {@link MultiMap#containsSimpleValue(Object)}
+ */
+ @Test
+ public void testContainsSimpleValue()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+ mm.putValues("food", "apple", "cherry", "raspberry");
+ mm.put("color", "red");
+ mm.putValues("amount", "bushel", "pint");
+
+ assertTrue(mm.containsSimpleValue("red"), "Contains Value [red]");
+ assertFalse(mm.containsValue("nutrition"), "Contains Value [nutrition]");
+ }
+
+ /**
+ * Tests {@link MultiMap#containsValue(Object)}
+ */
+ @Test
+ public void testContainsValue()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+ mm.putValues("food", "apple", "cherry", "raspberry");
+ mm.put("color", "red");
+ mm.putValues("amount", "bushel", "pint");
+
+ List<String> acr = new ArrayList<>();
+ acr.add("apple");
+ acr.add("cherry");
+ acr.add("raspberry");
+ assertTrue(mm.containsValue(acr), "Contains Value [apple,cherry,raspberry]");
+ assertFalse(mm.containsValue("nutrition"), "Contains Value [nutrition]");
+ }
+
+ /**
+ * Tests {@link MultiMap#containsValue(Object)}
+ */
+ @Test
+ public void testContainsValueLazyList()
+ {
+ MultiMap<String> mm = new MultiMap<>();
+ mm.putValues("food", "apple", "cherry", "raspberry");
+ mm.put("color", "red");
+ mm.putValues("amount", "bushel", "pint");
+
+ Object list = LazyList.add(null, "bushel");
+ list = LazyList.add(list, "pint");
+
+ assertTrue(mm.containsValue(list), "Contains Value [" + list + "]");
+ }
+
+ private void assertArray(String prefix, Object[] actualValues, Object... expectedValues)
+ {
+ assertEquals(expectedValues.length, actualValues.length, prefix + ".size");
+ int len = actualValues.length;
+ for (int i = 0; i < len; i++)
+ {
+ assertEquals(expectedValues[i], actualValues[i], prefix + "[" + i + "]");
+ }
+ }
+
+ private void assertValues(MultiMap<String> mm, String key, Object... expectedValues)
+ {
+ List<String> values = mm.getValues(key);
+
+ String prefix = "MultiMap.getValues(" + key + ")";
+
+ assertThat(prefix + ".size", values.size(), is(expectedValues.length));
+ int len = expectedValues.length;
+ for (int i = 0; i < len; i++)
+ {
+ if (expectedValues[i] == null)
+ {
+ assertThat(prefix + "[" + i + "]", values.get(i), is(nullValue()));
+ }
+ else
+ {
+ assertThat(prefix + "[" + i + "]", values.get(i), is(expectedValues[i]));
+ }
+ }
+ }
+
+ private void assertNullValues(MultiMap<String> mm, String key)
+ {
+ List<String> values = mm.getValues(key);
+
+ String prefix = "MultiMap.getValues(" + key + ")";
+
+ assertThat(prefix + ".size", values, nullValue());
+ }
+
+ private void assertEmptyValues(MultiMap<String> mm, String key)
+ {
+ List<String> values = mm.getValues(key);
+
+ String prefix = "MultiMap.getValues(" + key + ")";
+
+ assertEquals(0, LazyList.size(values), prefix + ".size");
+ }
+
+ private void assertMapSize(MultiMap<String> mm, int expectedSize)
+ {
+ assertEquals(expectedSize, mm.size(), "MultiMap.size");
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java
new file mode 100644
index 0000000..4671981
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java
@@ -0,0 +1,1081 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.MultipartConfigElement;
+import javax.servlet.ReadListener;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.Part;
+
+import org.eclipse.jetty.util.MultiPartInputStreamParser.MultiPart;
+import org.eclipse.jetty.util.MultiPartInputStreamParser.NonCompliance;
+import org.junit.jupiter.api.Test;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * MultiPartInputStreamTest
+ */
+@SuppressWarnings("deprecation")
+public class MultiPartInputStreamTest
+{
+ private static final String FILENAME = "stuff.txt";
+ protected String _contentType = "multipart/form-data, boundary=AaB03x";
+ protected String _multi = createMultipartRequestString(FILENAME);
+ protected String _dirname = System.getProperty("java.io.tmpdir") + File.separator + "myfiles-" + TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ protected File _tmpDir = new File(_dirname);
+
+ public MultiPartInputStreamTest()
+ {
+ _tmpDir.deleteOnExit();
+ }
+
+ @Test
+ public void testBadMultiPartRequest()
+ throws Exception
+ {
+ String boundary = "X0Y0";
+ String str = "--" + boundary + "\r\n" +
+ "Content-Disposition: form-data; name=\"fileup\"; filename=\"test.upload\"\r\n" +
+ "Content-Type: application/octet-stream\r\n\r\n" +
+ "How now brown cow." +
+ "\r\n--" + boundary + "-\r\n\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(str.getBytes()),
+ "multipart/form-data, boundary=" + boundary,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ try
+ {
+ mpis.getParts();
+ fail("Multipart incomplete");
+ }
+ catch (IOException e)
+ {
+ assertTrue(e.getMessage().startsWith("Incomplete"));
+ }
+ }
+
+ @Test
+ public void testFinalBoundaryOnly()
+ throws Exception
+ {
+ String delimiter = "\r\n";
+ final String boundary = "MockMultiPartTestBoundary";
+
+ // Malformed multipart request body containing only an arbitrary string of text, followed by the final boundary marker, delimited by empty lines.
+ String str =
+ delimiter +
+ "Hello world" +
+ delimiter + // Two delimiter markers, which make an empty line.
+ delimiter +
+ "--" + boundary + "--" + delimiter;
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(str.getBytes()),
+ "multipart/form-data, boundary=" + boundary,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertTrue(mpis.getParts().isEmpty());
+ assertEquals(EnumSet.noneOf(NonCompliance.class), mpis.getNonComplianceWarnings());
+ }
+
+ @Test
+ public void testEmpty()
+ throws Exception
+ {
+ String delimiter = "\r\n";
+ final String boundary = "MockMultiPartTestBoundary";
+
+ String str =
+ delimiter +
+ "--" + boundary + "--" + delimiter;
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(str.getBytes()),
+ "multipart/form-data, boundary=" + boundary,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ assertTrue(mpis.getParts().isEmpty());
+ assertEquals(EnumSet.noneOf(NonCompliance.class), mpis.getNonComplianceWarnings());
+ }
+
+ @Test
+ public void testNoBoundaryRequest()
+ throws Exception
+ {
+ String str = "--\r\n" +
+ "Content-Disposition: form-data; name=\"fileName\"\r\n" +
+ "Content-Type: text/plain; charset=US-ASCII\r\n" +
+ "Content-Transfer-Encoding: 8bit\r\n" +
+ "\r\n" +
+ "abc\r\n" +
+ "--\r\n" +
+ "Content-Disposition: form-data; name=\"desc\"\r\n" +
+ "Content-Type: text/plain; charset=US-ASCII\r\n" +
+ "Content-Transfer-Encoding: 8bit\r\n" +
+ "\r\n" +
+ "123\r\n" +
+ "--\r\n" +
+ "Content-Disposition: form-data; name=\"title\"\r\n" +
+ "Content-Type: text/plain; charset=US-ASCII\r\n" +
+ "Content-Transfer-Encoding: 8bit\r\n" +
+ "\r\n" +
+ "ttt\r\n" +
+ "--\r\n" +
+ "Content-Disposition: form-data; name=\"datafile5239138112980980385.txt\"; filename=\"datafile5239138112980980385.txt\"\r\n" +
+ "Content-Type: application/octet-stream; charset=ISO-8859-1\r\n" +
+ "Content-Transfer-Encoding: binary\r\n" +
+ "\r\n" +
+ "000\r\n" +
+ "----\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(str.getBytes()),
+ "multipart/form-data",
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(4));
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ Part fileName = mpis.getPart("fileName");
+ assertThat(fileName, notNullValue());
+ assertThat(fileName.getSize(), is(3L));
+ IO.copy(fileName.getInputStream(), baos);
+ assertThat(baos.toString("US-ASCII"), is("abc"));
+
+ baos = new ByteArrayOutputStream();
+ Part desc = mpis.getPart("desc");
+ assertThat(desc, notNullValue());
+ assertThat(desc.getSize(), is(3L));
+ IO.copy(desc.getInputStream(), baos);
+ assertThat(baos.toString("US-ASCII"), is("123"));
+
+ baos = new ByteArrayOutputStream();
+ Part title = mpis.getPart("title");
+ assertThat(title, notNullValue());
+ assertThat(title.getSize(), is(3L));
+ IO.copy(title.getInputStream(), baos);
+ assertThat(baos.toString("US-ASCII"), is("ttt"));
+
+ assertEquals(EnumSet.noneOf(NonCompliance.class), mpis.getNonComplianceWarnings());
+ }
+
+ @Test
+ public void testNonMultiPartRequest()
+ throws Exception
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(_multi.getBytes()),
+ "Content-type: text/plain",
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ assertTrue(mpis.getParts().isEmpty());
+ assertEquals(EnumSet.noneOf(NonCompliance.class), mpis.getNonComplianceWarnings());
+ }
+
+ @Test
+ public void testNoBody()
+ throws Exception
+ {
+ String body = "";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(body.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ try
+ {
+ mpis.getParts();
+ fail("Multipart missing body");
+ }
+ catch (IOException e)
+ {
+ assertTrue(e.getMessage().startsWith("Missing content"));
+ }
+ }
+
+ @Test
+ public void testBodyAlreadyConsumed()
+ throws Exception
+ {
+ ServletInputStream is = new ServletInputStream()
+ {
+
+ @Override
+ public boolean isFinished()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean isReady()
+ {
+ return false;
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener)
+ {
+ }
+
+ @Override
+ public int read() throws IOException
+ {
+ return 0;
+ }
+ };
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(is,
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertEquals(0, parts.size());
+ }
+
+ @Test
+ public void testWhitespaceBodyWithCRLF()
+ throws Exception
+ {
+ String whitespace = " \n\n\n\r\n\r\n\r\n\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(whitespace.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ try
+ {
+ mpis.getParts();
+ fail("Multipart missing body");
+ }
+ catch (IOException e)
+ {
+ assertTrue(e.getMessage().startsWith("Missing initial"));
+ }
+ }
+
+ @Test
+ public void testWhitespaceBody()
+ throws Exception
+ {
+ String whitespace = " ";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(whitespace.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ try
+ {
+ mpis.getParts();
+ fail("Multipart missing body");
+ }
+ catch (IOException e)
+ {
+ assertTrue(e.getMessage().startsWith("Missing initial"));
+ }
+ }
+
+ @SuppressWarnings("Duplicates")
+ @Test
+ public void testLeadingWhitespaceBodyWithCRLF()
+ throws Exception
+ {
+ String body = " \n\n\n\r\n\r\n\r\n\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"field1\"\r\n" +
+ "\r\n" +
+ "Joe Blow\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"" + "foo.txt" + "\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + "aaaa" +
+ "bbbbb" + "\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(body.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts, notNullValue());
+ assertThat(parts.size(), is(2));
+
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
+ {
+ Part field1 = mpis.getPart("field1");
+ assertThat(field1, notNullValue());
+ IO.copy(field1.getInputStream(), baos);
+ assertThat(baos.toString("US-ASCII"), is("Joe Blow"));
+ }
+
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
+ {
+ Part stuff = mpis.getPart("stuff");
+ assertThat(stuff, notNullValue());
+ IO.copy(stuff.getInputStream(), baos);
+ assertThat(baos.toString("US-ASCII"), containsString("aaaa"));
+ }
+
+ assertEquals(EnumSet.of(NonCompliance.LF_LINE_TERMINATION), mpis.getNonComplianceWarnings());
+ }
+
+ @Test
+ public void testLeadingWhitespaceBodyWithoutCRLF()
+ throws Exception
+ {
+ String body = " " +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"field1\"\r\n" +
+ "\r\n" +
+ "Joe Blow\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"" + "foo.txt" + "\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + "aaaa" +
+ "bbbbb" + "\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(body.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts, notNullValue());
+ assertThat(parts.size(), is(2));
+
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
+ {
+ Part field1 = mpis.getPart("field1");
+ assertThat(field1, notNullValue());
+ IO.copy(field1.getInputStream(), baos);
+ assertThat(baos.toString("US-ASCII"), is("Joe Blow"));
+ }
+
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
+ {
+ Part stuff = mpis.getPart("stuff");
+ assertThat(stuff, notNullValue());
+ IO.copy(stuff.getInputStream(), baos);
+ assertThat(baos.toString("US-ASCII"), containsString("bbbbb"));
+ }
+
+ assertEquals(EnumSet.of(NonCompliance.NO_CRLF_AFTER_PREAMBLE), mpis.getNonComplianceWarnings());
+ }
+
+ @Test
+ public void testNoLimits()
+ throws Exception
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(_multi.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertFalse(parts.isEmpty());
+ }
+
+ @Test
+ public void testRequestTooBig()
+ throws Exception
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 60, 100, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(_multi.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = null;
+ try
+ {
+ parts = mpis.getParts();
+ fail("Request should have exceeded maxRequestSize");
+ }
+ catch (IllegalStateException e)
+ {
+ assertTrue(e.getMessage().startsWith("Request exceeds maxRequestSize"));
+ }
+ }
+
+ @Test
+ public void testRequestTooBigThrowsErrorOnGetParts()
+ throws Exception
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 60, 100, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(_multi.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = null;
+
+ //cause parsing
+ try
+ {
+ parts = mpis.getParts();
+ fail("Request should have exceeded maxRequestSize");
+ }
+ catch (IllegalStateException e)
+ {
+ assertTrue(e.getMessage().startsWith("Request exceeds maxRequestSize"));
+ }
+
+ //try again
+ try
+ {
+ parts = mpis.getParts();
+ fail("Request should have exceeded maxRequestSize");
+ }
+ catch (IllegalStateException e)
+ {
+ assertTrue(e.getMessage().startsWith("Request exceeds maxRequestSize"));
+ }
+ }
+
+ @Test
+ public void testFileTooBig()
+ throws Exception
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 40, 1024, 30);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(_multi.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = null;
+ try
+ {
+ parts = mpis.getParts();
+ fail("stuff.txt should have been larger than maxFileSize");
+ }
+ catch (IllegalStateException e)
+ {
+ assertTrue(e.getMessage().startsWith("Multipart Mime part"));
+ }
+ }
+
+ @Test
+ public void testFileTooBigThrowsErrorOnGetParts()
+ throws Exception
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 40, 1024, 30);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(_multi.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = null;
+ try
+ {
+ parts = mpis.getParts(); //caused parsing
+ fail("stuff.txt should have been larger than maxFileSize");
+ }
+ catch (IllegalStateException e)
+ {
+ assertTrue(e.getMessage().startsWith("Multipart Mime part"));
+ }
+
+ //test again after the parsing
+ try
+ {
+ parts = mpis.getParts(); //caused parsing
+ fail("stuff.txt should have been larger than maxFileSize");
+ }
+ catch (IllegalStateException e)
+ {
+ assertTrue(e.getMessage().startsWith("Multipart Mime part"));
+ }
+ }
+
+ @Test
+ public void testPartFileNotDeleted() throws Exception
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(createMultipartRequestString("tptfd").getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+
+ MultiPart part = (MultiPart)mpis.getPart("stuff");
+ File stuff = ((MultiPartInputStreamParser.MultiPart)part).getFile();
+ assertThat(stuff, notNullValue()); // longer than 100 bytes, should already be a tmp file
+ part.write("tptfd.txt");
+ File tptfd = new File(_dirname + File.separator + "tptfd.txt");
+ assertThat(tptfd.exists(), is(true));
+ assertThat(stuff.exists(), is(false)); //got renamed
+ part.cleanUp();
+ assertThat(tptfd.exists(), is(true)); //explicitly written file did not get removed after cleanup
+ tptfd.deleteOnExit(); //clean up test
+ }
+
+ @Test
+ public void testPartTmpFileDeletion() throws Exception
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(createMultipartRequestString("tptfd").getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+
+ MultiPart part = (MultiPart)mpis.getPart("stuff");
+ File stuff = ((MultiPartInputStreamParser.MultiPart)part).getFile();
+ assertThat(stuff, notNullValue()); // longer than 100 bytes, should already be a tmp file
+ assertThat(stuff.exists(), is(true));
+ part.cleanUp();
+ assertThat(stuff.exists(), is(false)); //tmp file was removed after cleanup
+ }
+
+ @Test
+ public void testLFOnlyRequest()
+ throws Exception
+ {
+ String str = "--AaB03x\n" +
+ "content-disposition: form-data; name=\"field1\"\n" +
+ "\n" +
+ "Joe Blow\n" +
+ "--AaB03x\n" +
+ "content-disposition: form-data; name=\"field2\"\n" +
+ "\n" +
+ "Other\n" +
+ "--AaB03x--\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(str.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(2));
+ Part p1 = mpis.getPart("field1");
+ assertThat(p1, notNullValue());
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ IO.copy(p1.getInputStream(), baos);
+ assertThat(baos.toString("UTF-8"), is("Joe Blow"));
+
+ Part p2 = mpis.getPart("field2");
+ assertThat(p2, notNullValue());
+ baos = new ByteArrayOutputStream();
+ IO.copy(p2.getInputStream(), baos);
+ assertThat(baos.toString("UTF-8"), is("Other"));
+
+ assertEquals(EnumSet.of(NonCompliance.LF_LINE_TERMINATION), mpis.getNonComplianceWarnings());
+ }
+
+ @Test
+ public void testCROnlyRequest()
+ throws Exception
+ {
+ String str = "--AaB03x\r" +
+ "content-disposition: form-data; name=\"field1\"\r" +
+ "\r" +
+ "Joe Blow\r" +
+ "--AaB03x\r" +
+ "content-disposition: form-data; name=\"field2\"\r" +
+ "\r" +
+ "Other\r" +
+ "--AaB03x--\r";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(str.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(2));
+
+ assertThat(parts.size(), is(2));
+ Part p1 = mpis.getPart("field1");
+ assertThat(p1, notNullValue());
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ IO.copy(p1.getInputStream(), baos);
+ assertThat(baos.toString("UTF-8"), is("Joe Blow"));
+
+ Part p2 = mpis.getPart("field2");
+ assertThat(p2, notNullValue());
+ baos = new ByteArrayOutputStream();
+ IO.copy(p2.getInputStream(), baos);
+ assertThat(baos.toString("UTF-8"), is("Other"));
+
+ assertEquals(EnumSet.of(NonCompliance.CR_LINE_TERMINATION), mpis.getNonComplianceWarnings());
+ }
+
+ @Test
+ public void testCRandLFMixRequest()
+ throws Exception
+ {
+ String str = "--AaB03x\r" +
+ "content-disposition: form-data; name=\"field1\"\r" +
+ "\r" +
+ "\nJoe Blow\n" +
+ "\r" +
+ "--AaB03x\r" +
+ "content-disposition: form-data; name=\"field2\"\r" +
+ "\r" +
+ "Other\r" +
+ "--AaB03x--\r";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(str.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(2));
+
+ Part p1 = mpis.getPart("field1");
+ assertThat(p1, notNullValue());
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ IO.copy(p1.getInputStream(), baos);
+ assertThat(baos.toString("UTF-8"), is("\nJoe Blow\n"));
+
+ Part p2 = mpis.getPart("field2");
+ assertThat(p2, notNullValue());
+ baos = new ByteArrayOutputStream();
+ IO.copy(p2.getInputStream(), baos);
+ assertThat(baos.toString("UTF-8"), is("Other"));
+
+ assertEquals(EnumSet.of(NonCompliance.CR_LINE_TERMINATION), mpis.getNonComplianceWarnings());
+ }
+
+ @Test
+ public void testBufferOverflowNoCRLF() throws Exception
+ {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ baos.write("--AaB03x".getBytes());
+ for (int i = 0; i < 8500; i++) //create content that will overrun default buffer size of BufferedInputStream
+ {
+ baos.write('a');
+ }
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(baos.toByteArray()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ try
+ {
+ mpis.getParts();
+ fail("Multipart buffer overrun");
+ }
+ catch (IOException e)
+ {
+ assertTrue(e.getMessage().startsWith("Buffer size exceeded"));
+ }
+ }
+
+ @Test
+ public void testCharsetEncoding() throws Exception
+ {
+ String contentType = "multipart/form-data; boundary=TheBoundary; charset=ISO-8859-1";
+ String str = "--TheBoundary\r" +
+ "content-disposition: form-data; name=\"field1\"\r" +
+ "\r" +
+ "\nJoe Blow\n" +
+ "\r" +
+ "--TheBoundary--\r";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(str.getBytes()),
+ contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(1));
+ }
+
+ @Test
+ public void testBadlyEncodedFilename() throws Exception
+ {
+
+ String contents = "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"" + "Taken on Aug 22 \\ 2012.jpg" + "\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + "stuff" +
+ "aaa" + "\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(contents.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(1));
+ assertThat(((MultiPartInputStreamParser.MultiPart)parts.iterator().next()).getSubmittedFileName(), is("Taken on Aug 22 \\ 2012.jpg"));
+ }
+
+ @Test
+ public void testBadlyEncodedMSFilename() throws Exception
+ {
+
+ String contents = "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"" + "c:\\this\\really\\is\\some\\path\\to\\a\\file.txt" + "\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + "stuff" +
+ "aaa" + "\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(contents.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(1));
+ assertThat(((MultiPartInputStreamParser.MultiPart)parts.iterator().next()).getSubmittedFileName(), is("c:\\this\\really\\is\\some\\path\\to\\a\\file.txt"));
+ }
+
+ @Test
+ public void testCorrectlyEncodedMSFilename() throws Exception
+ {
+ String contents = "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"" + "c:\\\\this\\\\really\\\\is\\\\some\\\\path\\\\to\\\\a\\\\file.txt" + "\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + "stuff" +
+ "aaa" + "\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(contents.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(1));
+ assertThat(((MultiPartInputStreamParser.MultiPart)parts.iterator().next()).getSubmittedFileName(), is("c:\\this\\really\\is\\some\\path\\to\\a\\file.txt"));
+ }
+
+ public void testMulti()
+ throws Exception
+ {
+ testMulti(FILENAME);
+ }
+
+ @Test
+ public void testMultiWithSpaceInFilename() throws Exception
+ {
+ testMulti("stuff with spaces.txt");
+ }
+
+ @Test
+ public void testWriteFilesIfContentDispositionFilename()
+ throws Exception
+ {
+ String s = "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"field1\"; filename=\"frooble.txt\"\r\n" +
+ "\r\n" +
+ "Joe Blow\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + "sss" +
+ "aaa" + "\r\n" +
+ "--AaB03x--\r\n";
+ //all default values for multipartconfig, ie file size threshold 0
+ MultipartConfigElement config = new MultipartConfigElement(_dirname);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(s.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ mpis.setWriteFilesWithFilenames(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(2));
+ Part field1 = mpis.getPart("field1"); //has a filename, should be written to a file
+ File f = ((MultiPartInputStreamParser.MultiPart)field1).getFile();
+ assertThat(f, notNullValue()); // longer than 100 bytes, should already be a tmp file
+
+ Part stuff = mpis.getPart("stuff");
+ f = ((MultiPartInputStreamParser.MultiPart)stuff).getFile(); //should only be in memory, no filename
+ assertThat(f, nullValue());
+ }
+
+ private void testMulti(String filename) throws IOException, ServletException, InterruptedException
+ {
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(createMultipartRequestString(filename).getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertThat(parts.size(), is(2));
+ Part field1 = mpis.getPart("field1"); //field 1 too small to go into tmp file, should be in internal buffer
+ assertThat(field1, notNullValue());
+ assertThat(field1.getName(), is("field1"));
+ InputStream is = field1.getInputStream();
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ IO.copy(is, os);
+ assertEquals("Joe Blow", new String(os.toByteArray()));
+ assertEquals(8, field1.getSize());
+
+ assertNotNull(((MultiPartInputStreamParser.MultiPart)field1).getBytes()); //in internal buffer
+ field1.write("field1.txt");
+ assertNull(((MultiPartInputStreamParser.MultiPart)field1).getBytes()); //no longer in internal buffer
+ File f = new File(_dirname + File.separator + "field1.txt");
+ assertTrue(f.exists());
+ field1.write("another_field1.txt"); //write after having already written
+ File f2 = new File(_dirname + File.separator + "another_field1.txt");
+ assertTrue(f2.exists());
+ assertFalse(f.exists()); //should have been renamed
+ field1.delete(); //file should be deleted
+ assertFalse(f.exists()); //original file was renamed
+ assertFalse(f2.exists()); //2nd written file was explicitly deleted
+
+ MultiPart stuff = (MultiPart)mpis.getPart("stuff");
+ assertThat(stuff.getSubmittedFileName(), is(filename));
+ assertThat(stuff.getContentType(), is("text/plain"));
+ assertThat(stuff.getHeader("Content-Type"), is("text/plain"));
+ assertThat(stuff.getHeaders("content-type").size(), is(1));
+ assertThat(stuff.getHeader("content-disposition"), is("form-data; name=\"stuff\"; filename=\"" + filename + "\""));
+ assertThat(stuff.getHeaderNames().size(), is(2));
+ assertThat(stuff.getSize(), is(51L));
+
+ File tmpfile = ((MultiPartInputStreamParser.MultiPart)stuff).getFile();
+ assertThat(tmpfile, notNullValue()); // longer than 100 bytes, should already be a tmp file
+ assertThat(((MultiPartInputStreamParser.MultiPart)stuff).getBytes(), nullValue()); //not in an internal buffer
+ assertThat(tmpfile.exists(), is(true));
+ assertThat(tmpfile.getName(), is(not("stuff with space.txt")));
+ stuff.write(filename);
+ f = new File(_dirname + File.separator + filename);
+ assertThat(f.exists(), is(true));
+ assertThat(tmpfile.exists(), is(false));
+ try
+ {
+ stuff.getInputStream();
+ }
+ catch (Exception e)
+ {
+ fail("Part.getInputStream() after file rename operation");
+ }
+ f.deleteOnExit(); //clean up after test
+ }
+
+ @Test
+ public void testMultiSameNames()
+ throws Exception
+ {
+ String sameNames = "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"stuff1.txt\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "00000\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"stuff2.txt\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "110000000000000000000000000000000000000000000000000\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(sameNames.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertEquals(2, parts.size());
+ for (Part p : parts)
+ {
+ assertEquals("stuff", p.getName());
+ }
+
+ //if they all have the name name, then only retrieve the first one
+ Part p = mpis.getPart("stuff");
+ assertNotNull(p);
+ assertEquals(5, p.getSize());
+ }
+
+ @Test
+ public void testBase64EncodedContent() throws Exception
+ {
+ String contentWithEncodedPart =
+ "--AaB03x\r\n" +
+ "Content-disposition: form-data; name=\"other\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "other" + "\r\n" +
+ "--AaB03x\r\n" +
+ "Content-disposition: form-data; name=\"stuff\"; filename=\"stuff.txt\"\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "Content-Type: application/octet-stream\r\n" +
+ "\r\n" +
+ Base64.getEncoder().encodeToString("hello jetty".getBytes(ISO_8859_1)) + "\r\n" +
+ "--AaB03x\r\n" +
+ "Content-disposition: form-data; name=\"final\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "the end" + "\r\n" +
+ "--AaB03x--\r\n";
+
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(contentWithEncodedPart.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertEquals(3, parts.size());
+
+ Part p1 = mpis.getPart("other");
+ assertNotNull(p1);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ IO.copy(p1.getInputStream(), baos);
+ assertEquals("other", baos.toString("US-ASCII"));
+
+ Part p2 = mpis.getPart("stuff");
+ assertNotNull(p2);
+ baos = new ByteArrayOutputStream();
+ IO.copy(p2.getInputStream(), baos);
+ assertEquals("hello jetty", baos.toString("US-ASCII"));
+
+ Part p3 = mpis.getPart("final");
+ assertNotNull(p3);
+ baos = new ByteArrayOutputStream();
+ IO.copy(p3.getInputStream(), baos);
+ assertEquals("the end", baos.toString("US-ASCII"));
+
+ assertEquals(EnumSet.of(NonCompliance.BASE64_TRANSFER_ENCODING), mpis.getNonComplianceWarnings());
+ }
+
+ @Test
+ public void testQuotedPrintableEncoding() throws Exception
+ {
+ String contentWithEncodedPart =
+ "--AaB03x\r\n" +
+ "Content-disposition: form-data; name=\"other\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "other" + "\r\n" +
+ "--AaB03x\r\n" +
+ "Content-disposition: form-data; name=\"stuff\"; filename=\"stuff.txt\"\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "truth=3Dbeauty" + "\r\n" +
+ "--AaB03x--\r\n";
+ MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
+ MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(contentWithEncodedPart.getBytes()),
+ _contentType,
+ config,
+ _tmpDir);
+ mpis.setDeleteOnExit(true);
+ Collection<Part> parts = mpis.getParts();
+ assertEquals(2, parts.size());
+
+ Part p1 = mpis.getPart("other");
+ assertNotNull(p1);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ IO.copy(p1.getInputStream(), baos);
+ assertEquals("other", baos.toString("US-ASCII"));
+
+ Part p2 = mpis.getPart("stuff");
+ assertNotNull(p2);
+ baos = new ByteArrayOutputStream();
+ IO.copy(p2.getInputStream(), baos);
+ assertEquals("truth=beauty", baos.toString("US-ASCII"));
+
+ assertEquals(EnumSet.of(NonCompliance.QUOTED_PRINTABLE_TRANSFER_ENCODING), mpis.getNonComplianceWarnings());
+ }
+
+ private String createMultipartRequestString(String filename)
+ {
+ int length = filename.length();
+ String name = filename;
+ if (length > 10)
+ name = filename.substring(0, 10);
+ StringBuffer filler = new StringBuffer();
+ int i = name.length();
+ while (i < 51)
+ {
+ filler.append("0");
+ i++;
+ }
+
+ return "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"field1\"; filename=\"frooble.txt\"\r\n" +
+ "\r\n" +
+ "Joe Blow\r\n" +
+ "--AaB03x\r\n" +
+ "content-disposition: form-data; name=\"stuff\"; filename=\"" + filename + "\"\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" + name +
+ filler.toString() + "\r\n" +
+ "--AaB03x--\r\n";
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/MultiReleaseJarFileTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/MultiReleaseJarFileTest.java
new file mode 100644
index 0000000..9b6805a
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/MultiReleaseJarFileTest.java
@@ -0,0 +1,137 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.stream.Collectors;
+
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.MultiReleaseJarFile.VersionedJarEntry;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnJre;
+import org.junit.jupiter.api.condition.JRE;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class MultiReleaseJarFileTest
+{
+ private File example = MavenTestingUtils.getTestResourceFile("example.jar");
+
+ @Test
+ public void testExampleJarIsMR() throws Exception
+ {
+ try (MultiReleaseJarFile jarFile = new MultiReleaseJarFile(example))
+ {
+ assertTrue(jarFile.isMultiRelease(), "Expected " + example + " to be MultiRelease JAR File");
+ }
+ }
+
+ @Test
+ public void testBase() throws Exception
+ {
+ try (MultiReleaseJarFile jarFile = new MultiReleaseJarFile(example, 8, false))
+ {
+ assertThat(jarFile.getEntry("META-INF/MANIFEST.MF").getVersion(), is(0));
+ assertThat(jarFile.getEntry("org/example/OnlyInBase.class").getVersion(), is(0));
+ assertThat(jarFile.getEntry("org/example/InBoth$InnerBase.class").getVersion(), is(0));
+ assertThat(jarFile.getEntry("org/example/InBoth$InnerBoth.class").getVersion(), is(0));
+ assertThat(jarFile.getEntry("org/example/InBoth.class").getVersion(), is(0));
+
+ assertThat(jarFile.stream().map(VersionedJarEntry::getName).collect(Collectors.toSet()),
+ Matchers.containsInAnyOrder(
+ "META-INF/MANIFEST.MF",
+ "org/example/OnlyInBase.class",
+ "org/example/InBoth$InnerBase.class",
+ "org/example/InBoth$InnerBoth.class",
+ "org/example/InBoth.class",
+ "WEB-INF/web.xml",
+ "WEB-INF/classes/App.class",
+ "WEB-INF/lib/depend.jar"));
+ }
+ }
+
+ @Test
+ public void test9() throws Exception
+ {
+ try (MultiReleaseJarFile jarFile = new MultiReleaseJarFile(example, 9, false))
+ {
+ assertThat(jarFile.getEntry("META-INF/MANIFEST.MF").getVersion(), is(0));
+ assertThat(jarFile.getEntry("org/example/OnlyInBase.class").getVersion(), is(0));
+ assertThat(jarFile.getEntry("org/example/InBoth$InnerBoth.class").getVersion(), is(9));
+ assertThat(jarFile.getEntry("org/example/InBoth.class").getVersion(), is(9));
+ assertThat(jarFile.getEntry("org/example/OnlyIn9.class").getVersion(), is(9));
+ assertThat(jarFile.getEntry("org/example/onlyIn9/OnlyIn9.class").getVersion(), is(9));
+ assertThat(jarFile.getEntry("org/example/InBoth$Inner9.class").getVersion(), is(9));
+
+ assertThat(jarFile.stream().map(VersionedJarEntry::getName).collect(Collectors.toSet()),
+ Matchers.containsInAnyOrder(
+ "META-INF/MANIFEST.MF",
+ "org/example/OnlyInBase.class",
+ "org/example/InBoth$InnerBoth.class",
+ "org/example/InBoth.class",
+ "org/example/OnlyIn9.class",
+ "org/example/onlyIn9/OnlyIn9.class",
+ "org/example/InBoth$Inner9.class",
+ "WEB-INF/web.xml",
+ "WEB-INF/classes/App.class",
+ "WEB-INF/lib/depend.jar"));
+ }
+ }
+
+ @Test
+ public void test10() throws Exception
+ {
+ try (MultiReleaseJarFile jarFile = new MultiReleaseJarFile(example, 10, false))
+ {
+ assertThat(jarFile.getEntry("META-INF/MANIFEST.MF").getVersion(), is(0));
+ assertThat(jarFile.getEntry("org/example/OnlyInBase.class").getVersion(), is(0));
+ assertThat(jarFile.getEntry("org/example/InBoth.class").getVersion(), is(10));
+ assertThat(jarFile.getEntry("org/example/In10Only.class").getVersion(), is(10));
+
+ assertThat(jarFile.stream().map(VersionedJarEntry::getName).collect(Collectors.toSet()),
+ Matchers.containsInAnyOrder(
+ "META-INF/MANIFEST.MF",
+ "org/example/OnlyInBase.class",
+ "org/example/InBoth.class",
+ "org/example/In10Only.class",
+ "WEB-INF/web.xml",
+ "WEB-INF/classes/App.class",
+ "WEB-INF/lib/depend.jar"));
+ }
+ }
+
+ @Test
+ @DisabledOnJre(JRE.JAVA_8)
+ public void testClassLoaderJava9() throws Exception
+ {
+ try (URLClassLoader loader = new URLClassLoader(new URL[]{example.toURI().toURL()}))
+ {
+ assertThat(IO.toString(loader.getResource("org/example/OnlyInBase.class").openStream()), is("org/example/OnlyInBase.class"));
+ assertThat(IO.toString(loader.getResource("org/example/OnlyIn9.class").openStream()), is("META-INF/versions/9/org/example/OnlyIn9.class"));
+ assertThat(IO.toString(loader.getResource("WEB-INF/web.xml").openStream()), is("META-INF/versions/9/WEB-INF/web.xml"));
+ assertThat(IO.toString(loader.getResource("WEB-INF/classes/App.class").openStream()), is("META-INF/versions/9/WEB-INF/classes/App.class"));
+ assertThat(IO.toString(loader.getResource("WEB-INF/lib/depend.jar").openStream()), is("META-INF/versions/9/WEB-INF/lib/depend.jar"));
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/PathWatcherDemo.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/PathWatcherDemo.java
new file mode 100644
index 0000000..d9b29a4
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/PathWatcherDemo.java
@@ -0,0 +1,134 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jetty.util.PathWatcher.PathWatchEvent;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class PathWatcherDemo implements PathWatcher.Listener
+{
+ private static final Logger LOG = Log.getLogger(PathWatcherDemo.class);
+
+ public static void main(String[] args)
+ {
+ List<Path> paths = new ArrayList<>();
+ for (String arg : args)
+ {
+ paths.add(new File(arg).toPath());
+ }
+
+ if (paths.isEmpty())
+ {
+ LOG.warn("No paths specified on command line");
+ System.exit(-1);
+ }
+
+ PathWatcherDemo demo = new PathWatcherDemo();
+ try
+ {
+ demo.run(paths);
+ }
+ catch (Throwable t)
+ {
+ LOG.warn(t);
+ }
+ }
+
+ public void run(List<Path> paths) throws Exception
+ {
+ PathWatcher watcher = new PathWatcher();
+ //watcher.addListener(new PathWatcherDemo());
+ watcher.addListener((PathWatcher.EventListListener)events ->
+ {
+ if (events == null)
+ {
+ LOG.warn("Null events received");
+ }
+ else if (events.isEmpty())
+ {
+ LOG.warn("Empty events received");
+ }
+ else
+ {
+ LOG.info("Bulk notification received");
+ for (PathWatchEvent e : events)
+ {
+ onPathWatchEvent(e);
+ }
+ }
+ });
+
+ watcher.setNotifyExistingOnStart(false);
+
+ List<String> excludes = new ArrayList<>();
+ excludes.add("glob:*.bak"); // ignore backup files
+ excludes.add("regex:^.*/\\~[^/]*$"); // ignore scratch files
+
+ for (Path path : paths)
+ {
+ if (Files.isDirectory(path))
+ {
+ PathWatcher.Config config = new PathWatcher.Config(path);
+ config.addExcludeHidden();
+ config.addExcludes(excludes);
+ config.setRecurseDepth(4);
+ watcher.watch(config);
+ }
+ else
+ {
+ watcher.watch(path);
+ }
+ }
+ watcher.start();
+
+ Thread.currentThread().join();
+ }
+
+ @Override
+ public void onPathWatchEvent(PathWatchEvent event)
+ {
+ StringBuilder msg = new StringBuilder();
+ msg.append("onPathWatchEvent: [");
+ msg.append(event.getType());
+ msg.append("] ");
+ msg.append(event.getPath());
+ msg.append(" (count=").append(event.getCount()).append(")");
+ if (Files.isRegularFile(event.getPath()))
+ {
+ try
+ {
+ String fsize = String.format(" (filesize=%,d)", Files.size(event.getPath()));
+ msg.append(fsize);
+ }
+ catch (IOException e)
+ {
+ LOG.warn("Unable to get filesize", e);
+ }
+ }
+ LOG.info("{}", msg.toString());
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/PathWatcherTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/PathWatcherTest.java
new file mode 100644
index 0000000..5bbe5a2
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/PathWatcherTest.java
@@ -0,0 +1,983 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.PathWatcher.PathWatchEvent;
+import org.eclipse.jetty.util.PathWatcher.PathWatchEventType;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.eclipse.jetty.util.PathWatcher.PathWatchEventType.ADDED;
+import static org.eclipse.jetty.util.PathWatcher.PathWatchEventType.DELETED;
+import static org.eclipse.jetty.util.PathWatcher.PathWatchEventType.MODIFIED;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(WorkDirExtension.class)
+public class PathWatcherTest
+{
+ public static final int QUIET_TIME;
+ public static final int WAIT_TIME;
+ public static final int LONG_TIME;
+
+ static
+ {
+ if (org.junit.jupiter.api.condition.OS.LINUX.isCurrentOs())
+ QUIET_TIME = 300;
+ else if (org.junit.jupiter.api.condition.OS.MAC.isCurrentOs())
+ QUIET_TIME = 5000;
+ else
+ QUIET_TIME = 1000;
+ WAIT_TIME = 2 * QUIET_TIME;
+ LONG_TIME = 5 * QUIET_TIME;
+ }
+
+ public static class PathWatchEventCapture implements PathWatcher.Listener
+ {
+ public static final String FINISH_TAG = "#finished#.tag";
+ private static final Logger LOG = Log.getLogger(PathWatcherTest.PathWatchEventCapture.class);
+ private final Path baseDir;
+
+ /**
+ * Map of relative paths seen, to their events seen (in order seen)
+ */
+ public Map<String, List<PathWatchEventType>> events = new HashMap<>();
+
+ public int latchCount = 1;
+ public CountDownLatch finishedLatch;
+ private PathWatchEventType triggerType;
+ private Path triggerPath;
+
+ public PathWatchEventCapture(Path baseDir)
+ {
+ this.baseDir = baseDir;
+ }
+
+ public void reset()
+ {
+ finishedLatch = new CountDownLatch(latchCount);
+ events.clear();
+ }
+
+ public void reset(int count)
+ {
+ setFinishTrigger(count);
+ events.clear();
+ }
+
+ @Override
+ public void onPathWatchEvent(PathWatchEvent event)
+ {
+ synchronized (events)
+ {
+ Path relativePath = this.baseDir.relativize(event.getPath());
+ String key = StringUtil.replace(relativePath.toString(), File.separatorChar, '/');
+
+ List<PathWatchEventType> types = this.events.get(key);
+ if (types == null)
+ {
+ types = new ArrayList<>();
+ }
+ types.add(event.getType());
+ this.events.put(key, types);
+ LOG.debug("Captured Event: {} | {}", event.getType(), key);
+ }
+ //if triggered by path
+ if (triggerPath != null)
+ {
+
+ if (triggerPath.equals(event.getPath()) && (event.getType() == triggerType))
+ {
+ LOG.debug("Encountered finish trigger: {} on {}", event.getType(), event.getPath());
+ finishedLatch.countDown();
+ }
+ }
+ else if (finishedLatch != null)
+ {
+ finishedLatch.countDown();
+ }
+ }
+
+ /**
+ * Validate the events seen match expectations.
+ * <p>
+ * Note: order of events is only important when looking at a specific file or directory. Events for multiple
+ * files can overlap in ways that this assertion doesn't care about.
+ *
+ * @param expectedEvents the events expected
+ */
+ public void assertEvents(Map<String, PathWatchEventType[]> expectedEvents)
+ {
+ try
+ {
+ assertThat("Event match (file|directory) count", this.events.size(), is(expectedEvents.size()));
+
+ for (Map.Entry<String, PathWatchEventType[]> entry : expectedEvents.entrySet())
+ {
+ String relativePath = entry.getKey();
+ PathWatchEventType[] expectedTypes = entry.getValue();
+ assertEvents(relativePath, expectedTypes);
+ }
+ }
+ catch (Throwable th)
+ {
+ System.err.println(this.events);
+ throw th;
+ }
+ }
+
+ /**
+ * Validate the events seen match expectations.
+ * <p>
+ * Note: order of events is only important when looking at a specific file or directory. Events for multiple
+ * files can overlap in ways that this assertion doesn't care about.
+ *
+ * @param relativePath the test relative path to look for
+ * @param expectedEvents the events expected
+ */
+ public void assertEvents(String relativePath, PathWatchEventType... expectedEvents)
+ {
+ synchronized (events)
+ {
+ List<PathWatchEventType> actualEvents = this.events.get(relativePath);
+ assertThat("Events for path [" + relativePath + "]", actualEvents, contains(expectedEvents));
+ }
+ }
+
+ /**
+ * Set the path and type that will trigger this capture to be finished
+ *
+ * @param triggerPath the trigger path we look for to know that the capture is complete
+ * @param triggerType the trigger type we look for to know that the capture is complete
+ */
+ public void setFinishTrigger(Path triggerPath, PathWatchEventType triggerType)
+ {
+ this.triggerPath = triggerPath;
+ this.triggerType = triggerType;
+ this.latchCount = 1;
+ this.finishedLatch = new CountDownLatch(1);
+ LOG.debug("Setting finish trigger {} for path {}", triggerType, triggerPath);
+ }
+
+ public void setFinishTrigger(int count)
+ {
+ latchCount = count;
+ finishedLatch = new CountDownLatch(latchCount);
+ }
+
+ /**
+ * Await the countdown latch on the finish trigger
+ *
+ * @param pathWatcher the watcher instance we are waiting on
+ * @throws IOException if unable to create the finish tag file
+ * @throws InterruptedException if unable to await the finish of the run
+ * @see #setFinishTrigger(Path, PathWatchEventType)
+ */
+ public void awaitFinish(PathWatcher pathWatcher) throws IOException, InterruptedException
+ {
+ //assertThat("Trigger Path must be set",triggerPath,notNullValue());
+ //assertThat("Trigger Type must be set",triggerType,notNullValue());
+ double multiplier = 25.0;
+ long awaitMillis = (long)((double)pathWatcher.getUpdateQuietTimeMillis() * multiplier);
+ LOG.debug("Waiting for finish ({} ms)", awaitMillis);
+ assertThat("Timed Out (" + awaitMillis + "ms) waiting for capture to finish", finishedLatch.await(awaitMillis, TimeUnit.MILLISECONDS), is(true));
+ LOG.debug("Finished capture");
+ }
+
+ @Override
+ public String toString()
+ {
+ return events.toString();
+ }
+ }
+
+ private static void updateFile(Path path, String newContents) throws IOException
+ {
+ try (FileOutputStream out = new FileOutputStream(path.toFile()))
+ {
+ out.write(newContents.getBytes(StandardCharsets.UTF_8));
+ out.flush();
+ out.getChannel().force(true);
+ out.getFD().sync();
+ }
+ }
+
+ /**
+ * Update (optionally create) a file over time.
+ * <p>
+ * The file will be created in a slowed down fashion, over the time specified.
+ *
+ * @param path the file to update / create
+ * @param timeDuration the time duration to take to create the file (approximate, not 100% accurate)
+ * @param timeUnit the time unit to take to create the file
+ * @throws IOException if unable to write file
+ * @throws InterruptedException if sleep between writes was interrupted
+ */
+ private void updateFileOverTime(Path path, int timeDuration, TimeUnit timeUnit)
+ {
+ try
+ {
+ // how long to sleep between writes
+ int sleepMs = 200;
+
+ // average chunk buffer
+ int chunkBufLen = 16;
+ byte[] chunkBuf = new byte[chunkBufLen];
+ Arrays.fill(chunkBuf, (byte)'x');
+ long end = System.nanoTime() + timeUnit.toNanos(timeDuration);
+
+ try (FileOutputStream out = new FileOutputStream(path.toFile()))
+ {
+ while (System.nanoTime() < end)
+ {
+ out.write(chunkBuf);
+ out.flush();
+ out.getChannel().force(true);
+ // Force file to actually write to disk.
+ // Skipping any sort of filesystem caching of the write
+ out.getFD().sync();
+ TimeUnit.MILLISECONDS.sleep(sleepMs);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Sleep longer than the quiet time.
+ *
+ * @param pathWatcher the path watcher to inspect for its quiet time
+ * @throws InterruptedException if unable to sleep
+ */
+ private static void awaitQuietTime(PathWatcher pathWatcher) throws InterruptedException
+ {
+ TimeUnit.MILLISECONDS.sleep(WAIT_TIME);
+ }
+
+ private static final int KB = 1024;
+ private static final int MB = KB * KB;
+
+ public WorkDir testdir;
+
+ @Test
+ public void testSequence() throws Exception
+ {
+ Path dir = testdir.getEmptyPathDir();
+
+ // Files we are interested in
+ Files.createFile(dir.resolve("file0"));
+ Files.createDirectories(dir.resolve("subdir0/subsubdir0"));
+ Files.createFile(dir.resolve("subdir0/fileA"));
+ Files.createFile(dir.resolve("subdir0/subsubdir0/unseen"));
+
+ PathWatcher pathWatcher = new PathWatcher();
+ pathWatcher.setUpdateQuietTime(QUIET_TIME, TimeUnit.MILLISECONDS);
+
+ // Add listener
+ PathWatchEventCapture capture = new PathWatchEventCapture(dir);
+ pathWatcher.addListener(capture);
+
+ // Add test dir configuration
+ PathWatcher.Config config = new PathWatcher.Config(dir);
+ config.setRecurseDepth(1);
+ pathWatcher.watch(config);
+
+ try
+ {
+ Map<String, PathWatchEventType[]> expected = new HashMap<>();
+
+ // Check initial scan events
+ capture.setFinishTrigger(4);
+ pathWatcher.start();
+ expected.put("file0", new PathWatchEventType[]{ADDED});
+ expected.put("subdir0", new PathWatchEventType[]{ADDED});
+ expected.put("subdir0/fileA", new PathWatchEventType[]{ADDED});
+ expected.put("subdir0/subsubdir0", new PathWatchEventType[]{ADDED});
+
+ capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS);
+ capture.assertEvents(expected);
+ Thread.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+
+ // Check adding files
+ capture.reset(3);
+ expected.clear();
+ Files.createFile(dir.resolve("subdir0/subsubdir0/toodeep"));
+ expected.put("subdir0/subsubdir0", new PathWatchEventType[]{MODIFIED});
+ Files.createFile(dir.resolve("file1"));
+ expected.put("file1", new PathWatchEventType[]{ADDED});
+ Files.createFile(dir.resolve("subdir0/fileB"));
+ expected.put("subdir0/fileB", new PathWatchEventType[]{ADDED});
+
+ capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS);
+ capture.assertEvents(expected);
+ Thread.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+
+ // Check slow modification
+ capture.reset(1);
+ expected.clear();
+ long start = System.nanoTime();
+ new Thread(() ->
+ {
+ updateFileOverTime(dir.resolve("file1"), 2 * QUIET_TIME, TimeUnit.MILLISECONDS);
+ }).start();
+ expected.put("file1", new PathWatchEventType[]{MODIFIED});
+ capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS);
+ long end = System.nanoTime();
+ capture.assertEvents(expected);
+ assertThat(end - start, greaterThan(TimeUnit.MILLISECONDS.toNanos(2 * QUIET_TIME)));
+ Thread.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+
+ // Check slow add
+ capture.reset(1);
+ expected.clear();
+ start = System.nanoTime();
+ new Thread(() ->
+ {
+ updateFileOverTime(dir.resolve("file2"), 2 * QUIET_TIME, TimeUnit.MILLISECONDS);
+ }).start();
+ expected.put("file2", new PathWatchEventType[]{ADDED});
+ capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS);
+ end = System.nanoTime();
+ capture.assertEvents(expected);
+ assertThat(end - start, greaterThan(TimeUnit.MILLISECONDS.toNanos(QUIET_TIME)));
+ Thread.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+
+ // Check move directory
+ if (org.junit.jupiter.api.condition.OS.LINUX.isCurrentOs())
+ {
+ capture.reset(5);
+ expected.clear();
+ Files.move(dir.resolve("subdir0"), dir.resolve("subdir1"), StandardCopyOption.ATOMIC_MOVE);
+ expected.put("subdir0", new PathWatchEventType[]{DELETED});
+ // TODO expected.put("subdir0/fileA",new PathWatchEventType[] { DELETED });
+ // TODO expected.put("subdir0/subsubdir0",new PathWatchEventType[] { DELETED });
+ expected.put("subdir1", new PathWatchEventType[]{ADDED});
+ expected.put("subdir1/fileA", new PathWatchEventType[]{ADDED});
+ expected.put("subdir1/fileB", new PathWatchEventType[]{ADDED});
+ expected.put("subdir1/subsubdir0", new PathWatchEventType[]{ADDED});
+
+ capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS);
+ capture.assertEvents(expected);
+ Thread.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+ }
+
+ // Check delete file
+ capture.reset(2);
+ expected.clear();
+ Files.delete(dir.resolve("file1"));
+ expected.put("file1", new PathWatchEventType[]{DELETED});
+ Files.delete(dir.resolve("file2"));
+ expected.put("file2", new PathWatchEventType[]{DELETED});
+
+ capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS);
+ capture.assertEvents(expected);
+ Thread.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+ }
+ finally
+ {
+ pathWatcher.stop();
+ }
+ }
+
+ @Test
+ public void testRestart() throws Exception
+ {
+ Path dir = testdir.getEmptyPathDir();
+ Files.createDirectories(dir.resolve("b/c"));
+ Files.createFile(dir.resolve("a.txt"));
+ Files.createFile(dir.resolve("b.txt"));
+
+ PathWatcher pathWatcher = new PathWatcher();
+ pathWatcher.setNotifyExistingOnStart(true);
+ pathWatcher.setUpdateQuietTime(QUIET_TIME, TimeUnit.MILLISECONDS);
+
+ // Add listener
+ PathWatchEventCapture capture = new PathWatchEventCapture(dir);
+ capture.setFinishTrigger(2);
+ pathWatcher.addListener(capture);
+
+ PathWatcher.Config config = new PathWatcher.Config(dir);
+ config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH);
+ config.addIncludeGlobRelative("*.txt");
+ pathWatcher.watch(config);
+ try
+ {
+ pathWatcher.start();
+
+ // Let quiet time do its thing
+ awaitQuietTime(pathWatcher);
+
+ Map<String, PathWatchEventType[]> expected = new HashMap<>();
+
+ expected.put("a.txt", new PathWatchEventType[]{ADDED});
+ expected.put("b.txt", new PathWatchEventType[]{ADDED});
+
+ Thread.currentThread().sleep(1000); // TODO poor test
+
+ capture.assertEvents(expected);
+
+ //stop it
+ pathWatcher.stop();
+
+ capture.reset();
+
+ Thread.currentThread().sleep(1000); // TODO poor test
+
+ pathWatcher.start();
+
+ awaitQuietTime(pathWatcher);
+
+ capture.assertEvents(expected);
+ }
+ finally
+ {
+ pathWatcher.stop();
+ }
+ }
+
+ /**
+ * When starting up the PathWatcher, the events should occur
+ * indicating files that are of interest that already exist
+ * on the filesystem.
+ *
+ * @throws Exception on test failure
+ */
+ @Test
+ public void testStartupFindFiles() throws Exception
+ {
+ Path dir = testdir.getEmptyPathDir();
+
+ // Files we are interested in
+ Files.createFile(dir.resolve("foo.war"));
+ Files.createDirectories(dir.resolve("bar/WEB-INF"));
+ Files.createFile(dir.resolve("bar/WEB-INF/web.xml"));
+
+ // Files we don't care about
+ Files.createFile(dir.resolve("foo.war.backup"));
+
+ String hiddenWar = ".hidden.war";
+ if (org.junit.jupiter.api.condition.OS.WINDOWS.isCurrentOs())
+ hiddenWar = "hidden.war";
+ Files.createFile(dir.resolve(hiddenWar));
+ if (org.junit.jupiter.api.condition.OS.WINDOWS.isCurrentOs())
+ Files.setAttribute(dir.resolve(hiddenWar), "dos:hidden", Boolean.TRUE);
+ Files.createDirectories(dir.resolve(".wat/WEB-INF"));
+ Files.createFile(dir.resolve(".wat/huh.war"));
+ Files.createFile(dir.resolve(".wat/WEB-INF/web.xml"));
+
+ PathWatcher pathWatcher = new PathWatcher();
+ pathWatcher.setUpdateQuietTime(QUIET_TIME, TimeUnit.MILLISECONDS);
+
+ // Add listener
+ PathWatchEventCapture capture = new PathWatchEventCapture(dir);
+ pathWatcher.addListener(capture);
+
+ // Add test dir configuration
+ PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
+ baseDirConfig.setRecurseDepth(2);
+ baseDirConfig.addExcludeHidden();
+ baseDirConfig.addIncludeGlobRelative("*.war");
+ baseDirConfig.addIncludeGlobRelative("*/WEB-INF/web.xml");
+ pathWatcher.watch(baseDirConfig);
+
+ try
+ {
+ capture.setFinishTrigger(2);
+ pathWatcher.start();
+
+ // Let quiet time do its thing
+ capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS);
+
+ Map<String, PathWatchEventType[]> expected = new HashMap<>();
+ expected.put("bar/WEB-INF/web.xml", new PathWatchEventType[]{ADDED});
+ expected.put("foo.war", new PathWatchEventType[]{ADDED});
+
+ capture.assertEvents(expected);
+ TimeUnit.MILLISECONDS.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+ }
+ finally
+ {
+ pathWatcher.stop();
+ }
+ }
+
+ @Test
+ public void testGlobPattern() throws Exception
+ {
+ Path dir = testdir.getEmptyPathDir();
+
+ // Files we are interested in
+ Files.createFile(dir.resolve("a.txt"));
+ Files.createDirectories(dir.resolve("b/b.txt"));
+ Files.createDirectories(dir.resolve("c/d"));
+ Files.createFile(dir.resolve("c/d/d.txt"));
+ Files.createFile(dir.resolve(".foo.txt"));
+
+ // Files we don't care about
+ Files.createFile(dir.resolve("txt.foo"));
+ Files.createFile(dir.resolve("b/foo.xml"));
+
+ PathWatcher pathWatcher = new PathWatcher();
+ pathWatcher.setUpdateQuietTime(QUIET_TIME, TimeUnit.MILLISECONDS);
+
+ // Add listener
+ PathWatchEventCapture capture = new PathWatchEventCapture(dir);
+ pathWatcher.addListener(capture);
+
+ // Add test dir configuration
+ PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
+ baseDirConfig.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH);
+ baseDirConfig.addExcludeHidden();
+ baseDirConfig.addIncludeGlobRelative("**.txt");
+ pathWatcher.watch(baseDirConfig);
+
+ try
+ {
+ capture.setFinishTrigger(3);
+ pathWatcher.start();
+ assertTrue(capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS));
+
+ Map<String, PathWatchEventType[]> expected = new HashMap<>();
+
+ expected.put("a.txt", new PathWatchEventType[]{ADDED});
+ expected.put("b/b.txt", new PathWatchEventType[]{ADDED});
+ expected.put("c/d/d.txt", new PathWatchEventType[]{ADDED});
+ capture.assertEvents(expected);
+ TimeUnit.MILLISECONDS.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+ }
+ finally
+ {
+ pathWatcher.stop();
+ }
+ }
+
+ @Test
+ public void testDeployFilesUpdateDelete() throws Exception
+ {
+ Path dir = testdir.getEmptyPathDir();
+
+ // Files we are interested in
+ Files.createFile(dir.resolve("foo.war"));
+ Files.createDirectories(dir.resolve("bar/WEB-INF"));
+ Files.createFile(dir.resolve("bar/WEB-INF/web.xml"));
+
+ PathWatcher pathWatcher = new PathWatcher();
+ pathWatcher.setUpdateQuietTime(QUIET_TIME, TimeUnit.MILLISECONDS);
+
+ // Add listener
+ PathWatchEventCapture capture = new PathWatchEventCapture(dir);
+ pathWatcher.addListener(capture);
+
+ // Add test dir configuration
+ PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
+ baseDirConfig.setRecurseDepth(100);
+ baseDirConfig.addExcludeHidden();
+ baseDirConfig.addIncludeGlobRelative("*.war");
+ baseDirConfig.addIncludeGlobRelative("*/WEB-INF/web.xml");
+ pathWatcher.watch(baseDirConfig);
+
+ try
+ {
+ capture.setFinishTrigger(2);
+ pathWatcher.start();
+
+ assertTrue(capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS));
+
+ capture.setFinishTrigger(3);
+
+ // Update web.xml
+ Path webFile = dir.resolve("bar/WEB-INF/web.xml");
+ //capture.setFinishTrigger(webFile,MODIFIED);
+ updateFile(webFile, "Hello Update");
+
+ // Delete war
+ Files.delete(dir.resolve("foo.war"));
+
+ // Add a another new war
+ Files.createFile(dir.resolve("bar.war"));
+
+ // Let capture complete
+ assertTrue(capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS));
+
+ Map<String, PathWatchEventType[]> expected = new HashMap<>();
+
+ expected.put("bar/WEB-INF/web.xml", new PathWatchEventType[]{ADDED, MODIFIED});
+ expected.put("foo.war", new PathWatchEventType[]{ADDED, DELETED});
+ expected.put("bar.war", new PathWatchEventType[]{ADDED});
+
+ capture.assertEvents(expected);
+ TimeUnit.MILLISECONDS.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+ }
+ finally
+ {
+ pathWatcher.stop();
+ }
+ }
+
+ @Test
+ public void testDeployFilesNewWar() throws Exception
+ {
+ Path dir = testdir.getEmptyPathDir();
+
+ // Files we are interested in
+ Files.createFile(dir.resolve("foo.war"));
+ Files.createDirectories(dir.resolve("bar/WEB-INF"));
+ Files.createFile(dir.resolve("bar/WEB-INF/web.xml"));
+
+ PathWatcher pathWatcher = new PathWatcher();
+ pathWatcher.setUpdateQuietTime(QUIET_TIME, TimeUnit.MILLISECONDS);
+
+ // Add listener
+ PathWatchEventCapture capture = new PathWatchEventCapture(dir);
+ pathWatcher.addListener(capture);
+
+ // Add test dir configuration
+ PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
+ baseDirConfig.setRecurseDepth(2);
+ baseDirConfig.addExcludeHidden();
+ baseDirConfig.addIncludeGlobRelative("*.war");
+ baseDirConfig.addIncludeGlobRelative("*/WEB-INF/web.xml");
+ pathWatcher.watch(baseDirConfig);
+
+ try
+ {
+ capture.setFinishTrigger(2);
+ pathWatcher.start();
+
+ // Pretend that startup occurred
+ assertTrue(capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS));
+
+ // New war added
+ capture.setFinishTrigger(1);
+ Path warFile = dir.resolve("hello.war");
+ updateFile(warFile, "Create Hello");
+ Thread.sleep(QUIET_TIME / 2);
+ updateFile(warFile, "Hello 1");
+ Thread.sleep(QUIET_TIME / 2);
+ updateFile(warFile, "Hello two");
+ Thread.sleep(QUIET_TIME / 2);
+ updateFile(warFile, "Hello three");
+
+ // Let capture finish
+ assertTrue(capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS));
+
+ Map<String, PathWatchEventType[]> expected = new HashMap<>();
+
+ expected.put("bar/WEB-INF/web.xml", new PathWatchEventType[]{ADDED});
+ expected.put("foo.war", new PathWatchEventType[]{ADDED});
+ expected.put("hello.war", new PathWatchEventType[]{ADDED});
+
+ capture.assertEvents(expected);
+ TimeUnit.MILLISECONDS.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+ }
+ finally
+ {
+ pathWatcher.stop();
+ }
+ }
+
+ @Test
+ public void testDeployFilesNewDir() throws Exception
+ {
+ Path dir = testdir.getEmptyPathDir();
+
+ // Files we are interested in
+ Files.createFile(dir.resolve("foo.war"));
+
+ PathWatcher pathWatcher = new PathWatcher();
+ pathWatcher.setUpdateQuietTime(QUIET_TIME, TimeUnit.MILLISECONDS);
+
+ // Add listener
+ PathWatchEventCapture capture = new PathWatchEventCapture(dir);
+ pathWatcher.addListener(capture);
+
+ // Add test dir configuration
+ PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
+ baseDirConfig.setRecurseDepth(2);
+ baseDirConfig.addExcludeHidden();
+ baseDirConfig.addIncludeGlobRelative("*.war");
+ baseDirConfig.addIncludeGlobRelative("*/WEB-INF/web.xml");
+ pathWatcher.watch(baseDirConfig);
+
+ try
+ {
+ capture.setFinishTrigger(1);
+ pathWatcher.start();
+
+ // Pretend that startup occurred
+ assertTrue(capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS));
+
+ // New war added
+ capture.setFinishTrigger(1);
+
+ Files.createDirectories(dir.resolve("bar/WEB-INF"));
+ Thread.sleep(QUIET_TIME / 2);
+ Files.createFile(dir.resolve("bar/WEB-INF/web.xml"));
+ Thread.sleep(QUIET_TIME / 2);
+ updateFile(dir.resolve("bar/WEB-INF/web.xml"), "Update");
+ Thread.sleep(QUIET_TIME / 2);
+ updateFile(dir.resolve("bar/WEB-INF/web.xml"), "Update web.xml");
+
+ // Let capture finish
+ assertTrue(capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS));
+
+ Map<String, PathWatchEventType[]> expected = new HashMap<>();
+
+ expected.put("bar/WEB-INF/web.xml", new PathWatchEventType[]{ADDED});
+ expected.put("foo.war", new PathWatchEventType[]{ADDED});
+
+ capture.assertEvents(expected);
+ TimeUnit.MILLISECONDS.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+ }
+ finally
+ {
+ pathWatcher.stop();
+ }
+ }
+
+ @Test
+ public void testDeployFilesBeyondDepthLimit() throws Exception
+ {
+ Path dir = testdir.getEmptyPathDir();
+
+ // Files we are interested in
+ Files.createDirectories(dir.resolve("foo/WEB-INF/lib"));
+ Files.createDirectories(dir.resolve("bar/WEB-INF/lib"));
+
+ PathWatcher pathWatcher = new PathWatcher();
+ pathWatcher.setUpdateQuietTime(QUIET_TIME, TimeUnit.MILLISECONDS);
+
+ // Add listener
+ PathWatchEventCapture capture = new PathWatchEventCapture(dir);
+ pathWatcher.addListener(capture);
+
+ // Add test dir configuration
+ PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
+ baseDirConfig.setRecurseDepth(0);
+ pathWatcher.watch(baseDirConfig);
+
+ try
+ {
+ capture.setFinishTrigger(2);
+ pathWatcher.start();
+
+ // Pretend that startup occurred
+ assertTrue(capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS));
+
+ Map<String, PathWatchEventType[]> expected = new HashMap<>();
+ expected.put("foo", new PathWatchEventType[]{ADDED});
+ expected.put("bar", new PathWatchEventType[]{ADDED});
+
+ capture.assertEvents(expected);
+
+ capture.reset(1);
+ expected.clear();
+ expected.put("bar", new PathWatchEventType[]{MODIFIED});
+ Files.createFile(dir.resolve("bar/index.html"));
+ assertTrue(capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS));
+
+ capture.assertEvents(expected);
+ TimeUnit.MILLISECONDS.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+
+ capture.reset(1);
+ expected.clear();
+ expected.put("bob", new PathWatchEventType[]{ADDED});
+ Files.createFile(dir.resolve("bar/WEB-INF/lib/ignored"));
+ PathWatcher.LOG.debug("create bob");
+ Files.createDirectories(dir.resolve("bob/WEB-INF/lib"));
+ Thread.sleep(QUIET_TIME / 2);
+ PathWatcher.LOG.debug("create bob/index.html");
+ Files.createFile(dir.resolve("bob/index.html"));
+ Thread.sleep(QUIET_TIME / 2);
+ PathWatcher.LOG.debug("update bob/index.html");
+ updateFile(dir.resolve("bob/index.html"), "Update");
+ Thread.sleep(QUIET_TIME / 2);
+ PathWatcher.LOG.debug("update bob/index.html");
+ updateFile(dir.resolve("bob/index.html"), "Update index.html");
+
+ assertTrue(capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS));
+ capture.assertEvents(expected);
+ TimeUnit.MILLISECONDS.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+ }
+ finally
+ {
+ pathWatcher.stop();
+ }
+ }
+
+ @Test
+ public void testWatchFile() throws Exception
+ {
+ Path dir = testdir.getEmptyPathDir();
+
+ // Files we are interested in
+ Files.createDirectories(dir.resolve("bar/WEB-INF"));
+ Files.createFile(dir.resolve("bar/WEB-INF/web.xml"));
+
+ PathWatcher pathWatcher = new PathWatcher();
+ pathWatcher.setUpdateQuietTime(QUIET_TIME, TimeUnit.MILLISECONDS);
+
+ // Add listener
+ PathWatchEventCapture capture = new PathWatchEventCapture(dir);
+ pathWatcher.addListener(capture);
+
+ // Add test configuration
+ pathWatcher.watch(dir.resolve("bar/WEB-INF/web.xml"));
+ pathWatcher.setNotifyExistingOnStart(false);
+
+ try
+ {
+ pathWatcher.start();
+ Thread.sleep(WAIT_TIME);
+ assertThat(capture.events.size(), is(0));
+
+ Files.createFile(dir.resolve("bar/index.htnl"));
+ Files.createFile(dir.resolve("bar/WEB-INF/other.xml"));
+ Files.createDirectories(dir.resolve("bar/WEB-INF/lib"));
+
+ Thread.sleep(WAIT_TIME);
+ assertThat(capture.events.size(), is(0));
+
+ capture.setFinishTrigger(1);
+ updateFile(dir.resolve("bar/WEB-INF/web.xml"), "Update web.xml");
+ assertTrue(capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS));
+
+ Map<String, PathWatchEventType[]> expected = new HashMap<>();
+
+ expected.put("bar/WEB-INF/web.xml", new PathWatchEventType[]{MODIFIED});
+
+ capture.assertEvents(expected);
+ TimeUnit.MILLISECONDS.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+ }
+ finally
+ {
+ pathWatcher.stop();
+ }
+ }
+
+ /**
+ * Pretend to modify a new war file that is large, and being copied into place
+ * using some sort of technique that is slow enough that it takes a while for
+ * the entire war file to exist in place.
+ * <p>
+ * This is to test the quiet time logic to ensure that only a single MODIFIED event occurs on this new war file
+ *
+ * @throws Exception on test failure
+ */
+ @Test
+ public void testDeployFilesModifyWarLargeSlowCopy() throws Exception
+ {
+ Path dir = testdir.getEmptyPathDir();
+
+ // Files we are interested in
+ Files.createFile(dir.resolve("foo.war"));
+ Files.createFile(dir.resolve("hello.war"));
+ Files.createDirectories(dir.resolve("bar/WEB-INF"));
+ Files.createFile(dir.resolve("bar/WEB-INF/web.xml"));
+
+ PathWatcher pathWatcher = new PathWatcher();
+ pathWatcher.setUpdateQuietTime(QUIET_TIME, TimeUnit.MILLISECONDS);
+
+ // Add listener
+ PathWatchEventCapture capture = new PathWatchEventCapture(dir);
+ pathWatcher.addListener(capture);
+
+ // Add test dir configuration
+ PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
+ baseDirConfig.setRecurseDepth(2);
+ baseDirConfig.addExcludeHidden();
+ baseDirConfig.addIncludeGlobRelative("*.war");
+ baseDirConfig.addIncludeGlobRelative("*/WEB-INF/web.xml");
+ pathWatcher.watch(baseDirConfig);
+
+ try
+ {
+ capture.setFinishTrigger(3);
+ pathWatcher.start();
+
+ // Pretend that startup occurred
+ assertTrue(capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS));
+
+ // New war added (slowly)
+ capture.setFinishTrigger(1);
+ Path warFile = dir.resolve("hello.war");
+ long start = System.nanoTime();
+ new Thread(() ->
+ {
+ updateFileOverTime(warFile, 2 * QUIET_TIME, TimeUnit.MILLISECONDS);
+ }).start();
+
+ assertTrue(capture.finishedLatch.await(4 * QUIET_TIME, TimeUnit.MILLISECONDS));
+ long end = System.nanoTime();
+ assertThat(end - start, greaterThan(TimeUnit.MILLISECONDS.toNanos(2 * QUIET_TIME)));
+
+ Map<String, PathWatchEventType[]> expected = new HashMap<>();
+ expected.put("bar/WEB-INF/web.xml", new PathWatchEventType[]{ADDED});
+ expected.put("foo.war", new PathWatchEventType[]{ADDED});
+ expected.put("hello.war", new PathWatchEventType[]{ADDED, MODIFIED});
+
+ capture.assertEvents(expected);
+ TimeUnit.MILLISECONDS.sleep(WAIT_TIME);
+ capture.assertEvents(expected);
+ }
+ finally
+ {
+ pathWatcher.stop();
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/PoolTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/PoolTest.java
new file mode 100644
index 0000000..8b319a9
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/PoolTest.java
@@ -0,0 +1,698 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static java.util.stream.Collectors.toList;
+import static org.eclipse.jetty.util.Pool.StrategyType.FIRST;
+import static org.eclipse.jetty.util.Pool.StrategyType.RANDOM;
+import static org.eclipse.jetty.util.Pool.StrategyType.ROUND_ROBIN;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class PoolTest
+{
+ interface Factory
+ {
+ Pool<CloseableHolder> getPool(int maxSize);
+ }
+
+ private static class CloseableHolder implements Closeable
+ {
+ private boolean closed;
+ private final String value;
+
+ public CloseableHolder(String value)
+ {
+ this.value = value;
+ }
+
+ @Override
+ public void close()
+ {
+ closed = true;
+ }
+ }
+
+ public static Stream<Object[]> strategy()
+ {
+ List<Object[]> data = new ArrayList<>();
+ data.add(new Object[]{(Factory)s -> new Pool<>(FIRST, s)});
+ data.add(new Object[]{(Factory)s -> new Pool<>(RANDOM, s)});
+ data.add(new Object[]{(Factory)s -> new Pool<>(FIRST, s, true)});
+ data.add(new Object[]{(Factory)s -> new Pool<>(ROUND_ROBIN, s)});
+ return data.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testAcquireRelease(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+ assertThat(pool.size(), is(1));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getIdleCount(), is(1));
+ assertThat(pool.getInUseCount(), is(0));
+
+ Pool<CloseableHolder>.Entry e1 = pool.acquire();
+ assertThat(e1.getPooled().value, equalTo("aaa"));
+ assertThat(pool.size(), is(1));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getIdleCount(), is(0));
+ assertThat(pool.getInUseCount(), is(1));
+
+ assertNull(pool.acquire());
+
+ e1.release();
+ assertThat(pool.size(), is(1));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getIdleCount(), is(1));
+ assertThat(pool.getInUseCount(), is(0));
+
+ assertThrows(IllegalStateException.class, e1::release);
+
+ Pool<CloseableHolder>.Entry e2 = pool.acquire();
+ assertThat(e2.getPooled().value, equalTo("aaa"));
+ assertThat(pool.size(), is(1));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getIdleCount(), is(0));
+ assertThat(pool.getInUseCount(), is(1));
+
+ pool.release(e2);
+ assertThat(pool.size(), is(1));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getIdleCount(), is(1));
+ assertThat(pool.getInUseCount(), is(0));
+
+ assertThrows(IllegalStateException.class, () -> pool.release(e2));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testRemoveBeforeRelease(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+
+ Pool<CloseableHolder>.Entry e1 = pool.acquire();
+ assertThat(pool.remove(e1), is(true));
+ assertThat(pool.remove(e1), is(false));
+ assertThat(pool.release(e1), is(false));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testCloseBeforeRelease(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+
+ Pool<CloseableHolder>.Entry e1 = pool.acquire();
+ assertThat(pool.size(), is(1));
+ pool.close();
+ assertThat(pool.size(), is(0));
+ assertThat(pool.release(e1), is(false));
+ assertThat(e1.getPooled().closed, is(true));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testMaxPoolSize(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ assertThat(pool.size(), is(0));
+ assertThat(pool.reserve(), notNullValue());
+ assertThat(pool.size(), is(1));
+ assertThat(pool.reserve(), nullValue());
+ assertThat(pool.size(), is(1));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testReserve(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(2);
+ pool.setMaxMultiplex(2);
+
+ // Reserve an entry
+ Pool<CloseableHolder>.Entry e1 = pool.reserve();
+ assertThat(pool.size(), is(1));
+ assertThat(pool.getReservedCount(), is(1));
+ assertThat(pool.getIdleCount(), is(0));
+ assertThat(pool.getInUseCount(), is(0));
+
+ // enable the entry
+ e1.enable(new CloseableHolder("aaa"), false);
+ assertThat(pool.size(), is(1));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getIdleCount(), is(1));
+ assertThat(pool.getInUseCount(), is(0));
+
+ // Reserve another entry
+ Pool<CloseableHolder>.Entry e2 = pool.reserve();
+ assertThat(pool.size(), is(2));
+ assertThat(pool.getReservedCount(), is(1));
+ assertThat(pool.getIdleCount(), is(1));
+ assertThat(pool.getInUseCount(), is(0));
+
+ // remove the reservation
+ e2.remove();
+ assertThat(pool.size(), is(1));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getIdleCount(), is(1));
+ assertThat(pool.getInUseCount(), is(0));
+
+ // Reserve another entry
+ Pool<CloseableHolder>.Entry e3 = pool.reserve();
+ assertThat(pool.size(), is(2));
+ assertThat(pool.getReservedCount(), is(1));
+ assertThat(pool.getIdleCount(), is(1));
+ assertThat(pool.getInUseCount(), is(0));
+
+ // enable and acquire the entry
+ e3.enable(new CloseableHolder("bbb"), true);
+ assertThat(pool.size(), is(2));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getIdleCount(), is(1));
+ assertThat(pool.getInUseCount(), is(1));
+
+ // can't reenable
+ assertThrows(IllegalStateException.class, () -> e3.enable(new CloseableHolder("xxx"), false));
+
+ // Can't enable acquired entry
+ Pool<CloseableHolder>.Entry e = pool.acquire();
+ assertThrows(IllegalStateException.class, () -> e.enable(new CloseableHolder("xxx"), false));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testReserveNegativeMaxPending(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(2);
+ assertThat(pool.reserve(), notNullValue());
+ assertThat(pool.reserve(), notNullValue());
+ assertThat(pool.reserve(), nullValue());
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testClose(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ CloseableHolder holder = new CloseableHolder("aaa");
+ pool.reserve().enable(holder, false);
+ assertThat(pool.isClosed(), is(false));
+ pool.close();
+ pool.close();
+
+ assertThat(pool.isClosed(), is(true));
+ assertThat(pool.size(), is(0));
+ assertThat(pool.acquire(), nullValue());
+ assertThat(pool.reserve(), nullValue());
+ assertThat(holder.closed, is(true));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testRemove(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+
+ Pool<CloseableHolder>.Entry e1 = pool.acquire();
+ assertThat(pool.remove(e1), is(true));
+ assertThat(pool.remove(e1), is(false));
+ assertThat(pool.release(e1), is(false));
+ assertThat(pool.acquire(), nullValue());
+ assertThrows(NullPointerException.class, () -> pool.remove(null));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testValuesSize(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(2);
+
+ assertThat(pool.size(), is(0));
+ assertThat(pool.values().isEmpty(), is(true));
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+ pool.reserve().enable(new CloseableHolder("bbb"), false);
+ assertThat(pool.values().stream().map(Pool.Entry::getPooled).map(closeableHolder -> closeableHolder.value).collect(toList()), equalTo(Arrays.asList("aaa", "bbb")));
+ assertThat(pool.size(), is(2));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testValuesContainsAcquiredEntries(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(2);
+
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+ pool.reserve().enable(new CloseableHolder("bbb"), false);
+ assertThat(pool.acquire(), notNullValue());
+ assertThat(pool.acquire(), notNullValue());
+ assertThat(pool.acquire(), nullValue());
+ assertThat(pool.values().isEmpty(), is(false));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testMaxUsageCount(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ pool.setMaxUsageCount(3);
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+
+ Pool<CloseableHolder>.Entry e1 = pool.acquire();
+ assertThat(pool.release(e1), is(true));
+ e1 = pool.acquire();
+ assertThat(pool.release(e1), is(true));
+ e1 = pool.acquire();
+ assertThat(pool.release(e1), is(false));
+ assertThat(pool.acquire(), nullValue());
+ assertThat(pool.size(), is(1));
+ assertThat(pool.remove(e1), is(true));
+ assertThat(pool.remove(e1), is(false));
+ assertThat(pool.size(), is(0));
+ Pool<CloseableHolder>.Entry e1Copy = e1;
+ assertThat(pool.release(e1Copy), is(false));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testMaxMultiplex(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(2);
+ pool.setMaxMultiplex(3);
+
+ Map<String, AtomicInteger> counts = new HashMap<>();
+ AtomicInteger a = new AtomicInteger();
+ AtomicInteger b = new AtomicInteger();
+ counts.put("a", a);
+ counts.put("b", b);
+ pool.reserve().enable(new CloseableHolder("a"), false);
+ pool.reserve().enable(new CloseableHolder("b"), false);
+
+ counts.get(pool.acquire().getPooled().value).incrementAndGet();
+ counts.get(pool.acquire().getPooled().value).incrementAndGet();
+ counts.get(pool.acquire().getPooled().value).incrementAndGet();
+ counts.get(pool.acquire().getPooled().value).incrementAndGet();
+
+ assertThat(a.get(), greaterThan(0));
+ assertThat(a.get(), lessThanOrEqualTo(3));
+ assertThat(b.get(), greaterThan(0));
+ assertThat(b.get(), lessThanOrEqualTo(3));
+
+ counts.get(pool.acquire().getPooled().value).incrementAndGet();
+ counts.get(pool.acquire().getPooled().value).incrementAndGet();
+
+ assertThat(a.get(), is(3));
+ assertThat(b.get(), is(3));
+
+ assertNull(pool.acquire());
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testRemoveMultiplexed(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ pool.setMaxMultiplex(2);
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+
+ Pool<CloseableHolder>.Entry e1 = pool.acquire();
+ assertThat(e1, notNullValue());
+ Pool<CloseableHolder>.Entry e2 = pool.acquire();
+ assertThat(e2, notNullValue());
+ assertThat(e2, sameInstance(e1));
+ assertThat(e2.getUsageCount(), is(2));
+
+ assertThat(pool.values().stream().findFirst().get().isIdle(), is(false));
+
+ assertThat(pool.remove(e1), is(false));
+ assertThat(pool.values().stream().findFirst().get().isIdle(), is(false));
+ assertThat(pool.values().stream().findFirst().get().isClosed(), is(true));
+ assertThat(pool.remove(e1), is(true));
+ assertThat(pool.size(), is(0));
+
+ assertThat(pool.remove(e1), is(false));
+
+ assertThat(pool.release(e1), is(false));
+
+ assertThat(pool.remove(e1), is(false));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testMultiplexRemoveThenAcquireThenReleaseRemove(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ pool.setMaxMultiplex(2);
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+
+ Pool<CloseableHolder>.Entry e1 = pool.acquire();
+ Pool<CloseableHolder>.Entry e2 = pool.acquire();
+
+ assertThat(pool.remove(e1), is(false));
+ assertThat(e1.isClosed(), is(true));
+ assertThat(pool.acquire(), nullValue());
+ assertThat(pool.release(e2), is(false));
+ assertThat(pool.remove(e2), is(true));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testNonMultiplexRemoveAfterAcquire(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ pool.setMaxMultiplex(2);
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+
+ Pool<CloseableHolder>.Entry e1 = pool.acquire();
+ assertThat(pool.remove(e1), is(true));
+ assertThat(pool.size(), is(0));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testMultiplexRemoveAfterAcquire(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ pool.setMaxMultiplex(2);
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+
+ Pool<CloseableHolder>.Entry e1 = pool.acquire();
+ Pool<CloseableHolder>.Entry e2 = pool.acquire();
+
+ assertThat(pool.remove(e1), is(false));
+ assertThat(pool.remove(e2), is(true));
+ assertThat(pool.size(), is(0));
+
+ assertThat(pool.release(e1), is(false));
+ assertThat(pool.size(), is(0));
+
+ Pool<CloseableHolder>.Entry e3 = pool.acquire();
+ assertThat(e3, nullValue());
+
+ assertThat(pool.release(e2), is(false));
+ assertThat(pool.size(), is(0));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testReleaseThenRemoveNonEnabledEntry(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ Pool<CloseableHolder>.Entry e = pool.reserve();
+ assertThat(pool.size(), is(1));
+ assertThat(pool.release(e), is(false));
+ assertThat(pool.size(), is(1));
+ assertThat(pool.remove(e), is(true));
+ assertThat(pool.size(), is(0));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testRemoveNonEnabledEntry(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ Pool<CloseableHolder>.Entry e = pool.reserve();
+ assertThat(pool.size(), is(1));
+ assertThat(pool.remove(e), is(true));
+ assertThat(pool.size(), is(0));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testMultiplexMaxUsageReachedAcquireThenRemove(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ pool.setMaxMultiplex(2);
+ pool.setMaxUsageCount(3);
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+
+ Pool<CloseableHolder>.Entry e0 = pool.acquire();
+
+ Pool<CloseableHolder>.Entry e1 = pool.acquire();
+ assertThat(pool.release(e1), is(true));
+ Pool<CloseableHolder>.Entry e2 = pool.acquire();
+ assertThat(pool.release(e2), is(true));
+ assertThat(pool.acquire(), nullValue());
+
+ assertThat(pool.remove(e0), is(true));
+ assertThat(pool.size(), is(0));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testMultiplexMaxUsageReachedAcquireThenReleaseThenRemove(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ pool.setMaxMultiplex(2);
+ pool.setMaxUsageCount(3);
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+
+ Pool<CloseableHolder>.Entry e0 = pool.acquire();
+
+ Pool<CloseableHolder>.Entry e1 = pool.acquire();
+ assertThat(pool.release(e1), is(true));
+ Pool<CloseableHolder>.Entry e2 = pool.acquire();
+ assertThat(pool.release(e2), is(true));
+ assertThat(pool.acquire(), nullValue());
+
+ assertThat(pool.release(e0), is(false));
+ assertThat(pool.values().stream().findFirst().get().isIdle(), is(true));
+ assertThat(pool.values().stream().findFirst().get().isClosed(), is(false));
+ assertThat(pool.size(), is(1));
+ assertThat(pool.remove(e0), is(true));
+ assertThat(pool.size(), is(0));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testUsageCountAfterReachingMaxMultiplexLimit(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ pool.setMaxMultiplex(2);
+ pool.setMaxUsageCount(10);
+ pool.reserve().enable(new CloseableHolder("aaa"), false);
+
+ Pool<CloseableHolder>.Entry e1 = pool.acquire();
+ assertThat(e1.getUsageCount(), is(1));
+ Pool<CloseableHolder>.Entry e2 = pool.acquire();
+ assertThat(e2, sameInstance(e1));
+ assertThat(e1.getUsageCount(), is(2));
+ assertThat(pool.acquire(), nullValue());
+ assertThat(e1.getUsageCount(), is(2));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testDynamicMaxUsageCountChangeOverflowMaxInt(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(1);
+ pool.setMaxMultiplex(1);
+ Pool<CloseableHolder>.Entry entry = pool.reserve();
+ entry.enable(new CloseableHolder("aaa"), false);
+ entry.setUsageCount(Integer.MAX_VALUE);
+
+ Pool<CloseableHolder>.Entry acquired1 = pool.acquire();
+ assertThat(acquired1, notNullValue());
+ assertThat(pool.release(acquired1), is(true));
+
+ pool.setMaxUsageCount(1);
+ Pool<CloseableHolder>.Entry acquired2 = pool.acquire();
+ assertThat(acquired2, nullValue());
+ assertThat(entry.getPooled().closed, is(true));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testDynamicMaxUsageCountChangeSweep(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(2);
+ pool.setMaxUsageCount(100);
+ Pool<CloseableHolder>.Entry entry1 = pool.reserve();
+ entry1.enable(new CloseableHolder("aaa"), false);
+ Pool<CloseableHolder>.Entry entry2 = pool.reserve();
+ entry2.enable(new CloseableHolder("bbb"), false);
+
+ Pool<CloseableHolder>.Entry acquired1 = pool.acquire();
+ assertThat(acquired1, notNullValue());
+ assertThat(pool.release(acquired1), is(true));
+
+ assertThat(pool.size(), is(2));
+ pool.setMaxUsageCount(1);
+ assertThat(pool.size(), is(1));
+ assertThat(entry1.getPooled().closed ^ entry2.getPooled().closed, is(true));
+ }
+
+ @Test
+ public void testConfigLimits()
+ {
+ assertThrows(IllegalArgumentException.class, () -> new Pool<CloseableHolder>(FIRST, 1).setMaxMultiplex(0));
+ assertThrows(IllegalArgumentException.class, () -> new Pool<CloseableHolder>(FIRST, 1).setMaxMultiplex(-1));
+ assertThrows(IllegalArgumentException.class, () -> new Pool<CloseableHolder>(FIRST, 1).setMaxUsageCount(0));
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "strategy")
+ public void testAcquireWithCreator(Factory factory)
+ {
+ Pool<CloseableHolder> pool = factory.getPool(2);
+
+ assertThat(pool.size(), is(0));
+ assertThat(pool.acquire(e -> null), nullValue());
+ assertThat(pool.size(), is(0));
+
+ Pool<CloseableHolder>.Entry e1 = pool.acquire(e -> new CloseableHolder("e1"));
+ assertThat(e1.getPooled().value, is("e1"));
+ assertThat(pool.size(), is(1));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getInUseCount(), is(1));
+
+ assertThat(pool.acquire(e -> null), nullValue());
+ assertThat(pool.size(), is(1));
+
+ Pool<CloseableHolder>.Entry e2 = pool.acquire(e -> new CloseableHolder("e2"));
+ assertThat(e2.getPooled().value, is("e2"));
+ assertThat(pool.size(), is(2));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getInUseCount(), is(2));
+
+ Pool<CloseableHolder>.Entry e3 = pool.acquire(e -> new CloseableHolder("e3"));
+ assertThat(e3, nullValue());
+ assertThat(pool.size(), is(2));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getInUseCount(), is(2));
+
+ assertThat(pool.acquire(e ->
+ {
+ throw new IllegalStateException();
+ }), nullValue());
+
+ e2.release();
+ assertThat(pool.size(), is(2));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getInUseCount(), is(1));
+
+ Pool<CloseableHolder>.Entry e4 = pool.acquire(e -> new CloseableHolder("e4"));
+ assertThat(e4.getPooled().value, is("e2"));
+ assertThat(pool.size(), is(2));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getInUseCount(), is(2));
+
+ pool.remove(e1);
+ assertThat(pool.size(), is(1));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getInUseCount(), is(1));
+
+ assertThrows(IllegalStateException.class, () -> pool.acquire(e ->
+ {
+ throw new IllegalStateException();
+ }));
+ assertThat(pool.size(), is(1));
+ assertThat(pool.getReservedCount(), is(0));
+ assertThat(pool.getInUseCount(), is(1));
+ }
+
+ @Test
+ public void testRoundRobinStrategy()
+ {
+ Pool<AtomicInteger> pool = new Pool<>(ROUND_ROBIN, 4);
+
+ Pool<AtomicInteger>.Entry e1 = pool.acquire(e -> new AtomicInteger());
+ Pool<AtomicInteger>.Entry e2 = pool.acquire(e -> new AtomicInteger());
+ Pool<AtomicInteger>.Entry e3 = pool.acquire(e -> new AtomicInteger());
+ Pool<AtomicInteger>.Entry e4 = pool.acquire(e -> new AtomicInteger());
+ assertNull(pool.acquire(e -> new AtomicInteger()));
+
+ pool.release(e1);
+ pool.release(e2);
+ pool.release(e3);
+ pool.release(e4);
+
+ Pool<AtomicInteger>.Entry last = null;
+ for (int i = 0; i < 8; i++)
+ {
+ Pool<AtomicInteger>.Entry e = pool.acquire();
+ if (last != null)
+ assertThat(e, not(sameInstance(last)));
+ e.getPooled().incrementAndGet();
+ pool.release(e);
+ last = e;
+ }
+
+ assertThat(e1.getPooled().get(), is(2));
+ assertThat(e2.getPooled().get(), is(2));
+ assertThat(e3.getPooled().get(), is(2));
+ assertThat(e4.getPooled().get(), is(2));
+ }
+
+ @Test
+ public void testRandomStrategy()
+ {
+ Pool<AtomicInteger> pool = new Pool<>(RANDOM, 4);
+
+ Pool<AtomicInteger>.Entry e1 = pool.acquire(e -> new AtomicInteger());
+ Pool<AtomicInteger>.Entry e2 = pool.acquire(e -> new AtomicInteger());
+ Pool<AtomicInteger>.Entry e3 = pool.acquire(e -> new AtomicInteger());
+ Pool<AtomicInteger>.Entry e4 = pool.acquire(e -> new AtomicInteger());
+ assertNull(pool.acquire(e -> new AtomicInteger()));
+
+ pool.release(e1);
+ pool.release(e2);
+ pool.release(e3);
+ pool.release(e4);
+
+ for (int i = 0; i < 400; i++)
+ {
+ Pool<AtomicInteger>.Entry e = pool.acquire();
+ e.getPooled().incrementAndGet();
+ pool.release(e);
+ }
+
+ assertThat(e1.getPooled().get(), greaterThan(10));
+ assertThat(e2.getPooled().get(), greaterThan(10));
+ assertThat(e3.getPooled().get(), greaterThan(10));
+ assertThat(e4.getPooled().get(), greaterThan(10));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ProcessorUtilsTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ProcessorUtilsTest.java
new file mode 100644
index 0000000..ff02329
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ProcessorUtilsTest.java
@@ -0,0 +1,61 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class ProcessorUtilsTest
+{
+ @Test
+ public void testSystemProperty()
+ {
+ // Classloading will trigger the static initializer.
+ int original = ProcessorUtils.availableProcessors();
+
+ // Verify that the static initializer logic is correct.
+ System.setProperty(ProcessorUtils.AVAILABLE_PROCESSORS, "42");
+ int processors = ProcessorUtils.init();
+ assertEquals(42, processors);
+
+ // Make sure the original value is preserved.
+ assertEquals(original, ProcessorUtils.availableProcessors());
+ }
+
+ @Test
+ public void testSetter()
+ {
+ // Classloading will trigger the static initializer.
+ int original = ProcessorUtils.availableProcessors();
+ try
+ {
+ assertThrows(IllegalArgumentException.class, () -> ProcessorUtils.setAvailableProcessors(0));
+
+ int processors = 42;
+ ProcessorUtils.setAvailableProcessors(processors);
+ assertEquals(processors, ProcessorUtils.availableProcessors());
+ }
+ finally
+ {
+ ProcessorUtils.setAvailableProcessors(original);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/QueueBenchmarkTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/QueueBenchmarkTest.java
new file mode 100644
index 0000000..6e9e908
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/QueueBenchmarkTest.java
@@ -0,0 +1,217 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+// TODO: Review - this is a HIGH_CPU, HIGH_MEMORY test that takes 20 minutes to execute.
+// perhaps this should not be a normal every day testcase?
+// Move to a different module? make it not a junit testcase?
+@Disabled
+public class QueueBenchmarkTest
+{
+ private static final Logger logger = Log.getLogger(QueueBenchmarkTest.class);
+ private static final Runnable ELEMENT = () ->
+ {
+ };
+ private static final Runnable END = () ->
+ {
+ };
+
+ @Test
+ public void testQueues() throws Exception
+ {
+ int cores = ProcessorUtils.availableProcessors();
+ assumeTrue(cores > 1);
+
+ final int readers = cores / 2;
+ final int writers = readers;
+ final int iterations = 16 * 1024 * 1024;
+
+ final List<Queue<Runnable>> queues = new ArrayList<>();
+ queues.add(new ConcurrentLinkedQueue<>()); // JDK lock-free queue, allocating nodes
+ queues.add(new ArrayBlockingQueue<>(iterations * writers)); // JDK lock-based, circular array queue
+ queues.add(new BlockingArrayQueue<>(iterations * writers)); // Jetty lock-based, circular array queue
+
+ testQueues(readers, writers, iterations, queues, false);
+ }
+
+ @Test
+ public void testBlockingQueues() throws Exception
+ {
+ int cores = ProcessorUtils.availableProcessors();
+ assumeTrue(cores > 1);
+
+ final int readers = cores / 2;
+ final int writers = readers;
+ final int iterations = 16 * 1024 * 1024;
+
+ final List<Queue<Runnable>> queues = new ArrayList<>();
+ queues.add(new LinkedBlockingQueue<>());
+ queues.add(new ArrayBlockingQueue<>(iterations * writers));
+ queues.add(new BlockingArrayQueue<>(iterations * writers));
+
+ testQueues(readers, writers, iterations, queues, true);
+ }
+
+ private void testQueues(final int readers, final int writers, final int iterations, List<Queue<Runnable>> queues, final boolean blocking) throws Exception
+ {
+ final int runs = 8;
+ int threads = readers + writers;
+ final CyclicBarrier barrier = new CyclicBarrier(threads + 1);
+
+ for (final Queue<Runnable> queue : queues)
+ {
+ for (int r = 0; r < runs; ++r)
+ {
+ for (int i = 0; i < readers; ++i)
+ {
+ Thread thread = new Thread()
+ {
+ @Override
+ public void run()
+ {
+ await(barrier);
+ consume(queue, writers, blocking);
+ await(barrier);
+ }
+ };
+ thread.start();
+ }
+ for (int i = 0; i < writers; ++i)
+ {
+ Thread thread = new Thread()
+ {
+ @Override
+ public void run()
+ {
+ await(barrier);
+ produce(queue, readers, iterations);
+ await(barrier);
+ }
+ };
+ thread.start();
+ }
+
+ await(barrier);
+ long begin = System.nanoTime();
+ await(barrier);
+ long end = System.nanoTime();
+ long elapsed = TimeUnit.NANOSECONDS.toMillis(end - begin);
+ logger.info("{} Readers/Writers: {}/{} => {} ms", queue.getClass().getSimpleName(), readers, writers, elapsed);
+ }
+ }
+ }
+
+ private static void consume(Queue<Runnable> queue, int writers, boolean blocking)
+ {
+ while (true)
+ {
+ Runnable element = blocking ? take(queue) : poll(queue);
+ if (element == END)
+ if (--writers == 0)
+ break;
+ }
+ }
+
+ private static void produce(Queue<Runnable> queue, int readers, int iterations)
+ {
+ for (int i = 0; i < iterations; ++i)
+ {
+ append(queue, ELEMENT);
+ }
+ for (int i = 0; i < readers; ++i)
+ {
+ append(queue, END);
+ }
+ }
+
+ private static void append(Queue<Runnable> queue, Runnable element)
+ {
+ if (!queue.offer(element))
+ logger.warn("Queue {} capacity is too small", queue);
+ }
+
+ private static Runnable take(Queue<Runnable> queue)
+ {
+ try
+ {
+ return ((BlockingQueue<Runnable>)queue).take();
+ }
+ catch (InterruptedException x)
+ {
+ throw new RuntimeException(x);
+ }
+ }
+
+ private static Runnable poll(Queue<Runnable> queue)
+ {
+ int loops = 0;
+ while (true)
+ {
+ Runnable element = queue.poll();
+ if (element != null)
+ return element;
+ // Busy loop
+ sleepMicros(1);
+ ++loops;
+ if (loops % 16 == 0)
+ logger.warn("Spin looping while polling empty queue: {} spins: ", loops);
+ }
+ }
+
+ private static void sleepMicros(long sleep)
+ {
+ try
+ {
+ TimeUnit.MICROSECONDS.sleep(sleep);
+ }
+ catch (InterruptedException x)
+ {
+ throw new RuntimeException(x);
+ }
+ }
+
+ private static void await(CyclicBarrier barrier)
+ {
+ try
+ {
+ barrier.await();
+ }
+ catch (Exception x)
+ {
+ throw new RuntimeException(x);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/QuotedStringTokenizerTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/QuotedStringTokenizerTest.java
new file mode 100644
index 0000000..84a30ea
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/QuotedStringTokenizerTest.java
@@ -0,0 +1,200 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+public class QuotedStringTokenizerTest
+{
+ /*
+ * Test for String nextToken()
+ */
+ @Test
+ public void testTokenizer0()
+ {
+ QuotedStringTokenizer tok =
+ new QuotedStringTokenizer("abc\n\"d\\\"'\"\n'p\\',y'\nz");
+ checkTok(tok, false, false);
+ }
+
+ /*
+ * Test for String nextToken()
+ */
+ @Test
+ public void testTokenizer1()
+ {
+ QuotedStringTokenizer tok =
+ new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z",
+ " ,");
+ checkTok(tok, false, false);
+ }
+
+ /*
+ * Test for String nextToken()
+ */
+ @Test
+ public void testTokenizer2()
+ {
+ QuotedStringTokenizer tok =
+ new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
+ false);
+ checkTok(tok, false, false);
+
+ tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
+ true);
+ checkTok(tok, true, false);
+ }
+
+ /*
+ * Test for String nextToken()
+ */
+ @Test
+ public void testTokenizer3()
+ {
+ QuotedStringTokenizer tok;
+
+ tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
+ false, false);
+ checkTok(tok, false, false);
+
+ tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
+ false, true);
+ checkTok(tok, false, true);
+
+ tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
+ true, false);
+ checkTok(tok, true, false);
+
+ tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
+ true, true);
+ checkTok(tok, true, true);
+ }
+
+ @Test
+ public void testQuote()
+ {
+ StringBuffer buf = new StringBuffer();
+
+ buf.setLength(0);
+ QuotedStringTokenizer.quote(buf, "abc \n efg");
+ assertEquals("\"abc \\n efg\"", buf.toString());
+
+ buf.setLength(0);
+ QuotedStringTokenizer.quote(buf, "abcefg");
+ assertEquals("\"abcefg\"", buf.toString());
+
+ buf.setLength(0);
+ QuotedStringTokenizer.quote(buf, "abcefg\"");
+ assertEquals("\"abcefg\\\"\"", buf.toString());
+ }
+
+ /*
+ * Test for String nextToken()
+ */
+ @Test
+ public void testTokenizer4()
+ {
+ QuotedStringTokenizer tok = new QuotedStringTokenizer("abc'def,ghi'jkl", ",");
+ tok.setSingle(false);
+ assertEquals("abc'def", tok.nextToken());
+ assertEquals("ghi'jkl", tok.nextToken());
+ tok = new QuotedStringTokenizer("abc'def,ghi'jkl", ",");
+ tok.setSingle(true);
+ assertEquals("abcdef,ghijkl", tok.nextToken());
+ }
+
+ private void checkTok(QuotedStringTokenizer tok, boolean delim, boolean quotes)
+ {
+ assertTrue(tok.hasMoreElements());
+ assertTrue(tok.hasMoreTokens());
+ assertEquals("abc", tok.nextToken());
+ if (delim)
+ assertEquals(",", tok.nextToken());
+ if (delim)
+ assertEquals(" ", tok.nextToken());
+
+ assertEquals(quotes ? "\"d\\\"'\"" : "d\"'", tok.nextElement());
+ if (delim)
+ assertEquals(",", tok.nextToken());
+ assertEquals(quotes ? "'p\\',y'" : "p',y", tok.nextToken());
+ if (delim)
+ assertEquals(" ", tok.nextToken());
+ assertEquals("z", tok.nextToken());
+ assertFalse(tok.hasMoreTokens());
+ }
+
+ /*
+ * Test for String quote(String, String)
+ */
+ @Test
+ public void testQuoteIfNeeded()
+ {
+ assertEquals("abc", QuotedStringTokenizer.quoteIfNeeded("abc", " ,"));
+ assertEquals("\"a c\"", QuotedStringTokenizer.quoteIfNeeded("a c", " ,"));
+ assertEquals("\"a'c\"", QuotedStringTokenizer.quoteIfNeeded("a'c", " ,"));
+ assertEquals("\"a\\n\\r\\t\"", QuotedStringTokenizer.quote("a\n\r\t"));
+ assertEquals("\"\\u0000\\u001f\"", QuotedStringTokenizer.quote("\u0000\u001f"));
+ }
+
+ @Test
+ public void testUnquote()
+ {
+ assertEquals("abc", QuotedStringTokenizer.unquote("abc"));
+ assertEquals("a\"c", QuotedStringTokenizer.unquote("\"a\\\"c\""));
+ assertEquals("a'c", QuotedStringTokenizer.unquote("\"a'c\""));
+ assertEquals("a\n\r\t", QuotedStringTokenizer.unquote("\"a\\n\\r\\t\""));
+ assertEquals("\u0000\u001f ", QuotedStringTokenizer.unquote("\"\u0000\u001f\u0020\""));
+ assertEquals("\u0000\u001f ", QuotedStringTokenizer.unquote("\"\u0000\u001f\u0020\""));
+ assertEquals("ab\u001ec", QuotedStringTokenizer.unquote("ab\u001ec"));
+ assertEquals("ab\u001ec", QuotedStringTokenizer.unquote("\"ab\u001ec\""));
+ }
+
+ @Test
+ public void testUnquoteOnly()
+ {
+ assertEquals("abc", QuotedStringTokenizer.unquoteOnly("abc"));
+ assertEquals("a\"c", QuotedStringTokenizer.unquoteOnly("\"a\\\"c\""));
+ assertEquals("a'c", QuotedStringTokenizer.unquoteOnly("\"a'c\""));
+ assertEquals("a\\n\\r\\t", QuotedStringTokenizer.unquoteOnly("\"a\\\\n\\\\r\\\\t\""));
+ assertEquals("ba\\uXXXXaaa", QuotedStringTokenizer.unquoteOnly("\"ba\\\\uXXXXaaa\""));
+ }
+
+ /**
+ * When encountering a Content-Disposition line during a multi-part mime file
+ * upload, the filename="..." field can contain '\' characters that do not
+ * belong to a proper escaping sequence, this tests QuotedStringTokenizer to
+ * ensure that it preserves those slashes for where they cannot be escaped.
+ */
+ @Test
+ public void testNextTokenOnContentDisposition()
+ {
+ String contentDisposition = "form-data; name=\"fileup\"; filename=\"Taken on Aug 22 \\ 2012.jpg\"";
+
+ QuotedStringTokenizer tok = new QuotedStringTokenizer(contentDisposition, ";", false, true);
+
+ assertEquals("form-data", tok.nextToken().trim());
+ assertEquals("name=\"fileup\"", tok.nextToken().trim());
+ assertEquals("filename=\"Taken on Aug 22 \\ 2012.jpg\"", tok.nextToken().trim());
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ReadLineInputStreamTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ReadLineInputStreamTest.java
new file mode 100644
index 0000000..ecb1e1f
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ReadLineInputStreamTest.java
@@ -0,0 +1,256 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.EnumSet;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.util.ReadLineInputStream.Termination;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ReadLineInputStreamTest
+{
+ BlockingArrayQueue<String> _queue = new BlockingArrayQueue<>();
+ PipedInputStream _pin;
+ volatile PipedOutputStream _pout;
+ ReadLineInputStream _in;
+ volatile Thread _writer;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ _queue.clear();
+ _pin = new PipedInputStream();
+ _pout = new PipedOutputStream(_pin);
+ _in = new ReadLineInputStream(_pin);
+ _writer = new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ OutputStream out = _pout;
+ while (out != null)
+ {
+ String s = _queue.poll(100, TimeUnit.MILLISECONDS);
+ if (s != null)
+ {
+ if ("__CLOSE__".equals(s))
+ _pout.close();
+ else
+ {
+ _pout.write(s.getBytes(StandardCharsets.UTF_8));
+ Thread.sleep(50);
+ }
+ }
+ out = _pout;
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ finally
+ {
+ _writer = null;
+ }
+ }
+ };
+ _writer.start();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _pout = null;
+ while (_writer != null)
+ {
+ Thread.sleep(10);
+ }
+ }
+
+ @Test
+ public void testCR() throws Exception
+ {
+ _queue.add("\rHello\rWorld\r\r");
+ _queue.add("__CLOSE__");
+
+ assertEquals("", _in.readLine());
+ assertEquals("Hello", _in.readLine());
+ assertEquals("World", _in.readLine());
+ assertEquals("", _in.readLine());
+ assertEquals(null, _in.readLine());
+ assertEquals(EnumSet.of(Termination.CR), _in.getLineTerminations());
+ }
+
+ @Test
+ public void testLF() throws Exception
+ {
+ _queue.add("\nHello\nWorld\n\n");
+ _queue.add("__CLOSE__");
+
+ assertEquals("", _in.readLine());
+ assertEquals("Hello", _in.readLine());
+ assertEquals("World", _in.readLine());
+ assertEquals("", _in.readLine());
+ assertEquals(null, _in.readLine());
+ assertEquals(EnumSet.of(Termination.LF), _in.getLineTerminations());
+ }
+
+ @Test
+ public void testCRLF() throws Exception
+ {
+ _queue.add("\r\nHello\r\nWorld\r\n\r\n");
+ _queue.add("__CLOSE__");
+
+ assertEquals("", _in.readLine());
+ assertEquals("Hello", _in.readLine());
+ assertEquals("World", _in.readLine());
+ assertEquals("", _in.readLine());
+ assertEquals(null, _in.readLine());
+ assertEquals(EnumSet.of(Termination.CRLF), _in.getLineTerminations());
+ }
+
+ @Test
+ public void testCRBlocking() throws Exception
+ {
+ _queue.add("");
+ _queue.add("\r");
+ _queue.add("Hello");
+ _queue.add("\rWorld\r");
+ _queue.add("\r");
+ _queue.add("__CLOSE__");
+
+ assertEquals("", _in.readLine());
+ assertEquals("Hello", _in.readLine());
+ assertEquals("World", _in.readLine());
+ assertEquals("", _in.readLine());
+ assertEquals(null, _in.readLine());
+ assertEquals(EnumSet.of(Termination.CR), _in.getLineTerminations());
+ }
+
+ @Test
+ public void testLFBlocking() throws Exception
+ {
+ _queue.add("");
+ _queue.add("\n");
+ _queue.add("Hello");
+ _queue.add("\nWorld\n");
+ _queue.add("\n");
+ _queue.add("__CLOSE__");
+
+ assertEquals("", _in.readLine());
+ assertEquals("Hello", _in.readLine());
+ assertEquals("World", _in.readLine());
+ assertEquals("", _in.readLine());
+ assertEquals(null, _in.readLine());
+ assertEquals(EnumSet.of(Termination.LF), _in.getLineTerminations());
+ }
+
+ @Test
+ public void testCRLFBlocking() throws Exception
+ {
+ _queue.add("\r");
+ _queue.add("\nHello");
+ _queue.add("\r\nWorld\r");
+ _queue.add("\n\r");
+ _queue.add("\n");
+ _queue.add("");
+ _queue.add("__CLOSE__");
+
+ assertEquals("", _in.readLine());
+ assertEquals("Hello", _in.readLine());
+ assertEquals("World", _in.readLine());
+ assertEquals("", _in.readLine());
+ assertEquals(null, _in.readLine());
+ assertEquals(EnumSet.of(Termination.CRLF), _in.getLineTerminations());
+ }
+
+ @Test
+ public void testHeaderLFBodyLF() throws Exception
+ {
+ _queue.add("Header\n");
+ _queue.add("\n");
+ _queue.add("\nBody\n");
+ _queue.add("\n");
+ _queue.add("__CLOSE__");
+
+ assertEquals("Header", _in.readLine());
+ assertEquals("", _in.readLine());
+
+ byte[] body = new byte[6];
+ _in.read(body);
+ assertEquals("\nBody\n", new String(body, 0, 6, StandardCharsets.UTF_8));
+
+ assertEquals("", _in.readLine());
+ assertEquals(null, _in.readLine());
+ assertEquals(EnumSet.of(Termination.LF), _in.getLineTerminations());
+ }
+
+ @Test
+ public void testHeaderCRBodyLF() throws Exception
+ {
+ _queue.add("Header\r");
+ _queue.add("\r");
+ _queue.add("\nBody\n");
+ _queue.add("\r");
+ _queue.add("__CLOSE__");
+
+ assertEquals("Header", _in.readLine());
+ assertEquals("", _in.readLine());
+
+ byte[] body = new byte[6];
+ _in.read(body);
+ assertEquals("\nBody\n", new String(body, 0, 6, StandardCharsets.UTF_8));
+
+ assertEquals("", _in.readLine());
+ assertEquals(null, _in.readLine());
+ assertEquals(EnumSet.of(Termination.CR), _in.getLineTerminations());
+ }
+
+ @Test
+ public void testHeaderCRLFBodyLF() throws Exception
+ {
+ _queue.add("Header\r\n");
+ _queue.add("\r\n");
+ _queue.add("\nBody\n");
+ _queue.add("\r\n");
+ _queue.add("__CLOSE__");
+
+ assertEquals("Header", _in.readLine());
+ assertEquals("", _in.readLine());
+
+ byte[] body = new byte[6];
+ _in.read(body);
+ assertEquals("\nBody\n", new String(body, 0, 6, StandardCharsets.UTF_8));
+
+ assertEquals("", _in.readLine());
+ assertEquals(null, _in.readLine());
+ assertEquals(EnumSet.of(Termination.CRLF), _in.getLineTerminations());
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/RegexSetTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/RegexSetTest.java
new file mode 100644
index 0000000..bcd4acc
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/RegexSetTest.java
@@ -0,0 +1,84 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class RegexSetTest
+{
+
+ @Test
+ public void testEmpty()
+ {
+ RegexSet set = new RegexSet();
+
+ assertEquals(false, set.contains("foo"));
+ assertEquals(false, set.matches("foo"));
+ assertEquals(false, set.matches(""));
+ }
+
+ @Test
+ public void testSimple()
+ {
+ RegexSet set = new RegexSet();
+ set.add("foo.*");
+
+ assertEquals(true, set.contains("foo.*"));
+ assertEquals(true, set.matches("foo"));
+ assertEquals(true, set.matches("foobar"));
+ assertEquals(false, set.matches("bar"));
+ assertEquals(false, set.matches(""));
+ }
+
+ @Test
+ public void testSimpleTerminated()
+ {
+ RegexSet set = new RegexSet();
+ set.add("^foo.*$");
+
+ assertEquals(true, set.contains("^foo.*$"));
+ assertEquals(true, set.matches("foo"));
+ assertEquals(true, set.matches("foobar"));
+ assertEquals(false, set.matches("bar"));
+ assertEquals(false, set.matches(""));
+ }
+
+ @Test
+ public void testCombined()
+ {
+ RegexSet set = new RegexSet();
+ set.add("^foo.*$");
+ set.add("bar");
+ set.add("[a-z][0-9][a-z][0-9]");
+
+ assertEquals(true, set.contains("^foo.*$"));
+ assertEquals(true, set.matches("foo"));
+ assertEquals(true, set.matches("foobar"));
+ assertEquals(true, set.matches("bar"));
+ assertEquals(true, set.matches("c3p0"));
+ assertEquals(true, set.matches("r2d2"));
+
+ assertEquals(false, set.matches("wibble"));
+ assertEquals(false, set.matches("barfoo"));
+ assertEquals(false, set.matches("2b!b"));
+ assertEquals(false, set.matches(""));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/RolloverFileOutputStreamTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/RolloverFileOutputStreamTest.java
new file mode 100644
index 0000000..6bf1ed1
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/RolloverFileOutputStreamTest.java
@@ -0,0 +1,387 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAccessor;
+import java.util.Arrays;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+@ExtendWith(WorkDirExtension.class)
+public class RolloverFileOutputStreamTest
+{
+ public WorkDir testingDir;
+
+ private static ZoneId toZoneId(String timezoneId)
+ {
+ ZoneId zone = TimeZone.getTimeZone(timezoneId).toZoneId();
+ // System.out.printf(".toZoneId(\"%s\") = [id=%s,normalized=%s]%n", timezoneId, zone.getId(), zone.normalized());
+ return zone;
+ }
+
+ private static ZonedDateTime toDateTime(String timendate, ZoneId zone)
+ {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd-hh:mm:ss.S a z")
+ .withZone(zone);
+ return ZonedDateTime.parse(timendate, formatter);
+ }
+
+ private static String toString(TemporalAccessor date)
+ {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd-hh:mm:ss.S a z");
+ return formatter.format(date);
+ }
+
+ private void assertSequence(ZonedDateTime midnight, Object[][] expected)
+ {
+ ZonedDateTime nextEvent = midnight;
+
+ for (int i = 0; i < expected.length; i++)
+ {
+ long currentMillis = nextEvent.toInstant().toEpochMilli();
+ nextEvent = nextEvent.toLocalDate().plus(1, ChronoUnit.DAYS).atStartOfDay(nextEvent.getZone());
+ assertThat("Next Event", toString(nextEvent), is(expected[i][0]));
+ long duration = (nextEvent.toInstant().toEpochMilli() - currentMillis);
+ assertThat("Duration to next event", duration, is((long)expected[i][1]));
+ }
+ }
+
+ /**
+ * <a href="Issue #1507">https://github.com/eclipse/jetty.project/issues/1507</a>
+ */
+ @Test
+ public void testMidnightRolloverCalcPDTIssue1507()
+ {
+ ZoneId zone = toZoneId("PST");
+ ZonedDateTime initialDate = toDateTime("2017.04.26-08:00:00.0 PM PDT", zone);
+
+ ZonedDateTime midnight = RolloverFileOutputStream.toMidnight(initialDate);
+ assertThat("Midnight", toString(midnight), is("2017.04.27-12:00:00.0 AM PDT"));
+
+ Object[][] expected = {
+ {"2017.04.27-12:00:00.0 AM PDT", 14_400_000L},
+ {"2017.04.28-12:00:00.0 AM PDT", 86_400_000L},
+ {"2017.04.29-12:00:00.0 AM PDT", 86_400_000L},
+ {"2017.04.30-12:00:00.0 AM PDT", 86_400_000L},
+ {"2017.05.01-12:00:00.0 AM PDT", 86_400_000L},
+ {"2017.05.02-12:00:00.0 AM PDT", 86_400_000L},
+ };
+
+ assertSequence(initialDate, expected);
+ }
+
+ @Test
+ public void testMidnightRolloverCalcPSTDSTStart()
+ {
+ ZoneId zone = toZoneId("PST");
+ ZonedDateTime initialDate = toDateTime("2016.03.10-01:23:45.0 PM PST", zone);
+
+ ZonedDateTime midnight = RolloverFileOutputStream.toMidnight(initialDate);
+ assertThat("Midnight", toString(midnight), is("2016.03.11-12:00:00.0 AM PST"));
+
+ Object[][] expected = {
+ {"2016.03.12-12:00:00.0 AM PST", 86_400_000L},
+ {"2016.03.13-12:00:00.0 AM PST", 86_400_000L},
+ {"2016.03.14-12:00:00.0 AM PDT", 82_800_000L}, // the short day
+ {"2016.03.15-12:00:00.0 AM PDT", 86_400_000L},
+ {"2016.03.16-12:00:00.0 AM PDT", 86_400_000L},
+ };
+
+ assertSequence(midnight, expected);
+ }
+
+ @Test
+ public void testMidnightRolloverCalcPSTDSTEnd()
+ {
+ ZoneId zone = toZoneId("PST");
+ ZonedDateTime initialDate = toDateTime("2016.11.03-11:22:33.0 AM PDT", zone);
+
+ ZonedDateTime midnight = RolloverFileOutputStream.toMidnight(initialDate);
+ assertThat("Midnight", toString(midnight), is("2016.11.04-12:00:00.0 AM PDT"));
+
+ Object[][] expected = {
+ {"2016.11.05-12:00:00.0 AM PDT", 86_400_000L},
+ {"2016.11.06-12:00:00.0 AM PDT", 86_400_000L},
+ {"2016.11.07-12:00:00.0 AM PST", 90_000_000L}, // the long day
+ {"2016.11.08-12:00:00.0 AM PST", 86_400_000L},
+ {"2016.11.09-12:00:00.0 AM PST", 86_400_000L},
+ };
+
+ assertSequence(midnight, expected);
+ }
+
+ @Test
+ public void testMidnightRolloverCalcSydneyDSTStart()
+ {
+ ZoneId zone = toZoneId("Australia/Sydney");
+ ZonedDateTime initialDate = toDateTime("2016.09.31-01:23:45.0 PM AEST", zone);
+
+ ZonedDateTime midnight = RolloverFileOutputStream.toMidnight(initialDate);
+ assertThat("Midnight", toString(midnight), is("2016.10.01-12:00:00.0 AM AEST"));
+
+ Object[][] expected = {
+ {"2016.10.02-12:00:00.0 AM AEST", 86_400_000L},
+ {"2016.10.03-12:00:00.0 AM AEDT", 82_800_000L}, // the short day
+ {"2016.10.04-12:00:00.0 AM AEDT", 86_400_000L},
+ {"2016.10.05-12:00:00.0 AM AEDT", 86_400_000L},
+ {"2016.10.06-12:00:00.0 AM AEDT", 86_400_000L},
+ };
+
+ assertSequence(midnight, expected);
+ }
+
+ @Test
+ public void testMidnightRolloverCalcSydneyDSTEnd()
+ {
+ ZoneId zone = toZoneId("Australia/Sydney");
+ ZonedDateTime initialDate = toDateTime("2016.04.01-11:22:33.0 AM AEDT", zone);
+
+ ZonedDateTime midnight = RolloverFileOutputStream.toMidnight(initialDate);
+ assertThat("Midnight", toString(midnight), is("2016.04.02-12:00:00.0 AM AEDT"));
+
+ Object[][] expected = {
+ {"2016.04.03-12:00:00.0 AM AEDT", 86_400_000L},
+ {"2016.04.04-12:00:00.0 AM AEST", 90_000_000L}, // The long day
+ {"2016.04.05-12:00:00.0 AM AEST", 86_400_000L},
+ {"2016.04.06-12:00:00.0 AM AEST", 86_400_000L},
+ {"2016.04.07-12:00:00.0 AM AEST", 86_400_000L},
+ };
+
+ assertSequence(midnight, expected);
+ }
+
+ @Test
+ public void testFileHandling() throws Exception
+ {
+ Path testPath = testingDir.getEmptyPathDir();
+
+ ZoneId zone = toZoneId("Australia/Sydney");
+ ZonedDateTime now = toDateTime("2016.04.10-08:30:12.3 AM AEDT", zone);
+
+ Path template = testPath.resolve("test-rofos-yyyy_mm_dd.log");
+ String templateString = template.toAbsolutePath().toString();
+
+ try (RolloverFileOutputStream rofos =
+ new RolloverFileOutputStream(templateString, false, 3, TimeZone.getTimeZone(zone), null, null, now))
+ {
+ rofos.write("TICK".getBytes());
+ rofos.flush();
+ }
+
+ now = now.plus(5, ChronoUnit.MINUTES);
+
+ try (RolloverFileOutputStream rofos =
+ new RolloverFileOutputStream(templateString, false, 3, TimeZone.getTimeZone(zone), null, null, now))
+ {
+ rofos.write("TOCK".getBytes());
+ rofos.flush();
+ String[] ls = ls(testPath);
+ assertThat(ls.length, is(2));
+ String backup = null;
+ for (String n : ls)
+ {
+ if (!"test-rofos-2016_04_10.log".equals(n))
+ backup = n;
+ }
+
+ assertThat(Arrays.asList(ls), Matchers.containsInAnyOrder(backup, "test-rofos-2016_04_10.log"));
+
+ Files.setLastModifiedTime(testPath.resolve(backup), FileTime.from(now.toInstant()));
+ Files.setLastModifiedTime(testPath.resolve("test-rofos-2016_04_10.log"), FileTime.from(now.toInstant()));
+
+ ZonedDateTime time = now.minus(1, ChronoUnit.DAYS);
+ for (int i = 10; i-- > 5; )
+ {
+ String file = "test-rofos-2016_04_0" + i + ".log";
+ Path path = testPath.resolve(file);
+ FS.touch(path);
+ Files.setLastModifiedTime(path, FileTime.from(time.toInstant()));
+
+ if (i % 2 == 0)
+ {
+ file = "test-rofos-2016_04_0" + i + ".log.083512300";
+ path = testPath.resolve(file);
+ FS.touch(path);
+ Files.setLastModifiedTime(path, FileTime.from(time.toInstant()));
+ time = time.minus(1, ChronoUnit.DAYS);
+ }
+
+ file = "unrelated-" + i;
+ path = testPath.resolve(file);
+ FS.touch(path);
+ Files.setLastModifiedTime(path, FileTime.from(time.toInstant()));
+
+ time = time.minus(1, ChronoUnit.DAYS);
+ }
+
+ ls = ls(testPath);
+ assertThat(ls.length, is(14));
+ assertThat(Arrays.asList(ls), Matchers.containsInAnyOrder(
+ "test-rofos-2016_04_05.log",
+ "test-rofos-2016_04_06.log",
+ "test-rofos-2016_04_07.log",
+ "test-rofos-2016_04_08.log",
+ "test-rofos-2016_04_09.log",
+ "test-rofos-2016_04_10.log",
+ "test-rofos-2016_04_06.log.083512300",
+ "test-rofos-2016_04_08.log.083512300",
+ "test-rofos-2016_04_10.log.083512300",
+ "unrelated-9",
+ "unrelated-8",
+ "unrelated-7",
+ "unrelated-6",
+ "unrelated-5"
+ ));
+
+ rofos.removeOldFiles(now);
+ ls = ls(testPath);
+ assertThat(ls.length, is(10));
+ assertThat(Arrays.asList(ls), Matchers.containsInAnyOrder(
+ "test-rofos-2016_04_08.log",
+ "test-rofos-2016_04_09.log",
+ "test-rofos-2016_04_10.log",
+ "test-rofos-2016_04_08.log.083512300",
+ "test-rofos-2016_04_10.log.083512300",
+ "unrelated-9",
+ "unrelated-8",
+ "unrelated-7",
+ "unrelated-6",
+ "unrelated-5"));
+
+ assertThat(readPath(testPath.resolve(backup)), is("TICK"));
+ assertThat(readPath(testPath.resolve("test-rofos-2016_04_10.log")), is("TOCK"));
+ }
+ }
+
+ @Test
+ public void testRollover() throws Exception
+ {
+ Path testPath = testingDir.getEmptyPathDir();
+
+ ZoneId zone = toZoneId("Australia/Sydney");
+ ZonedDateTime now = toDateTime("2016.04.10-11:59:55.0 PM AEDT", zone);
+
+ Path template = testPath.resolve("test-rofos-yyyy_mm_dd.log");
+ String templateString = template.toAbsolutePath().toString();
+
+ try (RolloverFileOutputStream rofos =
+ new RolloverFileOutputStream(templateString, false, 0, TimeZone.getTimeZone(zone), null, null, now))
+ {
+ rofos.write("BEFORE".getBytes());
+ rofos.flush();
+ String[] ls = ls(testPath);
+ assertThat(ls.length, is(1));
+ assertThat(ls[0], is("test-rofos-2016_04_10.log"));
+
+ TimeUnit.SECONDS.sleep(10);
+ rofos.write("AFTER".getBytes());
+ ls = ls(testPath);
+ assertThat(ls.length, is(2));
+
+ for (String n : ls)
+ {
+ String content = readPath(testPath.resolve(n));
+ if ("test-rofos-2016_04_10.log".equals(n))
+ {
+ assertThat(content, is("BEFORE"));
+ }
+ else
+ {
+ assertThat(content, is("AFTER"));
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testRolloverBackup() throws Exception
+ {
+ Path testPath = testingDir.getEmptyPathDir();
+
+ ZoneId zone = toZoneId("Australia/Sydney");
+ ZonedDateTime now = toDateTime("2016.04.10-11:59:55.0 PM AEDT", zone);
+
+ Path template = testPath.resolve("test-rofosyyyy_mm_dd.log");
+ String templateString = template.toAbsolutePath().toString();
+
+ try (RolloverFileOutputStream rofos =
+ new RolloverFileOutputStream(templateString, false, 0, TimeZone.getTimeZone(zone), "", null, now))
+ {
+ rofos.write("BEFORE".getBytes());
+ rofos.flush();
+ String[] ls = ls(testPath);
+ assertThat("File List.length", ls.length, is(1));
+ assertThat(ls[0], is("test-rofos.log"));
+
+ TimeUnit.SECONDS.sleep(10);
+ rofos.write("AFTER".getBytes());
+ ls = ls(testPath);
+ assertThat("File List.length", ls.length, is(2));
+
+ for (String n : ls)
+ {
+ String content = readPath(testPath.resolve(n));
+ if ("test-rofos.log".equals(n))
+ {
+ assertThat(content, is("AFTER"));
+ }
+ else
+ {
+ assertThat(content, is("BEFORE"));
+ }
+ }
+ }
+ }
+
+ private String readPath(Path path) throws IOException
+ {
+ try (BufferedReader reader = Files.newBufferedReader(path))
+ {
+ return IO.toString(reader);
+ }
+ }
+
+ private String[] ls(Path path) throws IOException
+ {
+ try (Stream<Path> s = Files.list(path))
+ {
+ return s.map(p -> p.getFileName().toString()).toArray(String[]::new);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ScannerTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ScannerTest.java
new file mode 100644
index 0000000..71dae85
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ScannerTest.java
@@ -0,0 +1,447 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.Scanner.Notification;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ScannerTest
+{
+ static Path _directory;
+ static Scanner _scanner;
+ static BlockingQueue<Event> _queue = new LinkedBlockingQueue<>();
+ static BlockingQueue<List<String>> _bulk = new LinkedBlockingQueue<>();
+
+ @BeforeAll
+ public static void setUpBeforeClass() throws Exception
+ {
+ _directory = MavenTestingUtils.getTargetTestingPath(ScannerTest.class.getSimpleName());
+ FS.ensureEmpty(_directory);
+
+ _directory = _directory.toRealPath();
+
+ _scanner = new Scanner();
+ _scanner.addDirectory(_directory);
+ _scanner.setScanInterval(0);
+ _scanner.setReportDirs(false);
+ _scanner.setReportExistingFilesOnStartup(false);
+ _scanner.addListener(new Scanner.DiscreteListener()
+ {
+ @Override
+ public void fileRemoved(String filename)
+ {
+ _queue.add(new Event(filename, Notification.REMOVED));
+ }
+
+ @Override
+ public void fileChanged(String filename)
+ {
+ _queue.add(new Event(filename, Notification.CHANGED));
+ }
+
+ @Override
+ public void fileAdded(String filename)
+ {
+ _queue.add(new Event(filename, Notification.ADDED));
+ }
+ });
+ _scanner.addListener((Scanner.BulkListener)filenames -> _bulk.add(filenames));
+
+ _scanner.start();
+ _scanner.scan();
+
+ assertTrue(_queue.isEmpty());
+ assertTrue(_bulk.isEmpty());
+ }
+
+ @AfterAll
+ public static void tearDownAfterClass() throws Exception
+ {
+ _scanner.stop();
+ }
+
+ static class Event
+ {
+ String _filename;
+ Scanner.Notification _notification;
+
+ public Event(String filename, Notification notification)
+ {
+ _filename = filename;
+ _notification = notification;
+ }
+ }
+
+ @Test
+ public void testDepth() throws Exception
+ {
+ File root = new File(_directory.toFile(), "root");
+ FS.ensureDirExists(root);
+ FS.touch(new File(root, "foo.foo"));
+ FS.touch(new File(root, "foo2.foo"));
+ File dir = new File(root, "xxx");
+ FS.ensureDirExists(dir);
+ File x1 = new File(dir, "xxx.foo");
+ FS.touch(x1);
+ File x2 = new File(dir, "xxx2.foo");
+ FS.touch(x2);
+ File dir2 = new File(dir, "yyy");
+ FS.ensureDirExists(dir2);
+ File y1 = new File(dir2, "yyy.foo");
+ FS.touch(y1);
+ File y2 = new File(dir2, "yyy2.foo");
+ FS.touch(y2);
+
+ BlockingQueue<Event> queue = new LinkedBlockingQueue<>();
+ Scanner scanner = new Scanner();
+ scanner.setScanInterval(0);
+ scanner.setScanDepth(0);
+ scanner.setReportDirs(true);
+ scanner.setReportExistingFilesOnStartup(true);
+ scanner.addDirectory(root.toPath());
+ scanner.addListener(new Scanner.DiscreteListener()
+ {
+ @Override
+ public void fileRemoved(String filename)
+ {
+ queue.add(new Event(filename, Notification.REMOVED));
+ }
+
+ @Override
+ public void fileChanged(String filename)
+ {
+ queue.add(new Event(filename, Notification.CHANGED));
+ }
+
+ @Override
+ public void fileAdded(String filename)
+ {
+ queue.add(new Event(filename, Notification.ADDED));
+ }
+ });
+
+ scanner.start();
+ Event e = queue.take();
+ assertNotNull(e);
+ assertEquals(Notification.ADDED, e._notification);
+ assertTrue(e._filename.endsWith(root.getName()));
+ queue.clear();
+ scanner.stop();
+ scanner.reset();
+
+ //Depth one should report the dir itself and its file and dir direct children
+ scanner.setScanDepth(1);
+ scanner.addDirectory(root.toPath());
+ scanner.start();
+ assertEquals(4, queue.size());
+ queue.clear();
+ scanner.stop();
+ scanner.reset();
+
+ //Depth 2 should report the dir itself, all file children, xxx and xxx's children
+ scanner.setScanDepth(2);
+ scanner.addDirectory(root.toPath());
+ scanner.start();
+
+ assertEquals(7, queue.size());
+ scanner.stop();
+ }
+
+ @Test
+ public void testPatterns() throws Exception
+ {
+ //test include and exclude patterns
+ File root = new File(_directory.toFile(), "proot");
+ FS.ensureDirExists(root);
+
+ File ttt = new File(root, "ttt.txt");
+ FS.touch(ttt);
+ FS.touch(new File(root, "ttt.foo"));
+ File dir = new File(root, "xxx");
+ FS.ensureDirExists(dir);
+
+ File x1 = new File(dir, "ttt.xxx");
+ FS.touch(x1);
+ File x2 = new File(dir, "xxx.txt");
+ FS.touch(x2);
+
+ File dir2 = new File(dir, "yyy");
+ FS.ensureDirExists(dir2);
+ File y1 = new File(dir2, "ttt.yyy");
+ FS.touch(y1);
+ File y2 = new File(dir2, "yyy.txt");
+ FS.touch(y2);
+
+ BlockingQueue<Event> queue = new LinkedBlockingQueue<>();
+ //only scan the *.txt files for changes
+ Scanner scanner = new Scanner();
+ IncludeExcludeSet<PathMatcher, Path> pattern = scanner.addDirectory(root.toPath());
+ pattern.exclude(root.toPath().getFileSystem().getPathMatcher("glob:**/*.foo"));
+ pattern.exclude(root.toPath().getFileSystem().getPathMatcher("glob:**/ttt.xxx"));
+ scanner.setScanInterval(0);
+ scanner.setScanDepth(2); //should never see any files from subdir yyy
+ scanner.setReportDirs(false);
+ scanner.setReportExistingFilesOnStartup(false);
+ scanner.addListener(new Scanner.DiscreteListener()
+ {
+ @Override
+ public void fileRemoved(String filename)
+ {
+ queue.add(new Event(filename, Notification.REMOVED));
+ }
+
+ @Override
+ public void fileChanged(String filename)
+ {
+ queue.add(new Event(filename, Notification.CHANGED));
+ }
+
+ @Override
+ public void fileAdded(String filename)
+ {
+ queue.add(new Event(filename, Notification.ADDED));
+ }
+ });
+
+ scanner.start();
+ assertTrue(queue.isEmpty());
+
+ Thread.sleep(1100); // make sure time in seconds changes
+ FS.touch(ttt);
+ FS.touch(x2);
+ FS.touch(x1);
+ FS.touch(y2);
+ scanner.scan();
+ scanner.scan(); //2 scans for file to be considered settled
+
+ assertThat(queue.size(), Matchers.equalTo(2));
+ for (Event e : queue)
+ {
+ assertTrue(e._filename.endsWith("ttt.txt") || e._filename.endsWith("xxx.txt"));
+ }
+ }
+
+ @Test
+ @Tag("Slow")
+ public void testAddedChangeRemove() throws Exception
+ {
+ touch("a0");
+
+ // takes 2 scans to notice a0 and check that it is stable
+ _scanner.scan();
+ _scanner.scan();
+
+ Event event = _queue.poll();
+ assertNotNull(event, "Event should not be null");
+ assertEquals(_directory.resolve("a0").toString(), event._filename);
+ assertEquals(Notification.ADDED, event._notification);
+
+ // add 3 more files
+ Thread.sleep(1100); // make sure time in seconds changes
+ touch("a1");
+ touch("a2");
+ touch("a3");
+
+ // not stable after 1 scan so should not be seen yet.
+ _scanner.scan();
+ event = _queue.poll();
+ assertNull(event);
+
+ // Keep a2 unstable and remove a3 before it stabilized
+ Thread.sleep(1100); // make sure time in seconds changes
+ touch("a2");
+ delete("a3");
+
+ // only a1 is stable so it should be seen.
+ _scanner.scan();
+ event = _queue.poll();
+ assertNotNull(event);
+ assertEquals(_directory.resolve("a1").toString(), event._filename);
+ assertEquals(Notification.ADDED, event._notification);
+ assertTrue(_queue.isEmpty());
+
+ // Now a2 is stable
+ _scanner.scan();
+ event = _queue.poll();
+ assertNotNull(event);
+ assertEquals(_directory.resolve("a2").toString(), event._filename);
+ assertEquals(Notification.ADDED, event._notification);
+ assertTrue(_queue.isEmpty());
+
+ // We never see a3 as it was deleted before it stabalised
+
+ // touch a1 and a2
+ Thread.sleep(1100); // make sure time in seconds changes
+ touch("a1");
+ touch("a2");
+ // not stable after 1scan so nothing should not be seen yet.
+ _scanner.scan();
+ event = _queue.poll();
+ assertNull(event);
+
+ // Keep a2 unstable
+ Thread.sleep(1100); // make sure time in seconds changes
+ touch("a2");
+
+ // only a1 is stable so it should be seen.
+ _scanner.scan();
+ event = _queue.poll();
+ assertNotNull(event);
+ assertEquals(_directory.resolve("a1").toString(), event._filename);
+ assertEquals(Notification.CHANGED, event._notification);
+ assertTrue(_queue.isEmpty());
+
+ // Now a2 is stable
+ _scanner.scan();
+ event = _queue.poll();
+ assertNotNull(event);
+ assertEquals(_directory.resolve("a2").toString(), event._filename);
+ assertEquals(Notification.CHANGED, event._notification);
+ assertTrue(_queue.isEmpty());
+
+ // delete a1 and a2
+ delete("a1");
+ delete("a2");
+ // not stable after 1scan so nothing should not be seen yet.
+ _scanner.scan();
+ event = _queue.poll();
+ assertNull(event);
+
+ // readd a2
+ touch("a2");
+
+ // only a1 is stable so it should be seen.
+ _scanner.scan();
+ event = _queue.poll();
+ assertNotNull(event);
+ assertEquals(_directory.resolve("a1").toString(), event._filename);
+ assertEquals(Notification.REMOVED, event._notification);
+ assertTrue(_queue.isEmpty());
+
+ // Now a2 is stable and is a changed file rather than a remove
+ _scanner.scan();
+ event = _queue.poll();
+ assertNotNull(event);
+ assertEquals(_directory.resolve("a2").toString(), event._filename);
+ assertEquals(Notification.CHANGED, event._notification);
+ assertTrue(_queue.isEmpty());
+ }
+
+ @Test
+ public void testSizeChange() throws Exception
+ {
+ touch("tsc0");
+ _scanner.scan();
+ _scanner.scan();
+
+ // takes 2s to notice tsc0 and check that it is stable. This syncs us with the scan
+ Event event = _queue.poll();
+ assertNotNull(event);
+ assertEquals(_directory.resolve("tsc0").toString(), event._filename);
+ assertEquals(Notification.ADDED, event._notification);
+
+ // Create a new file by writing to it.
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ File file = new File(_directory.toFile(), "st");
+ try (OutputStream out = new FileOutputStream(file, true))
+ {
+ out.write('x');
+ out.flush();
+ file.setLastModified(now);
+
+ // Not stable yet so no notification.
+ _scanner.scan();
+ event = _queue.poll();
+ assertNull(event);
+
+ // Modify size only
+ out.write('x');
+ out.flush();
+ file.setLastModified(now);
+
+ // Still not stable yet so no notification.
+ _scanner.scan();
+ event = _queue.poll();
+ assertNull(event);
+
+ // now stable so finally see the ADDED
+ _scanner.scan();
+ event = _queue.poll();
+ assertNotNull(event);
+ assertEquals(_directory.resolve("st").toString(), event._filename);
+ assertEquals(Notification.ADDED, event._notification);
+
+ // Modify size only
+ out.write('x');
+ out.flush();
+ file.setLastModified(now);
+
+ // Still not stable yet so no notification.
+ _scanner.scan();
+ event = _queue.poll();
+ assertNull(event);
+
+ // now stable so finally see the ADDED
+ _scanner.scan();
+ event = _queue.poll();
+ assertNotNull(event);
+ assertEquals(_directory.resolve("st").toString(), event._filename);
+ assertEquals(Notification.CHANGED, event._notification);
+ }
+ }
+
+ private void delete(String string) throws IOException
+ {
+ Path file = _directory.resolve(string);
+ Files.deleteIfExists(file);
+ }
+
+ private void touch(String string) throws IOException
+ {
+ File file = new File(_directory.toFile(), string);
+ if (file.exists())
+ file.setLastModified(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()));
+ else
+ file.createNewFile();
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/SearchPatternTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/SearchPatternTest.java
new file mode 100644
index 0000000..b1e3a17
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/SearchPatternTest.java
@@ -0,0 +1,239 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class SearchPatternTest
+{
+
+ @Test
+ public void testBasicSearch()
+ {
+ byte[] p1 = "truth".getBytes(StandardCharsets.US_ASCII);
+ byte[] p2 = "evident".getBytes(StandardCharsets.US_ASCII);
+ byte[] p3 = "we".getBytes(StandardCharsets.US_ASCII);
+ byte[] d = "we hold these truths to be self evident".getBytes(StandardCharsets.US_ASCII);
+
+ // Testing Compiled Pattern p1 "truth"
+ SearchPattern sp1 = SearchPattern.compile(p1);
+ assertEquals(14, sp1.match(d, 0, d.length));
+ assertEquals(14, sp1.match(d, 14, p1.length));
+ assertEquals(14, sp1.match(d, 14, p1.length + 1));
+ assertEquals(-1, sp1.match(d, 14, p1.length - 1));
+ assertEquals(-1, sp1.match(d, 15, d.length - 15));
+
+ // Testing Compiled Pattern p2 "evident"
+ SearchPattern sp2 = SearchPattern.compile(p2);
+ assertEquals(32, sp2.match(d, 0, d.length));
+ assertEquals(32, sp2.match(d, 32, p2.length));
+ assertEquals(32, sp2.match(d, 32, p2.length));
+ assertEquals(-1, sp2.match(d, 32, p2.length - 1));
+ assertEquals(-1, sp2.match(d, 33, d.length - 33));
+
+ // Testing Compiled Pattern p3 "evident"
+ SearchPattern sp3 = SearchPattern.compile(p3);
+ assertEquals(0, sp3.match(d, 0, d.length));
+ assertEquals(0, sp3.match(d, 0, p3.length));
+ assertEquals(0, sp3.match(d, 0, p3.length + 1));
+ assertEquals(-1, sp3.match(d, 0, p3.length - 1));
+ assertEquals(-1, sp3.match(d, 1, d.length - 1));
+ }
+
+ @Test
+ public void testDoubleMatch()
+ {
+ byte[] p = "violent".getBytes(StandardCharsets.US_ASCII);
+ byte[] d = "These violent delights have violent ends.".getBytes(StandardCharsets.US_ASCII);
+ SearchPattern sp = SearchPattern.compile(p);
+ assertEquals(6, sp.match(d, 0, d.length));
+ assertEquals(-1, sp.match(d, 6, p.length - 1));
+ assertEquals(28, sp.match(d, 7, d.length - 7));
+ assertEquals(28, sp.match(d, 28, d.length - 28));
+ assertEquals(-1, sp.match(d, 29, d.length - 29));
+ }
+
+ @Test
+ public void testSearchInBinary()
+ {
+ byte[] random = new byte[8192];
+ ThreadLocalRandom.current().nextBytes(random);
+ // Arrays.fill(random,(byte)-67);
+ String preamble = "Blah blah blah";
+ String epilogue = "The End! Blah Blah Blah";
+
+ ByteBuffer data = BufferUtil.allocate(preamble.length() + random.length + epilogue.length());
+ BufferUtil.append(data, BufferUtil.toBuffer(preamble));
+ BufferUtil.append(data, ByteBuffer.wrap(random));
+ BufferUtil.append(data, BufferUtil.toBuffer(epilogue));
+
+ SearchPattern sp = SearchPattern.compile("The End!");
+
+ assertEquals(preamble.length() + random.length, sp.match(data.array(), data.arrayOffset() + data.position(), data.remaining()));
+ }
+
+ @Test
+ public void testSearchBinaryKey()
+ {
+ byte[] random = new byte[8192];
+ ThreadLocalRandom.current().nextBytes(random);
+ byte[] key = new byte[64];
+ ThreadLocalRandom.current().nextBytes(key);
+
+ ByteBuffer data = BufferUtil.allocate(random.length + key.length);
+ BufferUtil.append(data, ByteBuffer.wrap(random));
+ BufferUtil.append(data, ByteBuffer.wrap(key));
+ SearchPattern sp = SearchPattern.compile(key);
+
+ assertEquals(random.length, sp.match(data.array(), data.arrayOffset() + data.position(), data.remaining()));
+ }
+
+ @Test
+ public void testAlmostMatch()
+ {
+ byte[] p = "violent".getBytes(StandardCharsets.US_ASCII);
+ byte[] d = "vio lent violen v iolent violin vioviolenlent viiolent".getBytes(StandardCharsets.US_ASCII);
+ SearchPattern sp = SearchPattern.compile(p);
+ assertEquals(-1, sp.match(d, 0, d.length));
+ }
+
+ @Test
+ public void testOddSizedPatterns()
+ {
+ // Test Large Pattern
+ byte[] p = "pneumonoultramicroscopicsilicovolcanoconiosis".getBytes(StandardCharsets.US_ASCII);
+ byte[] d = "pneumon".getBytes(StandardCharsets.US_ASCII);
+ SearchPattern sp = SearchPattern.compile(p);
+ assertEquals(-1, sp.match(d, 0, d.length));
+
+ // Test Single Character Pattern
+ p = "s".getBytes(StandardCharsets.US_ASCII);
+ d = "the cake is a lie".getBytes(StandardCharsets.US_ASCII);
+ sp = SearchPattern.compile(p);
+ assertEquals(10, sp.match(d, 0, d.length));
+ }
+
+ @Test
+ public void testEndsWith()
+ {
+ byte[] p = "pneumonoultramicroscopicsilicovolcanoconiosis".getBytes(StandardCharsets.US_ASCII);
+ byte[] d = "pneumonoultrami".getBytes(StandardCharsets.US_ASCII);
+ SearchPattern sp = SearchPattern.compile(p);
+ assertEquals(15, sp.endsWith(d, 0, d.length));
+
+ p = "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII);
+ d = "abcdefghijklmnopqrstuvwxyzabcdefghijklmno".getBytes(StandardCharsets.US_ASCII);
+ sp = SearchPattern.compile(p);
+ assertEquals(0, sp.match(d, 0, d.length));
+ assertEquals(-1, sp.match(d, 1, d.length - 1));
+ assertEquals(15, sp.endsWith(d, 0, d.length));
+
+ p = "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII);
+ d = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII);
+ sp = SearchPattern.compile(p);
+ assertEquals(0, sp.match(d, 0, d.length));
+ assertEquals(26, sp.match(d, 1, d.length - 1));
+ assertEquals(26, sp.endsWith(d, 0, d.length));
+
+ //test no match
+ p = "hello world".getBytes(StandardCharsets.US_ASCII);
+ d = "there is definitely no match in here".getBytes(StandardCharsets.US_ASCII);
+ sp = SearchPattern.compile(p);
+ assertEquals(0, sp.endsWith(d, 0, d.length));
+
+ //Test with range on array.
+ p = "abcde".getBytes(StandardCharsets.US_ASCII);
+ d = "?abc00000".getBytes(StandardCharsets.US_ASCII);
+ sp = SearchPattern.compile(p);
+ assertEquals(3, sp.endsWith(d, 0, 4));
+ }
+
+ @Test
+ public void testStartsWithNoOffset()
+ {
+ testStartsWith("");
+ }
+
+ @Test
+ public void testStartsWithOffset()
+ {
+ testStartsWith("abcdef");
+ }
+
+ private void testStartsWith(String offset)
+ {
+ byte[] p = "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII);
+ byte[] d = (offset + "ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz").getBytes(StandardCharsets.US_ASCII);
+ SearchPattern sp = SearchPattern.compile(p);
+ assertEquals(18 + offset.length(), sp.match(d, offset.length(), d.length - offset.length()));
+ assertEquals(-1, sp.match(d, offset.length() + 19, d.length - 19 - offset.length()));
+ assertEquals(26, sp.startsWith(d, offset.length(), d.length - offset.length(), 8));
+
+ p = "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII);
+ d = (offset + "ijklmnopqrstuvwxyNOMATCH").getBytes(StandardCharsets.US_ASCII);
+ sp = SearchPattern.compile(p);
+ assertEquals(0, sp.startsWith(d, offset.length(), d.length - offset.length(), 8));
+
+ p = "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII);
+ d = (offset + "abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz").getBytes(StandardCharsets.US_ASCII);
+ sp = SearchPattern.compile(p);
+ assertEquals(26, sp.startsWith(d, offset.length(), d.length - offset.length(), 0));
+
+ //test no match
+ p = "hello world".getBytes(StandardCharsets.US_ASCII);
+ d = (offset + "there is definitely no match in here").getBytes(StandardCharsets.US_ASCII);
+ sp = SearchPattern.compile(p);
+ assertEquals(0, sp.startsWith(d, offset.length(), d.length - offset.length(), 0));
+
+ //test large pattern small buffer
+ p = "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII);
+ d = (offset + "mnopqrs").getBytes(StandardCharsets.US_ASCII);
+ sp = SearchPattern.compile(p);
+ assertEquals(19, sp.startsWith(d, offset.length(), d.length - offset.length(), 12));
+
+ //partial pattern
+ p = "abcdef".getBytes(StandardCharsets.US_ASCII);
+ d = (offset + "cde").getBytes(StandardCharsets.US_ASCII);
+ sp = SearchPattern.compile(p);
+ assertEquals(5, sp.startsWith(d, offset.length(), d.length - offset.length(), 2));
+ }
+
+ @Test
+ public void testExampleFrom4673()
+ {
+ SearchPattern pattern = SearchPattern.compile("\r\n------WebKitFormBoundaryhXfFAMfUnUKhmqT8".getBytes(StandardCharsets.US_ASCII));
+ byte[] data = new byte[]{
+ 118, 97, 108, 117, 101, 49,
+ '\r', '\n', '-', '-', '-', '-',
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+ };
+ int length = 12;
+
+ int partialMatch = pattern.endsWith(data, 0, length);
+ System.err.println("match1: " + partialMatch);
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/SharedBlockingCallbackTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/SharedBlockingCallbackTest.java
new file mode 100644
index 0000000..b036de8
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/SharedBlockingCallbackTest.java
@@ -0,0 +1,291 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jetty.util.SharedBlockingCallback.Blocker;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.lessThan;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class SharedBlockingCallbackTest
+{
+ private static final Logger LOG = Log.getLogger(SharedBlockingCallback.class);
+
+ final AtomicInteger notComplete = new AtomicInteger();
+ final SharedBlockingCallback sbcb = new SharedBlockingCallback()
+ {
+ @Override
+ protected long getIdleTimeout()
+ {
+ return 150;
+ }
+
+ @Override
+ protected void notComplete(Blocker blocker)
+ {
+ super.notComplete(blocker);
+ notComplete.incrementAndGet();
+ }
+ };
+
+ @Test
+ public void testDone() throws Exception
+ {
+ long start;
+ try (Blocker blocker = sbcb.acquire())
+ {
+ blocker.succeeded();
+ start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ blocker.block();
+ }
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, lessThan(500L));
+ assertEquals(0, notComplete.get());
+ }
+
+ @Test
+ public void testGetDone() throws Exception
+ {
+ long start;
+ try (final Blocker blocker = sbcb.acquire())
+ {
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ new Thread(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ latch.countDown();
+ try
+ {
+ TimeUnit.MILLISECONDS.sleep(100);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ blocker.succeeded();
+ }
+ }).start();
+
+ latch.await();
+ start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ blocker.block();
+ }
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, greaterThan(10L));
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, lessThan(1000L));
+ assertEquals(0, notComplete.get());
+ }
+
+ @Test
+ public void testFailed() throws Exception
+ {
+ final Exception ex = new Exception("FAILED");
+ long start = Long.MIN_VALUE;
+ try
+ {
+ try (final Blocker blocker = sbcb.acquire())
+ {
+ blocker.failed(ex);
+ blocker.block();
+ }
+ fail("Should have thrown IOException");
+ }
+ catch (IOException e)
+ {
+ start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ assertEquals(ex, e.getCause());
+ }
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, lessThan(100L));
+ assertEquals(0, notComplete.get());
+ }
+
+ @Test
+ public void testGetFailed() throws Exception
+ {
+ final Exception ex = new Exception("FAILED");
+ long start = Long.MIN_VALUE;
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ try
+ {
+ try (final Blocker blocker = sbcb.acquire())
+ {
+
+ new Thread(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ latch.countDown();
+ try
+ {
+ TimeUnit.MILLISECONDS.sleep(100);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ blocker.failed(ex);
+ }
+ }).start();
+
+ latch.await();
+ start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ blocker.block();
+ }
+ fail("Should have thrown IOException");
+ }
+ catch (IOException e)
+ {
+ assertEquals(ex, e.getCause());
+ }
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, greaterThan(10L));
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, lessThan(1000L));
+ assertEquals(0, notComplete.get());
+ }
+
+ @Test
+ public void testAcquireBlocked() throws Exception
+ {
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ new Thread(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ try (Blocker blocker = sbcb.acquire())
+ {
+ latch.countDown();
+ TimeUnit.MILLISECONDS.sleep(100);
+ blocker.succeeded();
+ blocker.block();
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }).start();
+
+ latch.await();
+ long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ try (Blocker blocker = sbcb.acquire())
+ {
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, greaterThan(10L));
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, lessThan(500L));
+
+ blocker.succeeded();
+ blocker.block();
+ }
+ assertThat(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - start, lessThan(600L));
+ assertEquals(0, notComplete.get());
+ }
+
+ @Test
+ public void testBlockerClose() throws Exception
+ {
+ try (Blocker blocker = sbcb.acquire())
+ {
+ LOG.info("Blocker not complete " + blocker + " warning is expected...");
+ }
+
+ assertEquals(1, notComplete.get());
+ }
+
+ @Test
+ public void testBlockerTimeout() throws Exception
+ {
+ LOG.info("Succeeded after ... warning is expected...");
+ Blocker b0 = null;
+ try
+ {
+ try (Blocker blocker = sbcb.acquire())
+ {
+ b0 = blocker;
+ Thread.sleep(400);
+ blocker.block();
+ }
+ fail("Should have thrown IOException");
+ }
+ catch (IOException e)
+ {
+ Throwable cause = e.getCause();
+ assertThat(cause, instanceOf(TimeoutException.class));
+ }
+
+ assertEquals(0, notComplete.get());
+
+ try (Blocker blocker = sbcb.acquire())
+ {
+ assertThat(blocker, not(sameInstance(b0)));
+ b0.succeeded();
+ blocker.succeeded();
+ }
+ }
+
+ @Test
+ public void testInterruptedException() throws Exception
+ {
+ Blocker blocker0;
+ try (Blocker blocker = sbcb.acquire())
+ {
+ blocker0 = blocker;
+ Thread.currentThread().interrupt();
+ try
+ {
+ blocker.block();
+ fail();
+ }
+ catch (InterruptedIOException ignored)
+ {
+ }
+ }
+ // Blocker.close() has been called by try-with-resources.
+ // Simulate callback completion, must not throw.
+ LOG.info("Succeeded after ... warning is expected...");
+ blocker0.succeeded();
+
+ try (Blocker blocker = sbcb.acquire())
+ {
+ assertThat(blocker, not(sameInstance(blocker0)));
+ blocker.succeeded();
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/StringUtilTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/StringUtilTest.java
new file mode 100644
index 0000000..e438e09
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/StringUtilTest.java
@@ -0,0 +1,306 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.arrayContaining;
+import static org.hamcrest.Matchers.emptyArray;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+public class StringUtilTest
+{
+ @Test
+ @SuppressWarnings("ReferenceEquality")
+ public void testAsciiToLowerCase()
+ {
+ String lc = "\u0690bc def 1\u06903";
+ assertEquals(StringUtil.asciiToLowerCase("\u0690Bc DeF 1\u06903"), lc);
+ assertTrue(StringUtil.asciiToLowerCase(lc) == lc);
+ }
+
+ @Test
+ public void testStartsWithIgnoreCase()
+ {
+
+ assertTrue(StringUtil.startsWithIgnoreCase("\u0690b\u0690defg", "\u0690b\u0690"));
+ assertTrue(StringUtil.startsWithIgnoreCase("\u0690bcdefg", "\u0690bc"));
+ assertTrue(StringUtil.startsWithIgnoreCase("\u0690bcdefg", "\u0690Bc"));
+ assertTrue(StringUtil.startsWithIgnoreCase("\u0690Bcdefg", "\u0690bc"));
+ assertTrue(StringUtil.startsWithIgnoreCase("\u0690Bcdefg", "\u0690Bc"));
+ assertTrue(StringUtil.startsWithIgnoreCase("\u0690bcdefg", ""));
+ assertTrue(StringUtil.startsWithIgnoreCase("\u0690bcdefg", null));
+ assertTrue(StringUtil.startsWithIgnoreCase("\u0690bcdefg", "\u0690bcdefg"));
+
+ assertFalse(StringUtil.startsWithIgnoreCase(null, "xyz"));
+ assertFalse(StringUtil.startsWithIgnoreCase("\u0690bcdefg", "xyz"));
+ assertFalse(StringUtil.startsWithIgnoreCase("\u0690", "xyz"));
+ }
+
+ @Test
+ public void testEndsWithIgnoreCase()
+ {
+ assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcd\u0690f\u0690", "\u0690f\u0690"));
+ assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdefg", "efg"));
+ assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdefg", "eFg"));
+ assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdeFg", "efg"));
+ assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdeFg", "eFg"));
+ assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdefg", ""));
+ assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdefg", null));
+ assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdefg", "\u0690bcdefg"));
+
+ assertFalse(StringUtil.endsWithIgnoreCase(null, "xyz"));
+ assertFalse(StringUtil.endsWithIgnoreCase("\u0690bcdefg", "xyz"));
+ assertFalse(StringUtil.endsWithIgnoreCase("\u0690", "xyz"));
+ }
+
+ @Test
+ public void testIndexFrom()
+ {
+ assertEquals(StringUtil.indexFrom("\u0690bcd", "xyz"), -1);
+ assertEquals(StringUtil.indexFrom("\u0690bcd", "\u0690bcz"), 0);
+ assertEquals(StringUtil.indexFrom("\u0690bcd", "bcz"), 1);
+ assertEquals(StringUtil.indexFrom("\u0690bcd", "dxy"), 3);
+ }
+
+ @Test
+ @SuppressWarnings("ReferenceEquality")
+ public void testReplace()
+ {
+ String s = "\u0690bc \u0690bc \u0690bc";
+ assertEquals(StringUtil.replace(s, "\u0690bc", "xyz"), "xyz xyz xyz");
+ assertTrue(StringUtil.replace(s, "xyz", "pqy") == s);
+
+ s = " \u0690bc ";
+ assertEquals(StringUtil.replace(s, "\u0690bc", "xyz"), " xyz ");
+ }
+
+ public static Stream<String[]> replaceFirstArgs()
+ {
+ List<String[]> data = new ArrayList<>();
+
+ // [original, target, replacement, expected]
+
+ // no match
+ data.add(new String[]{"abc", "z", "foo", "abc"});
+
+ // matches at start of string
+ data.add(new String[]{"abc", "a", "foo", "foobc"});
+ data.add(new String[]{"abcabcabc", "a", "foo", "foobcabcabc"});
+
+ // matches in middle of string
+ data.add(new String[]{"abc", "b", "foo", "afooc"});
+ data.add(new String[]{"abcabcabc", "b", "foo", "afoocabcabc"});
+ data.add(new String[]{"abcabcabc", "cab", "X", "abXcabc"});
+
+ // matches at end of string
+ data.add(new String[]{"abc", "c", "foo", "abfoo"});
+
+ return data.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "replaceFirstArgs")
+ public void testReplaceFirst(String original, String target, String replacement, String expected)
+ {
+ assertThat(StringUtil.replaceFirst(original, target, replacement), is(expected));
+ }
+
+ @Test
+ @SuppressWarnings("ReferenceEquality")
+ public void testUnquote()
+ {
+ String uq = " not quoted ";
+ assertTrue(StringUtil.unquote(uq) == uq);
+ assertEquals(StringUtil.unquote("' quoted string '"), " quoted string ");
+ assertEquals(StringUtil.unquote("\" quoted string \""), " quoted string ");
+ assertEquals(StringUtil.unquote("' quoted\"string '"), " quoted\"string ");
+ assertEquals(StringUtil.unquote("\" quoted'string \""), " quoted'string ");
+ }
+
+ @Test
+ @SuppressWarnings("ReferenceEquality")
+ public void testNonNull()
+ {
+ String nn = "non empty string";
+ assertTrue(nn == StringUtil.nonNull(nn));
+ assertEquals("", StringUtil.nonNull(null));
+ }
+
+ /*
+ * Test for boolean equals(String, char[], int, int)
+ */
+ @Test
+ public void testEqualsStringcharArrayintint()
+ {
+ assertTrue(StringUtil.equals("\u0690bc", new char[]{'x', '\u0690', 'b', 'c', 'z'}, 1, 3));
+ assertFalse(StringUtil.equals("axc", new char[]{'x', 'a', 'b', 'c', 'z'}, 1, 3));
+ }
+
+ @Test
+ public void testAppend()
+ {
+ StringBuilder buf = new StringBuilder();
+ buf.append('a');
+ StringUtil.append(buf, "abc", 1, 1);
+ StringUtil.append(buf, (byte)12, 16);
+ StringUtil.append(buf, (byte)16, 16);
+ StringUtil.append(buf, (byte)-1, 16);
+ StringUtil.append(buf, (byte)-16, 16);
+ assertEquals("ab0c10fff0", buf.toString());
+ }
+
+ @Test
+ @Deprecated
+ public void testSidConversion() throws Exception
+ {
+ String sid4 = "S-1-4-21-3623811015-3361044348-30300820";
+ String sid5 = "S-1-5-21-3623811015-3361044348-30300820-1013";
+ String sid6 = "S-1-6-21-3623811015-3361044348-30300820-1013-23445";
+ String sid12 = "S-1-12-21-3623811015-3361044348-30300820-1013-23445-21-3623811015-3361044348-30300820-1013-23445";
+
+ byte[] sid4Bytes = StringUtil.sidStringToBytes(sid4);
+ byte[] sid5Bytes = StringUtil.sidStringToBytes(sid5);
+ byte[] sid6Bytes = StringUtil.sidStringToBytes(sid6);
+ byte[] sid12Bytes = StringUtil.sidStringToBytes(sid12);
+
+ assertEquals(sid4, StringUtil.sidBytesToString(sid4Bytes));
+ assertEquals(sid5, StringUtil.sidBytesToString(sid5Bytes));
+ assertEquals(sid6, StringUtil.sidBytesToString(sid6Bytes));
+ assertEquals(sid12, StringUtil.sidBytesToString(sid12Bytes));
+ }
+
+ @Test
+ public void testHasControlCharacter()
+ {
+ assertThat(StringUtil.indexOfControlChars("\r\n"), is(0));
+ assertThat(StringUtil.indexOfControlChars("\t"), is(0));
+ assertThat(StringUtil.indexOfControlChars(";\n"), is(1));
+ assertThat(StringUtil.indexOfControlChars("abc\fz"), is(3));
+ //@checkstyle-disable-check : IllegalTokenText
+ assertThat(StringUtil.indexOfControlChars("z\010"), is(1));
+ //@checkstyle-enable-check : IllegalTokenText
+ assertThat(StringUtil.indexOfControlChars(":\u001c"), is(1));
+
+ assertThat(StringUtil.indexOfControlChars(null), is(-1));
+ assertThat(StringUtil.indexOfControlChars(""), is(-1));
+ assertThat(StringUtil.indexOfControlChars(" "), is(-1));
+ assertThat(StringUtil.indexOfControlChars("a"), is(-1));
+ assertThat(StringUtil.indexOfControlChars("."), is(-1));
+ assertThat(StringUtil.indexOfControlChars(";"), is(-1));
+ assertThat(StringUtil.indexOfControlChars("Euro is \u20ac"), is(-1));
+ }
+
+ @Test
+ public void testIsBlank()
+ {
+ assertTrue(StringUtil.isBlank(null));
+ assertTrue(StringUtil.isBlank(""));
+ assertTrue(StringUtil.isBlank("\r\n"));
+ assertTrue(StringUtil.isBlank("\t"));
+ assertTrue(StringUtil.isBlank(" "));
+
+ assertFalse(StringUtil.isBlank("a"));
+ assertFalse(StringUtil.isBlank(" a"));
+ assertFalse(StringUtil.isBlank("a "));
+ assertFalse(StringUtil.isBlank("."));
+ assertFalse(StringUtil.isBlank(";\n"));
+ }
+
+ @Test
+ public void testIsNotBlank()
+ {
+ assertFalse(StringUtil.isNotBlank(null));
+ assertFalse(StringUtil.isNotBlank(""));
+ assertFalse(StringUtil.isNotBlank("\r\n"));
+ assertFalse(StringUtil.isNotBlank("\t"));
+ assertFalse(StringUtil.isNotBlank(" "));
+
+ assertTrue(StringUtil.isNotBlank("a"));
+ assertTrue(StringUtil.isNotBlank(" a"));
+ assertTrue(StringUtil.isNotBlank("a "));
+ assertTrue(StringUtil.isNotBlank("."));
+ assertTrue(StringUtil.isNotBlank(";\n"));
+ }
+
+ @Test
+ public void testIsEmpty()
+ {
+ assertTrue(StringUtil.isEmpty(null));
+ assertTrue(StringUtil.isEmpty(""));
+ assertFalse(StringUtil.isEmpty("\r\n"));
+ assertFalse(StringUtil.isEmpty("\t"));
+ assertFalse(StringUtil.isEmpty(" "));
+
+ assertFalse(StringUtil.isEmpty("a"));
+ assertFalse(StringUtil.isEmpty(" a"));
+ assertFalse(StringUtil.isEmpty("a "));
+ assertFalse(StringUtil.isEmpty("."));
+ assertFalse(StringUtil.isEmpty(";\n"));
+ }
+
+ @Test
+ public void testSanitizeHTML()
+ {
+ assertEquals(null, StringUtil.sanitizeXmlString(null));
+ assertEquals("", StringUtil.sanitizeXmlString(""));
+ assertEquals("<&>", StringUtil.sanitizeXmlString("<&>"));
+ assertEquals("Hello <Cruel> World", StringUtil.sanitizeXmlString("Hello <Cruel> World"));
+ assertEquals("Hello ? World", StringUtil.sanitizeXmlString("Hello \u0000 World"));
+ }
+
+ @Test
+ public void testSplit()
+ {
+ assertThat(StringUtil.csvSplit(null), nullValue());
+ assertThat(StringUtil.csvSplit(null), nullValue());
+
+ assertThat(StringUtil.csvSplit(""), emptyArray());
+ assertThat(StringUtil.csvSplit(" \t\n"), emptyArray());
+
+ assertThat(StringUtil.csvSplit("aaa"), arrayContaining("aaa"));
+ assertThat(StringUtil.csvSplit(" \taaa\n"), arrayContaining("aaa"));
+ assertThat(StringUtil.csvSplit(" \ta\n"), arrayContaining("a"));
+ assertThat(StringUtil.csvSplit(" \t\u1234\n"), arrayContaining("\u1234"));
+
+ assertThat(StringUtil.csvSplit("aaa,bbb,ccc"), arrayContaining("aaa", "bbb", "ccc"));
+ assertThat(StringUtil.csvSplit("aaa,,ccc"), arrayContaining("aaa", "", "ccc"));
+ assertThat(StringUtil.csvSplit(",b b,"), arrayContaining("", "b b"));
+ assertThat(StringUtil.csvSplit(",,bbb,,"), arrayContaining("", "", "bbb", ""));
+
+ assertThat(StringUtil.csvSplit(" aaa, bbb, ccc"), arrayContaining("aaa", "bbb", "ccc"));
+ assertThat(StringUtil.csvSplit("aaa,\t,ccc"), arrayContaining("aaa", "", "ccc"));
+ assertThat(StringUtil.csvSplit(" , b b , "), arrayContaining("", "b b"));
+ assertThat(StringUtil.csvSplit(" ,\n,bbb, , "), arrayContaining("", "", "bbb", ""));
+
+ assertThat(StringUtil.csvSplit("\"aaa\", \" b,\\\"\",\"\""), arrayContaining("aaa", " b,\"", ""));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/TestIntrospectionUtil.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/TestIntrospectionUtil.java
new file mode 100644
index 0000000..0f3e49f
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/TestIntrospectionUtil.java
@@ -0,0 +1,237 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * TestInjection
+ */
+public class TestIntrospectionUtil
+{
+ public static final Class<?>[] __INTEGER_ARG = new Class[]{Integer.class};
+ static Field privateAField;
+ static Field protectedAField;
+ static Field publicAField;
+ static Field defaultAField;
+ static Field privateBField;
+ static Field protectedBField;
+ static Field publicBField;
+ static Field defaultBField;
+ static Method privateCMethod;
+ static Method protectedCMethod;
+ static Method publicCMethod;
+ static Method defaultCMethod;
+ static Method privateDMethod;
+ static Method protectedDMethod;
+ static Method publicDMethod;
+ static Method defaultDMethod;
+
+ public class ServletA
+ {
+ private Integer privateA;
+ protected Integer protectedA;
+ Integer defaultA;
+ public Integer publicA;
+ }
+
+ public class ServletB extends ServletA
+ {
+ private String privateB;
+ protected String protectedB;
+ public String publicB;
+ String defaultB;
+ }
+
+ public class ServletC
+ {
+ private void setPrivateC(Integer c)
+ {
+ }
+
+ protected void setProtectedC(Integer c)
+ {
+ }
+
+ public void setPublicC(Integer c)
+ {
+ }
+
+ void setDefaultC(Integer c)
+ {
+ }
+ }
+
+ public class ServletD extends ServletC
+ {
+ private void setPrivateD(Integer d)
+ {
+ }
+
+ protected void setProtectedD(Integer d)
+ {
+ }
+
+ public void setPublicD(Integer d)
+ {
+ }
+
+ void setDefaultD(Integer d)
+ {
+ }
+ }
+
+ @BeforeAll
+ public static void setUp()
+ throws Exception
+ {
+ privateAField = ServletA.class.getDeclaredField("privateA");
+ protectedAField = ServletA.class.getDeclaredField("protectedA");
+ publicAField = ServletA.class.getDeclaredField("publicA");
+ defaultAField = ServletA.class.getDeclaredField("defaultA");
+ privateBField = ServletB.class.getDeclaredField("privateB");
+ protectedBField = ServletB.class.getDeclaredField("protectedB");
+ publicBField = ServletB.class.getDeclaredField("publicB");
+ defaultBField = ServletB.class.getDeclaredField("defaultB");
+ privateCMethod = ServletC.class.getDeclaredMethod("setPrivateC", __INTEGER_ARG);
+ protectedCMethod = ServletC.class.getDeclaredMethod("setProtectedC", __INTEGER_ARG);
+ publicCMethod = ServletC.class.getDeclaredMethod("setPublicC", __INTEGER_ARG);
+ defaultCMethod = ServletC.class.getDeclaredMethod("setDefaultC", __INTEGER_ARG);
+ privateDMethod = ServletD.class.getDeclaredMethod("setPrivateD", __INTEGER_ARG);
+ protectedDMethod = ServletD.class.getDeclaredMethod("setProtectedD", __INTEGER_ARG);
+ publicDMethod = ServletD.class.getDeclaredMethod("setPublicD", __INTEGER_ARG);
+ defaultDMethod = ServletD.class.getDeclaredMethod("setDefaultD", __INTEGER_ARG);
+ }
+
+ @Test
+ public void testFieldPrivate()
+ throws Exception
+ {
+ //direct
+ Field f = IntrospectionUtil.findField(ServletA.class, "privateA", Integer.class, true, false);
+ assertEquals(privateAField, f);
+
+ //inheritance
+ assertThrows(NoSuchFieldException.class, () ->
+ {
+ // Private fields should not be inherited
+ IntrospectionUtil.findField(ServletB.class, "privateA", Integer.class, true, false);
+ });
+ }
+
+ @Test
+ public void testFieldProtected()
+ throws Exception
+ {
+ //direct
+ Field f = IntrospectionUtil.findField(ServletA.class, "protectedA", Integer.class, true, false);
+ assertEquals(f, protectedAField);
+
+ //inheritance
+ f = IntrospectionUtil.findField(ServletB.class, "protectedA", Integer.class, true, false);
+ assertEquals(f, protectedAField);
+ }
+
+ @Test
+ public void testFieldPublic()
+ throws Exception
+ {
+ //direct
+ Field f = IntrospectionUtil.findField(ServletA.class, "publicA", Integer.class, true, false);
+ assertEquals(f, publicAField);
+
+ //inheritance
+ f = IntrospectionUtil.findField(ServletB.class, "publicA", Integer.class, true, false);
+ assertEquals(f, publicAField);
+ }
+
+ @Test
+ public void testFieldDefault()
+ throws Exception
+ {
+ //direct
+ Field f = IntrospectionUtil.findField(ServletA.class, "defaultA", Integer.class, true, false);
+ assertEquals(f, defaultAField);
+
+ //inheritance
+ f = IntrospectionUtil.findField(ServletB.class, "defaultA", Integer.class, true, false);
+ assertEquals(f, defaultAField);
+ }
+
+ @Test
+ public void testMethodPrivate()
+ throws Exception
+ {
+ //direct
+ Method m = IntrospectionUtil.findMethod(ServletC.class, "setPrivateC", __INTEGER_ARG, true, false);
+ assertEquals(m, privateCMethod);
+
+ //inheritance
+ assertThrows(NoSuchMethodException.class, () ->
+ {
+ IntrospectionUtil.findMethod(ServletD.class, "setPrivateC", __INTEGER_ARG, true, false);
+ });
+ }
+
+ @Test
+ public void testMethodProtected()
+ throws Exception
+ {
+ // direct
+ Method m = IntrospectionUtil.findMethod(ServletC.class, "setProtectedC", __INTEGER_ARG, true, false);
+ assertEquals(m, protectedCMethod);
+
+ //inherited
+ m = IntrospectionUtil.findMethod(ServletD.class, "setProtectedC", __INTEGER_ARG, true, false);
+ assertEquals(m, protectedCMethod);
+ }
+
+ @Test
+ public void testMethodPublic()
+ throws Exception
+ {
+ // direct
+ Method m = IntrospectionUtil.findMethod(ServletC.class, "setPublicC", __INTEGER_ARG, true, false);
+ assertEquals(m, publicCMethod);
+
+ //inherited
+ m = IntrospectionUtil.findMethod(ServletD.class, "setPublicC", __INTEGER_ARG, true, false);
+ assertEquals(m, publicCMethod);
+ }
+
+ @Test
+ public void testMethodDefault()
+ throws Exception
+ {
+ // direct
+ Method m = IntrospectionUtil.findMethod(ServletC.class, "setDefaultC", __INTEGER_ARG, true, false);
+ assertEquals(m, defaultCMethod);
+
+ //inherited
+ m = IntrospectionUtil.findMethod(ServletD.class, "setDefaultC", __INTEGER_ARG, true, false);
+ assertEquals(m, defaultCMethod);
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/TopologicalSortTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/TopologicalSortTest.java
new file mode 100644
index 0000000..40d42b9
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/TopologicalSortTest.java
@@ -0,0 +1,195 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class TopologicalSortTest
+{
+
+ @Test
+ public void testNoDependencies()
+ {
+ String[] s = {"D", "E", "C", "B", "A"};
+ TopologicalSort<String> ts = new TopologicalSort<>();
+ ts.sort(s);
+
+ assertThat(s, Matchers.arrayContaining("D", "E", "C", "B", "A"));
+ }
+
+ @Test
+ public void testSimpleLinear()
+ {
+ String[] s = {"D", "E", "C", "B", "A"};
+ TopologicalSort<String> ts = new TopologicalSort<>();
+ ts.addDependency("B", "A");
+ ts.addDependency("C", "B");
+ ts.addDependency("D", "C");
+ ts.addDependency("E", "D");
+
+ ts.sort(s);
+
+ assertThat(s, Matchers.arrayContaining("A", "B", "C", "D", "E"));
+ }
+
+ @Test
+ public void testDisjoint()
+ {
+ String[] s = {"A", "C", "B", "CC", "AA", "BB"};
+
+ TopologicalSort<String> ts = new TopologicalSort<>();
+ ts.addDependency("B", "A");
+ ts.addDependency("C", "B");
+ ts.addDependency("BB", "AA");
+ ts.addDependency("CC", "BB");
+
+ ts.sort(s);
+
+ assertThat(s, Matchers.arrayContaining("A", "B", "C", "AA", "BB", "CC"));
+ }
+
+ @Test
+ public void testDisjointReversed()
+ {
+ String[] s = {"CC", "AA", "BB", "A", "C", "B"};
+
+ TopologicalSort<String> ts = new TopologicalSort<>();
+ ts.addDependency("B", "A");
+ ts.addDependency("C", "B");
+ ts.addDependency("BB", "AA");
+ ts.addDependency("CC", "BB");
+
+ ts.sort(s);
+
+ assertThat(s, Matchers.arrayContaining("AA", "BB", "CC", "A", "B", "C"));
+ }
+
+ @Test
+ public void testDisjointMixed()
+ {
+ String[] s = {"CC", "A", "AA", "C", "BB", "B"};
+
+ TopologicalSort<String> ts = new TopologicalSort<>();
+ ts.addDependency("B", "A");
+ ts.addDependency("C", "B");
+ ts.addDependency("BB", "AA");
+ ts.addDependency("CC", "BB");
+
+ ts.sort(s);
+
+ // Check direct ordering
+ assertThat(indexOf(s, "A"), lessThan(indexOf(s, "B")));
+ assertThat(indexOf(s, "B"), lessThan(indexOf(s, "C")));
+ assertThat(indexOf(s, "AA"), lessThan(indexOf(s, "BB")));
+ assertThat(indexOf(s, "BB"), lessThan(indexOf(s, "CC")));
+ }
+
+ @Test
+ public void testTree()
+ {
+ String[] s = {"LeafA0", "LeafB0", "LeafA1", "Root", "BranchA", "LeafB1", "BranchB"};
+
+ TopologicalSort<String> ts = new TopologicalSort<>();
+ ts.addDependency("BranchB", "Root");
+ ts.addDependency("BranchA", "Root");
+ ts.addDependency("LeafA1", "BranchA");
+ ts.addDependency("LeafA0", "BranchA");
+ ts.addDependency("LeafB0", "BranchB");
+ ts.addDependency("LeafB1", "BranchB");
+
+ ts.sort(s);
+
+ // Check direct ordering
+ assertThat(indexOf(s, "Root"), lessThan(indexOf(s, "BranchA")));
+ assertThat(indexOf(s, "Root"), lessThan(indexOf(s, "BranchB")));
+ assertThat(indexOf(s, "BranchA"), lessThan(indexOf(s, "LeafA0")));
+ assertThat(indexOf(s, "BranchA"), lessThan(indexOf(s, "LeafA1")));
+ assertThat(indexOf(s, "BranchB"), lessThan(indexOf(s, "LeafB0")));
+ assertThat(indexOf(s, "BranchB"), lessThan(indexOf(s, "LeafB1")));
+
+ // check remnant ordering of original list
+ assertThat(indexOf(s, "BranchA"), lessThan(indexOf(s, "BranchB")));
+ assertThat(indexOf(s, "LeafA0"), lessThan(indexOf(s, "LeafA1")));
+ assertThat(indexOf(s, "LeafB0"), lessThan(indexOf(s, "LeafB1")));
+ }
+
+ @Test
+ public void testPreserveOrder()
+ {
+ String[] s = {"Deep", "Foobar", "Wibble", "Bozo", "XXX", "12345", "Test"};
+
+ TopologicalSort<String> ts = new TopologicalSort<>();
+ ts.addDependency("Deep", "Test");
+ ts.addDependency("Deep", "Wibble");
+ ts.addDependency("Deep", "12345");
+ ts.addDependency("Deep", "XXX");
+ ts.addDependency("Deep", "Foobar");
+ ts.addDependency("Deep", "Bozo");
+
+ ts.sort(s);
+ assertThat(s, Matchers.arrayContaining("Foobar", "Wibble", "Bozo", "XXX", "12345", "Test", "Deep"));
+ }
+
+ @Test
+ public void testSimpleLoop()
+ {
+ String[] s = {"A", "B", "C", "D", "E"};
+ TopologicalSort<String> ts = new TopologicalSort<>();
+ ts.addDependency("B", "A");
+ ts.addDependency("A", "B");
+
+ assertThrows(IllegalStateException.class, () ->
+ {
+ ts.sort(s);
+ });
+ }
+
+ @Test
+ public void testDeepLoop()
+ {
+ String[] s = {"A", "B", "C", "D", "E"};
+ TopologicalSort<String> ts = new TopologicalSort<>();
+ ts.addDependency("B", "A");
+ ts.addDependency("C", "B");
+ ts.addDependency("D", "C");
+ ts.addDependency("E", "D");
+ ts.addDependency("A", "E");
+
+ assertThrows(IllegalStateException.class, () ->
+ {
+ ts.sort(s);
+ });
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ private int indexOf(String[] list, String s)
+ {
+ for (int i = 0; i < list.length; i++)
+ {
+ if (list[i] == s)
+ return i;
+ }
+ return -1;
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/TrieTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/TrieTest.java
new file mode 100644
index 0000000..6921dd7
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/TrieTest.java
@@ -0,0 +1,253 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.in;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TrieTest
+{
+ public static Stream<Arguments> implementations()
+ {
+ List<Trie> impls = new ArrayList<>();
+
+ impls.add(new ArrayTrie<Integer>(128));
+ impls.add(new TreeTrie<Integer>());
+ impls.add(new ArrayTernaryTrie<Integer>(128));
+
+ for (Trie<Integer> trie : impls)
+ {
+ trie.put("hello", 1);
+ trie.put("He", 2);
+ trie.put("HELL", 3);
+ trie.put("wibble", 4);
+ trie.put("Wobble", 5);
+ trie.put("foo-bar", 6);
+ trie.put("foo+bar", 7);
+ trie.put("HELL4", 8);
+ trie.put("", 9);
+ }
+
+ return impls.stream().map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testOverflow(Trie<Integer> trie) throws Exception
+ {
+ int i = 0;
+ while (true)
+ {
+ if (++i > 10000)
+ break; // must not be fixed size
+ if (!trie.put("prefix" + i, i))
+ {
+ assertTrue(trie.isFull());
+ break;
+ }
+ }
+
+ assertTrue(!trie.isFull() || !trie.put("overflow", 0));
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testKeySet(Trie<Integer> trie) throws Exception
+ {
+ String[] values = new String[]{
+ "hello",
+ "He",
+ "HELL",
+ "wibble",
+ "Wobble",
+ "foo-bar",
+ "foo+bar",
+ "HELL4",
+ ""
+ };
+
+ for (String value : values)
+ {
+ assertThat(value, is(in(trie.keySet())));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testGetString(Trie<Integer> trie) throws Exception
+ {
+ assertEquals(1, trie.get("hello").intValue());
+ assertEquals(2, trie.get("He").intValue());
+ assertEquals(3, trie.get("HELL").intValue());
+ assertEquals(4, trie.get("wibble").intValue());
+ assertEquals(5, trie.get("Wobble").intValue());
+ assertEquals(6, trie.get("foo-bar").intValue());
+ assertEquals(7, trie.get("foo+bar").intValue());
+
+ assertEquals(1, trie.get("Hello").intValue());
+ assertEquals(2, trie.get("HE").intValue());
+ assertEquals(3, trie.get("heLL").intValue());
+ assertEquals(4, trie.get("Wibble").intValue());
+ assertEquals(5, trie.get("wobble").intValue());
+ assertEquals(6, trie.get("Foo-bar").intValue());
+ assertEquals(7, trie.get("FOO+bar").intValue());
+ assertEquals(8, trie.get("HELL4").intValue());
+ assertEquals(9, trie.get("").intValue());
+
+ assertEquals(null, trie.get("helloworld"));
+ assertEquals(null, trie.get("Help"));
+ assertEquals(null, trie.get("Blah"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testGetBuffer(Trie<Integer> trie) throws Exception
+ {
+ assertEquals(1, trie.get(BufferUtil.toBuffer("xhellox"), 1, 5).intValue());
+ assertEquals(2, trie.get(BufferUtil.toBuffer("xhellox"), 1, 2).intValue());
+ assertEquals(3, trie.get(BufferUtil.toBuffer("xhellox"), 1, 4).intValue());
+ assertEquals(4, trie.get(BufferUtil.toBuffer("wibble"), 0, 6).intValue());
+ assertEquals(5, trie.get(BufferUtil.toBuffer("xWobble"), 1, 6).intValue());
+ assertEquals(6, trie.get(BufferUtil.toBuffer("xfoo-barx"), 1, 7).intValue());
+ assertEquals(7, trie.get(BufferUtil.toBuffer("xfoo+barx"), 1, 7).intValue());
+
+ assertEquals(1, trie.get(BufferUtil.toBuffer("xhellox"), 1, 5).intValue());
+ assertEquals(2, trie.get(BufferUtil.toBuffer("xHELLox"), 1, 2).intValue());
+ assertEquals(3, trie.get(BufferUtil.toBuffer("xhellox"), 1, 4).intValue());
+ assertEquals(4, trie.get(BufferUtil.toBuffer("Wibble"), 0, 6).intValue());
+ assertEquals(5, trie.get(BufferUtil.toBuffer("xwobble"), 1, 6).intValue());
+ assertEquals(6, trie.get(BufferUtil.toBuffer("xFOO-barx"), 1, 7).intValue());
+ assertEquals(7, trie.get(BufferUtil.toBuffer("xFOO+barx"), 1, 7).intValue());
+
+ assertEquals(null, trie.get(BufferUtil.toBuffer("xHelloworldx"), 1, 10));
+ assertEquals(null, trie.get(BufferUtil.toBuffer("xHelpx"), 1, 4));
+ assertEquals(null, trie.get(BufferUtil.toBuffer("xBlahx"), 1, 4));
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testGetDirectBuffer(Trie<Integer> trie) throws Exception
+ {
+ assertEquals(1, trie.get(BufferUtil.toDirectBuffer("xhellox"), 1, 5).intValue());
+ assertEquals(2, trie.get(BufferUtil.toDirectBuffer("xhellox"), 1, 2).intValue());
+ assertEquals(3, trie.get(BufferUtil.toDirectBuffer("xhellox"), 1, 4).intValue());
+ assertEquals(4, trie.get(BufferUtil.toDirectBuffer("wibble"), 0, 6).intValue());
+ assertEquals(5, trie.get(BufferUtil.toDirectBuffer("xWobble"), 1, 6).intValue());
+ assertEquals(6, trie.get(BufferUtil.toDirectBuffer("xfoo-barx"), 1, 7).intValue());
+ assertEquals(7, trie.get(BufferUtil.toDirectBuffer("xfoo+barx"), 1, 7).intValue());
+
+ assertEquals(1, trie.get(BufferUtil.toDirectBuffer("xhellox"), 1, 5).intValue());
+ assertEquals(2, trie.get(BufferUtil.toDirectBuffer("xHELLox"), 1, 2).intValue());
+ assertEquals(3, trie.get(BufferUtil.toDirectBuffer("xhellox"), 1, 4).intValue());
+ assertEquals(4, trie.get(BufferUtil.toDirectBuffer("Wibble"), 0, 6).intValue());
+ assertEquals(5, trie.get(BufferUtil.toDirectBuffer("xwobble"), 1, 6).intValue());
+ assertEquals(6, trie.get(BufferUtil.toDirectBuffer("xFOO-barx"), 1, 7).intValue());
+ assertEquals(7, trie.get(BufferUtil.toDirectBuffer("xFOO+barx"), 1, 7).intValue());
+
+ assertEquals(null, trie.get(BufferUtil.toDirectBuffer("xHelloworldx"), 1, 10));
+ assertEquals(null, trie.get(BufferUtil.toDirectBuffer("xHelpx"), 1, 4));
+ assertEquals(null, trie.get(BufferUtil.toDirectBuffer("xBlahx"), 1, 4));
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testGetBestArray(Trie<Integer> trie) throws Exception
+ {
+ assertEquals(1, trie.getBest(StringUtil.getUtf8Bytes("xhelloxxxx"), 1, 8).intValue());
+ assertEquals(2, trie.getBest(StringUtil.getUtf8Bytes("xhelxoxxxx"), 1, 8).intValue());
+ assertEquals(3, trie.getBest(StringUtil.getUtf8Bytes("xhellxxxxx"), 1, 8).intValue());
+ assertEquals(6, trie.getBest(StringUtil.getUtf8Bytes("xfoo-barxx"), 1, 8).intValue());
+ assertEquals(8, trie.getBest(StringUtil.getUtf8Bytes("xhell4xxxx"), 1, 8).intValue());
+
+ assertEquals(1, trie.getBest(StringUtil.getUtf8Bytes("xHELLOxxxx"), 1, 8).intValue());
+ assertEquals(2, trie.getBest(StringUtil.getUtf8Bytes("xHELxoxxxx"), 1, 8).intValue());
+ assertEquals(3, trie.getBest(StringUtil.getUtf8Bytes("xHELLxxxxx"), 1, 8).intValue());
+ assertEquals(6, trie.getBest(StringUtil.getUtf8Bytes("xfoo-BARxx"), 1, 8).intValue());
+ assertEquals(8, trie.getBest(StringUtil.getUtf8Bytes("xHELL4xxxx"), 1, 8).intValue());
+ assertEquals(9, trie.getBest(StringUtil.getUtf8Bytes("xZZZZZxxxx"), 1, 8).intValue());
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testGetBestBuffer(Trie<Integer> trie) throws Exception
+ {
+ assertEquals(1, trie.getBest(BufferUtil.toBuffer("xhelloxxxx"), 1, 8).intValue());
+ assertEquals(2, trie.getBest(BufferUtil.toBuffer("xhelxoxxxx"), 1, 8).intValue());
+ assertEquals(3, trie.getBest(BufferUtil.toBuffer("xhellxxxxx"), 1, 8).intValue());
+ assertEquals(6, trie.getBest(BufferUtil.toBuffer("xfoo-barxx"), 1, 8).intValue());
+ assertEquals(8, trie.getBest(BufferUtil.toBuffer("xhell4xxxx"), 1, 8).intValue());
+
+ assertEquals(1, trie.getBest(BufferUtil.toBuffer("xHELLOxxxx"), 1, 8).intValue());
+ assertEquals(2, trie.getBest(BufferUtil.toBuffer("xHELxoxxxx"), 1, 8).intValue());
+ assertEquals(3, trie.getBest(BufferUtil.toBuffer("xHELLxxxxx"), 1, 8).intValue());
+ assertEquals(6, trie.getBest(BufferUtil.toBuffer("xfoo-BARxx"), 1, 8).intValue());
+ assertEquals(8, trie.getBest(BufferUtil.toBuffer("xHELL4xxxx"), 1, 8).intValue());
+ assertEquals(9, trie.getBest(BufferUtil.toBuffer("xZZZZZxxxx"), 1, 8).intValue());
+
+ ByteBuffer buffer = (ByteBuffer)BufferUtil.toBuffer("xhelloxxxxxxx").position(2);
+ assertEquals(1, trie.getBest(buffer, -1, 10).intValue());
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testGetBestDirectBuffer(Trie<Integer> trie) throws Exception
+ {
+ assertEquals(1, trie.getBest(BufferUtil.toDirectBuffer("xhelloxxxx"), 1, 8).intValue());
+ assertEquals(2, trie.getBest(BufferUtil.toDirectBuffer("xhelxoxxxx"), 1, 8).intValue());
+ assertEquals(3, trie.getBest(BufferUtil.toDirectBuffer("xhellxxxxx"), 1, 8).intValue());
+ assertEquals(6, trie.getBest(BufferUtil.toDirectBuffer("xfoo-barxx"), 1, 8).intValue());
+ assertEquals(8, trie.getBest(BufferUtil.toDirectBuffer("xhell4xxxx"), 1, 8).intValue());
+
+ assertEquals(1, trie.getBest(BufferUtil.toDirectBuffer("xHELLOxxxx"), 1, 8).intValue());
+ assertEquals(2, trie.getBest(BufferUtil.toDirectBuffer("xHELxoxxxx"), 1, 8).intValue());
+ assertEquals(3, trie.getBest(BufferUtil.toDirectBuffer("xHELLxxxxx"), 1, 8).intValue());
+ assertEquals(6, trie.getBest(BufferUtil.toDirectBuffer("xfoo-BARxx"), 1, 8).intValue());
+ assertEquals(8, trie.getBest(BufferUtil.toDirectBuffer("xHELL4xxxx"), 1, 8).intValue());
+ assertEquals(9, trie.getBest(BufferUtil.toDirectBuffer("xZZZZZxxxx"), 1, 8).intValue());
+
+ ByteBuffer buffer = (ByteBuffer)BufferUtil.toDirectBuffer("xhelloxxxxxxx").position(2);
+ assertEquals(1, trie.getBest(buffer, -1, 10).intValue());
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testFull(Trie<Integer> trie) throws Exception
+ {
+ if (!(trie instanceof ArrayTrie<?> || trie instanceof ArrayTernaryTrie<?>))
+ return;
+
+ assertFalse(trie.put("Large: This is a really large key and should blow the maximum size of the array trie as lots of nodes should already be used.", 99));
+ testGetString(trie);
+ testGetBestArray(trie);
+ testGetBestBuffer(trie);
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java
new file mode 100644
index 0000000..d8d819a
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java
@@ -0,0 +1,218 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.eclipse.jetty.util.resource.Resource;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnJre;
+import org.junit.jupiter.api.condition.EnabledOnJre;
+import org.junit.jupiter.api.condition.JRE;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+public class TypeUtilTest
+{
+ @Test
+ public void convertHexDigitTest()
+ {
+ assertEquals((byte)0, TypeUtil.convertHexDigit((byte)'0'));
+ assertEquals((byte)9, TypeUtil.convertHexDigit((byte)'9'));
+ assertEquals((byte)10, TypeUtil.convertHexDigit((byte)'a'));
+ assertEquals((byte)10, TypeUtil.convertHexDigit((byte)'A'));
+ assertEquals((byte)15, TypeUtil.convertHexDigit((byte)'f'));
+ assertEquals((byte)15, TypeUtil.convertHexDigit((byte)'F'));
+
+ assertEquals((int)0, TypeUtil.convertHexDigit((int)'0'));
+ assertEquals((int)9, TypeUtil.convertHexDigit((int)'9'));
+ assertEquals((int)10, TypeUtil.convertHexDigit((int)'a'));
+ assertEquals((int)10, TypeUtil.convertHexDigit((int)'A'));
+ assertEquals((int)15, TypeUtil.convertHexDigit((int)'f'));
+ assertEquals((int)15, TypeUtil.convertHexDigit((int)'F'));
+ }
+
+ @Test
+ public void testToHexInt() throws Exception
+ {
+ StringBuilder b = new StringBuilder();
+
+ b.setLength(0);
+ TypeUtil.toHex((int)0, b);
+ assertEquals("00000000", b.toString());
+
+ b.setLength(0);
+ TypeUtil.toHex(Integer.MAX_VALUE, b);
+ assertEquals("7FFFFFFF", b.toString());
+
+ b.setLength(0);
+ TypeUtil.toHex(Integer.MIN_VALUE, b);
+ assertEquals("80000000", b.toString());
+
+ b.setLength(0);
+ TypeUtil.toHex(0x12345678, b);
+ assertEquals("12345678", b.toString());
+
+ b.setLength(0);
+ TypeUtil.toHex(0x9abcdef0, b);
+ assertEquals("9ABCDEF0", b.toString());
+ }
+
+ @Test
+ public void testToHexLong() throws Exception
+ {
+ StringBuilder b = new StringBuilder();
+
+ b.setLength(0);
+ TypeUtil.toHex((long)0, b);
+ assertEquals("0000000000000000", b.toString());
+
+ b.setLength(0);
+ TypeUtil.toHex(Long.MAX_VALUE, b);
+ assertEquals("7FFFFFFFFFFFFFFF", b.toString());
+
+ b.setLength(0);
+ TypeUtil.toHex(Long.MIN_VALUE, b);
+ assertEquals("8000000000000000", b.toString());
+
+ b.setLength(0);
+ TypeUtil.toHex(0x123456789abcdef0L, b);
+ assertEquals("123456789ABCDEF0", b.toString());
+ }
+
+ @Test
+ public void testIsTrue()
+ {
+ assertTrue(TypeUtil.isTrue(Boolean.TRUE));
+ assertTrue(TypeUtil.isTrue(true));
+ assertTrue(TypeUtil.isTrue("true"));
+ assertTrue(TypeUtil.isTrue(new Object()
+ {
+ @Override
+ public String toString()
+ {
+ return "true";
+ }
+ }));
+
+ assertFalse(TypeUtil.isTrue(Boolean.FALSE));
+ assertFalse(TypeUtil.isTrue(false));
+ assertFalse(TypeUtil.isTrue("false"));
+ assertFalse(TypeUtil.isTrue("blargle"));
+ assertFalse(TypeUtil.isTrue(new Object()
+ {
+ @Override
+ public String toString()
+ {
+ return "false";
+ }
+ }));
+ }
+
+ @Test
+ public void testIsFalse()
+ {
+ assertTrue(TypeUtil.isFalse(Boolean.FALSE));
+ assertTrue(TypeUtil.isFalse(false));
+ assertTrue(TypeUtil.isFalse("false"));
+ assertTrue(TypeUtil.isFalse(new Object()
+ {
+ @Override
+ public String toString()
+ {
+ return "false";
+ }
+ }));
+
+ assertFalse(TypeUtil.isFalse(Boolean.TRUE));
+ assertFalse(TypeUtil.isFalse(true));
+ assertFalse(TypeUtil.isFalse("true"));
+ assertFalse(TypeUtil.isFalse("blargle"));
+ assertFalse(TypeUtil.isFalse(new Object()
+ {
+ @Override
+ public String toString()
+ {
+ return "true";
+ }
+ }));
+ }
+
+ @Test
+ public void testGetLocationOfClassFromMavenRepo() throws Exception
+ {
+ String mavenRepoPathProperty = System.getProperty("mavenRepoPath");
+ assumeTrue(mavenRepoPathProperty != null);
+ Path mavenRepoPath = Paths.get(mavenRepoPathProperty);
+
+ // Classes from maven dependencies
+ Resource resource = Resource.newResource(TypeUtil.getLocationOfClass(org.junit.jupiter.api.Assertions.class).toASCIIString());
+ assertThat(resource.getFile().getCanonicalPath(), Matchers.startsWith(mavenRepoPath.toFile().getCanonicalPath()));
+ }
+
+ @Test
+ public void getLocationOfClassClassDirectory()
+ {
+ // Class from project dependencies
+ assertThat(TypeUtil.getLocationOfClass(TypeUtil.class).toASCIIString(), containsString("/classes/"));
+ }
+
+ @Test
+ @DisabledOnJre(JRE.JAVA_8)
+ public void testGetLocationJvmCoreJPMS()
+ {
+ // Class from JVM core
+ String expectedJavaBase = "/java.base";
+ assertThat(TypeUtil.getLocationOfClass(String.class).toASCIIString(), containsString(expectedJavaBase));
+ }
+
+ @Test
+ @DisabledOnJre(JRE.JAVA_8)
+ public void testGetLocationJavaLangThreadDeathJPMS()
+ {
+ // Class from JVM core
+ String expectedJavaBase = "/java.base";
+ assertThat(TypeUtil.getLocationOfClass(java.lang.ThreadDeath.class).toASCIIString(), containsString(expectedJavaBase));
+ }
+
+ @Test
+ @EnabledOnJre(JRE.JAVA_8)
+ public void testGetLocationJvmCoreJava8RT()
+ {
+ // Class from JVM core
+ String expectedJavaBase = "/rt.jar";
+ assertThat(TypeUtil.getLocationOfClass(String.class).toASCIIString(), containsString(expectedJavaBase));
+ }
+
+ @Test
+ @EnabledOnJre(JRE.JAVA_8)
+ public void testGetLocationJavaLangThreadDeathJava8RT()
+ {
+ // Class from JVM core
+ String expectedJavaBase = "/rt.jar";
+ assertThat(TypeUtil.getLocationOfClass(java.lang.ThreadDeath.class).toASCIIString(), containsString(expectedJavaBase));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java
new file mode 100644
index 0000000..a52a644
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java
@@ -0,0 +1,185 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.util.ArrayList;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+
+public class URIUtilCanonicalPathTest
+{
+ public static Stream<Arguments> paths()
+ {
+ String[][] canonical =
+ {
+ // Examples from RFC
+ {"/a/b/c/./../../g", "/a/g"},
+ {"mid/content=5/../6", "mid/6"},
+
+ // Basic examples (no changes expected)
+ {"/hello.html", "/hello.html"},
+ {"/css/main.css", "/css/main.css"},
+ {"/", "/"},
+ {"", ""},
+ {"/aaa/bbb/", "/aaa/bbb/"},
+ {"/aaa/bbb", "/aaa/bbb"},
+ {"aaa/bbb", "aaa/bbb"},
+ {"aaa/", "aaa/"},
+ {"aaa", "aaa"},
+ {"a", "a"},
+ {"a/", "a/"},
+
+ // Extra slashes
+ {"/aaa//bbb/", "/aaa//bbb/"},
+ {"/aaa//bbb", "/aaa//bbb"},
+ {"/aaa///bbb/", "/aaa///bbb/"},
+
+ // Path traversal with current references "./"
+ {"/aaa/./bbb/", "/aaa/bbb/"},
+ {"/aaa/./bbb", "/aaa/bbb"},
+ {"./bbb/", "bbb/"},
+ {"./aaa", "aaa"},
+ {"./aaa/", "aaa/"},
+ {"/./aaa/", "/aaa/"},
+ {"./aaa/../bbb/", "bbb/"},
+ {"/foo/.", "/foo/"},
+ {"/foo/./", "/foo/"},
+ {"./", ""},
+ {".", ""},
+ {".//", "/"},
+ {".///", "//"},
+ {"/.", "/"},
+ {"//.", "//"},
+ {"///.", "///"},
+
+ // Path traversal directory (but not past root)
+ {"/aaa/../bbb/", "/bbb/"},
+ {"/aaa/../bbb", "/bbb"},
+ {"/aaa..bbb/", "/aaa..bbb/"},
+ {"/aaa..bbb", "/aaa..bbb"},
+ {"/aaa/..bbb/", "/aaa/..bbb/"},
+ {"/aaa/..bbb", "/aaa/..bbb"},
+ {"/aaa/./../bbb/", "/bbb/"},
+ {"/aaa/./../bbb", "/bbb"},
+ {"/aaa/bbb/ccc/../../ddd/", "/aaa/ddd/"},
+ {"/aaa/bbb/ccc/../../ddd", "/aaa/ddd"},
+ {"/foo/../bar//", "/bar//"},
+ {"/ctx/../bar/../ctx/all/index.txt", "/ctx/all/index.txt"},
+ {"/down/.././index.html", "/index.html"},
+ {"/aaa/bbb/ccc/..", "/aaa/bbb/"},
+
+ // Path traversal up past root
+ {"..", null},
+ {"./..", null},
+ {"aaa/../..", null},
+ {"/foo/bar/../../..", null},
+ {"/../foo", null},
+ {"a/.", "a/"},
+ {"a/..", ""},
+ {"a/../..", null},
+ {"/foo/../../bar", null},
+
+ // Encoded ?
+ {"/ctx/dir%3f/../index.html", "/ctx/index.html"},
+
+ // Known windows shell quirks
+ {"file.txt ", "file.txt "}, // with spaces
+ {"file.txt...", "file.txt..."}, // extra dots ignored by windows
+ // BREAKS Jenkins: {"file.txt\u0000", "file.txt\u0000"}, // null terminated is ignored by windows
+ {"file.txt\r", "file.txt\r"}, // CR terminated is ignored by windows
+ {"file.txt\n", "file.txt\n"}, // LF terminated is ignored by windows
+ {"file.txt\"\"\"\"", "file.txt\"\"\"\""}, // extra quotes ignored by windows
+ {"file.txt<<<>>><", "file.txt<<<>>><"}, // angle brackets at end of path ignored by windows
+ {"././././././file.txt", "file.txt"},
+
+ // Oddball requests that look like path traversal, but are not
+ {"/....", "/...."},
+ {"/..../ctx/..../blah/logo.jpg", "/..../ctx/..../blah/logo.jpg"},
+
+ // paths with encoded segments should remain encoded
+ // canonicalPath() is not responsible for decoding characters
+ {"%2e%2e/", "%2e%2e/"},
+ {"/%2e%2e/", "/%2e%2e/"},
+
+ // paths with parameters are not elided
+ {"/foo/.;/bar", "/foo/.;/bar"},
+ {"/foo/..;/bar", "/foo/..;/bar"},
+ {"/foo/..;/..;/bar", "/foo/..;/..;/bar"},
+
+ // Trailing / is preserved
+ {"/foo/bar/..", "/foo/"},
+ {"/foo/bar/../", "/foo/"},
+ };
+
+ ArrayList<Arguments> ret = new ArrayList<>();
+ for (String[] args : canonical)
+ {
+ ret.add(Arguments.of((Object[])args));
+ }
+ return ret.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("paths")
+ public void testCanonicalPath(String input, String expectedResult)
+ {
+ // Check canonicalPath
+ assertThat(URIUtil.canonicalPath(input), is(expectedResult));
+
+ // Check canonicalURI
+ if (expectedResult == null)
+ {
+ assertThat(URIUtil.canonicalURI(input), nullValue());
+ }
+ else
+ {
+ // mostly encodedURI will be the same
+ assertThat(URIUtil.canonicalURI(input), is(expectedResult));
+ // but will terminate on fragments and queries
+ assertThat(URIUtil.canonicalURI(input + "?/foo/../bar/."), is(expectedResult + "?/foo/../bar/."));
+ assertThat(URIUtil.canonicalURI(input + "#/foo/../bar/."), is(expectedResult + "#/foo/../bar/."));
+ }
+ }
+
+ public static Stream<Arguments> queries()
+ {
+ String[][] data =
+ {
+ {"/ctx/../dir?/../index.html", "/dir?/../index.html"},
+ {"/get-files?file=/etc/passwd", "/get-files?file=/etc/passwd"},
+ {"/get-files?file=../../../../../passwd", "/get-files?file=../../../../../passwd"}
+ };
+ return Stream.of(data).map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("queries")
+ public void testQuery(String input, String expectedPath)
+ {
+ String actual = URIUtil.canonicalURI(input);
+ assertThat(actual, is(expectedPath));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java
new file mode 100644
index 0000000..fea195a
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java
@@ -0,0 +1,757 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.FileVisitOption;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * URIUtil Tests.
+ */
+@SuppressWarnings("SpellCheckingInspection")
+@ExtendWith(WorkDirExtension.class)
+public class URIUtilTest
+{
+ private static final Logger LOG = Log.getLogger(URIUtilTest.class);
+ public WorkDir workDir;
+
+ public static Stream<Arguments> encodePathSource()
+ {
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+ return Stream.of(
+ Arguments.of("/foo/\n/bar", "/foo/%0A/bar"),
+ Arguments.of("/foo%23+;,:=/b a r/?info ", "/foo%2523+%3B,:=/b%20a%20r/%3Finfo%20"),
+ Arguments.of("/context/'list'/\"me\"/;<script>window.alert('xss');</script>",
+ "/context/%27list%27/%22me%22/%3B%3Cscript%3Ewindow.alert(%27xss%27)%3B%3C/script%3E"),
+ Arguments.of("test\u00f6?\u00f6:\u00df", "test%C3%B6%3F%C3%B6:%C3%9F"),
+ Arguments.of("test?\u00f6?\u00f6:\u00df", "test%3F%C3%B6%3F%C3%B6:%C3%9F")
+ );
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}")
+ @MethodSource("encodePathSource")
+ public void testEncodePath(String rawPath, String expectedEncoded)
+ {
+ // test basic encode/decode
+ StringBuilder buf = new StringBuilder();
+ buf.setLength(0);
+ URIUtil.encodePath(buf, rawPath);
+ assertEquals(expectedEncoded, buf.toString());
+ }
+
+ @Test
+ public void testEncodeString()
+ {
+ StringBuilder buf = new StringBuilder();
+ buf.setLength(0);
+ URIUtil.encodeString(buf, "foo%23;,:=b a r", ";,= ");
+ assertEquals("foo%2523%3b%2c:%3db%20a%20r", buf.toString());
+ }
+
+ public static Stream<Arguments> decodePathSource()
+ {
+ List<Arguments> arguments = new ArrayList<>();
+ arguments.add(Arguments.of("/foo/bar", "/foo/bar"));
+
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+ arguments.add(Arguments.of("/f%20o/b%20r", "/f o/b r"));
+ arguments.add(Arguments.of("fää%2523%3b%2c:%3db%20a%20r%3D", "f\u00e4\u00e4%23;,:=b a r="));
+ arguments.add(Arguments.of("f%d8%a9%d8%a9%2523%3b%2c:%3db%20a%20r", "f\u0629\u0629%23;,:=b a r"));
+
+ // path parameters should be ignored
+ arguments.add(Arguments.of("/foo;ignore/bar;ignore", "/foo/bar"));
+ arguments.add(Arguments.of("/f\u00e4\u00e4;ignore/bar;ignore", "/fää/bar"));
+ arguments.add(Arguments.of("/f%d8%a9%d8%a9%2523;ignore/bar;ignore", "/f\u0629\u0629%23/bar"));
+ arguments.add(Arguments.of("foo%2523%3b%2c:%3db%20a%20r;rubbish", "foo%23;,:=b a r"));
+
+ // Test for null character (real world ugly test case)
+ byte[] oddBytes = {'/', 0x00, '/'};
+ String odd = new String(oddBytes, StandardCharsets.ISO_8859_1);
+ arguments.add(Arguments.of("/%00/", odd));
+
+ // Deprecated Microsoft Percent-U encoding
+ arguments.add(Arguments.of("abc%u3040", "abc\u3040"));
+ return arguments.stream();
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}")
+ @MethodSource("decodePathSource")
+ public void testDecodePath(String encodedPath, String expectedPath)
+ {
+ String path = URIUtil.decodePath(encodedPath);
+ assertEquals(expectedPath, path);
+ }
+
+ public static Stream<Arguments> decodeBadPathSource()
+ {
+ List<Arguments> arguments = new ArrayList<>();
+
+ // Test for null character (real world ugly test case)
+ // TODO is this a bad decoding or a bad URI ?
+ // arguments.add(Arguments.of("/%00/"));
+
+ // Deprecated Microsoft Percent-U encoding
+ // TODO still supported for now ?
+ // arguments.add(Arguments.of("abc%u3040"));
+
+ // Bad %## encoding
+ arguments.add(Arguments.of("abc%xyz"));
+
+ // Incomplete %## encoding
+ arguments.add(Arguments.of("abc%"));
+ arguments.add(Arguments.of("abc%A"));
+
+ // Invalid microsoft %u#### encoding
+ arguments.add(Arguments.of("abc%uvwxyz"));
+ arguments.add(Arguments.of("abc%uEFGHIJ"));
+
+ // Incomplete microsoft %u#### encoding
+ arguments.add(Arguments.of("abc%uABC"));
+ arguments.add(Arguments.of("abc%uAB"));
+ arguments.add(Arguments.of("abc%uA"));
+ arguments.add(Arguments.of("abc%u"));
+
+ // Invalid UTF-8 and ISO8859-1
+ // TODO currently ISO8859 is too forgiving to detect these
+ /*
+ arguments.add(Arguments.of("abc%C3%28")); // invalid 2 octext sequence
+ arguments.add(Arguments.of("abc%A0%A1")); // invalid 2 octext sequence
+ arguments.add(Arguments.of("abc%e2%28%a1")); // invalid 3 octext sequence
+ arguments.add(Arguments.of("abc%e2%82%28")); // invalid 3 octext sequence
+ arguments.add(Arguments.of("abc%f0%28%8c%bc")); // invalid 4 octext sequence
+ arguments.add(Arguments.of("abc%f0%90%28%bc")); // invalid 4 octext sequence
+ arguments.add(Arguments.of("abc%f0%28%8c%28")); // invalid 4 octext sequence
+ arguments.add(Arguments.of("abc%f8%a1%a1%a1%a1")); // valid sequence, but not unicode
+ arguments.add(Arguments.of("abc%fc%a1%a1%a1%a1%a1")); // valid sequence, but not unicode
+ arguments.add(Arguments.of("abc%f8%a1%a1%a1")); // incomplete sequence
+ */
+
+ return arguments.stream();
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}")
+ @MethodSource("decodeBadPathSource")
+ public void testBadDecodePath(String encodedPath)
+ {
+ assertThrows(IllegalArgumentException.class, () -> URIUtil.decodePath(encodedPath));
+ }
+
+ @Test
+ public void testDecodePathSubstring()
+ {
+ String path = URIUtil.decodePath("xx/foo/barxx", 2, 8);
+ assertEquals("/foo/bar", path);
+
+ path = URIUtil.decodePath("xxx/foo/bar%2523%3b%2c:%3db%20a%20r%3Dxxx;rubbish", 3, 35);
+ assertEquals("/foo/bar%23;,:=b a r=", path);
+ }
+
+ public static Stream<Arguments> addEncodedPathsSource()
+ {
+ return Stream.of(
+ Arguments.of(null, null, null),
+ Arguments.of(null, "", ""),
+ Arguments.of(null, "bbb", "bbb"),
+ Arguments.of(null, "/", "/"),
+ Arguments.of(null, "/bbb", "/bbb"),
+
+ Arguments.of("", null, ""),
+ Arguments.of("", "", ""),
+ Arguments.of("", "bbb", "bbb"),
+ Arguments.of("", "/", "/"),
+ Arguments.of("", "/bbb", "/bbb"),
+
+ Arguments.of("aaa", null, "aaa"),
+ Arguments.of("aaa", "", "aaa"),
+ Arguments.of("aaa", "bbb", "aaa/bbb"),
+ Arguments.of("aaa", "/", "aaa/"),
+ Arguments.of("aaa", "/bbb", "aaa/bbb"),
+
+ Arguments.of("/", null, "/"),
+ Arguments.of("/", "", "/"),
+ Arguments.of("/", "bbb", "/bbb"),
+ Arguments.of("/", "/", "/"),
+ Arguments.of("/", "/bbb", "/bbb"),
+
+ Arguments.of("aaa/", null, "aaa/"),
+ Arguments.of("aaa/", "", "aaa/"),
+ Arguments.of("aaa/", "bbb", "aaa/bbb"),
+ Arguments.of("aaa/", "/", "aaa/"),
+ Arguments.of("aaa/", "/bbb", "aaa/bbb"),
+
+ Arguments.of(";JS", null, ";JS"),
+ Arguments.of(";JS", "", ";JS"),
+ Arguments.of(";JS", "bbb", "bbb;JS"),
+ Arguments.of(";JS", "/", "/;JS"),
+ Arguments.of(";JS", "/bbb", "/bbb;JS"),
+
+ Arguments.of("aaa;JS", null, "aaa;JS"),
+ Arguments.of("aaa;JS", "", "aaa;JS"),
+ Arguments.of("aaa;JS", "bbb", "aaa/bbb;JS"),
+ Arguments.of("aaa;JS", "/", "aaa/;JS"),
+ Arguments.of("aaa;JS", "/bbb", "aaa/bbb;JS"),
+
+ Arguments.of("aaa/;JS", null, "aaa/;JS"),
+ Arguments.of("aaa/;JS", "", "aaa/;JS"),
+ Arguments.of("aaa/;JS", "bbb", "aaa/bbb;JS"),
+ Arguments.of("aaa/;JS", "/", "aaa/;JS"),
+ Arguments.of("aaa/;JS", "/bbb", "aaa/bbb;JS"),
+
+ Arguments.of("?A=1", null, "?A=1"),
+ Arguments.of("?A=1", "", "?A=1"),
+ Arguments.of("?A=1", "bbb", "bbb?A=1"),
+ Arguments.of("?A=1", "/", "/?A=1"),
+ Arguments.of("?A=1", "/bbb", "/bbb?A=1"),
+
+ Arguments.of("aaa?A=1", null, "aaa?A=1"),
+ Arguments.of("aaa?A=1", "", "aaa?A=1"),
+ Arguments.of("aaa?A=1", "bbb", "aaa/bbb?A=1"),
+ Arguments.of("aaa?A=1", "/", "aaa/?A=1"),
+ Arguments.of("aaa?A=1", "/bbb", "aaa/bbb?A=1"),
+
+ Arguments.of("aaa/?A=1", null, "aaa/?A=1"),
+ Arguments.of("aaa/?A=1", "", "aaa/?A=1"),
+ Arguments.of("aaa/?A=1", "bbb", "aaa/bbb?A=1"),
+ Arguments.of("aaa/?A=1", "/", "aaa/?A=1"),
+ Arguments.of("aaa/?A=1", "/bbb", "aaa/bbb?A=1"),
+
+ Arguments.of(";JS?A=1", null, ";JS?A=1"),
+ Arguments.of(";JS?A=1", "", ";JS?A=1"),
+ Arguments.of(";JS?A=1", "bbb", "bbb;JS?A=1"),
+ Arguments.of(";JS?A=1", "/", "/;JS?A=1"),
+ Arguments.of(";JS?A=1", "/bbb", "/bbb;JS?A=1"),
+
+ Arguments.of("aaa;JS?A=1", null, "aaa;JS?A=1"),
+ Arguments.of("aaa;JS?A=1", "", "aaa;JS?A=1"),
+ Arguments.of("aaa;JS?A=1", "bbb", "aaa/bbb;JS?A=1"),
+ Arguments.of("aaa;JS?A=1", "/", "aaa/;JS?A=1"),
+ Arguments.of("aaa;JS?A=1", "/bbb", "aaa/bbb;JS?A=1"),
+
+ Arguments.of("aaa/;JS?A=1", null, "aaa/;JS?A=1"),
+ Arguments.of("aaa/;JS?A=1", "", "aaa/;JS?A=1"),
+ Arguments.of("aaa/;JS?A=1", "bbb", "aaa/bbb;JS?A=1"),
+ Arguments.of("aaa/;JS?A=1", "/", "aaa/;JS?A=1"),
+ Arguments.of("aaa/;JS?A=1", "/bbb", "aaa/bbb;JS?A=1")
+ );
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}+{1}")
+ @MethodSource("addEncodedPathsSource")
+ public void testAddEncodedPaths(String path1, String path2, String expected)
+ {
+ String actual = URIUtil.addEncodedPaths(path1, path2);
+ assertEquals(expected, actual, String.format("%s+%s", path1, path2));
+ }
+
+ public static Stream<Arguments> addDecodedPathsSource()
+ {
+ return Stream.of(
+ Arguments.of(null, null, null),
+ Arguments.of(null, "", ""),
+ Arguments.of(null, "bbb", "bbb"),
+ Arguments.of(null, "/", "/"),
+ Arguments.of(null, "/bbb", "/bbb"),
+
+ Arguments.of("", null, ""),
+ Arguments.of("", "", ""),
+ Arguments.of("", "bbb", "bbb"),
+ Arguments.of("", "/", "/"),
+ Arguments.of("", "/bbb", "/bbb"),
+
+ Arguments.of("aaa", null, "aaa"),
+ Arguments.of("aaa", "", "aaa"),
+ Arguments.of("aaa", "bbb", "aaa/bbb"),
+ Arguments.of("aaa", "/", "aaa/"),
+ Arguments.of("aaa", "/bbb", "aaa/bbb"),
+
+ Arguments.of("/", null, "/"),
+ Arguments.of("/", "", "/"),
+ Arguments.of("/", "bbb", "/bbb"),
+ Arguments.of("/", "/", "/"),
+ Arguments.of("/", "/bbb", "/bbb"),
+
+ Arguments.of("aaa/", null, "aaa/"),
+ Arguments.of("aaa/", "", "aaa/"),
+ Arguments.of("aaa/", "bbb", "aaa/bbb"),
+ Arguments.of("aaa/", "/", "aaa/"),
+ Arguments.of("aaa/", "/bbb", "aaa/bbb"),
+
+ Arguments.of(";JS", null, ";JS"),
+ Arguments.of(";JS", "", ";JS"),
+ Arguments.of(";JS", "bbb", ";JS/bbb"),
+ Arguments.of(";JS", "/", ";JS/"),
+ Arguments.of(";JS", "/bbb", ";JS/bbb"),
+
+ Arguments.of("aaa;JS", null, "aaa;JS"),
+ Arguments.of("aaa;JS", "", "aaa;JS"),
+ Arguments.of("aaa;JS", "bbb", "aaa;JS/bbb"),
+ Arguments.of("aaa;JS", "/", "aaa;JS/"),
+ Arguments.of("aaa;JS", "/bbb", "aaa;JS/bbb"),
+
+ Arguments.of("aaa/;JS", null, "aaa/;JS"),
+ Arguments.of("aaa/;JS", "", "aaa/;JS"),
+ Arguments.of("aaa/;JS", "bbb", "aaa/;JS/bbb"),
+ Arguments.of("aaa/;JS", "/", "aaa/;JS/"),
+ Arguments.of("aaa/;JS", "/bbb", "aaa/;JS/bbb"),
+
+ Arguments.of("?A=1", null, "?A=1"),
+ Arguments.of("?A=1", "", "?A=1"),
+ Arguments.of("?A=1", "bbb", "?A=1/bbb"),
+ Arguments.of("?A=1", "/", "?A=1/"),
+ Arguments.of("?A=1", "/bbb", "?A=1/bbb"),
+
+ Arguments.of("aaa?A=1", null, "aaa?A=1"),
+ Arguments.of("aaa?A=1", "", "aaa?A=1"),
+ Arguments.of("aaa?A=1", "bbb", "aaa?A=1/bbb"),
+ Arguments.of("aaa?A=1", "/", "aaa?A=1/"),
+ Arguments.of("aaa?A=1", "/bbb", "aaa?A=1/bbb"),
+
+ Arguments.of("aaa/?A=1", null, "aaa/?A=1"),
+ Arguments.of("aaa/?A=1", "", "aaa/?A=1"),
+ Arguments.of("aaa/?A=1", "bbb", "aaa/?A=1/bbb"),
+ Arguments.of("aaa/?A=1", "/", "aaa/?A=1/"),
+ Arguments.of("aaa/?A=1", "/bbb", "aaa/?A=1/bbb")
+ );
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}+{1}")
+ @MethodSource("addDecodedPathsSource")
+ public void testAddDecodedPaths(String path1, String path2, String expected)
+ {
+ String actual = URIUtil.addPaths(path1, path2);
+ assertEquals(expected, actual, String.format("%s+%s", path1, path2));
+ }
+
+ public static Stream<Arguments> compactPathSource()
+ {
+ return Stream.of(
+ Arguments.of("/foo/bar", "/foo/bar"),
+ Arguments.of("/foo/bar?a=b//c", "/foo/bar?a=b//c"),
+
+ Arguments.of("//foo//bar", "/foo/bar"),
+ Arguments.of("//foo//bar?a=b//c", "/foo/bar?a=b//c"),
+
+ Arguments.of("/foo///bar", "/foo/bar"),
+ Arguments.of("/foo///bar?a=b//c", "/foo/bar?a=b//c")
+ );
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}")
+ @MethodSource("compactPathSource")
+ public void testCompactPath(String path, String expected)
+ {
+ String actual = URIUtil.compactPath(path);
+ assertEquals(expected, actual);
+ }
+
+ public static Stream<Arguments> parentPathSource()
+ {
+ return Stream.of(
+ Arguments.of("/aaa/bbb/", "/aaa/"),
+ Arguments.of("/aaa/bbb", "/aaa/"),
+ Arguments.of("/aaa/", "/"),
+ Arguments.of("/aaa", "/"),
+ Arguments.of("/", null),
+ Arguments.of(null, null)
+ );
+ }
+
+ @ParameterizedTest(name = "[{index}] {0}")
+ @MethodSource("parentPathSource")
+ public void testParentPath(String path, String expectedPath)
+ {
+ String actual = URIUtil.parentPath(path);
+ assertEquals(expectedPath, actual, String.format("parent %s", path));
+ }
+
+ public static Stream<Arguments> equalsIgnoreEncodingStringTrueSource()
+ {
+ return Stream.of(
+ Arguments.of("http://example.com/foo/bar", "http://example.com/foo/bar"),
+ Arguments.of("/barry's", "/barry%27s"),
+ Arguments.of("/barry%27s", "/barry's"),
+ Arguments.of("/barry%27s", "/barry%27s"),
+ Arguments.of("/b rry's", "/b%20rry%27s"),
+ Arguments.of("/b rry%27s", "/b%20rry's"),
+ Arguments.of("/b rry%27s", "/b%20rry%27s"),
+
+ Arguments.of("/foo%2fbar", "/foo%2fbar"),
+ Arguments.of("/foo%2fbar", "/foo%2Fbar"),
+
+ // encoded vs not-encode ("%" symbol is encoded as "%25")
+ Arguments.of("/abc%25xyz", "/abc%xyz"),
+ Arguments.of("/abc%25xy", "/abc%xy"),
+ Arguments.of("/abc%25x", "/abc%x"),
+ Arguments.of("/zzz%25", "/zzz%")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("equalsIgnoreEncodingStringTrueSource")
+ public void testEqualsIgnoreEncodingStringTrue(String uriA, String uriB)
+ {
+ assertTrue(URIUtil.equalsIgnoreEncodings(uriA, uriB));
+ }
+
+ public static Stream<Arguments> equalsIgnoreEncodingStringFalseSource()
+ {
+ return Stream.of(
+ // case difference
+ Arguments.of("ABC", "abc"),
+ // Encoding difference ("'" is "%27")
+ Arguments.of("/barry's", "/barry%26s"),
+ // Never match on "%2f" differences - only intested in filename / directory name differences
+ // This could be a directory called "foo" with a file called "bar" on the left, and just a file "foo%2fbar" on the right
+ Arguments.of("/foo/bar", "/foo%2fbar"),
+ // not actually encoded
+ Arguments.of("/foo2fbar", "/foo/bar"),
+ // encoded vs not-encode ("%" symbol is encoded as "%25")
+ Arguments.of("/yyy%25zzz", "/aaa%xxx"),
+ Arguments.of("/zzz%25", "/aaa%")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("equalsIgnoreEncodingStringFalseSource")
+ public void testEqualsIgnoreEncodingStringFalse(String uriA, String uriB)
+ {
+ assertFalse(URIUtil.equalsIgnoreEncodings(uriA, uriB));
+ }
+
+ public static Stream<Arguments> equalsIgnoreEncodingURITrueSource()
+ {
+ return Stream.of(
+ Arguments.of(
+ URI.create("jar:file:/path/to/main.jar!/META-INF/versions/"),
+ URI.create("jar:file:/path/to/main.jar!/META-INF/%76ersions/")
+ ),
+ Arguments.of(
+ URI.create("JAR:FILE:/path/to/main.jar!/META-INF/versions/"),
+ URI.create("jar:file:/path/to/main.jar!/META-INF/versions/")
+ )
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("equalsIgnoreEncodingURITrueSource")
+ public void testEqualsIgnoreEncodingURITrue(URI uriA, URI uriB)
+ {
+ assertTrue(URIUtil.equalsIgnoreEncodings(uriA, uriB));
+ }
+
+ public static Stream<Arguments> getJarSourceStringSource()
+ {
+ return Stream.of(
+ Arguments.of("file:///tmp/", "file:///tmp/"),
+ Arguments.of("jar:file:///tmp/foo.jar", "file:///tmp/foo.jar"),
+ Arguments.of("jar:file:///tmp/foo.jar!/some/path", "file:///tmp/foo.jar")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("getJarSourceStringSource")
+ public void testJarSourceString(String uri, String expectedJarUri) throws Exception
+ {
+ assertThat(URIUtil.getJarSource(uri), is(expectedJarUri));
+ }
+
+ public static Stream<Arguments> getJarSourceURISource()
+ {
+ return Stream.of(
+ Arguments.of(URI.create("file:///tmp/"), URI.create("file:///tmp/")),
+ Arguments.of(URI.create("jar:file:///tmp/foo.jar"), URI.create("file:///tmp/foo.jar")),
+ Arguments.of(URI.create("jar:file:///tmp/foo.jar!/some/path"), URI.create("file:///tmp/foo.jar"))
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("getJarSourceURISource")
+ public void testJarSourceURI(URI uri, URI expectedJarUri) throws Exception
+ {
+ assertThat(URIUtil.getJarSource(uri), is(expectedJarUri));
+ }
+
+ public static Stream<Arguments> encodeSpacesSource()
+ {
+ return Stream.of(
+ // null
+ Arguments.of(null, null),
+
+ // no spaces
+ Arguments.of("abc", "abc"),
+
+ // match
+ Arguments.of("a c", "a%20c"),
+ Arguments.of(" ", "%20%20%20"),
+ Arguments.of("a%20space", "a%20space")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("encodeSpacesSource")
+ public void testEncodeSpaces(String raw, String expected)
+ {
+ assertThat(URIUtil.encodeSpaces(raw), is(expected));
+ }
+
+ public static Stream<Arguments> encodeSpecific()
+ {
+ return Stream.of(
+ // [raw, chars, expected]
+
+ // null input
+ Arguments.of(null, null, null),
+
+ // null chars
+ Arguments.of("abc", null, "abc"),
+
+ // empty chars
+ Arguments.of("abc", "", "abc"),
+
+ // no matches
+ Arguments.of("abc", ".;", "abc"),
+ Arguments.of("xyz", ".;", "xyz"),
+ Arguments.of(":::", ".;", ":::"),
+
+ // matches
+ Arguments.of("a c", " ", "a%20c"),
+ Arguments.of("name=value", "=", "name%3Dvalue"),
+ Arguments.of("This has fewer then 10% hits.", ".%", "This has fewer then 10%25 hits%2E"),
+
+ // partially encoded already
+ Arguments.of("a%20name=value%20pair", "=", "a%20name%3Dvalue%20pair"),
+ Arguments.of("a%20name=value%20pair", "=%", "a%2520name%3Dvalue%2520pair")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "encodeSpecific")
+ public void testEncodeSpecific(String raw, String chars, String expected)
+ {
+ assertThat(URIUtil.encodeSpecific(raw, chars), is(expected));
+ }
+
+ public static Stream<Arguments> decodeSpecific()
+ {
+ return Stream.of(
+ // [raw, chars, expected]
+
+ // null input
+ Arguments.of(null, null, null),
+
+ // null chars
+ Arguments.of("abc", null, "abc"),
+
+ // empty chars
+ Arguments.of("abc", "", "abc"),
+
+ // no matches
+ Arguments.of("abc", ".;", "abc"),
+ Arguments.of("xyz", ".;", "xyz"),
+ Arguments.of(":::", ".;", ":::"),
+
+ // matches
+ Arguments.of("a%20c", " ", "a c"),
+ Arguments.of("name%3Dvalue", "=", "name=value"),
+ Arguments.of("This has fewer then 10%25 hits%2E", ".%", "This has fewer then 10% hits."),
+
+ // partially decode
+ Arguments.of("a%20name%3Dvalue%20pair", "=", "a%20name=value%20pair"),
+ Arguments.of("a%2520name%3Dvalue%2520pair", "=%", "a%20name=value%20pair")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "decodeSpecific")
+ public void testDecodeSpecific(String raw, String chars, String expected)
+ {
+ assertThat(URIUtil.decodeSpecific(raw, chars), is(expected));
+ }
+
+ public static Stream<Arguments> resourceUriLastSegmentSource()
+ {
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+ return Stream.of(
+ Arguments.of("test.war", "test.war"),
+ Arguments.of("a/b/c/test.war", "test.war"),
+ Arguments.of("bar%2Fbaz/test.war", "test.war"),
+ Arguments.of("fizz buzz/test.war", "test.war"),
+ Arguments.of("another one/bites the dust/", "bites the dust"),
+ Arguments.of("another+one/bites+the+dust/", "bites+the+dust"),
+ Arguments.of("another%20one/bites%20the%20dust/", "bites%20the%20dust"),
+ Arguments.of("spanish/n\u00FAmero.war", "n\u00FAmero.war"),
+ Arguments.of("spanish/n%C3%BAmero.war", "n%C3%BAmero.war"),
+ Arguments.of("a/b!/", "b"),
+ Arguments.of("a/b!/c/", "b"),
+ Arguments.of("a/b!/c/d/", "b"),
+ Arguments.of("a/b%21/", "b%21")
+ );
+ }
+
+ /**
+ * Using FileSystem provided URIs, attempt to get last URI path segment
+ */
+ @ParameterizedTest
+ @MethodSource("resourceUriLastSegmentSource")
+ public void testFileUriGetUriLastPathSegment(String basePath, String expectedName) throws IOException
+ {
+ Path root = workDir.getPath();
+ Path base = root.resolve(basePath);
+ if (basePath.endsWith("/"))
+ {
+ FS.ensureDirExists(base);
+ }
+ else
+ {
+ FS.ensureDirExists(base.getParent());
+ FS.touch(base);
+ }
+ URI uri = base.toUri();
+ if (OS.MAC.isCurrentOs())
+ {
+ // Normalize Unicode to NFD form that OSX Path/FileSystem produces
+ expectedName = Normalizer.normalize(expectedName, Normalizer.Form.NFD);
+ }
+ assertThat(URIUtil.getUriLastPathSegment(uri), is(expectedName));
+ }
+
+ public static Stream<Arguments> uriLastSegmentSource() throws URISyntaxException, IOException
+ {
+ final String TEST_RESOURCE_JAR = "test-base-resource.jar";
+ Path testJar = MavenTestingUtils.getTestResourcePathFile(TEST_RESOURCE_JAR);
+ URI uri = new URI("jar", testJar.toUri().toASCIIString(), null);
+
+ Map<String, Object> env = new HashMap<>();
+ env.put("multi-release", "runtime");
+
+ List<Arguments> arguments = new ArrayList<>();
+ arguments.add(Arguments.of(uri, TEST_RESOURCE_JAR));
+ try (FileSystem zipFs = FileSystems.newFileSystem(uri, env))
+ {
+ FileVisitOption[] fileVisitOptions = new FileVisitOption[]{};
+
+ for (Path root : zipFs.getRootDirectories())
+ {
+ Stream<Path> entryStream = Files.find(root, 10, (path, attrs) -> true, fileVisitOptions);
+ entryStream.forEach((path) ->
+ {
+ if (path.toString().endsWith("!/"))
+ {
+ // skip - JAR entry type not supported by Jetty
+ // TODO: re-enable once we start to use zipfs
+ LOG.warn("Skipping Unsupported entry: " + path.toUri());
+ }
+ else
+ {
+ arguments.add(Arguments.of(path.toUri(), TEST_RESOURCE_JAR));
+ }
+ });
+ }
+ }
+
+ return arguments.stream();
+ }
+
+ /**
+ * Tests of URIs last segment, including "jar:file:" based URIs.
+ */
+ @ParameterizedTest
+ @MethodSource("uriLastSegmentSource")
+ public void testGetUriLastPathSegment(URI uri, String expectedName)
+ {
+ assertThat(URIUtil.getUriLastPathSegment(uri), is(expectedName));
+ }
+
+ public static Stream<Arguments> addQueryParameterSource()
+ {
+ final String newQueryParam = "newParam=11";
+ return Stream.of(
+ Arguments.of(null, newQueryParam, is(newQueryParam)),
+ Arguments.of(newQueryParam, null, is(newQueryParam)),
+ Arguments.of("", newQueryParam, is(newQueryParam)),
+ Arguments.of(newQueryParam, "", is(newQueryParam)),
+ Arguments.of("existingParam=3", newQueryParam, is("existingParam=3&" + newQueryParam)),
+ Arguments.of(newQueryParam, "existingParam=3", is(newQueryParam + "&existingParam=3")),
+ Arguments.of("existingParam1=value1&existingParam2=value2", newQueryParam, is("existingParam1=value1&existingParam2=value2&" + newQueryParam)),
+ Arguments.of(newQueryParam, "existingParam1=value1&existingParam2=value2", is(newQueryParam + "&existingParam1=value1&existingParam2=value2"))
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("addQueryParameterSource")
+ public void testAddQueryParam(String param1, String param2, Matcher<String> matcher)
+ {
+ assertThat(URIUtil.addQueries(param1, param2), matcher);
+ }
+
+ @Test
+ public void testEncodeDecodeVisibleOnly()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append('/');
+ for (char i = 0; i < 0x7FFF; i++)
+ builder.append(i);
+ String path = builder.toString();
+ String encoded = URIUtil.encodePath(path);
+ // Check endoded is visible
+ for (char c : encoded.toCharArray())
+ {
+ assertTrue(c > 0x20 && c < 0x80);
+ assertFalse(Character.isWhitespace(c));
+ assertFalse(Character.isISOControl(c));
+ }
+ // check decode to original
+ String decoded = URIUtil.decodePath(encoded);
+ assertEquals(path, decoded);
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java
new file mode 100644
index 0000000..ead1890
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java
@@ -0,0 +1,258 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestFactory;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.DynamicTest.dynamicTest;
+
+/**
+ * URL Encoding / Decoding Tests
+ */
+// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+public class URLEncodedTest
+{
+ @TestFactory
+ public Iterator<DynamicTest> testUrlEncoded()
+ {
+ List<DynamicTest> tests = new ArrayList<>();
+
+ tests.add(dynamicTest("Initially not empty", () ->
+ {
+ UrlEncoded urlEncoded = new UrlEncoded();
+ assertEquals(0, urlEncoded.size());
+ }));
+
+ tests.add(dynamicTest("Not empty after decode(\"\")", () ->
+ {
+ UrlEncoded urlEncoded = new UrlEncoded();
+ urlEncoded.clear();
+ urlEncoded.decode("");
+ assertEquals(0, urlEncoded.size());
+ }));
+
+ tests.add(dynamicTest("Simple encode", () ->
+ {
+ UrlEncoded urlEncoded = new UrlEncoded();
+ urlEncoded.clear();
+ urlEncoded.decode("Name1=Value1");
+ assertEquals(1, urlEncoded.size(), "simple param size");
+ assertEquals("Name1=Value1", urlEncoded.encode(), "simple encode");
+ assertEquals("Value1", urlEncoded.getString("Name1"), "simple get");
+ }));
+
+ tests.add(dynamicTest("Dangling param", () ->
+ {
+ UrlEncoded urlEncoded = new UrlEncoded();
+ urlEncoded.clear();
+ urlEncoded.decode("Name2=");
+ assertEquals(1, urlEncoded.size(), "dangling param size");
+ assertEquals("Name2", urlEncoded.encode(), "dangling encode");
+ assertEquals("", urlEncoded.getString("Name2"), "dangling get");
+ }));
+
+ tests.add(dynamicTest("noValue param", () ->
+ {
+ UrlEncoded urlEncoded = new UrlEncoded();
+ urlEncoded.clear();
+ urlEncoded.decode("Name3");
+ assertEquals(1, urlEncoded.size(), "noValue param size");
+ assertEquals("Name3", urlEncoded.encode(), "noValue encode");
+ assertEquals("", urlEncoded.getString("Name3"), "noValue get");
+ }));
+
+ tests.add(dynamicTest("badly encoded param", () ->
+ {
+ UrlEncoded urlEncoded = new UrlEncoded();
+ urlEncoded.clear();
+ urlEncoded.decode("Name4=V\u0629lue+4%21");
+ assertEquals(1, urlEncoded.size(), "encoded param size");
+ assertEquals("Name4=V%D8%A9lue+4%21", urlEncoded.encode(), "encoded encode");
+ assertEquals("V\u0629lue 4!", urlEncoded.getString("Name4"), "encoded get");
+ }));
+
+ tests.add(dynamicTest("encoded param 1", () ->
+ {
+ UrlEncoded urlEncoded = new UrlEncoded();
+ urlEncoded.clear();
+ urlEncoded.decode("Name4=Value%2B4%21");
+ assertEquals(1, urlEncoded.size(), "encoded param size");
+ assertEquals("Name4=Value%2B4%21", urlEncoded.encode(), "encoded encode");
+ assertEquals("Value+4!", urlEncoded.getString("Name4"), "encoded get");
+ }));
+
+ tests.add(dynamicTest("encoded param 2", () ->
+ {
+ UrlEncoded urlEncoded = new UrlEncoded();
+ urlEncoded.clear();
+ urlEncoded.decode("Name4=Value+4%21%20%214");
+ assertEquals(1, urlEncoded.size(), "encoded param size");
+ assertEquals("Name4=Value+4%21+%214", urlEncoded.encode(), "encoded encode");
+ assertEquals("Value 4! !4", urlEncoded.getString("Name4"), "encoded get");
+ }));
+
+ tests.add(dynamicTest("multi param", () ->
+ {
+ UrlEncoded urlEncoded = new UrlEncoded();
+ urlEncoded.clear();
+ urlEncoded.decode("Name5=aaa&Name6=bbb");
+ assertEquals(2, urlEncoded.size(), "multi param size");
+ assertTrue(urlEncoded.encode().equals("Name5=aaa&Name6=bbb") ||
+ urlEncoded.encode().equals("Name6=bbb&Name5=aaa"),
+ "multi encode " + urlEncoded.encode());
+ assertEquals("aaa", urlEncoded.getString("Name5"), "multi get");
+ assertEquals("bbb", urlEncoded.getString("Name6"), "multi get");
+ }));
+
+ tests.add(dynamicTest("multiple value encoded", () ->
+ {
+ UrlEncoded urlEncoded = new UrlEncoded();
+ urlEncoded.clear();
+ urlEncoded.decode("Name7=aaa&Name7=b%2Cb&Name7=ccc");
+ assertEquals("Name7=aaa&Name7=b%2Cb&Name7=ccc", urlEncoded.encode(), "multi encode");
+ assertEquals("aaa,b,b,ccc", urlEncoded.getString("Name7"), "list get all");
+ assertEquals("aaa", urlEncoded.getValues("Name7").get(0), "list get");
+ assertEquals("b,b", urlEncoded.getValues("Name7").get(1), "list get");
+ assertEquals("ccc", urlEncoded.getValues("Name7").get(2), "list get");
+ }));
+
+ tests.add(dynamicTest("encoded param", () ->
+ {
+ UrlEncoded urlEncoded = new UrlEncoded();
+ urlEncoded.clear();
+ urlEncoded.decode("Name8=xx%2C++yy++%2Czz");
+ assertEquals(1, urlEncoded.size(), "encoded param size");
+ assertEquals("Name8=xx%2C++yy++%2Czz", urlEncoded.encode(), "encoded encode");
+ assertEquals("xx, yy ,zz", urlEncoded.getString("Name8"), "encoded get");
+ }));
+
+ return tests.iterator();
+ }
+
+ @TestFactory
+ public Iterator<DynamicTest> testUrlEncodedStream()
+ {
+ String[][] charsets = new String[][]
+ {
+ {StringUtil.__UTF8, null, "%30"},
+ {StringUtil.__ISO_8859_1, StringUtil.__ISO_8859_1, "%30"},
+ {StringUtil.__UTF8, StringUtil.__UTF8, "%30"},
+ {StringUtil.__UTF16, StringUtil.__UTF16, "%00%30"},
+ };
+
+ // Note: "%30" -> decode -> "0"
+
+ List<DynamicTest> tests = new ArrayList<>();
+
+ for (String[] params : charsets)
+ {
+ tests.add(dynamicTest(params[0] + ">" + params[1] + "|" + params[2],
+ () ->
+ {
+ try (ByteArrayInputStream in = new ByteArrayInputStream(("name\n=value+" + params[2] + "&name1=&name2&n\u00e3me3=value+3").getBytes(params[0])))
+ {
+ MultiMap<String> m = new MultiMap<>();
+ UrlEncoded.decodeTo(in, m, params[1] == null ? null : Charset.forName(params[1]), -1, -1);
+ assertEquals(4, m.size(), params[1] + " stream length");
+ assertThat(params[1] + " stream name\\n", m.getString("name\n"), is("value 0"));
+ assertThat(params[1] + " stream name1", m.getString("name1"), is(""));
+ assertThat(params[1] + " stream name2", m.getString("name2"), is(""));
+ assertThat(params[1] + " stream n\u00e3me3", m.getString("n\u00e3me3"), is("value 3"));
+ }
+ }
+ ));
+ }
+
+ if (java.nio.charset.Charset.isSupported("Shift_JIS"))
+ {
+ tests.add(dynamicTest("Shift_JIS",
+ () ->
+ {
+ try (ByteArrayInputStream in2 = new ByteArrayInputStream("name=%83e%83X%83g".getBytes(StandardCharsets.ISO_8859_1)))
+ {
+ MultiMap<String> m2 = new MultiMap<>();
+ UrlEncoded.decodeTo(in2, m2, Charset.forName("Shift_JIS"), -1, -1);
+ assertEquals(1, m2.size(), "stream length");
+ assertEquals("\u30c6\u30b9\u30c8", m2.getString("name"), "stream name");
+ }
+ }
+ ));
+ }
+
+ return tests.iterator();
+ }
+
+ @Test
+ @EnabledIfSystemProperty(named = "org.eclipse.jetty.util.UrlEncoding.charset", matches = "\\p{Alnum}")
+ public void testCharsetViaSystemProperty()
+ throws Exception
+ {
+ try (ByteArrayInputStream in3 = new ByteArrayInputStream("name=libell%E9".getBytes(StringUtil.__ISO_8859_1)))
+ {
+ MultiMap m3 = new MultiMap();
+ Charset nullCharset = null; // use the one from the system property
+ UrlEncoded.decodeTo(in3, m3, nullCharset, -1, -1);
+ assertEquals("libell\u00E9", m3.getString("name"), "stream name");
+ }
+ }
+
+ @Test
+ public void testUtf8()
+ throws Exception
+ {
+ UrlEncoded urlEncoded = new UrlEncoded();
+ assertEquals(0, urlEncoded.size(), "Empty");
+
+ urlEncoded.clear();
+ urlEncoded.decode("text=%E0%B8%9F%E0%B8%AB%E0%B8%81%E0%B8%A7%E0%B8%94%E0%B8%B2%E0%B9%88%E0%B8%81%E0%B8%9F%E0%B8%A7%E0%B8%AB%E0%B8%AA%E0%B8%94%E0%B8%B2%E0%B9%88%E0%B8%AB%E0%B8%9F%E0%B8%81%E0%B8%A7%E0%B8%94%E0%B8%AA%E0%B8%B2%E0%B8%9F%E0%B8%81%E0%B8%AB%E0%B8%A3%E0%B8%94%E0%B9%89%E0%B8%9F%E0%B8%AB%E0%B8%99%E0%B8%81%E0%B8%A3%E0%B8%94%E0%B8%B5&Action=Submit");
+
+ String hex = "E0B89FE0B8ABE0B881E0B8A7E0B894E0B8B2E0B988E0B881E0B89FE0B8A7E0B8ABE0B8AAE0B894E0B8B2E0B988E0B8ABE0B89FE0B881E0B8A7E0B894E0B8AAE0B8B2E0B89FE0B881E0B8ABE0B8A3E0B894E0B989E0B89FE0B8ABE0B899E0B881E0B8A3E0B894E0B8B5";
+ String expected = new String(TypeUtil.fromHexString(hex), "utf-8");
+ assertEquals(expected, urlEncoded.getString("text"));
+ }
+
+ @Test
+ public void testUtf8MultiByteCodePoint()
+ {
+ String input = "text=test%C3%A4";
+ UrlEncoded urlEncoded = new UrlEncoded();
+ urlEncoded.decode(input);
+
+ // http://www.ltg.ed.ac.uk/~richard/utf-8.cgi?input=00e4&mode=hex
+ // Should be "testä"
+ // "test" followed by a LATIN SMALL LETTER A WITH DIAERESIS
+
+ String expected = "test\u00e4";
+ assertThat(urlEncoded.getString("text"), is(expected));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/UptimeTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/UptimeTest.java
new file mode 100644
index 0000000..bad4410
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/UptimeTest.java
@@ -0,0 +1,31 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import org.junit.jupiter.api.Test;
+
+public class UptimeTest
+{
+ @Test
+ public void testUptime()
+ {
+ // should not throw an exception (if it does, the exception flows out and fails the testcase)
+ System.err.printf("Uptime = %,d%n", Uptime.getUptime());
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/UrlEncodedInvalidEncodingTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/UrlEncodedInvalidEncodingTest.java
new file mode 100644
index 0000000..b106fc6
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/UrlEncodedInvalidEncodingTest.java
@@ -0,0 +1,80 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class UrlEncodedInvalidEncodingTest
+{
+ public static Stream<Arguments> data()
+ {
+ ArrayList<Arguments> data = new ArrayList<>();
+ data.add(Arguments.of("Name=xx%zzyy", UTF_8, IllegalArgumentException.class));
+ data.add(Arguments.of("Name=%FF%FF%FF", UTF_8, Utf8Appendable.NotUtf8Exception.class));
+ data.add(Arguments.of("Name=%EF%EF%EF", UTF_8, Utf8Appendable.NotUtf8Exception.class));
+ data.add(Arguments.of("Name=%E%F%F", UTF_8, IllegalArgumentException.class));
+ data.add(Arguments.of("Name=x%", UTF_8, Utf8Appendable.NotUtf8Exception.class));
+ data.add(Arguments.of("Name=x%2", UTF_8, Utf8Appendable.NotUtf8Exception.class));
+ data.add(Arguments.of("Name=xxx%", UTF_8, Utf8Appendable.NotUtf8Exception.class));
+ data.add(Arguments.of("name=X%c0%afZ", UTF_8, Utf8Appendable.NotUtf8Exception.class));
+ return data.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testDecode(String inputString, Charset charset, Class<? extends Throwable> expectedThrowable)
+ {
+ assertThrows(expectedThrowable, () ->
+ {
+ UrlEncoded urlEncoded = new UrlEncoded();
+ urlEncoded.decode(inputString, charset);
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testDecodeUtf8ToMap(String inputString, Charset charset, Class<? extends Throwable> expectedThrowable)
+ {
+ assertThrows(expectedThrowable, () ->
+ {
+ MultiMap<String> map = new MultiMap<>();
+ UrlEncoded.decodeUtf8To(inputString, map);
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testDecodeTo(String inputString, Charset charset, Class<? extends Throwable> expectedThrowable)
+ {
+ assertThrows(expectedThrowable, () ->
+ {
+ MultiMap<String> map = new MultiMap<>();
+ UrlEncoded.decodeTo(inputString, map, charset);
+ });
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/UrlEncodedUtf8Test.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/UrlEncodedUtf8Test.java
new file mode 100644
index 0000000..d30821d
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/UrlEncodedUtf8Test.java
@@ -0,0 +1,122 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class UrlEncodedUtf8Test
+{
+
+ static final Logger LOG = Log.getLogger(UrlEncodedUtf8Test.class);
+
+ @Test
+ public void testIncompleteSequestAtTheEnd() throws Exception
+ {
+ byte[] bytes = {97, 98, 61, 99, -50};
+ String test = new String(bytes, StandardCharsets.UTF_8);
+ String expected = "c" + Utf8Appendable.REPLACEMENT;
+
+ fromString(test, test, "ab", expected, false);
+ fromInputStream(test, bytes, "ab", expected, false);
+ }
+
+ @Test
+ public void testIncompleteSequestAtTheEnd2() throws Exception
+ {
+ byte[] bytes = {97, 98, 61, -50};
+ String test = new String(bytes, StandardCharsets.UTF_8);
+ String expected = "" + Utf8Appendable.REPLACEMENT;
+
+ fromString(test, test, "ab", expected, false);
+ fromInputStream(test, bytes, "ab", expected, false);
+ }
+
+ @Test
+ public void testIncompleteSequestInName() throws Exception
+ {
+ byte[] bytes = {101, -50, 61, 102, 103, 38, 97, 98, 61, 99, 100};
+ String test = new String(bytes, StandardCharsets.UTF_8);
+ String name = "e" + Utf8Appendable.REPLACEMENT;
+ String value = "fg";
+
+ fromString(test, test, name, value, false);
+ fromInputStream(test, bytes, name, value, false);
+ }
+
+ @Test
+ public void testIncompleteSequestInValue() throws Exception
+ {
+ byte[] bytes = {101, 102, 61, 103, -50, 38, 97, 98, 61, 99, 100};
+ String test = new String(bytes, StandardCharsets.UTF_8);
+ String name = "ef";
+ String value = "g" + Utf8Appendable.REPLACEMENT;
+
+ fromString(test, test, name, value, false);
+ fromInputStream(test, bytes, name, value, false);
+ }
+
+ // TODO: Split thrown/not-thrown
+ static void fromString(String test, String s, String field, String expected, boolean thrown) throws Exception
+ {
+ MultiMap<String> values = new MultiMap<>();
+ try
+ {
+ UrlEncoded.decodeUtf8To(s, 0, s.length(), values);
+ if (thrown)
+ fail("Expected an exception");
+ assertThat(test, values.getString(field), is(expected));
+ }
+ catch (Exception e)
+ {
+ if (!thrown)
+ throw e;
+ LOG.ignore(e);
+ }
+ }
+
+ // TODO: Split thrown/not-thrown
+ static void fromInputStream(String test, byte[] b, String field, String expected, boolean thrown) throws Exception
+ {
+ InputStream is = new ByteArrayInputStream(b);
+ MultiMap<String> values = new MultiMap<>();
+ try
+ {
+ UrlEncoded.decodeUtf8To(is, values, 1000000, -1);
+ if (thrown)
+ fail("Expected an exception");
+ assertThat(test, values.getString(field), is(expected));
+ }
+ catch (Exception e)
+ {
+ if (!thrown)
+ throw e;
+ LOG.ignore(e);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/Utf8AppendableTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/Utf8AppendableTest.java
new file mode 100644
index 0000000..4cb8c79
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/Utf8AppendableTest.java
@@ -0,0 +1,324 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.DynamicTest.dynamicTest;
+
+// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+public class Utf8AppendableTest
+{
+ public static final List<Class<? extends Utf8Appendable>> APPENDABLE_IMPLS;
+
+ static
+ {
+ APPENDABLE_IMPLS = new ArrayList<>();
+ APPENDABLE_IMPLS.add(Utf8StringBuilder.class);
+ APPENDABLE_IMPLS.add(Utf8StringBuffer.class);
+ }
+
+ public static Stream<Arguments> implementations()
+ {
+ return APPENDABLE_IMPLS.stream().map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testUtf(Class<Utf8Appendable> impl) throws Exception
+ {
+ String source = "abcd012345\n\r\u0000\u00a4\u10fb\ufffdjetty";
+ byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
+ Utf8Appendable buffer = impl.getDeclaredConstructor().newInstance();
+ for (byte aByte : bytes)
+ {
+ buffer.append(aByte);
+ }
+ assertEquals(source, buffer.toString());
+ assertTrue(buffer.toString().endsWith("jetty"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testUtf8WithMissingByte(Class<Utf8Appendable> impl) throws Exception
+ {
+ assertThrows(IllegalArgumentException.class, () ->
+ {
+ String source = "abc\u10fb";
+ byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
+ Utf8Appendable buffer = impl.getDeclaredConstructor().newInstance();
+ for (int i = 0; i < bytes.length - 1; i++)
+ {
+ buffer.append(bytes[i]);
+ }
+ buffer.toString();
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testUtf8WithAdditionalByte(Class<Utf8Appendable> impl) throws Exception
+ {
+ assertThrows(Utf8Appendable.NotUtf8Exception.class, () ->
+ {
+ String source = "abcXX";
+ byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
+ bytes[3] = (byte)0xc0;
+ bytes[4] = (byte)0x00;
+
+ Utf8Appendable buffer = impl.getDeclaredConstructor().newInstance();
+ for (byte aByte : bytes)
+ {
+ buffer.append(aByte);
+ }
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testUTF32codes(Class<Utf8Appendable> impl) throws Exception
+ {
+ String source = "\uD842\uDF9F";
+ byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
+
+ String jvmcheck = new String(bytes, 0, bytes.length, StandardCharsets.UTF_8);
+ assertEquals(source, jvmcheck);
+
+ Utf8Appendable buffer = impl.getDeclaredConstructor().newInstance();
+ buffer.append(bytes, 0, bytes.length);
+ String result = buffer.toString();
+ assertEquals(source, result);
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testGermanUmlauts(Class<Utf8Appendable> impl) throws Exception
+ {
+ byte[] bytes = new byte[6];
+ bytes[0] = (byte)0xC3;
+ bytes[1] = (byte)0xBC;
+ bytes[2] = (byte)0xC3;
+ bytes[3] = (byte)0xB6;
+ bytes[4] = (byte)0xC3;
+ bytes[5] = (byte)0xA4;
+
+ Utf8Appendable buffer = impl.getDeclaredConstructor().newInstance();
+ for (int i = 0; i < bytes.length; i++)
+ {
+ buffer.append(bytes[i]);
+ }
+
+ assertEquals("\u00FC\u00F6\u00E4", buffer.toString());
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testInvalidUTF8(Class<Utf8Appendable> impl) throws UnsupportedEncodingException
+ {
+ assertThrows(Utf8Appendable.NotUtf8Exception.class, () ->
+ {
+ Utf8Appendable buffer = impl.getDeclaredConstructor().newInstance();
+ buffer.append((byte)0xC2);
+ buffer.append((byte)0xC2);
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testInvalidZeroUTF8(Class<Utf8Appendable> impl) throws UnsupportedEncodingException
+ {
+ // From https://datatracker.ietf.org/doc/html/rfc3629#section-10
+ assertThrows(Utf8Appendable.NotUtf8Exception.class, () ->
+ {
+ Utf8Appendable buffer = impl.getDeclaredConstructor().newInstance();
+ buffer.append((byte)0xC0);
+ buffer.append((byte)0x80);
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testInvalidAlternateDotEncodingUTF8(Class<Utf8Appendable> impl) throws UnsupportedEncodingException
+ {
+ // From https://datatracker.ietf.org/doc/html/rfc3629#section-10
+ assertThrows(Utf8Appendable.NotUtf8Exception.class, () ->
+ {
+ Utf8Appendable buffer = impl.getDeclaredConstructor().newInstance();
+ buffer.append((byte)0x2f);
+ buffer.append((byte)0xc0);
+ buffer.append((byte)0xae);
+ buffer.append((byte)0x2e);
+ buffer.append((byte)0x2f);
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testFastFail1(Class<Utf8Appendable> impl) throws Exception
+ {
+ byte[] part1 = TypeUtil.fromHexString("cebae1bdb9cf83cebcceb5");
+ byte[] part2 = TypeUtil.fromHexString("f4908080"); // INVALID
+ // Here for test tracking reasons, not needed to satisfy test
+ // byte[] part3 = TypeUtil.fromHexString("656469746564");
+
+ Utf8Appendable buffer = impl.getDeclaredConstructor().newInstance();
+ // Part 1 is valid
+ buffer.append(part1, 0, part1.length);
+
+ assertThrows(Utf8Appendable.NotUtf8Exception.class, () ->
+ {
+ // Part 2 is invalid
+ buffer.append(part2, 0, part2.length);
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testFastFail2(Class<Utf8Appendable> impl) throws Exception
+ {
+ byte[] part1 = TypeUtil.fromHexString("cebae1bdb9cf83cebcceb5f4");
+ byte[] part2 = TypeUtil.fromHexString("90"); // INVALID
+ // Here for test search/tracking reasons, not needed to satisfy test
+ // byte[] part3 = TypeUtil.fromHexString("8080656469746564");
+
+ Utf8Appendable buffer = impl.getDeclaredConstructor().newInstance();
+ // Part 1 is valid
+ buffer.append(part1, 0, part1.length);
+
+ assertThrows(Utf8Appendable.NotUtf8Exception.class, () ->
+ {
+ // Part 2 is invalid
+ buffer.append(part2, 0, part2.length);
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testPartialUnsplitCodepoint(Class<Utf8Appendable> impl) throws Exception
+ {
+ Utf8Appendable utf8 = impl.getDeclaredConstructor().newInstance();
+
+ String seq1 = "Hello-\uC2B5@\uC39F\uC3A4";
+ String seq2 = "\uC3BC\uC3A0\uC3A1-UTF-8!!";
+
+ utf8.append(BufferUtil.toBuffer(seq1, StandardCharsets.UTF_8));
+ String ret1 = utf8.takePartialString();
+
+ utf8.append(BufferUtil.toBuffer(seq2, StandardCharsets.UTF_8));
+ String ret2 = utf8.takePartialString();
+
+ assertThat("Seq1", ret1, is(seq1));
+ assertThat("Seq2", ret2, is(seq2));
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testPartialSplitCodepoint(Class<Utf8Appendable> impl) throws Exception
+ {
+ Utf8Appendable utf8 = impl.getDeclaredConstructor().newInstance();
+
+ String seq1 = "48656C6C6F2DEC8AB540EC8E9FEC8E";
+ String seq2 = "A4EC8EBCEC8EA0EC8EA12D5554462D382121";
+
+ utf8.append(TypeUtil.fromHexString(seq1));
+ String ret1 = utf8.takePartialString();
+
+ utf8.append(TypeUtil.fromHexString(seq2));
+ String ret2 = utf8.takePartialString();
+
+ assertThat("Seq1", ret1, is("Hello-\uC2B5@\uC39F"));
+ assertThat("Seq2", ret2, is("\uC3A4\uC3BC\uC3A0\uC3A1-UTF-8!!"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testPartialSplitCodepointWithNoBuf(Class<Utf8Appendable> impl) throws Exception
+ {
+ Utf8Appendable utf8 = impl.getDeclaredConstructor().newInstance();
+
+ String seq1 = "48656C6C6F2DEC8AB540EC8E9FEC8E";
+ String seq2 = "A4EC8EBCEC8EA0EC8EA12D5554462D382121";
+
+ utf8.append(TypeUtil.fromHexString(seq1));
+ String ret1 = utf8.takePartialString();
+
+ String ret2 = utf8.takePartialString();
+
+ utf8.append(TypeUtil.fromHexString(seq2));
+ String ret3 = utf8.takePartialString();
+
+ assertThat("Seq1", ret1, is("Hello-\uC2B5@\uC39F"));
+ assertThat("Seq2", ret2, is(""));
+ assertThat("Seq3", ret3, is("\uC3A4\uC3BC\uC3A0\uC3A1-UTF-8!!"));
+ }
+
+ @TestFactory
+ public Iterator<DynamicTest> testBadUtf8()
+ {
+ String[] samples = new String[]{
+ "c0af",
+ "EDA080",
+ "f08080af",
+ "f8808080af",
+ "e080af",
+ "F4908080",
+ "fbbfbfbfbf",
+ "10FFFF",
+ "CeBaE1BdB9Cf83CeBcCeB5EdA080656469746564",
+ // use of UTF-16 High Surrogates (in codepoint form)
+ "da07",
+ "d807",
+ // decoded UTF-16 High Surrogate "\ud807" (in UTF-8 form)
+ "EDA087"
+ };
+
+ List<DynamicTest> tests = new ArrayList<>();
+
+ for (Class<? extends Utf8Appendable> impl : APPENDABLE_IMPLS)
+ {
+ for (String hex : samples)
+ {
+ tests.add(dynamicTest(impl.getSimpleName() + " : " + hex, () ->
+ {
+ Utf8Appendable utf8 = impl.getDeclaredConstructor().newInstance();
+ assertThrows(NotUtf8Exception.class, () -> utf8.append(TypeUtil.fromHexString(hex)));
+ }));
+ }
+ }
+
+ return tests.iterator();
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/Utf8LineParserTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/Utf8LineParserTest.java
new file mode 100644
index 0000000..41cdc1c
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/Utf8LineParserTest.java
@@ -0,0 +1,191 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThan;
+
+public class Utf8LineParserTest
+{
+ private void appendUtf8(ByteBuffer buf, String line)
+ {
+ buf.put(ByteBuffer.wrap(StringUtil.getUtf8Bytes(line)));
+ }
+
+ private void assertEquals(List<String> expected, List<String> actual)
+ {
+ assertThat("Expected Line Count", actual.size(), is(expected.size()));
+ int len = expected.size();
+ for (int i = 0; i < len; i++)
+ {
+ String expectedLine = expected.get(i);
+ String actualLine = actual.get(i);
+
+ assertThat("Line[" + i + "]", actualLine, is(expectedLine));
+ }
+ }
+
+ /**
+ * Parse a basic line, with UNIX style line endings <code>"\n"</code>
+ */
+ @Test
+ public void testBasicParse()
+ {
+ ByteBuffer buf = ByteBuffer.allocate(64);
+ appendUtf8(buf, "Hello World\n");
+ BufferUtil.flipToFlush(buf, 0);
+
+ Utf8LineParser utfparser = new Utf8LineParser();
+
+ String line = utfparser.parse(buf);
+ assertThat("Line", line, is("Hello World"));
+ }
+
+ /**
+ * Parsing of a single line of HTTP header style line ending <code>"\r\n"</code>
+ */
+ @Test
+ public void testHttpLineParse()
+ {
+ ByteBuffer buf = ByteBuffer.allocate(64);
+ appendUtf8(buf, "Hello World\r\n");
+ BufferUtil.flipToFlush(buf, 0);
+
+ Utf8LineParser utfparser = new Utf8LineParser();
+
+ String line = utfparser.parse(buf);
+ assertThat("Line", line, is("Hello World"));
+ }
+
+ /**
+ * Parsing of an "in the wild" set HTTP response header lines.
+ */
+ @Test
+ public void testWildHttpRequestParse()
+ {
+ // Arbitrary Http Response Headers seen in the wild.
+ // Request URI -> http://www.eclipse.org/jetty/
+ List<String> expected = new ArrayList<>();
+ expected.add("HEAD /jetty/ HTTP/1.0");
+ expected.add("User-Agent: \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.6) Gecko/20060601 Firefox/2.0.0.6 (Ubuntu-feisty)\"");
+ expected.add("Accept: */*");
+ expected.add("Host: www.eclipse.org");
+ expected.add("Connection: Keep-Alive");
+ expected.add("");
+
+ // Prepare Buffer
+ ByteBuffer buf = ByteBuffer.allocate(512);
+ for (String line : expected)
+ {
+ appendUtf8(buf, line + "\r\n");
+ }
+
+ BufferUtil.flipToFlush(buf, 0);
+
+ // Parse Buffer
+ Utf8LineParser utfparser = new Utf8LineParser();
+
+ List<String> actual = new ArrayList<>();
+ int count = 0;
+ int excessive = expected.size() + 10; // fail-safe for bad code
+ boolean done = false;
+ while (!done)
+ {
+ String line = utfparser.parse(buf);
+ if (line != null)
+ {
+ actual.add(line);
+ }
+ else
+ {
+ done = true;
+ }
+ count++;
+ assertThat("Parse Count is excessive (bug in code!)", count, lessThan(excessive));
+ }
+
+ // Validate Results
+ assertEquals(expected, actual);
+ }
+
+ /**
+ * Parsing of an "in the wild" set HTTP response header lines.
+ */
+ @Test
+ public void testWildHttpResponseParse()
+ {
+ // Arbitrary Http Response Headers seen in the wild.
+ // Request URI -> https://ssl.google-analytics.com/__utm.gif
+ List<String> expected = new ArrayList<>();
+ expected.add("HTTP/1.0 200 OK");
+ expected.add("Date: Thu, 09 Aug 2012 16:16:39 GMT");
+ expected.add("Content-Length: 35");
+ expected.add("X-Content-Type-Options: nosniff");
+ expected.add("Pragma: no-cache");
+ expected.add("Expires: Wed, 19 Apr 2000 11:43:00 GMT");
+ expected.add("Last-Modified: Wed, 21 Jan 2004 19:51:30 GMT");
+ expected.add("Content-Type: image/gif");
+ expected.add("Cache-Control: private, no-cache, no-cache=Set-Cookie, proxy-revalidate");
+ expected.add("Age: 518097");
+ expected.add("Server: GFE/2.0");
+ expected.add("Connection: Keep-Alive");
+ expected.add("");
+
+ // Prepare Buffer
+ ByteBuffer buf = ByteBuffer.allocate(512);
+ for (String line : expected)
+ {
+ appendUtf8(buf, line + "\r\n");
+ }
+
+ BufferUtil.flipToFlush(buf, 0);
+
+ // Parse Buffer
+ Utf8LineParser utfparser = new Utf8LineParser();
+
+ List<String> actual = new ArrayList<>();
+ int count = 0;
+ int excessive = expected.size() + 10; // fail-safe for bad code
+ boolean done = false;
+ while (!done)
+ {
+ String line = utfparser.parse(buf);
+ if (line != null)
+ {
+ actual.add(line);
+ }
+ else
+ {
+ done = true;
+ }
+ count++;
+ assertThat("Parse Count is excessive (bug in code!)", count, lessThan(excessive));
+ }
+
+ // Validate Results
+ assertEquals(expected, actual);
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/component/ContainerLifeCycleTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/component/ContainerLifeCycleTest.java
new file mode 100644
index 0000000..9d003b1
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/component/ContainerLifeCycleTest.java
@@ -0,0 +1,741 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jetty.util.TypeUtil;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ContainerLifeCycleTest
+{
+ @Test
+ public void testStartStop() throws Exception
+ {
+ ContainerLifeCycle a0 = new ContainerLifeCycle();
+ TestContainerLifeCycle a1 = new TestContainerLifeCycle();
+ a0.addBean(a1);
+
+ a0.start();
+ assertEquals(1, a1.started.get());
+ assertEquals(0, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.start();
+ assertEquals(1, a1.started.get());
+ assertEquals(0, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.stop();
+ assertEquals(1, a1.started.get());
+ assertEquals(1, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.start();
+ assertEquals(2, a1.started.get());
+ assertEquals(1, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.stop();
+ assertEquals(2, a1.started.get());
+ assertEquals(2, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+ }
+
+ @Test
+ public void testStartStopDestroy() throws Exception
+ {
+ ContainerLifeCycle a0 = new ContainerLifeCycle();
+ TestContainerLifeCycle a1 = new TestContainerLifeCycle();
+
+ a0.start();
+ assertEquals(0, a1.started.get());
+ assertEquals(0, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.addBean(a1);
+ assertEquals(0, a1.started.get());
+ assertEquals(0, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+ assertFalse(a0.isManaged(a1));
+
+ a0.start();
+ assertEquals(0, a1.started.get());
+ assertEquals(0, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a1.start();
+ a0.manage(a1);
+ assertEquals(1, a1.started.get());
+ assertEquals(0, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.removeBean(a1);
+ assertEquals(1, a1.started.get());
+ assertEquals(1, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.stop();
+ a0.destroy();
+ assertEquals(1, a1.started.get());
+ assertEquals(1, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a1.stop();
+ assertEquals(1, a1.started.get());
+ assertEquals(1, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a1.destroy();
+ assertEquals(1, a1.started.get());
+ assertEquals(1, a1.stopped.get());
+ assertEquals(1, a1.destroyed.get());
+ }
+
+ @Test
+ public void testIllegalToStartAfterDestroy() throws Exception
+ {
+ ContainerLifeCycle container = new ContainerLifeCycle();
+ container.start();
+ container.stop();
+ container.destroy();
+
+ assertThrows(IllegalStateException.class, container::start);
+ }
+
+ @Test
+ public void testDisJoint() throws Exception
+ {
+ ContainerLifeCycle a0 = new ContainerLifeCycle();
+ TestContainerLifeCycle a1 = new TestContainerLifeCycle();
+
+ // Start the a1 bean before adding, makes it auto disjoint
+ a1.start();
+
+ // Now add it
+ a0.addBean(a1);
+ assertFalse(a0.isManaged(a1));
+
+ a0.start();
+ assertEquals(1, a1.started.get());
+ assertEquals(0, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.start();
+ assertEquals(1, a1.started.get());
+ assertEquals(0, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.stop();
+ assertEquals(1, a1.started.get());
+ assertEquals(0, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a1.stop();
+ assertEquals(1, a1.started.get());
+ assertEquals(1, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.start();
+ assertEquals(1, a1.started.get());
+ assertEquals(1, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.manage(a1);
+ assertTrue(a0.isManaged(a1));
+
+ a0.stop();
+ assertEquals(1, a1.started.get());
+ assertEquals(1, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.start();
+ assertEquals(2, a1.started.get());
+ assertEquals(1, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.stop();
+ assertEquals(2, a1.started.get());
+ assertEquals(2, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a0.unmanage(a1);
+ assertFalse(a0.isManaged(a1));
+
+ a0.destroy();
+ assertEquals(2, a1.started.get());
+ assertEquals(2, a1.stopped.get());
+ assertEquals(0, a1.destroyed.get());
+
+ a1.destroy();
+ assertEquals(2, a1.started.get());
+ assertEquals(2, a1.stopped.get());
+ assertEquals(1, a1.destroyed.get());
+ }
+
+ @Test
+ public void testDumpable() throws Exception
+ {
+ ContainerLifeCycle a0 = new ContainerLifeCycle();
+ String dump = trim(a0.dump());
+ check(dump, "ContainerLifeCycl");
+
+ ContainerLifeCycle aa0 = new ContainerLifeCycle();
+ a0.addBean(aa0);
+ dump = trim(a0.dump());
+ dump = check(dump, "ContainerLifeCycl");
+ check(dump, "+? ContainerLife");
+
+ ContainerLifeCycle aa1 = new ContainerLifeCycle();
+ a0.addBean(aa1);
+ dump = trim(a0.dump());
+ dump = check(dump, "ContainerLifeCycl");
+ dump = check(dump, "+? ContainerLife");
+ dump = check(dump, "+? ContainerLife");
+ check(dump, "");
+
+ ContainerLifeCycle aa2 = new ContainerLifeCycle();
+ a0.addBean(aa2, false);
+ dump = trim(a0.dump());
+ dump = check(dump, "ContainerLifeCycl");
+ dump = check(dump, "+? ContainerLife");
+ dump = check(dump, "+? ContainerLife");
+ dump = check(dump, "+~ ContainerLife");
+ check(dump, "");
+
+ aa1.start();
+ a0.start();
+ dump = trim(a0.dump());
+ dump = check(dump, "ContainerLifeCycl");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, "+~ ContainerLife");
+ dump = check(dump, "+~ ContainerLife");
+ check(dump, "");
+
+ a0.manage(aa1);
+ a0.removeBean(aa2);
+ dump = trim(a0.dump());
+ dump = check(dump, "ContainerLifeCycl");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, "+= ContainerLife");
+ check(dump, "");
+
+ ContainerLifeCycle aaa0 = new ContainerLifeCycle();
+ aa0.addBean(aaa0);
+ dump = trim(a0.dump());
+ dump = check(dump, "ContainerLifeCycl");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, "| +~ Container");
+ dump = check(dump, "+= ContainerLife");
+ check(dump, "");
+
+ ContainerLifeCycle aa10 = new ContainerLifeCycle();
+ aa1.addBean(aa10, true);
+ dump = trim(a0.dump());
+ dump = check(dump, "ContainerLifeCycl");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, "| +~ Container");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, " += Container");
+ check(dump, "");
+
+ final ContainerLifeCycle a1 = new ContainerLifeCycle();
+ final ContainerLifeCycle a2 = new ContainerLifeCycle();
+ final ContainerLifeCycle a3 = new ContainerLifeCycle();
+ final ContainerLifeCycle a4 = new ContainerLifeCycle();
+
+ ContainerLifeCycle aa = new ContainerLifeCycle()
+ {
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ Dumpable.dumpObjects(out, indent, this.toString(), TypeUtil.asList(new Object[]{
+ a1, a2
+ }), TypeUtil.asList(new Object[]{a3, a4}));
+ }
+ };
+ a0.addBean(aa, true);
+
+ dump = trim(a0.dump());
+ dump = check(dump, "ContainerLifeCycl");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, "| +~ Container");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, "| += Container");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, " +> java.util.Arrays$ArrayList");
+ dump = check(dump, " | +: ContainerLifeCycle");
+ dump = check(dump, " | +: ContainerLifeCycle");
+ dump = check(dump, " +> java.util.Arrays$ArrayList");
+ dump = check(dump, " +: ContainerLifeCycle");
+ dump = check(dump, " +: ContainerLifeCycle");
+ check(dump, "");
+
+ a2.addBean(aa0, true);
+ dump = trim(a0.dump());
+ dump = check(dump, "ContainerLifeCycl");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, "| +~ Container");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, "| += Container");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, " +> java.util.Arrays$ArrayList");
+ dump = check(dump, " | +: ContainerLifeCycle");
+ dump = check(dump, " | +: ContainerLifeCycle");
+ dump = check(dump, " | += Conta");
+ dump = check(dump, " | +~ C");
+ dump = check(dump, " +> java.util.Arrays$ArrayList");
+ dump = check(dump, " +: ContainerLifeCycle");
+ dump = check(dump, " +: ContainerLifeCycle");
+ check(dump, "");
+
+ a2.unmanage(aa0);
+ dump = trim(a0.dump());
+ dump = check(dump, "ContainerLifeCycl");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, "| +~ Container");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, "| += Container");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, " +> java.util.Arrays$ArrayList");
+ dump = check(dump, " | +: ContainerLifeCycle");
+ dump = check(dump, " | +: ContainerLifeCycle");
+ dump = check(dump, " | +~ Conta");
+ dump = check(dump, " +> java.util.Arrays$ArrayList");
+ dump = check(dump, " +: ContainerLifeCycle");
+ dump = check(dump, " +: ContainerLifeCycle");
+ check(dump, "");
+
+ a0.unmanage(aa);
+ dump = trim(a0.dump());
+ dump = check(dump, "ContainerLifeCycl");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, "| +~ Container");
+ dump = check(dump, "+= ContainerLife");
+ dump = check(dump, "| += Container");
+ dump = check(dump, "+~ ContainerLife");
+ check(dump, "");
+ }
+
+ @Test
+ public void listenerTest() throws Exception
+ {
+ final Queue<String> handled = new ConcurrentLinkedQueue<>();
+ final Queue<String> operation = new ConcurrentLinkedQueue<>();
+ final Queue<Container> parent = new ConcurrentLinkedQueue<>();
+ final Queue<Object> child = new ConcurrentLinkedQueue<>();
+
+ Container.Listener listener = new Container.Listener()
+ {
+ @Override
+ public void beanRemoved(Container p, Object c)
+ {
+ handled.add(toString());
+ operation.add("removed");
+ parent.add(p);
+ child.add(c);
+ }
+
+ @Override
+ public void beanAdded(Container p, Object c)
+ {
+ handled.add(toString());
+ operation.add("added");
+ parent.add(p);
+ child.add(c);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "listener";
+ }
+ };
+
+ ContainerLifeCycle c0 = new ContainerLifeCycle()
+ {
+ @Override
+ public String toString()
+ {
+ return "c0";
+ }
+ };
+ ContainerLifeCycle c00 = new ContainerLifeCycle()
+ {
+ @Override
+ public String toString()
+ {
+ return "c00";
+ }
+ };
+ c0.addBean(c00);
+ String b000 = "b000";
+ c00.addBean(b000);
+
+ c0.addBean(listener);
+
+ assertEquals("listener", handled.poll());
+ assertEquals("added", operation.poll());
+ assertEquals(c0, parent.poll());
+ assertEquals(c00, child.poll());
+
+ assertEquals("listener", handled.poll());
+ assertEquals("added", operation.poll());
+ assertEquals(c0, parent.poll());
+ assertEquals(listener, child.poll());
+
+ Container.InheritedListener inherited = new Container.InheritedListener()
+ {
+ @Override
+ public void beanRemoved(Container p, Object c)
+ {
+ handled.add(toString());
+ operation.add("removed");
+ parent.add(p);
+ child.add(c);
+ }
+
+ @Override
+ public void beanAdded(Container p, Object c)
+ {
+ handled.add(toString());
+ operation.add("added");
+ parent.add(p);
+ child.add(c);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "inherited";
+ }
+ };
+
+ c0.addBean(inherited);
+
+ assertEquals("inherited", handled.poll());
+ assertEquals("added", operation.poll());
+ assertEquals(c0, parent.poll());
+ assertEquals(c00, child.poll());
+
+ assertEquals("inherited", handled.poll());
+ assertEquals("added", operation.poll());
+ assertEquals(c0, parent.poll());
+ assertEquals(listener, child.poll());
+
+ assertEquals("listener", handled.poll());
+ assertEquals("added", operation.poll());
+ assertEquals(c0, parent.poll());
+ assertEquals(inherited, child.poll());
+
+ assertEquals("inherited", handled.poll());
+ assertEquals("added", operation.poll());
+ assertEquals(c0, parent.poll());
+ assertEquals(inherited, child.poll());
+
+ c0.start();
+
+ assertEquals("inherited", handled.poll());
+ assertEquals("added", operation.poll());
+ assertEquals(c00, parent.poll());
+ assertEquals(b000, child.poll());
+
+ assertEquals("inherited", handled.poll());
+ assertEquals("added", operation.poll());
+ assertEquals(c00, parent.poll());
+ assertEquals(inherited, child.poll());
+
+ c0.removeBean(c00);
+
+ assertEquals("inherited", handled.poll());
+ assertEquals("removed", operation.poll());
+ assertEquals(c00, parent.poll());
+ assertEquals(inherited, child.poll());
+
+ assertEquals("inherited", handled.poll());
+ assertEquals("removed", operation.poll());
+ assertEquals(c00, parent.poll());
+ assertEquals(b000, child.poll());
+
+ assertEquals("listener", handled.poll());
+ assertEquals("removed", operation.poll());
+ assertEquals(c0, parent.poll());
+ assertEquals(c00, child.poll());
+
+ assertEquals("inherited", handled.poll());
+ assertEquals("removed", operation.poll());
+ assertEquals(c0, parent.poll());
+ assertEquals(c00, child.poll());
+ }
+
+ private static final class InheritedListenerLifeCycle extends AbstractLifeCycle implements Container.InheritedListener
+ {
+ @Override
+ public void beanRemoved(Container p, Object c)
+ {
+ }
+
+ @Override
+ public void beanAdded(Container p, Object c)
+ {
+ }
+
+ @Override
+ public String toString()
+ {
+ return "inherited";
+ }
+ }
+
+ @Test
+ public void testInheritedListener() throws Exception
+ {
+ ContainerLifeCycle c0 = new ContainerLifeCycle()
+ {
+ @Override
+ public String toString()
+ {
+ return "c0";
+ }
+ };
+ ContainerLifeCycle c00 = new ContainerLifeCycle()
+ {
+ @Override
+ public String toString()
+ {
+ return "c00";
+ }
+ };
+ ContainerLifeCycle c01 = new ContainerLifeCycle()
+ {
+ @Override
+ public String toString()
+ {
+ return "c01";
+ }
+ };
+ Container.InheritedListener inherited = new InheritedListenerLifeCycle();
+
+ c0.addBean(c00);
+ c0.start();
+ c0.addBean(inherited);
+ c0.manage(inherited);
+ c0.addBean(c01);
+ c01.start();
+ c0.manage(c01);
+
+ assertTrue(c0.isManaged(inherited));
+ assertFalse(c00.isManaged(inherited));
+ assertFalse(c01.isManaged(inherited));
+ }
+
+ String trim(String s) throws IOException
+ {
+ StringBuilder b = new StringBuilder();
+ BufferedReader reader = new BufferedReader(new StringReader(s));
+
+ for (String line = reader.readLine(); line != null; line = reader.readLine())
+ {
+ if (line.length() > 50)
+ line = line.substring(0, 50);
+ b.append(line).append('\n');
+ }
+
+ return b.toString();
+ }
+
+ String check(String s, String x)
+ {
+ String r = s;
+ int nl = s.indexOf('\n');
+ if (nl > 0)
+ {
+ r = s.substring(nl + 1);
+ s = s.substring(0, nl);
+ }
+
+ assertThat(s, Matchers.startsWith(x));
+
+ return r;
+ }
+
+ private static class TestContainerLifeCycle extends ContainerLifeCycle
+ {
+ private final AtomicInteger destroyed = new AtomicInteger();
+ private final AtomicInteger started = new AtomicInteger();
+ private final AtomicInteger stopped = new AtomicInteger();
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ started.incrementAndGet();
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ stopped.incrementAndGet();
+ super.doStop();
+ }
+
+ @Override
+ public void destroy()
+ {
+ destroyed.incrementAndGet();
+ super.destroy();
+ }
+ }
+
+ @Test
+ public void testGetBeans()
+ {
+ TestContainerLifeCycle root = new TestContainerLifeCycle();
+ TestContainerLifeCycle left = new TestContainerLifeCycle();
+ root.addBean(left);
+ TestContainerLifeCycle right = new TestContainerLifeCycle();
+ root.addBean(right);
+ TestContainerLifeCycle leaf = new TestContainerLifeCycle();
+ right.addBean(leaf);
+
+ Integer zero = 0;
+ Integer one = 1;
+ Integer two = 2;
+ Integer three = 3;
+ Integer four = 4;
+ root.addBean(zero);
+ root.addBean(one);
+ left.addBean(two);
+ right.addBean(three);
+ leaf.addBean(four);
+ leaf.addBean("leaf");
+
+ assertThat(root.getBeans(Container.class), containsInAnyOrder(left, right));
+ assertThat(root.getBeans(Integer.class), containsInAnyOrder(zero, one));
+ assertThat(root.getBeans(String.class), containsInAnyOrder());
+
+ assertThat(root.getContainedBeans(Container.class), containsInAnyOrder(left, right, leaf));
+ assertThat(root.getContainedBeans(Integer.class), containsInAnyOrder(zero, one, two, three, four));
+ assertThat(root.getContainedBeans(String.class), containsInAnyOrder("leaf"));
+ }
+
+ @Test
+ public void testBeanStoppingAddedToStartingBean() throws Exception
+ {
+ ContainerLifeCycle longLived = new ContainerLifeCycle()
+ {
+ @Override
+ protected void doStop() throws Exception
+ {
+ super.doStop();
+
+ ContainerLifeCycle shortLived = new ContainerLifeCycle();
+ shortLived.addBean(this);
+ shortLived.start();
+
+ assertTrue(shortLived.isStarted());
+ assertTrue(isStopping());
+ assertFalse(shortLived.isManaged(this));
+ }
+ };
+ longLived.start();
+ longLived.stop();
+ }
+
+ @Test
+ public void testFailedManagedBeanCanBeRestarted() throws Exception
+ {
+ AtomicBoolean fail = new AtomicBoolean();
+ ContainerLifeCycle container = new ContainerLifeCycle();
+ ContainerLifeCycle bean1 = new ContainerLifeCycle();
+ ContainerLifeCycle bean2 = new ContainerLifeCycle()
+ {
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+ // Fail only the first time.
+ if (fail.compareAndSet(false, true))
+ throw new RuntimeException();
+ }
+ };
+ ContainerLifeCycle bean3 = new ContainerLifeCycle();
+ container.addBean(bean1);
+ container.addBean(bean2);
+ container.addBean(bean3);
+
+ // Start the first time, it should fail.
+ assertThrows(RuntimeException.class, container::start);
+ assertTrue(container.isFailed());
+ assertTrue(bean1.isStopped());
+ assertTrue(bean2.isFailed());
+ assertTrue(bean3.isStopped());
+
+ // Re-start, it should succeed.
+ container.start();
+ assertTrue(container.isStarted());
+ assertTrue(bean1.isStarted());
+ assertTrue(bean2.isStarted());
+ assertTrue(bean3.isStarted());
+ }
+
+ @Test
+ public void testFailedAutoBeanIsNotRestarted() throws Exception
+ {
+ AtomicBoolean fail = new AtomicBoolean();
+ ContainerLifeCycle bean = new ContainerLifeCycle()
+ {
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+ // Fail only the first time.
+ if (fail.compareAndSet(false, true))
+ throw new RuntimeException();
+ }
+ };
+ // The bean is started externally and fails.
+ assertThrows(RuntimeException.class, bean::start);
+
+ // The same bean now becomes part of a container.
+ ContainerLifeCycle container = new ContainerLifeCycle();
+ container.addBean(bean);
+ assertTrue(container.isAuto(bean));
+
+ // Start the container, the bean must not be managed.
+ container.start();
+ assertTrue(container.isStarted());
+ assertTrue(bean.isFailed());
+ assertTrue(container.isUnmanaged(bean));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/component/DumpableTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/component/DumpableTest.java
new file mode 100644
index 0000000..fd30f84
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/component/DumpableTest.java
@@ -0,0 +1,53 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class DumpableTest
+{
+ @Test
+ public void testNullDumpableCollection() throws Exception
+ {
+ DumpableCollection dc = new DumpableCollection("null test", null);
+ String dump = dc.dump();
+ assertThat(dump, Matchers.containsString("size=0"));
+ }
+
+ @Test
+ public void testNonNullDumpableCollection() throws Exception
+ {
+ Collection<String> collection = new ArrayList<>();
+ collection.add("one");
+ collection.add("two");
+ collection.add("three");
+
+ DumpableCollection dc = new DumpableCollection("non null test", collection);
+ String dump = dc.dump();
+ assertThat(dump, Matchers.containsString("one"));
+ assertThat(dump, Matchers.containsString("two"));
+ assertThat(dump, Matchers.containsString("three"));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/component/LifeCycleListenerNestedTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/component/LifeCycleListenerNestedTest.java
new file mode 100644
index 0000000..b8a8a5f
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/component/LifeCycleListenerNestedTest.java
@@ -0,0 +1,289 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Testing for LifeCycleListener events on nested components
+ * during runtime.
+ */
+@Disabled
+public class LifeCycleListenerNestedTest
+{
+ // Set this true to use test-specific workaround.
+ private final boolean workaround = false;
+
+ public static class Foo extends ContainerLifeCycle
+ {
+ @Override
+ public String toString()
+ {
+ return Foo.class.getSimpleName();
+ }
+ }
+
+ public static class Bar extends ContainerLifeCycle
+ {
+ private final String id;
+
+ public Bar(String id)
+ {
+ this.id = id;
+ }
+
+ public String getId()
+ {
+ return id;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Bar other = (Bar)obj;
+ if (id == null)
+ {
+ if (other.id != null)
+ return false;
+ }
+ else if (!id.equals(other.id))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString()
+ {
+ return Bar.class.getSimpleName() + "(" + id + ")";
+ }
+ }
+
+ public static enum LifeCycleEvent
+ {
+ STARTING,
+ STARTED,
+ FAILURE,
+ STOPPING,
+ STOPPED
+ }
+
+ public static class CapturingListener implements LifeCycle.Listener, Container.InheritedListener
+ {
+ private List<String> events = new ArrayList<>();
+
+ private void addEvent(Object obj, LifeCycleEvent event)
+ {
+ events.add(String.format("%s - %s", obj.toString(), event.name()));
+ }
+
+ @Override
+ public void lifeCycleStarting(LifeCycle event)
+ {
+ addEvent(event, LifeCycleEvent.STARTING);
+ }
+
+ @Override
+ public void lifeCycleStarted(LifeCycle event)
+ {
+ addEvent(event, LifeCycleEvent.STARTED);
+ }
+
+ @Override
+ public void lifeCycleFailure(LifeCycle event, Throwable cause)
+ {
+ addEvent(event, LifeCycleEvent.FAILURE);
+ }
+
+ @Override
+ public void lifeCycleStopping(LifeCycle event)
+ {
+ addEvent(event, LifeCycleEvent.STOPPING);
+ }
+
+ @Override
+ public void lifeCycleStopped(LifeCycle event)
+ {
+ addEvent(event, LifeCycleEvent.STOPPED);
+ }
+
+ public List<String> getEvents()
+ {
+ return events;
+ }
+
+ public void assertEvents(Matcher<Iterable<? super String>> matcher)
+ {
+ assertThat(events, matcher);
+ }
+
+ @Override
+ public void beanAdded(Container parent, Object child)
+ {
+ if (child instanceof LifeCycle)
+ {
+ ((LifeCycle)child).addLifeCycleListener(this);
+ }
+ }
+
+ @Override
+ public void beanRemoved(Container parent, Object child)
+ {
+ if (child instanceof LifeCycle)
+ {
+ ((LifeCycle)child).removeLifeCycleListener(this);
+ }
+ }
+ }
+
+ @Test
+ public void testAddBeanAddListenerStart() throws Exception
+ {
+ Foo foo = new Foo();
+ Bar bara = new Bar("a");
+ Bar barb = new Bar("b");
+ foo.addBean(bara);
+ foo.addBean(barb);
+
+ CapturingListener listener = new CapturingListener();
+ foo.addLifeCycleListener(listener);
+ if (workaround)
+ foo.addEventListener(listener);
+
+ try
+ {
+ foo.start();
+
+ assertThat("Foo.started", foo.isStarted(), is(true));
+ assertThat("Bar(a).started", bara.isStarted(), is(true));
+ assertThat("Bar(b).started", barb.isStarted(), is(true));
+
+ listener.assertEvents(hasItem("Foo - STARTING"));
+ listener.assertEvents(hasItem("Foo - STARTED"));
+ listener.assertEvents(hasItem("Bar(a) - STARTING"));
+ listener.assertEvents(hasItem("Bar(a) - STARTED"));
+ listener.assertEvents(hasItem("Bar(b) - STARTING"));
+ listener.assertEvents(hasItem("Bar(b) - STARTED"));
+ }
+ finally
+ {
+ foo.stop();
+ }
+ }
+
+ @Test
+ public void testAddListenerAddBeanStart() throws Exception
+ {
+ Foo foo = new Foo();
+
+ CapturingListener listener = new CapturingListener();
+ foo.addLifeCycleListener(listener);
+ if (workaround)
+ foo.addEventListener(listener);
+
+ Bar bara = new Bar("a");
+ Bar barb = new Bar("b");
+ foo.addBean(bara);
+ foo.addBean(barb);
+
+ try
+ {
+ foo.start();
+
+ assertThat("Foo.started", foo.isStarted(), is(true));
+ assertThat("Bar(a).started", bara.isStarted(), is(true));
+ assertThat("Bar(b).started", barb.isStarted(), is(true));
+
+ listener.assertEvents(hasItem("Foo - STARTING"));
+ listener.assertEvents(hasItem("Foo - STARTED"));
+ listener.assertEvents(hasItem("Bar(a) - STARTING"));
+ listener.assertEvents(hasItem("Bar(a) - STARTED"));
+ listener.assertEvents(hasItem("Bar(b) - STARTING"));
+ listener.assertEvents(hasItem("Bar(b) - STARTED"));
+ }
+ finally
+ {
+ foo.stop();
+ }
+ }
+
+ @Test
+ public void testAddListenerStartAddBean() throws Exception
+ {
+ Foo foo = new Foo();
+ Bar bara = new Bar("a");
+ Bar barb = new Bar("b");
+
+ CapturingListener listener = new CapturingListener();
+ foo.addLifeCycleListener(listener);
+ if (workaround)
+ foo.addEventListener(listener);
+
+ try
+ {
+ foo.start();
+
+ listener.assertEvents(hasItem("Foo - STARTING"));
+ listener.assertEvents(hasItem("Foo - STARTED"));
+
+ foo.addBean(bara);
+ foo.addBean(barb);
+
+ bara.start();
+ barb.start();
+
+ assertThat("Bar(a).started", bara.isStarted(), is(true));
+ assertThat("Bar(b).started", barb.isStarted(), is(true));
+
+ listener.assertEvents(hasItem("Bar(a) - STARTING"));
+ listener.assertEvents(hasItem("Bar(a) - STARTED"));
+ listener.assertEvents(hasItem("Bar(b) - STARTING"));
+ listener.assertEvents(hasItem("Bar(b) - STARTED"));
+ }
+ finally
+ {
+ barb.stop();
+ bara.stop();
+ foo.stop();
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/component/LifeCycleListenerTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/component/LifeCycleListenerTest.java
new file mode 100644
index 0000000..dd0ef12
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/component/LifeCycleListenerTest.java
@@ -0,0 +1,231 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.component;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class LifeCycleListenerTest
+{
+ static Exception cause = new Exception("expected test exception");
+
+ @Test
+ public void testStart() throws Exception
+ {
+ TestLifeCycle lifecycle = new TestLifeCycle();
+ TestListener listener = new TestListener();
+ lifecycle.addLifeCycleListener(listener);
+
+ lifecycle.setCause(cause);
+
+ try (StacklessLogging stackless = new StacklessLogging(AbstractLifeCycle.class))
+ {
+ lifecycle.start();
+ assertTrue(false);
+ }
+ catch (Exception e)
+ {
+ assertEquals(cause, e);
+ assertEquals(cause, listener.getCause());
+ }
+ lifecycle.setCause(null);
+
+ lifecycle.start();
+
+ // check that the starting event has been thrown
+ assertTrue(listener.starting, "The staring event didn't occur");
+
+ // check that the started event has been thrown
+ assertTrue(listener.started, "The started event didn't occur");
+
+ // check that the starting event occurs before the started event
+ assertTrue(listener.startingTime <= listener.startedTime, "The starting event must occur before the started event");
+
+ // check that the lifecycle's state is started
+ assertTrue(lifecycle.isStarted(), "The lifecycle state is not started");
+ }
+
+ @Test
+ public void testStop() throws Exception
+ {
+ TestLifeCycle lifecycle = new TestLifeCycle();
+ TestListener listener = new TestListener();
+ lifecycle.addLifeCycleListener(listener);
+
+ // need to set the state to something other than stopped or stopping or
+ // else
+ // stop() will return without doing anything
+
+ lifecycle.start();
+ lifecycle.setCause(cause);
+
+ try (StacklessLogging stackless = new StacklessLogging(AbstractLifeCycle.class))
+ {
+ lifecycle.stop();
+ assertTrue(false);
+ }
+ catch (Exception e)
+ {
+ assertEquals(cause, e);
+ assertEquals(cause, listener.getCause());
+ }
+
+ lifecycle.setCause(null);
+
+ lifecycle.stop();
+
+ // check that the stopping event has been thrown
+ assertTrue(listener.stopping, "The stopping event didn't occur");
+
+ // check that the stopped event has been thrown
+ assertTrue(listener.stopped, "The stopped event didn't occur");
+
+ // check that the stopping event occurs before the stopped event
+ assertTrue(listener.stoppingTime <= listener.stoppedTime, "The stopping event must occur before the stopped event");
+ // System.out.println("STOPING TIME : " + listener.stoppingTime + " : " + listener.stoppedTime);
+
+ // check that the lifecycle's state is stopped
+ assertTrue(lifecycle.isStopped(), "The lifecycle state is not stooped");
+ }
+
+ @Test
+ public void testRemoveLifecycleListener()
+ throws Exception
+ {
+ TestLifeCycle lifecycle = new TestLifeCycle();
+ TestListener listener = new TestListener();
+ lifecycle.addLifeCycleListener(listener);
+
+ lifecycle.start();
+ assertTrue(listener.starting, "The starting event didn't occur");
+ lifecycle.removeLifeCycleListener(listener);
+ lifecycle.stop();
+ assertFalse(listener.stopping, "The stopping event occurred");
+ }
+
+ private static class TestLifeCycle extends AbstractLifeCycle
+ {
+ Exception cause;
+
+ private TestLifeCycle()
+ {
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (cause != null)
+ throw cause;
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ if (cause != null)
+ throw cause;
+ super.doStop();
+ }
+
+ public void setCause(Exception e)
+ {
+ cause = e;
+ }
+ }
+
+ private class TestListener extends AbstractLifeCycle.AbstractLifeCycleListener
+ {
+ @SuppressWarnings("unused")
+ private boolean failure = false;
+ private boolean started = false;
+ private boolean starting = false;
+ private boolean stopped = false;
+ private boolean stopping = false;
+
+ private long startedTime;
+ private long startingTime;
+ private long stoppedTime;
+ private long stoppingTime;
+
+ private Throwable cause = null;
+
+ public void lifeCycleFailure(LifeCycle event, Throwable cause)
+ {
+ failure = true;
+ this.cause = cause;
+ }
+
+ public Throwable getCause()
+ {
+ return cause;
+ }
+
+ public void lifeCycleStarted(LifeCycle event)
+ {
+ started = true;
+ startedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ }
+
+ public void lifeCycleStarting(LifeCycle event)
+ {
+ starting = true;
+ startingTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+
+ // need to sleep to make sure the starting and started times are not
+ // the same
+ try
+ {
+ Thread.sleep(1);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ public void lifeCycleStopped(LifeCycle event)
+ {
+ stopped = true;
+ stoppedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ }
+
+ public void lifeCycleStopping(LifeCycle event)
+ {
+ stopping = true;
+ stoppingTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+
+ // need to sleep to make sure the stopping and stopped times are not
+ // the same
+ try
+ {
+ Thread.sleep(1);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/Blue.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/Blue.java
new file mode 100644
index 0000000..3549f2f
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/Blue.java
@@ -0,0 +1,32 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+public class Blue
+{
+ private static final Logger LOG = Log.getLogger(Blue.class);
+
+ public void generateLogs()
+ {
+ LOG.debug("My color is {}", Blue.class.getSimpleName());
+ LOG.info("I represent the emotion Admiration");
+ LOG.warn("I can also mean Disgust");
+ LOG.ignore(new RuntimeException("Yawn"));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/CapturingJULHandler.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/CapturingJULHandler.java
new file mode 100644
index 0000000..cc02621
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/CapturingJULHandler.java
@@ -0,0 +1,87 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.logging.Handler;
+import java.util.logging.LogRecord;
+
+import org.eclipse.jetty.util.IO;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+
+public class CapturingJULHandler extends Handler
+{
+ private static final String LN = System.getProperty("line.separator");
+ private StringBuilder output = new StringBuilder();
+
+ @Override
+ public void publish(LogRecord record)
+ {
+ StringBuilder buf = new StringBuilder();
+ buf.append(record.getLevel().getName()).append("|");
+ buf.append(record.getLoggerName()).append("|");
+ buf.append(record.getMessage());
+
+ output.append(buf);
+ if (record.getMessage().length() > 0)
+ {
+ output.append(LN);
+ }
+
+ if (record.getThrown() != null)
+ {
+ StringWriter sw = new StringWriter(128);
+ PrintWriter capture = new PrintWriter(sw);
+ record.getThrown().printStackTrace(capture);
+ capture.flush();
+ output.append(sw.toString());
+ IO.close(capture);
+ }
+ }
+
+ public void clear()
+ {
+ output.setLength(0);
+ }
+
+ @Override
+ public void flush()
+ {
+ /* do nothing */
+ }
+
+ @Override
+ public void close() throws SecurityException
+ {
+ /* do nothing */
+ }
+
+ public void dump()
+ {
+ System.out.println(output);
+ }
+
+ public void assertContainsLine(String line)
+ {
+ assertThat(output.toString(), containsString(line));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/Green.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/Green.java
new file mode 100644
index 0000000..bee5f75
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/Green.java
@@ -0,0 +1,32 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+public class Green
+{
+ private static final Logger LOG = Log.getLogger(Green.class);
+
+ public void generateLogs()
+ {
+ LOG.debug("My color is {}", Green.class.getSimpleName());
+ LOG.info("I represent the emotion Trust");
+ LOG.warn("I can also mean Fear");
+ LOG.ignore(new RuntimeException("Ick"));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/JavaUtilLogTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/JavaUtilLogTest.java
new file mode 100644
index 0000000..96f81b8
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/JavaUtilLogTest.java
@@ -0,0 +1,243 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class JavaUtilLogTest
+{
+ private static Handler[] originalHandlers;
+ private static CapturingJULHandler jul;
+
+ @BeforeAll
+ public static void setJUL()
+ {
+ LogManager lmgr = LogManager.getLogManager();
+ java.util.logging.Logger root = lmgr.getLogger("");
+ // Remember original handlers
+ originalHandlers = root.getHandlers();
+ // Remove original handlers
+ for (Handler existing : originalHandlers)
+ {
+ root.removeHandler(existing);
+ }
+ // Set test/capturing handler
+ jul = new CapturingJULHandler();
+ root.addHandler(jul);
+ }
+
+ @AfterAll
+ public static void restoreJUL()
+ {
+ LogManager lmgr = LogManager.getLogManager();
+ java.util.logging.Logger root = lmgr.getLogger("");
+ // Remove test handlers
+ for (Handler existing : root.getHandlers())
+ {
+ root.removeHandler(existing);
+ }
+ // Restore original handlers
+ for (Handler original : originalHandlers)
+ {
+ root.addHandler(original);
+ }
+ }
+
+ @Test
+ public void testNamedLogger()
+ {
+ jul.clear();
+ JavaUtilLog log = new JavaUtilLog("test");
+ log.info("Info test");
+
+ jul.assertContainsLine("INFO|test|Info test");
+
+ JavaUtilLog loglong = new JavaUtilLog("test.a.long.name");
+ loglong.info("Long test");
+
+ jul.assertContainsLine("INFO|test.a.long.name|Long test");
+ }
+
+ @Test
+ public void testDebugOutput()
+ {
+ jul.clear();
+
+ // Common Throwable (for test)
+ Throwable th = new Throwable("Message");
+
+ // Capture raw string form
+ StringWriter tout = new StringWriter();
+ th.printStackTrace(new PrintWriter(tout));
+ String ths = tout.toString();
+
+ // Tests
+ JavaUtilLog log = new JavaUtilLog("test.de.bug");
+ setJulLevel("test.de.bug", Level.FINE);
+
+ log.debug("Simple debug");
+ log.debug("Debug with {} parameter", 1);
+ log.debug("Debug with {} {} parameters", 2, "spiffy");
+ log.debug("Debug with throwable", th);
+ log.debug(th);
+
+ // jul.dump();
+
+ jul.assertContainsLine("FINE|test.de.bug|Simple debug");
+ jul.assertContainsLine("FINE|test.de.bug|Debug with 1 parameter");
+ jul.assertContainsLine("FINE|test.de.bug|Debug with 2 spiffy parameters");
+ jul.assertContainsLine("FINE|test.de.bug|Debug with throwable");
+ jul.assertContainsLine(ths);
+ }
+
+ @Test
+ public void testInfoOutput()
+ {
+ jul.clear();
+
+ // Common Throwable (for test)
+ Throwable th = new Throwable("Message");
+
+ // Capture raw string form
+ StringWriter tout = new StringWriter();
+ th.printStackTrace(new PrintWriter(tout));
+ String ths = tout.toString();
+
+ // Tests
+ JavaUtilLog log = new JavaUtilLog("test.in.fo");
+ setJulLevel("test.in.fo", Level.INFO);
+
+ log.info("Simple info");
+ log.info("Info with {} parameter", 1);
+ log.info("Info with {} {} parameters", 2, "spiffy");
+ log.info("Info with throwable", th);
+ log.info(th);
+
+ // jul.dump();
+
+ jul.assertContainsLine("INFO|test.in.fo|Simple info");
+ jul.assertContainsLine("INFO|test.in.fo|Info with 1 parameter");
+ jul.assertContainsLine("INFO|test.in.fo|Info with 2 spiffy parameters");
+ jul.assertContainsLine("INFO|test.in.fo|Info with throwable");
+ jul.assertContainsLine(ths);
+ }
+
+ @Test
+ public void testWarnOutput()
+ {
+ jul.clear();
+
+ // Common Throwable (for test)
+ Throwable th = new Throwable("Message");
+
+ // Capture raw string form
+ StringWriter tout = new StringWriter();
+ th.printStackTrace(new PrintWriter(tout));
+ String ths = tout.toString();
+
+ // Tests
+ JavaUtilLog log = new JavaUtilLog("test.wa.rn");
+ setJulLevel("test.wa.rn", Level.WARNING);
+
+ log.warn("Simple warn");
+ log.warn("Warn with {} parameter", 1);
+ log.warn("Warn with {} {} parameters", 2, "spiffy");
+ log.warn("Warn with throwable", th);
+ log.warn(th);
+
+ // jul.dump();
+
+ jul.assertContainsLine("WARNING|test.wa.rn|Simple warn");
+ jul.assertContainsLine("WARNING|test.wa.rn|Warn with 1 parameter");
+ jul.assertContainsLine("WARNING|test.wa.rn|Warn with 2 spiffy parameters");
+ jul.assertContainsLine("WARNING|test.wa.rn|Warn with throwable");
+ jul.assertContainsLine(ths);
+ }
+
+ @Test
+ public void testFormattingWithNulls()
+ {
+ jul.clear();
+
+ JavaUtilLog log = new JavaUtilLog("test.nu.ll");
+ setJulLevel("test.nu.ll", Level.INFO);
+
+ log.info("Testing info(msg,null,null) - {} {}", "arg0", "arg1");
+ log.info("Testing info(msg,null,null) - {}/{}", null, null);
+ log.info("Testing info(msg,null,null) > {}", null, null);
+ log.info("Testing info(msg,null,null)", null, null);
+ log.info(null, "Testing", "info(null,arg0,arg1)");
+ log.info(null, null, null);
+
+ //jul.dump();
+
+ jul.assertContainsLine("INFO|test.nu.ll|Testing info(msg,null,null) - null/null");
+ jul.assertContainsLine("INFO|test.nu.ll|Testing info(msg,null,null) > null null");
+ jul.assertContainsLine("INFO|test.nu.ll|Testing info(msg,null,null) null null");
+ jul.assertContainsLine("INFO|test.nu.ll|null Testing info(null,arg0,arg1)");
+ jul.assertContainsLine("INFO|test.nu.ll|null null null");
+ }
+
+ @Test
+ public void testIsDebugEnabled()
+ {
+ JavaUtilLog log = new JavaUtilLog("test.legacy");
+
+ setJulLevel("test.legacy", Level.ALL);
+ assertThat("log.level(all).isDebugEnabled", log.isDebugEnabled(), is(true));
+
+ setJulLevel("test.legacy", Level.FINEST);
+ assertThat("log.level(finest).isDebugEnabled", log.isDebugEnabled(), is(true));
+
+ setJulLevel("test.legacy", Level.FINER);
+ assertThat("log.level(finer).isDebugEnabled", log.isDebugEnabled(), is(true));
+
+ setJulLevel("test.legacy", Level.FINE);
+ assertThat("log.level(fine).isDebugEnabled", log.isDebugEnabled(), is(true));
+
+ setJulLevel("test.legacy", Level.INFO);
+ assertThat("log.level(info).isDebugEnabled", log.isDebugEnabled(), is(false));
+
+ setJulLevel("test.legacy", Level.WARNING);
+ assertThat("log.level(warn).isDebugEnabled", log.isDebugEnabled(), is(false));
+
+ log.setDebugEnabled(true);
+ assertThat("log.isDebugEnabled", log.isDebugEnabled(), is(true));
+
+ log.setDebugEnabled(false);
+ assertThat("log.isDebugEnabled", log.isDebugEnabled(), is(false));
+ }
+
+ private void setJulLevel(String name, Level lvl)
+ {
+ java.util.logging.Logger log = java.util.logging.Logger.getLogger(name);
+ log.setLevel(lvl);
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/LogTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/LogTest.java
new file mode 100644
index 0000000..0df026c
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/LogTest.java
@@ -0,0 +1,141 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class LogTest
+{
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+ private static Logger originalLogger;
+ private static Map<String, Logger> originalLoggers;
+
+ @BeforeAll
+ public static void rememberOriginalLogger()
+ {
+ originalLogger = Log.getLog();
+ originalLoggers = new HashMap<>(Log.getLoggers());
+ Log.getMutableLoggers().clear();
+ }
+
+ @AfterAll
+ public static void restoreOriginalLogger()
+ {
+ Log.setLog(originalLogger);
+ Log.getMutableLoggers().clear();
+ Log.getMutableLoggers().putAll(originalLoggers);
+ }
+
+ @Test
+ public void testDefaultLogging()
+ {
+ Logger log = Log.getLogger(LogTest.class);
+ log.info("Test default logging");
+ }
+
+ @Test
+ public void testNamedLogNamedStdErrLog()
+ {
+ Log.setLog(new StdErrLog());
+
+ assertNamedLogging(Red.class);
+ assertNamedLogging(Blue.class);
+ assertNamedLogging(Green.class);
+ }
+
+ @Test
+ public void testNamedLogNamedJUL()
+ {
+ Log.setLog(new JavaUtilLog());
+
+ assertNamedLogging(Red.class);
+ assertNamedLogging(Blue.class);
+ assertNamedLogging(Green.class);
+ }
+
+ @Test
+ public void testNamedLogNamedSlf4J() throws Exception
+ {
+ Log.setLog(new Slf4jLog());
+
+ assertNamedLogging(Red.class);
+ assertNamedLogging(Blue.class);
+ assertNamedLogging(Green.class);
+ }
+
+ private void assertNamedLogging(Class<?> clazz)
+ {
+ Logger lc = Log.getLogger(clazz);
+ assertEquals(lc.getName(), clazz.getName(), "Named logging (impl=" + Log.getLog().getClass().getName() + ")");
+ }
+
+ public static Stream<Arguments> packageCases()
+ {
+ return Stream.of(
+ // null entry
+ Arguments.of(null, ""),
+ // empty entry
+ Arguments.of("", ""),
+ // all whitespace entry
+ Arguments.of(" \t ", ""),
+ // bad / invalid characters
+ Arguments.of("org.eclipse.Foo.\u0000", "oe.Foo"),
+ Arguments.of("org.eclipse.\u20ac.Euro", "oe\u20ac.Euro"),
+ // bad package segments
+ Arguments.of(".foo", "foo"),
+ Arguments.of(".bar.Foo", "b.Foo"),
+ Arguments.of("org...bar..Foo", "ob.Foo"),
+ Arguments.of("org . . . bar . . Foo ", "ob.Foo"),
+ Arguments.of("org . . . bar . . Foo ", "ob.Foo"),
+ // long-ish classname
+ Arguments.of("org.eclipse.jetty.websocket.common.extensions.compress.DeflateFrameExtension", "oejwcec.DeflateFrameExtension"),
+ // internal class
+ Arguments.of("org.eclipse.jetty.foo.Bar$Internal", "oejf.Bar$Internal")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("packageCases")
+ public void testCondensePackageViaLogger(String input, String expected)
+ {
+ StdErrLog log = new StdErrLog();
+ StdErrLog logger = (StdErrLog)log.newLogger(input);
+ assertThat("log[" + input + "] condenses to name", logger._abbrevname, is(expected));
+ }
+
+ @ParameterizedTest
+ @MethodSource("packageCases")
+ public void testCondensePackageDirect(String input, String expected)
+ {
+ assertThat("log[" + input + "] condenses to name", AbstractLogger.condensePackageString(input), is(expected));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/NamedLogTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/NamedLogTest.java
new file mode 100644
index 0000000..50d222d
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/NamedLogTest.java
@@ -0,0 +1,59 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import org.junit.jupiter.api.Test;
+
+public class NamedLogTest
+{
+ @Test
+ public void testNamedLogging()
+ {
+ Red red = new Red();
+ Green green = new Green();
+ Blue blue = new Blue();
+
+ StdErrCapture output = new StdErrCapture();
+
+ setLoggerOptions(Red.class, output);
+ setLoggerOptions(Green.class, output);
+ setLoggerOptions(Blue.class, output);
+
+ red.generateLogs();
+ green.generateLogs();
+ blue.generateLogs();
+
+ output.assertContains(Red.class.getName());
+ output.assertContains(Green.class.getName());
+ output.assertContains(Blue.class.getName());
+ }
+
+ private void setLoggerOptions(Class<?> clazz, StdErrCapture output)
+ {
+ Logger logger = Log.getLogger(clazz);
+ logger.setDebugEnabled(true);
+
+ if (logger instanceof StdErrLog)
+ {
+ StdErrLog sel = (StdErrLog)logger;
+ sel.setPrintLongNames(true);
+ output.capture(sel);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/Red.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/Red.java
new file mode 100644
index 0000000..1a7106f
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/Red.java
@@ -0,0 +1,32 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+public class Red
+{
+ private static final Logger LOG = Log.getLogger(Red.class);
+
+ public void generateLogs()
+ {
+ LOG.debug("My color is {}", Red.class.getSimpleName());
+ LOG.info("I represent the emotion Love");
+ LOG.warn("I can also mean Anger");
+ LOG.ignore(new RuntimeException("Nom"));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/Slf4jHelper.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/Slf4jHelper.java
new file mode 100644
index 0000000..9ace587
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/Slf4jHelper.java
@@ -0,0 +1,61 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+public final class Slf4jHelper
+{
+ public static ClassLoader createTestClassLoader(ClassLoader parentClassLoader) throws MalformedURLException
+ {
+ File testJarDir = MavenTestingUtils.getTargetFile("test-jars");
+ assumeTrue(testJarDir.exists()); // trigger @Ignore if dir not there
+
+ File[] jarfiles = testJarDir.listFiles(new FileFilter()
+ {
+ public boolean accept(File path)
+ {
+ if (!path.isFile())
+ {
+ return false;
+ }
+ return path.getName().endsWith(".jar");
+ }
+ });
+
+ assumeTrue(jarfiles.length > 0); // trigger @Ignore if no jar files.
+
+ URL[] urls = new URL[jarfiles.length];
+ for (int i = 0; i < jarfiles.length; i++)
+ {
+ urls[i] = jarfiles[i].toURI().toURL();
+ // System.out.println("Adding test-jar => " + urls[i]);
+ }
+
+ return new URLClassLoader(urls, parentClassLoader);
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/StdErrCapture.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/StdErrCapture.java
new file mode 100644
index 0000000..0ba0a5d
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/StdErrCapture.java
@@ -0,0 +1,70 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+
+public class StdErrCapture
+{
+ private ByteArrayOutputStream test;
+ private PrintStream err;
+
+ public StdErrCapture(StdErrLog log)
+ {
+ this();
+ log.setStdErrStream(err);
+ }
+
+ public StdErrCapture()
+ {
+ test = new ByteArrayOutputStream();
+ err = new PrintStream(test);
+ }
+
+ public void capture(StdErrLog log)
+ {
+ log.setStdErrStream(err);
+ }
+
+ public void assertContains(String expectedString)
+ {
+ err.flush();
+ String output = new String(test.toByteArray());
+ assertThat(output, containsString(expectedString));
+ }
+
+ public void assertNotContains(String unexpectedString)
+ {
+ err.flush();
+ String output = new String(test.toByteArray());
+ assertThat(output, not(containsString(unexpectedString)));
+ }
+
+ @Override
+ public String toString()
+ {
+ err.flush();
+ return new String(test.toByteArray());
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/StdErrLogTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/StdErrLogTest.java
new file mode 100644
index 0000000..8ef114a
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/log/StdErrLogTest.java
@@ -0,0 +1,862 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Tests for StdErrLog
+ */
+public class StdErrLogTest
+{
+ static
+ {
+ StdErrLog.setTagPad(0);
+ }
+
+ @BeforeEach
+ public void before()
+ {
+ Thread.currentThread().setName("tname");
+ }
+
+ @Test
+ public void testStdErrLogFormat()
+ {
+ StdErrLog log = new StdErrLog(LogTest.class.getName(), new Properties());
+ StdErrCapture output = new StdErrCapture(log);
+
+ log.info("testing:{},{}", "test", "format1");
+ log.info("testing:{}", "test", "format2");
+ log.info("testing", "test", "format3");
+ log.info("testing:{},{}", "test", null);
+ log.info("testing {} {}", null, null);
+ log.info("testing:{}", null, null);
+ log.info("testing", null, null);
+
+ System.err.println(output);
+ output.assertContains("INFO:oejul.LogTest:tname: testing:test,format1");
+ output.assertContains("INFO:oejul.LogTest:tname: testing:test format2");
+ output.assertContains("INFO:oejul.LogTest:tname: testing test format3");
+ output.assertContains("INFO:oejul.LogTest:tname: testing:test,");
+ output.assertContains("INFO:oejul.LogTest:tname: testing");
+ output.assertContains("INFO:oejul.LogTest:tname: testing:");
+ output.assertContains("INFO:oejul.LogTest:tname: testing");
+ }
+
+ @Test
+ public void testStdErrLogDebug()
+ {
+ StdErrLog log = new StdErrLog("xxx", new Properties());
+ StdErrCapture output = new StdErrCapture(log);
+
+ log.setLevel(StdErrLog.LEVEL_DEBUG);
+ log.debug("testing {} {}", "test", "debug");
+ log.info("testing {} {}", "test", "info");
+ log.warn("testing {} {}", "test", "warn");
+ log.setLevel(StdErrLog.LEVEL_INFO);
+ log.debug("YOU SHOULD NOT SEE THIS!", null, null);
+
+ // Test for backward compat with old (now deprecated) method
+ Logger before = log.getLogger("before");
+ log.setDebugEnabled(true);
+ Logger after = log.getLogger("after");
+ before.debug("testing {} {}", "test", "debug-before");
+ log.debug("testing {} {}", "test", "debug-deprecated");
+ after.debug("testing {} {}", "test", "debug-after");
+
+ log.setDebugEnabled(false);
+ before.debug("testing {} {}", "test", "debug-before-false");
+ log.debug("testing {} {}", "test", "debug-deprecated-false");
+ after.debug("testing {} {}", "test", "debug-after-false");
+
+ output.assertContains("DBUG:xxx:tname: testing test debug");
+ output.assertContains("INFO:xxx:tname: testing test info");
+ output.assertContains("WARN:xxx:tname: testing test warn");
+ output.assertNotContains("YOU SHOULD NOT SEE THIS!");
+ output.assertContains("DBUG:x.before:tname: testing test debug-before");
+ output.assertContains("DBUG:xxx:tname: testing test debug-deprecated");
+ output.assertContains("DBUG:x.after:tname: testing test debug-after");
+ output.assertNotContains("DBUG:x.before:tname: testing test debug-before-false");
+ output.assertNotContains("DBUG:xxx:tname: testing test debug-deprecated-false");
+ output.assertNotContains("DBUG:x.after:tname: testing test debug-after-false");
+ }
+
+ @Test
+ public void testStdErrLogName()
+ {
+ StdErrLog log = new StdErrLog("testX", new Properties());
+ log.setPrintLongNames(true);
+ StdErrCapture output = new StdErrCapture(log);
+
+ assertThat("Log.name", log.getName(), is("testX"));
+ Logger next = log.getLogger("next");
+ assertThat("Log.name(child)", next.getName(), is("testX.next"));
+ next.info("testing {} {}", "next", "info");
+
+ output.assertContains(":testX.next:tname: testing next info");
+ }
+
+ @Test
+ public void testStdErrMsgThrowable()
+ {
+ // The test Throwable
+ Throwable th = new Throwable("Message");
+
+ // Initialize Logger
+ StdErrLog log = new StdErrLog("testX", new Properties());
+ StdErrCapture output = new StdErrCapture(log);
+
+ // Test behavior
+ log.warn("ex", th); // Behavior here is being tested
+ output.assertContains(asString(th));
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ @Test
+ public void testStdErrMsgThrowableNull()
+ {
+ // Initialize Logger
+ StdErrLog log = new StdErrLog("testX", new Properties());
+ StdErrCapture output = new StdErrCapture(log);
+
+ // Test behavior
+ Throwable th = null;
+ log.warn("ex", th);
+ output.assertContains("testX");
+ output.assertNotContains("null");
+ }
+
+ @Test
+ public void testStdErrThrowable()
+ {
+ // The test Throwable
+ Throwable th = new Throwable("Message");
+
+ // Initialize Logger
+ StdErrLog log = new StdErrLog("testX", new Properties());
+ StdErrCapture output = new StdErrCapture(log);
+
+ // Test behavior
+ log.warn(th);
+ output.assertContains(asString(th));
+ }
+
+ @Test
+ public void testStdErrThrowableNull()
+ {
+ // Initialize Logger
+ StdErrLog log = new StdErrLog("testX", new Properties());
+ StdErrCapture output = new StdErrCapture(log);
+
+ // Test behavior
+ Throwable th = null;
+ log.warn(th); // Behavior here is being tested
+ output.assertContains("testX");
+ output.assertNotContains("null");
+ }
+
+ @Test
+ public void testStdErrFormatArgsThrowable()
+ {
+ // The test throwable
+ Throwable th = new Throwable("Reasons Explained");
+
+ // Initialize Logger
+ StdErrLog log = new StdErrLog("testX", new Properties());
+ StdErrCapture output = new StdErrCapture(log);
+
+ // Test behavior
+ log.warn("Ex {}", "Reasons", th);
+ output.assertContains("Reasons");
+ output.assertContains(asString(th));
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ @Test
+ public void testStdErrFormatArgsThrowableNull()
+ {
+ // The test throwable
+ Throwable th = null;
+
+ // Initialize Logger
+ StdErrLog log = new StdErrLog("testX", new Properties());
+ StdErrCapture output = new StdErrCapture(log);
+
+ // Test behavior
+ log.warn("Ex {}", "Reasons", th);
+ output.assertContains("Reasons");
+ output.assertNotContains("null");
+ }
+
+ @Test
+ public void testStdErrMsgThrowableWithControlChars()
+ {
+ // The test throwable, using "\b" (backspace) character
+ Throwable th = new Throwable("Message with \b backspace");
+
+ // Initialize Logger
+ StdErrLog log = new StdErrLog("testX", new Properties());
+ StdErrCapture output = new StdErrCapture(log);
+
+ // Test behavior
+ log.warn("ex", th);
+ output.assertNotContains("Message with \b backspace");
+ output.assertContains("Message with ? backspace");
+ }
+
+ @Test
+ public void testStdErrMsgStringThrowableWithControlChars()
+ {
+ // The test throwable, using "\b" (backspace) character
+ Throwable th = new Throwable("Message with \b backspace");
+
+ // Initialize Logger
+ StdErrLog log = new StdErrLog("testX", new Properties());
+ StdErrCapture output = new StdErrCapture(log);
+
+ // Test behavior
+ log.info(th.toString());
+ output.assertNotContains("Message with \b backspace");
+ output.assertContains("Message with ? backspace");
+ }
+
+ private String asString(Throwable cause)
+ {
+ StringWriter tout = new StringWriter();
+ cause.printStackTrace(new PrintWriter(tout));
+ return tout.toString();
+ }
+
+ /**
+ * Test to make sure that using a Null parameter on parameterized messages does not result in a NPE
+ */
+ @Test
+ public void testParameterizedMessageNullValues()
+ {
+ StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(), new Properties());
+ log.setLevel(StdErrLog.LEVEL_DEBUG);
+ try (StacklessLogging ignored = new StacklessLogging(log))
+ {
+ log.info("Testing info(msg,null,null) - {} {}", "arg0", "arg1");
+ log.info("Testing info(msg,null,null) - {} {}", null, null);
+ log.info("Testing info(msg,null,null) - {}", null, null);
+ log.info("Testing info(msg,null,null)", null, null);
+ log.info(null, "Testing", "info(null,arg0,arg1)");
+ log.info(null, null, null);
+
+ log.debug("Testing debug(msg,null,null) - {} {}", "arg0", "arg1");
+ log.debug("Testing debug(msg,null,null) - {} {}", null, null);
+ log.debug("Testing debug(msg,null,null) - {}", null, null);
+ log.debug("Testing debug(msg,null,null)", null, null);
+ log.debug(null, "Testing", "debug(null,arg0,arg1)");
+ log.debug(null, null, null);
+
+ log.debug("Testing debug(msg,null)");
+ log.debug(null, new Throwable("Testing debug(null,thrw)").fillInStackTrace());
+
+ log.warn("Testing warn(msg,null,null) - {} {}", "arg0", "arg1");
+ log.warn("Testing warn(msg,null,null) - {} {}", null, null);
+ log.warn("Testing warn(msg,null,null) - {}", null, null);
+ log.warn("Testing warn(msg,null,null)", null, null);
+ log.warn(null, "Testing", "warn(msg,arg0,arg1)");
+ log.warn(null, null, null);
+
+ log.warn("Testing warn(msg,null)");
+ log.warn(null, new Throwable("Testing warn(msg,thrw)").fillInStackTrace());
+ }
+ }
+
+ @Test
+ public void testGetLoggingLevelDefault()
+ {
+ Properties props = new Properties();
+
+ // Default Levels
+ assertEquals(StdErrLog.LEVEL_INFO, StdErrLog.getLoggingLevel(props, null), "Default Logging Level");
+ assertEquals(StdErrLog.LEVEL_INFO, StdErrLog.getLoggingLevel(props, ""), "Default Logging Level");
+ assertEquals(StdErrLog.LEVEL_INFO, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty"), "Default Logging Level");
+ assertEquals(StdErrLog.LEVEL_INFO, StdErrLog.getLoggingLevel(props, StdErrLogTest.class.getName()), "Default Logging Level");
+ }
+
+ @Test
+ public void testGetLoggingLevelBad()
+ {
+ Properties props = new Properties();
+ props.setProperty("log.LEVEL", "WARN");
+ props.setProperty("org.eclipse.jetty.bad.LEVEL", "EXPECTED_BAD_LEVEL");
+
+ // Default Level (because of bad level value)
+ assertEquals(StdErrLog.LEVEL_WARN, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty.bad"), "Bad Logging Level");
+ }
+
+ @Test
+ public void testGetLoggingLevelLowercase()
+ {
+ Properties props = new Properties();
+ props.setProperty("log.LEVEL", "warn");
+ props.setProperty("org.eclipse.jetty.util.LEVEL", "info");
+
+ // Default Level
+ assertEquals(StdErrLog.LEVEL_WARN, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty"), "Lowercase Level");
+ // Specific Level
+ assertEquals(StdErrLog.LEVEL_INFO, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty.util"), "Lowercase Level");
+ }
+
+ @Test
+ public void testGetLoggingLevelRoot()
+ {
+ Properties props = new Properties();
+ props.setProperty("log.LEVEL", "DEBUG");
+
+ // Default Levels
+ assertEquals(StdErrLog.LEVEL_DEBUG, StdErrLog.getLoggingLevel(props, null), "Default Logging Level");
+ assertEquals(StdErrLog.LEVEL_DEBUG, StdErrLog.getLoggingLevel(props, ""), "Default Logging Level");
+ assertEquals(StdErrLog.LEVEL_DEBUG, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty"), "Default Logging Level");
+ assertEquals(StdErrLog.LEVEL_DEBUG, StdErrLog.getLoggingLevel(props, StdErrLogTest.class.getName()), "Default Logging Level");
+ }
+
+ @Test
+ public void testGetLoggingLevelFQCN()
+ {
+ String name = StdErrLogTest.class.getName();
+ Properties props = new Properties();
+ props.setProperty(name + ".LEVEL", "ALL");
+
+ // Default Levels
+ assertEquals(StdErrLog.LEVEL_INFO, StdErrLog.getLoggingLevel(props, null));
+ assertEquals(StdErrLog.LEVEL_INFO, StdErrLog.getLoggingLevel(props, ""));
+ assertEquals(StdErrLog.LEVEL_INFO, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty"));
+
+ // Specified Level
+ assertEquals(StdErrLog.LEVEL_ALL, StdErrLog.getLoggingLevel(props, name));
+ }
+
+ @Test
+ public void testGetLoggingLevelUtilLevel()
+ {
+ Properties props = new Properties();
+ props.setProperty("org.eclipse.jetty.util.LEVEL", "DEBUG");
+
+ // Default Levels
+ assertEquals(StdErrLog.LEVEL_INFO, StdErrLog.getLoggingLevel(props, null));
+ assertEquals(StdErrLog.LEVEL_INFO, StdErrLog.getLoggingLevel(props, ""));
+ assertEquals(StdErrLog.LEVEL_INFO, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty"));
+ assertEquals(StdErrLog.LEVEL_INFO, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty.server.BogusObject"));
+
+ // Configured Level
+ assertEquals(StdErrLog.LEVEL_DEBUG, StdErrLog.getLoggingLevel(props, StdErrLogTest.class.getName()));
+ assertEquals(StdErrLog.LEVEL_DEBUG, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty.util.Bogus"));
+ assertEquals(StdErrLog.LEVEL_DEBUG, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty.util"));
+ assertEquals(StdErrLog.LEVEL_DEBUG, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty.util.resource.FileResource"));
+ }
+
+ @Test
+ public void testGetLoggingLevelMixedLevels()
+ {
+ Properties props = new Properties();
+ props.setProperty("log.LEVEL", "DEBUG");
+ props.setProperty("org.eclipse.jetty.util.LEVEL", "WARN");
+ props.setProperty("org.eclipse.jetty.util.ConcurrentHashMap.LEVEL", "ALL");
+
+ // Default Levels
+ assertEquals(StdErrLog.LEVEL_DEBUG, StdErrLog.getLoggingLevel(props, null));
+ assertEquals(StdErrLog.LEVEL_DEBUG, StdErrLog.getLoggingLevel(props, ""));
+ assertEquals(StdErrLog.LEVEL_DEBUG, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty"));
+ assertEquals(StdErrLog.LEVEL_DEBUG, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty.server.ServerObject"));
+
+ // Configured Level
+ assertEquals(StdErrLog.LEVEL_WARN, StdErrLog.getLoggingLevel(props, StdErrLogTest.class.getName()));
+ assertEquals(StdErrLog.LEVEL_WARN, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty.util.MagicUtil"));
+ assertEquals(StdErrLog.LEVEL_WARN, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty.util"));
+ assertEquals(StdErrLog.LEVEL_WARN, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty.util.resource.FileResource"));
+ assertEquals(StdErrLog.LEVEL_ALL, StdErrLog.getLoggingLevel(props, "org.eclipse.jetty.util.ConcurrentHashMap"));
+ }
+
+ /**
+ * Tests StdErrLog.warn() methods with level filtering.
+ * <p>
+ * Should always see WARN level messages, regardless of set level.
+ */
+ @Test
+ public void testWarnFiltering()
+ {
+ StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(), new Properties());
+ try (StacklessLogging ignored = new StacklessLogging(log))
+ {
+ StdErrCapture output = new StdErrCapture(log);
+
+ // Start with default level
+ log.warn("See Me");
+
+ // Set to debug level
+ log.setLevel(StdErrLog.LEVEL_DEBUG);
+ log.warn("Hear Me");
+
+ // Set to warn level
+ log.setLevel(StdErrLog.LEVEL_WARN);
+ log.warn("Cheer Me");
+
+ log.warn("<zoom>", new Throwable("out of focus"));
+ log.warn(new Throwable("scene lost"));
+
+ // Validate Output
+ // System.err.print(output);
+ output.assertContains("See Me");
+ output.assertContains("Hear Me");
+ output.assertContains("Cheer Me");
+
+ // Validate Stack Traces
+ output.assertContains(".StdErrLogTest:tname: <zoom>");
+ output.assertContains("java.lang.Throwable: out of focus");
+ output.assertContains("java.lang.Throwable: scene lost");
+ }
+ }
+
+ /**
+ * Tests StdErrLog.info() methods with level filtering.
+ * <p>
+ * Should only see INFO level messages when level is set to {@link StdErrLog#LEVEL_INFO} and below.
+ */
+ @Test
+ public void testInfoFiltering()
+ {
+ StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(), new Properties());
+ try (StacklessLogging ignored = new StacklessLogging(log))
+ {
+ StdErrCapture output = new StdErrCapture(log);
+
+ // Normal/Default behavior
+ log.info("I will not buy");
+
+ // Level Debug
+ log.setLevel(StdErrLog.LEVEL_DEBUG);
+ log.info("this record");
+
+ // Level All
+ log.setLevel(StdErrLog.LEVEL_ALL);
+ log.info("it is scratched.");
+
+ log.info("<zoom>", new Throwable("out of focus"));
+ log.info(new Throwable("scene lost"));
+
+ // Level Warn
+ log.setLevel(StdErrLog.LEVEL_WARN);
+ log.info("sorry?");
+ log.info("<spoken line>", new Throwable("on editing room floor"));
+
+ // Validate Output
+ output.assertContains("I will not buy");
+ output.assertContains("this record");
+ output.assertContains("it is scratched.");
+ output.assertNotContains("sorry?");
+
+ // Validate Stack Traces
+ output.assertNotContains("<spoken line>");
+ output.assertNotContains("on editing room floor");
+
+ output.assertContains(".StdErrLogTest:tname: <zoom>");
+ output.assertContains("java.lang.Throwable: out of focus");
+ output.assertContains("java.lang.Throwable: scene lost");
+ }
+ }
+
+ /**
+ * Tests {@link StdErrLog#LEVEL_OFF} filtering.
+ */
+ @Test
+ public void testOffFiltering()
+ {
+ StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(), new Properties());
+ try (StacklessLogging ignored = new StacklessLogging(log))
+ {
+ log.setLevel(StdErrLog.LEVEL_OFF);
+
+ StdErrCapture output = new StdErrCapture(log);
+
+ // Various logging events
+ log.debug("Squelch");
+ log.debug("Squelch", new RuntimeException("Squelch"));
+ log.info("Squelch");
+ log.info("Squelch", new IllegalStateException("Squelch"));
+ log.warn("Squelch");
+ log.warn("Squelch", new Exception("Squelch"));
+ log.ignore(new Throwable("Squelch"));
+
+ // Validate Output
+ output.assertNotContains("Squelch");
+ }
+ }
+
+ /**
+ * Tests StdErrLog.debug() methods with level filtering.
+ * <p>
+ * Should only see DEBUG level messages when level is set to {@link StdErrLog#LEVEL_DEBUG} and below.
+ */
+ @Test
+ public void testDebugFiltering()
+ {
+ StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(), new Properties());
+ try (StacklessLogging ignored = new StacklessLogging(log))
+ {
+ StdErrCapture output = new StdErrCapture(log);
+
+ // Normal/Default behavior
+ log.debug("Tobacconist");
+ log.debug("<spoken line>", new Throwable("on editing room floor"));
+
+ // Level Debug
+ log.setLevel(StdErrLog.LEVEL_DEBUG);
+ log.debug("my hovercraft is");
+
+ log.debug("<zoom>", new Throwable("out of focus"));
+ log.debug(new Throwable("scene lost"));
+
+ // Level All
+ log.setLevel(StdErrLog.LEVEL_ALL);
+ log.debug("full of eels.");
+
+ // Level Warn
+ log.setLevel(StdErrLog.LEVEL_WARN);
+ log.debug("what?");
+
+ // Validate Output
+ // System.err.print(output);
+ output.assertNotContains("Tobacconist");
+ output.assertContains("my hovercraft is");
+ output.assertContains("full of eels.");
+ output.assertNotContains("what?");
+
+ // Validate Stack Traces
+ output.assertNotContains("<spoken line>");
+ output.assertNotContains("on editing room floor");
+
+ output.assertContains(".StdErrLogTest:tname: <zoom>");
+ output.assertContains("java.lang.Throwable: out of focus");
+ output.assertContains("java.lang.Throwable: scene lost");
+ }
+ }
+
+ /**
+ * Tests StdErrLog with {@link Logger#ignore(Throwable)} use.
+ * <p>
+ * Should only see IGNORED level messages when level is set to {@link StdErrLog#LEVEL_ALL}.
+ */
+ @Test
+ public void testIgnores()
+ {
+ StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(), new Properties());
+ try (StacklessLogging ignored = new StacklessLogging(log))
+ {
+ StdErrCapture output = new StdErrCapture(log);
+
+ // Normal/Default behavior
+ log.ignore(new Throwable("IGNORE ME"));
+
+ // Show Ignored
+ log.setLevel(StdErrLog.LEVEL_ALL);
+ log.ignore(new Throwable("Don't ignore me"));
+
+ // Set to Debug level
+ log.setLevel(StdErrLog.LEVEL_DEBUG);
+ log.ignore(new Throwable("Debug me"));
+
+ // Validate Output
+ // System.err.print(output);
+ output.assertNotContains("IGNORE ME");
+ output.assertContains("Don't ignore me");
+ output.assertNotContains("Debug me");
+ }
+ }
+
+ @Test
+ public void testIsDebugEnabled()
+ {
+ StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(), new Properties());
+ try (StacklessLogging ignored = new StacklessLogging(log))
+ {
+ log.setLevel(StdErrLog.LEVEL_ALL);
+ assertThat("log.level(all).isDebugEnabled", log.isDebugEnabled(), is(true));
+
+ log.setLevel(StdErrLog.LEVEL_DEBUG);
+ assertThat("log.level(debug).isDebugEnabled", log.isDebugEnabled(), is(true));
+
+ log.setLevel(StdErrLog.LEVEL_INFO);
+ assertThat("log.level(info).isDebugEnabled", log.isDebugEnabled(), is(false));
+
+ log.setLevel(StdErrLog.LEVEL_WARN);
+ assertThat("log.level(warn).isDebugEnabled", log.isDebugEnabled(), is(false));
+
+ log.setLevel(StdErrLog.LEVEL_OFF);
+ assertThat("log.level(off).isDebugEnabled", log.isDebugEnabled(), is(false));
+ }
+ }
+
+ @Test
+ public void testSetGetLevel()
+ {
+ StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(), new Properties());
+ try (StacklessLogging ignored = new StacklessLogging(log))
+ {
+ log.setLevel(StdErrLog.LEVEL_ALL);
+ assertThat("log.level(all).getLevel()", log.getLevel(), is(StdErrLog.LEVEL_ALL));
+
+ log.setLevel(StdErrLog.LEVEL_DEBUG);
+ assertThat("log.level(debug).getLevel()", log.getLevel(), is(StdErrLog.LEVEL_DEBUG));
+
+ log.setLevel(StdErrLog.LEVEL_INFO);
+ assertThat("log.level(info).getLevel()", log.getLevel(), is(StdErrLog.LEVEL_INFO));
+
+ log.setLevel(StdErrLog.LEVEL_WARN);
+ assertThat("log.level(warn).getLevel()", log.getLevel(), is(StdErrLog.LEVEL_WARN));
+
+ log.setLevel(StdErrLog.LEVEL_OFF);
+ assertThat("log.level(off).getLevel()", log.getLevel(), is(StdErrLog.LEVEL_OFF));
+ }
+ }
+
+ @Test
+ public void testGetChildLoggerSimple()
+ {
+ String baseName = "jetty";
+ StdErrLog log = new StdErrLog(baseName, new Properties());
+ try (StacklessLogging ignored = new StacklessLogging(log))
+ {
+ assertThat("Logger.name", log.getName(), is("jetty"));
+
+ Logger log2 = log.getLogger("child");
+ assertThat("Logger.child.name", log2.getName(), is("jetty.child"));
+ }
+ }
+
+ @Test
+ public void testGetChildLoggerDeep()
+ {
+ String baseName = "jetty";
+ StdErrLog log = new StdErrLog(baseName, new Properties());
+ try (StacklessLogging ignored = new StacklessLogging(log))
+ {
+ assertThat("Logger.name", log.getName(), is("jetty"));
+
+ Logger log2 = log.getLogger("child.of.the.sixties");
+ assertThat("Logger.child.name", log2.getName(), is("jetty.child.of.the.sixties"));
+ }
+ }
+
+ @Test
+ public void testGetChildLoggerNull()
+ {
+ String baseName = "jetty";
+ StdErrLog log = new StdErrLog(baseName, new Properties());
+ try (StacklessLogging ignored = new StacklessLogging(log))
+ {
+ assertThat("Logger.name", log.getName(), is("jetty"));
+
+ // Pass null as child reference, should return parent logger
+ Logger log2 = log.getLogger((String)null);
+ assertThat("Logger.child.name", log2.getName(), is("jetty"));
+ assertSame(log2, log, "Should have returned same logger");
+ }
+ }
+
+ @Test
+ public void testGetChildLoggerEmptyName()
+ {
+ String baseName = "jetty";
+ StdErrLog log = new StdErrLog(baseName, new Properties());
+ try (StacklessLogging ignored = new StacklessLogging(log))
+ {
+ assertThat("Logger.name", log.getName(), is("jetty"));
+
+ // Pass empty name as child reference, should return parent logger
+ Logger log2 = log.getLogger("");
+ assertThat("Logger.child.name", log2.getName(), is("jetty"));
+ assertSame(log2, log, "Should have returned same logger");
+ }
+ }
+
+ @Test
+ public void testGetChildLoggerEmptyNameSpaces()
+ {
+ String baseName = "jetty";
+ StdErrLog log = new StdErrLog(baseName, new Properties());
+ try (StacklessLogging ignored = new StacklessLogging(log))
+ {
+ assertThat("Logger.name", log.getName(), is("jetty"));
+
+ // Pass empty name as child reference, should return parent logger
+ Logger log2 = log.getLogger(" ");
+ assertThat("Logger.child.name", log2.getName(), is("jetty"));
+ assertSame(log2, log, "Should have returned same logger");
+ }
+ }
+
+ @Test
+ public void testGetChildLoggerNullParent()
+ {
+ AbstractLogger log = new StdErrLog(null, new Properties());
+
+ assertThat("Logger.name", log.getName(), is(""));
+
+ Logger log2 = log.getLogger("jetty");
+ assertThat("Logger.child.name", log2.getName(), is("jetty"));
+ assertNotSame(log2, log, "Should have returned same logger");
+ }
+
+ @Test
+ public void testToString()
+ {
+ StdErrLog log = new StdErrLog("jetty", new Properties());
+
+ log.setLevel(StdErrLog.LEVEL_ALL);
+ assertThat("Logger.toString", log.toString(), is("StdErrLog:jetty:LEVEL=ALL"));
+
+ log.setLevel(StdErrLog.LEVEL_DEBUG);
+ assertThat("Logger.toString", log.toString(), is("StdErrLog:jetty:LEVEL=DEBUG"));
+
+ log.setLevel(StdErrLog.LEVEL_INFO);
+ assertThat("Logger.toString", log.toString(), is("StdErrLog:jetty:LEVEL=INFO"));
+
+ log.setLevel(StdErrLog.LEVEL_WARN);
+ assertThat("Logger.toString", log.toString(), is("StdErrLog:jetty:LEVEL=WARN"));
+
+ log.setLevel(99); // intentionally bogus level
+ assertThat("Logger.toString", log.toString(), is("StdErrLog:jetty:LEVEL=?"));
+ }
+
+ @Test
+ public void testPrintSource()
+ {
+ Properties props = new Properties();
+ props.put("test.SOURCE", "true");
+ StdErrLog log = new StdErrLog("test", props);
+ log.setLevel(StdErrLog.LEVEL_DEBUG);
+
+ ByteArrayOutputStream test = new ByteArrayOutputStream();
+ PrintStream err = new PrintStream(test);
+ log.setStdErrStream(err);
+
+ log.debug("Show me the source!");
+
+ String output = new String(test.toByteArray(), StandardCharsets.UTF_8);
+ // System.err.print(output);
+
+ assertThat(output, containsString(".StdErrLogTest#testPrintSource(StdErrLogTest.java:"));
+
+ props.put("test.SOURCE", "false");
+ }
+
+ @Test
+ public void testConfiguredAndSetDebugEnabled()
+ {
+ Properties props = new Properties();
+ props.setProperty("org.eclipse.jetty.util.LEVEL", "WARN");
+ props.setProperty("org.eclipse.jetty.io.LEVEL", "WARN");
+
+ StdErrLog root = new StdErrLog("", props);
+ assertLevel(root, StdErrLog.LEVEL_INFO); // default
+
+ StdErrLog log = (StdErrLog)root.getLogger(StdErrLogTest.class.getName());
+ assertThat("Log.isDebugEnabled()", log.isDebugEnabled(), is(false));
+ assertLevel(log, StdErrLog.LEVEL_WARN); // as configured
+
+ // Boot stomp it all to debug
+ root.setDebugEnabled(true);
+ assertThat("Log.isDebugEnabled()", log.isDebugEnabled(), is(true));
+ assertLevel(log, StdErrLog.LEVEL_DEBUG); // as stomped
+
+ // Restore configured
+ root.setDebugEnabled(false);
+ assertThat("Log.isDebugEnabled()", log.isDebugEnabled(), is(false));
+ assertLevel(log, StdErrLog.LEVEL_WARN); // as configured
+ }
+
+ @Test
+ public void testSuppressed()
+ {
+ StdErrLog log = new StdErrLog("xxx", new Properties());
+ StdErrCapture output = new StdErrCapture(log);
+
+ Exception inner = new Exception("inner");
+ inner.addSuppressed(new IllegalStateException()
+ {
+ {
+ addSuppressed(new Exception("branch0"));
+ }
+ });
+ IOException outer = new IOException("outer", inner);
+
+ outer.addSuppressed(new IllegalStateException()
+ {
+ {
+ addSuppressed(new Exception("branch1"));
+ }
+ });
+ outer.addSuppressed(new IllegalArgumentException()
+ {
+ {
+ addSuppressed(new Exception("branch2"));
+ }
+ });
+
+ log.warn("problem", outer);
+
+ output.assertContains("\t|\t|java.lang.Exception: branch2");
+ output.assertContains("\t|\t|java.lang.Exception: branch1");
+ output.assertContains("\t|\t|java.lang.Exception: branch0");
+ }
+
+ private void assertLevel(StdErrLog log, int expectedLevel)
+ {
+ assertThat("Log[" + log.getName() + "].level", levelToString(log.getLevel()), is(levelToString(expectedLevel)));
+ }
+
+ private String levelToString(int level)
+ {
+ switch (level)
+ {
+ case StdErrLog.LEVEL_ALL:
+ return "ALL";
+ case StdErrLog.LEVEL_DEBUG:
+ return "DEBUG";
+ case StdErrLog.LEVEL_INFO:
+ return "INFO";
+ case StdErrLog.LEVEL_WARN:
+ return "WARN";
+ default:
+ return Integer.toString(level);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ClassPathResourceTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ClassPathResourceTest.java
new file mode 100644
index 0000000..675d41a
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ClassPathResourceTest.java
@@ -0,0 +1,114 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.File;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ClassPathResourceTest
+{
+ /**
+ * Test a class path resource for existence.
+ */
+ @Test
+ public void testClassPathResourceClassRelative()
+ {
+ final String classPathName = "Resource.class";
+
+ try (Resource resource = Resource.newClassPathResource(classPathName);)
+ {
+ // A class path cannot be a directory
+ assertFalse(resource.isDirectory(), "Class path cannot be a directory.");
+
+ // A class path must exist
+ assertTrue(resource.exists(), "Class path resource does not exist.");
+ }
+ }
+
+ /**
+ * Test a class path resource for existence.
+ */
+ @Test
+ public void testClassPathResourceClassAbsolute()
+ {
+ final String classPathName = "/org/eclipse/jetty/util/resource/Resource.class";
+
+ Resource resource = Resource.newClassPathResource(classPathName);
+
+ // A class path cannot be a directory
+ assertFalse(resource.isDirectory(), "Class path cannot be a directory.");
+
+ // A class path must exist
+ assertTrue(resource.exists(), "Class path resource does not exist.");
+ }
+
+ /**
+ * Test a class path resource for directories.
+ *
+ * @throws Exception failed test
+ */
+ @Test
+ public void testClassPathResourceDirectory() throws Exception
+ {
+ final String classPathName = "/";
+
+ Resource resource = Resource.newClassPathResource(classPathName);
+
+ // A class path must be a directory
+ assertTrue(resource.isDirectory(), "Class path must be a directory.");
+
+ assertTrue(resource.getFile().isDirectory(), "Class path returned file must be a directory.");
+
+ // A class path must exist
+ assertTrue(resource.exists(), "Class path resource does not exist.");
+ }
+
+ /**
+ * Test a class path resource for a file.
+ *
+ * @throws Exception failed test
+ */
+ @Test
+ public void testClassPathResourceFile() throws Exception
+ {
+ final String fileName = "resource.txt";
+ final String classPathName = "/" + fileName;
+
+ // Will locate a resource in the class path
+ Resource resource = Resource.newClassPathResource(classPathName);
+
+ // A class path cannot be a directory
+ assertFalse(resource.isDirectory(), "Class path must be a directory.");
+
+ assertTrue(resource != null);
+
+ File file = resource.getFile();
+
+ assertEquals(fileName, file.getName(), "File name from class path is not equal.");
+ assertTrue(file.isFile(), "File returned from class path should be a file.");
+
+ // A class path must exist
+ assertTrue(resource.exists(), "Class path resource does not exist.");
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java
new file mode 100644
index 0000000..3f61fb4
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java
@@ -0,0 +1,1547 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemException;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.IO;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.BufferUtil;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import static org.junit.jupiter.api.condition.OS.LINUX;
+import static org.junit.jupiter.api.condition.OS.MAC;
+import static org.junit.jupiter.api.condition.OS.WINDOWS;
+
+@ExtendWith(WorkDirExtension.class)
+public class FileSystemResourceTest
+{
+ public WorkDir workDir;
+
+ static Stream<Class> fsResourceProvider()
+ {
+ return Stream.of(FileResource.class, PathResource.class);
+ }
+
+ public Resource newResource(Class<? extends Resource> resourceClass, URL url) throws Exception
+ {
+ try
+ {
+ return resourceClass.getConstructor(URL.class).newInstance(url);
+ }
+ catch (InvocationTargetException e)
+ {
+ try
+ {
+ throw e.getTargetException();
+ }
+ catch (Exception | Error ex)
+ {
+ throw ex;
+ }
+ catch (Throwable th)
+ {
+ throw new Error(th);
+ }
+ }
+ }
+
+ public Resource newResource(Class<? extends Resource> resourceClass, URI uri) throws Exception
+ {
+ try
+ {
+ return resourceClass.getConstructor(URI.class).newInstance(uri);
+ }
+ catch (InvocationTargetException e)
+ {
+ try
+ {
+ throw e.getTargetException();
+ }
+ catch (Exception | Error ex)
+ {
+ throw ex;
+ }
+ catch (Throwable th)
+ {
+ throw new Error(th);
+ }
+ }
+ }
+
+ public Resource newResource(Class<? extends Resource> resourceClass, File file) throws Exception
+ {
+ try
+ {
+ return resourceClass.getConstructor(File.class).newInstance(file);
+ }
+ catch (InvocationTargetException e)
+ {
+ try
+ {
+ throw e.getTargetException();
+ }
+ catch (Exception | Error ex)
+ {
+ throw ex;
+ }
+ catch (Throwable th)
+ {
+ throw new Error(th);
+ }
+ }
+ }
+
+ private Matcher<Resource> hasNoAlias()
+ {
+ return new BaseMatcher<Resource>()
+ {
+ @Override
+ public boolean matches(Object item)
+ {
+ final Resource res = (Resource)item;
+ return !res.isAlias();
+ }
+
+ @Override
+ public void describeTo(Description description)
+ {
+ description.appendText("getAlias should return null");
+ }
+
+ @Override
+ public void describeMismatch(Object item, Description description)
+ {
+ description.appendText("was ").appendValue(((Resource)item).getAlias());
+ }
+ };
+ }
+
+ private Matcher<Resource> isAliasFor(final Resource resource)
+ {
+ return new BaseMatcher<Resource>()
+ {
+ @Override
+ public boolean matches(Object item)
+ {
+ final Resource ritem = (Resource)item;
+ final URI alias = ritem.getAlias();
+ if (alias == null)
+ {
+ return resource.getAlias() == null;
+ }
+ else
+ {
+ return alias.equals(resource.getURI());
+ }
+ }
+
+ @Override
+ public void describeTo(Description description)
+ {
+ description.appendText("getAlias should return ").appendValue(resource.getURI());
+ }
+
+ @Override
+ public void describeMismatch(Object item, Description description)
+ {
+ description.appendText("was ").appendValue(((Resource)item).getAlias());
+ }
+ };
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testNonAbsoluteURI(Class resourceClass)
+ {
+ assertThrows(IllegalArgumentException.class,
+ () -> newResource(resourceClass, new URI("path/to/resource")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testNotFileURI(Class resourceClass)
+ {
+ assertThrows(IllegalArgumentException.class,
+ () -> newResource(resourceClass, new URI("https://www.eclipse.org/jetty/")));
+ }
+
+ @ParameterizedTest
+ @EnabledOnOs(WINDOWS)
+ @MethodSource("fsResourceProvider")
+ public void testBogusFilenameWindows(Class resourceClass)
+ {
+ // "CON" is a reserved name under windows
+ assertThrows(IllegalArgumentException.class,
+ () -> newResource(resourceClass, new URI("file://CON")));
+ }
+
+ @ParameterizedTest
+ @EnabledOnOs({LINUX, MAC})
+ @MethodSource("fsResourceProvider")
+ public void testBogusFilenameUnix(Class resourceClass)
+ {
+ // A windows path is invalid under unix
+ assertThrows(IllegalArgumentException.class,
+ () -> newResource(resourceClass, new URI("file://Z:/:")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testNewResourceWithSpace(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getPath().normalize().toRealPath();
+
+ Path baseDir = dir.resolve("base with spaces");
+ FS.ensureDirExists(baseDir.toFile());
+
+ Path subdir = baseDir.resolve("sub");
+ FS.ensureDirExists(subdir.toFile());
+
+ URL baseUrl = baseDir.toUri().toURL();
+
+ assertThat("url.protocol", baseUrl.getProtocol(), is("file"));
+
+ try (Resource base = newResource(resourceClass, baseUrl))
+ {
+ Resource sub = base.addPath("sub");
+ assertThat("sub/.isDirectory", sub.isDirectory(), is(true));
+
+ Resource tmp = sub.addPath("/tmp");
+ assertThat("No root", tmp.exists(), is(false));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testAddPathClass(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+
+ Path subdir = dir.resolve("sub");
+ FS.ensureDirExists(subdir.toFile());
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource sub = base.addPath("sub");
+ assertThat("sub/.isDirectory", sub.isDirectory(), is(true));
+
+ Resource tmp = sub.addPath("/tmp");
+ assertThat("No root", tmp.exists(), is(false));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testAddRootPath(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Path subdir = dir.resolve("sub");
+ Files.createDirectories(subdir);
+
+ String readableRootDir = findRootDir(dir.getFileSystem());
+ assumeTrue(readableRootDir != null, "Readable Root Dir found");
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource sub = base.addPath("sub");
+ assertThat("sub", sub.isDirectory(), is(true));
+
+ try
+ {
+ Resource rrd = sub.addPath(readableRootDir);
+ // valid path for unix and OSX
+ assertThat("Readable Root Dir", rrd.exists(), is(false));
+ }
+ catch (MalformedURLException | InvalidPathException e)
+ {
+ // valid path on Windows
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testAccessUniCodeFile(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+
+ String readableRootDir = findRootDir(dir.getFileSystem());
+ assumeTrue(readableRootDir != null, "Readable Root Dir found");
+
+ Path subdir = dir.resolve("sub");
+ Files.createDirectories(subdir);
+
+ touchFile(subdir.resolve("swedish-å.txt"), "hi a-with-circle");
+ touchFile(subdir.resolve("swedish-ä.txt"), "hi a-with-two-dots");
+ touchFile(subdir.resolve("swedish-ö.txt"), "hi o-with-two-dots");
+
+ try (Resource base = newResource(resourceClass, subdir.toFile()))
+ {
+ Resource refA1 = base.addPath("swedish-å.txt");
+ Resource refA2 = base.addPath("swedish-ä.txt");
+ Resource refO1 = base.addPath("swedish-ö.txt");
+
+ assertThat("Ref A1 exists", refA1.exists(), is(true));
+ assertThat("Ref A2 exists", refA2.exists(), is(true));
+ assertThat("Ref O1 exists", refO1.exists(), is(true));
+ if (LINUX.isCurrentOs())
+ {
+ assertThat("Ref A1 alias", refA1.isAlias(), is(false));
+ assertThat("Ref A2 alias", refA2.isAlias(), is(false));
+ assertThat("Ref O1 alias", refO1.isAlias(), is(false));
+ }
+ assertThat("Ref A1 contents", toString(refA1), is("hi a-with-circle"));
+ assertThat("Ref A2 contents", toString(refA2), is("hi a-with-two-dots"));
+ assertThat("Ref O1 contents", toString(refO1), is("hi o-with-two-dots"));
+ }
+ }
+
+ private String findRootDir(FileSystem fs) throws IOException
+ {
+ // look for a directory off of a root path
+ for (Path rootDir : fs.getRootDirectories())
+ {
+ try (DirectoryStream<Path> dir = Files.newDirectoryStream(rootDir))
+ {
+ for (Path entry : dir)
+ {
+ if (Files.isDirectory(entry) && !Files.isHidden(entry) && !entry.getFileName().toString().contains("$"))
+ {
+ return entry.toAbsolutePath().toString();
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ // FIXME why ignoring exceptions??
+ }
+ }
+
+ return null;
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testIsContainedIn(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+ Path foo = dir.resolve("foo");
+ Files.createFile(foo);
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource res = base.addPath("foo");
+ assertThat("is contained in", res.isContainedIn(base), is(false));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testIsDirectory(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+ Path foo = dir.resolve("foo");
+ Files.createFile(foo);
+
+ Path subdir = dir.resolve("sub");
+ Files.createDirectories(subdir);
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource res = base.addPath("foo");
+ assertThat("foo.isDirectory", res.isDirectory(), is(false));
+
+ Resource sub = base.addPath("sub");
+ assertThat("sub/.isDirectory", sub.isDirectory(), is(true));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testLastModified(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ File file = workDir.getPathFile("foo").toFile();
+ file.createNewFile();
+
+ long expected = file.lastModified();
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource res = base.addPath("foo");
+ assertThat("foo.lastModified", res.lastModified() / 1000 * 1000, lessThanOrEqualTo(expected));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testLastModifiedNotExists(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource res = base.addPath("foo");
+ assertThat("foo.lastModified", res.lastModified(), is(0L));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testLength(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path file = dir.resolve("foo");
+ touchFile(file, "foo");
+
+ long expected = Files.size(file);
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource res = base.addPath("foo");
+ assertThat("foo.length", res.length(), is(expected));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testLengthNotExists(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource res = base.addPath("foo");
+ assertThat("foo.length", res.length(), is(0L));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testDelete(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+ Path file = dir.resolve("foo");
+ Files.createFile(file);
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ // Is it there?
+ Resource res = base.addPath("foo");
+ assertThat("foo.exists", res.exists(), is(true));
+ // delete it
+ assertThat("foo.delete", res.delete(), is(true));
+ // is it there?
+ assertThat("foo.exists", res.exists(), is(false));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testDeleteNotExists(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ // Is it there?
+ Resource res = base.addPath("foo");
+ assertThat("foo.exists", res.exists(), is(false));
+ // delete it
+ assertThat("foo.delete", res.delete(), is(false));
+ // is it there?
+ assertThat("foo.exists", res.exists(), is(false));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testName(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ String expected = dir.toAbsolutePath().toString();
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ assertThat("base.name", base.getName(), is(expected));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testInputStream(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path file = dir.resolve("foo");
+ String content = "Foo is here";
+ touchFile(file, content);
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource foo = base.addPath("foo");
+ try (InputStream stream = foo.getInputStream();
+ InputStreamReader reader = new InputStreamReader(stream);
+ StringWriter writer = new StringWriter())
+ {
+ IO.copy(reader, writer);
+ assertThat("Stream", writer.toString(), is(content));
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testReadableByteChannel(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path file = dir.resolve("foo");
+ String content = "Foo is here";
+
+ try (StringReader reader = new StringReader(content);
+ BufferedWriter writer = Files.newBufferedWriter(file))
+ {
+ IO.copy(reader, writer);
+ }
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource foo = base.addPath("foo");
+ try (ReadableByteChannel channel = foo.getReadableByteChannel())
+ {
+ ByteBuffer buf = ByteBuffer.allocate(256);
+ channel.read(buf);
+ buf.flip();
+ String actual = BufferUtil.toUTF8String(buf);
+ assertThat("ReadableByteChannel content", actual, is(content));
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testGetURI(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path file = dir.resolve("foo");
+ Files.createFile(file);
+
+ URI expected = file.toUri();
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource foo = base.addPath("foo");
+ assertThat("getURI", foo.getURI(), is(expected));
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testGetURL(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path file = dir.resolve("foo");
+ Files.createFile(file);
+
+ URL expected = file.toUri().toURL();
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource foo = base.addPath("foo");
+ assertThat("getURL", foo.getURL(), is(expected));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testList(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Files.createFile(dir.resolve("foo"));
+ Files.createFile(dir.resolve("bar"));
+ Files.createDirectories(dir.resolve("tick"));
+ Files.createDirectories(dir.resolve("tock"));
+
+ List<String> expected = new ArrayList<>();
+ expected.add("foo");
+ expected.add("bar");
+ expected.add("tick/");
+ expected.add("tock/");
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ String[] list = base.list();
+ List<String> actual = Arrays.asList(list);
+
+ assertEquals(expected.size(), actual.size());
+ for (String s : expected)
+ {
+ assertEquals(true, actual.contains(s));
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testSymlink(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+
+ Path foo = dir.resolve("foo");
+ Path bar = dir.resolve("bar");
+
+ try
+ {
+ Files.createFile(foo);
+ Files.createSymbolicLink(bar, foo);
+ }
+ catch (UnsupportedOperationException | FileSystemException e)
+ {
+ // if unable to create symlink, no point testing the rest
+ // this is the path that Microsoft Windows takes.
+ assumeTrue(false, "Not supported");
+ }
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource resFoo = base.addPath("foo");
+ Resource resBar = base.addPath("bar");
+
+ assertThat("resFoo.uri", resFoo.getURI(), is(foo.toUri()));
+
+ // Access to the same resource, but via a symlink means that they are not equivalent
+ assertThat("foo.equals(bar)", resFoo.equals(resBar), is(false));
+
+ assertThat("resource.alias", resFoo, hasNoAlias());
+ assertThat("resource.uri.alias", newResource(resourceClass, resFoo.getURI()), hasNoAlias());
+ assertThat("resource.file.alias", newResource(resourceClass, resFoo.getFile()), hasNoAlias());
+
+ assertThat("alias", resBar, isAliasFor(resFoo));
+ assertThat("uri.alias", newResource(resourceClass, resBar.getURI()), isAliasFor(resFoo));
+ assertThat("file.alias", newResource(resourceClass, resBar.getFile()), isAliasFor(resFoo));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(classes = PathResource.class) // FileResource does not support this
+ public void testNonExistantSymlink(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path foo = dir.resolve("foo");
+ Path bar = dir.resolve("bar");
+
+ try
+ {
+ Files.createSymbolicLink(bar, foo);
+ }
+ catch (UnsupportedOperationException | FileSystemException e)
+ {
+ // if unable to create symlink, no point testing the rest
+ // this is the path that Microsoft Windows takes.
+ assumeTrue(false, "Not supported");
+ }
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource resFoo = base.addPath("foo");
+ Resource resBar = base.addPath("bar");
+
+ assertThat("resFoo.uri", resFoo.getURI(), is(foo.toUri()));
+
+ // Access to the same resource, but via a symlink means that they are not equivalent
+ assertThat("foo.equals(bar)", resFoo.equals(resBar), is(false));
+
+ assertThat("resource.alias", resFoo, hasNoAlias());
+ assertThat("resource.uri.alias", newResource(resourceClass, resFoo.getURI()), hasNoAlias());
+ assertThat("resource.file.alias", newResource(resourceClass, resFoo.getFile()), hasNoAlias());
+
+ assertThat("alias", resBar, isAliasFor(resFoo));
+ assertThat("uri.alias", newResource(resourceClass, resBar.getURI()), isAliasFor(resFoo));
+ assertThat("file.alias", newResource(resourceClass, resBar.getFile()), isAliasFor(resFoo));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testCaseInsensitiveAlias(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+ Path path = dir.resolve("file");
+ Files.createFile(path);
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ // Reference to actual resource that exists
+ Resource resource = base.addPath("file");
+
+ assertThat("resource.alias", resource, hasNoAlias());
+ assertThat("resource.uri.alias", newResource(resourceClass, resource.getURI()), hasNoAlias());
+ assertThat("resource.file.alias", newResource(resourceClass, resource.getFile()), hasNoAlias());
+
+ // On some case insensitive file systems, lets see if an alternate
+ // case for the filename results in an alias reference
+ Resource alias = base.addPath("FILE");
+ if (alias.exists())
+ {
+ // If it exists, it must be an alias
+ assertThat("alias", alias, isAliasFor(resource));
+ assertThat("alias.uri", newResource(resourceClass, alias.getURI()), isAliasFor(resource));
+ assertThat("alias.file", newResource(resourceClass, alias.getFile()), isAliasFor(resource));
+ }
+ }
+ }
+
+ /**
+ * Test for Windows feature that exposes 8.3 filename references
+ * for long filenames.
+ * <p>
+ * See: http://support.microsoft.com/kb/142982
+ *
+ * @throws Exception failed test
+ */
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ @EnabledOnOs(WINDOWS)
+ public void testCase8dot3Alias(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path path = dir.resolve("TextFile.Long.txt");
+ Files.createFile(path);
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ // Long filename
+ Resource resource = base.addPath("TextFile.Long.txt");
+
+ assertThat("resource.alias", resource, hasNoAlias());
+ assertThat("resource.uri.alias", newResource(resourceClass, resource.getURI()), hasNoAlias());
+ assertThat("resource.file.alias", newResource(resourceClass, resource.getFile()), hasNoAlias());
+
+ // On some versions of Windows, the long filename can be referenced
+ // via a short 8.3 equivalent filename.
+ Resource alias = base.addPath("TEXTFI~1.TXT");
+ if (alias.exists())
+ {
+ // If it exists, it must be an alias
+ assertThat("alias", alias, isAliasFor(resource));
+ assertThat("alias.uri", newResource(resourceClass, alias.getURI()), isAliasFor(resource));
+ assertThat("alias.file", newResource(resourceClass, alias.getFile()), isAliasFor(resource));
+ }
+ }
+ }
+
+ /**
+ * NTFS Alternative Data / File Streams.
+ * <p>
+ * See: http://msdn.microsoft.com/en-us/library/windows/desktop/aa364404(v=vs.85).aspx
+ *
+ * @throws Exception failed test
+ */
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ @EnabledOnOs(WINDOWS)
+ public void testNTFSFileStreamAlias(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path path = dir.resolve("testfile");
+ Files.createFile(path);
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource resource = base.addPath("testfile");
+
+ assertThat("resource.alias", resource, hasNoAlias());
+ assertThat("resource.uri.alias", newResource(resourceClass, resource.getURI()), hasNoAlias());
+ assertThat("resource.file.alias", newResource(resourceClass, resource.getFile()), hasNoAlias());
+
+ try
+ {
+ // Attempt to reference same file, but via NTFS simple stream
+ Resource alias = base.addPath("testfile:stream");
+ if (alias.exists())
+ {
+ // If it exists, it must be an alias
+ assertThat("resource.alias", alias, isAliasFor(resource));
+ assertThat("resource.uri.alias", newResource(resourceClass, alias.getURI()), isAliasFor(resource));
+ assertThat("resource.file.alias", newResource(resourceClass, alias.getFile()), isAliasFor(resource));
+ }
+ }
+ catch (InvalidPathException e)
+ {
+ // NTFS filesystem streams are unsupported on some platforms.
+ assumeTrue(false, "NTFS filesystem streams not supported");
+ }
+ }
+ }
+
+ /**
+ * NTFS Alternative Data / File Streams.
+ * <p>
+ * See: http://msdn.microsoft.com/en-us/library/windows/desktop/aa364404(v=vs.85).aspx
+ *
+ * @throws Exception failed test
+ */
+ @ParameterizedTest
+ @ValueSource(classes = PathResource.class) // not supported on FileResource
+ @EnabledOnOs(WINDOWS)
+ public void testNTFSFileDataStreamAlias(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path path = dir.resolve("testfile");
+ Files.createFile(path);
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource resource = base.addPath("testfile");
+
+ assertThat("resource.alias", resource, hasNoAlias());
+ assertThat("resource.uri.alias", newResource(resourceClass, resource.getURI()), hasNoAlias());
+ assertThat("resource.file.alias", newResource(resourceClass, resource.getFile()), hasNoAlias());
+
+ try
+ {
+ // Attempt to reference same file, but via NTFS DATA stream
+ Resource alias = base.addPath("testfile::$DATA");
+ if (alias.exists())
+ {
+ assumeTrue(alias.getURI().getScheme() == "file");
+
+ // If it exists, it must be an alias
+ assertThat("resource.alias", alias, isAliasFor(resource));
+ assertThat("resource.uri.alias", newResource(resourceClass, alias.getURI()), isAliasFor(resource));
+ assertThat("resource.file.alias", newResource(resourceClass, alias.getFile()), isAliasFor(resource));
+ }
+ }
+ catch (InvalidPathException e)
+ {
+ // NTFS filesystem streams are unsupported on some platforms.
+ assumeTrue(false, "NTFS filesystem streams not supported");
+ }
+ }
+ }
+
+ /**
+ * NTFS Alternative Data / File Streams.
+ * <p>
+ * See: http://msdn.microsoft.com/en-us/library/windows/desktop/aa364404(v=vs.85).aspx
+ *
+ * @throws Exception failed test
+ */
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ @EnabledOnOs(WINDOWS)
+ public void testNTFSFileEncodedDataStreamAlias(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path path = dir.resolve("testfile");
+ Files.createFile(path);
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource resource = base.addPath("testfile");
+
+ assertThat("resource.alias", resource, hasNoAlias());
+ assertThat("resource.uri.alias", newResource(resourceClass, resource.getURI()), hasNoAlias());
+ assertThat("resource.file.alias", newResource(resourceClass, resource.getFile()), hasNoAlias());
+
+ try
+ {
+ // Attempt to reference same file, but via NTFS DATA stream (encoded addPath version)
+ Resource alias = base.addPath("testfile::%24DATA");
+ if (alias.exists())
+ {
+ // If it exists, it must be an alias
+ assertThat("resource.alias", alias, isAliasFor(resource));
+ assertThat("resource.uri.alias", newResource(resourceClass, alias.getURI()), isAliasFor(resource));
+ assertThat("resource.file.alias", newResource(resourceClass, alias.getFile()), isAliasFor(resource));
+ }
+ }
+ catch (InvalidPathException e)
+ {
+ // NTFS filesystem streams are unsupported on some platforms.
+ assumeTrue(false, "NFTS Dats streams not supported");
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testSemicolon(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+
+ try
+ {
+ // attempt to create file
+ Path foo = dir.resolve("foo;");
+ Files.createFile(foo);
+ }
+ catch (Exception e)
+ {
+ // if unable to create file, no point testing the rest.
+ // this is the path that Microsoft Windows takes.
+ assumeTrue(false, "Not supported on this OS");
+ }
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource res = base.addPath("foo;");
+ assertThat("Alias: " + res, res, hasNoAlias());
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testSingleQuote(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ try
+ {
+ // attempt to create file
+ Path foo = dir.resolve("foo' bar");
+ Files.createFile(foo);
+ }
+ catch (Exception e)
+ {
+ // if unable to create file, no point testing the rest.
+ // this is the path that Microsoft Windows takes.
+ assumeTrue(false, "Not supported on this OS");
+ }
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource res = base.addPath("foo' bar");
+ assertThat("Alias: " + res, res.getAlias(), nullValue());
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testSingleBackTick(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ try
+ {
+ // attempt to create file
+ Path foo = dir.resolve("foo` bar");
+ Files.createFile(foo);
+ }
+ catch (Exception e)
+ {
+ // if unable to create file, no point testing the rest.
+ // this is the path that Microsoft Windows takes.
+ assumeTrue(false, "Not supported on this OS");
+ }
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource res = base.addPath("foo` bar");
+ assertThat("Alias: " + res, res.getAlias(), nullValue());
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testBrackets(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ try
+ {
+ // attempt to create file
+ Path foo = dir.resolve("foo[1]");
+ Files.createFile(foo);
+ }
+ catch (Exception e)
+ {
+ // if unable to create file, no point testing the rest.
+ // this is the path that Microsoft Windows takes.
+ assumeTrue(false, "Not supported on this OS");
+ }
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource res = base.addPath("foo[1]");
+ assertThat("Alias: " + res, res.getAlias(), nullValue());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(classes = PathResource.class) // FileResource does not support this
+ public void testBraces(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ try
+ {
+ // attempt to create file
+ Path foo = dir.resolve("foo.{bar}.txt");
+ Files.createFile(foo);
+ }
+ catch (Exception e)
+ {
+ // if unable to create file, no point testing the rest.
+ // this is the path that Microsoft Windows takes.
+ assumeTrue(false, "Not supported on this OS");
+ }
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource res = base.addPath("foo.{bar}.txt");
+ assertThat("Alias: " + res, res.getAlias(), nullValue());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(classes = PathResource.class) // FileResource does not support this
+ public void testCaret(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ try
+ {
+ // attempt to create file
+ Path foo = dir.resolve("foo^3.txt");
+ Files.createFile(foo);
+ }
+ catch (Exception e)
+ {
+ // if unable to create file, no point testing the rest.
+ // this is the path that Microsoft Windows takes.
+ assumeTrue(false, "Not supported on this OS");
+ }
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource res = base.addPath("foo^3.txt");
+ assertThat("Alias: " + res, res.getAlias(), nullValue());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(classes = PathResource.class) // FileResource does not support this
+ public void testPipe(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ try
+ {
+ // attempt to create file
+ Path foo = dir.resolve("foo|bar.txt");
+ Files.createFile(foo);
+ }
+ catch (Exception e)
+ {
+ // if unable to create file, no point testing the rest.
+ // this is the path that Microsoft Windows takes.
+ assumeTrue(false, "Not supported on this OS");
+ }
+
+ try (Resource base = newResource(resourceClass, dir.toFile()))
+ {
+ Resource res = base.addPath("foo|bar.txt");
+ assertThat("Alias: " + res, res.getAlias(), nullValue());
+ }
+ }
+
+ /**
+ * The most basic access example
+ *
+ * @throws Exception failed test
+ */
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testExistNormal(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path path = dir.resolve("a.jsp");
+ Files.createFile(path);
+
+ URI ref = workDir.getPath().toUri().resolve("a.jsp");
+ try (Resource fileres = newResource(resourceClass, ref))
+ {
+ assertThat("Resource: " + fileres, fileres.exists(), is(true));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(classes = PathResource.class) // FileResource not supported here
+ public void testSingleQuoteInFileName(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path fooA = dir.resolve("foo's.txt");
+ Path fooB = dir.resolve("f o's.txt");
+
+ Files.createFile(fooA);
+ Files.createFile(fooB);
+
+ URI refQuoted = dir.resolve("foo's.txt").toUri();
+
+ try (Resource fileres = newResource(resourceClass, refQuoted))
+ {
+ assertThat("Exists: " + refQuoted, fileres.exists(), is(true));
+ assertThat("Alias: " + refQuoted, fileres, hasNoAlias());
+ }
+
+ URI refEncoded = dir.toUri().resolve("foo%27s.txt");
+
+ try (Resource fileres = newResource(resourceClass, refEncoded))
+ {
+ assertThat("Exists: " + refEncoded, fileres.exists(), is(true));
+ assertThat("Alias: " + refEncoded, fileres, hasNoAlias());
+ }
+
+ URI refQuoteSpace = dir.toUri().resolve("f%20o's.txt");
+
+ try (Resource fileres = newResource(resourceClass, refQuoteSpace))
+ {
+ assertThat("Exists: " + refQuoteSpace, fileres.exists(), is(true));
+ assertThat("Alias: " + refQuoteSpace, fileres, hasNoAlias());
+ }
+
+ URI refEncodedSpace = dir.toUri().resolve("f%20o%27s.txt");
+
+ try (Resource fileres = newResource(resourceClass, refEncodedSpace))
+ {
+ assertThat("Exists: " + refEncodedSpace, fileres.exists(), is(true));
+ assertThat("Alias: " + refEncodedSpace, fileres, hasNoAlias());
+ }
+
+ URI refA = dir.toUri().resolve("foo's.txt");
+ URI refB = dir.toUri().resolve("foo%27s.txt");
+
+ StringBuilder msg = new StringBuilder();
+ msg.append("URI[a].equals(URI[b])").append(System.lineSeparator());
+ msg.append("URI[a] = ").append(refA).append(System.lineSeparator());
+ msg.append("URI[b] = ").append(refB);
+
+ // show that simple URI.equals() doesn't work
+ assertThat(msg.toString(), refA.equals(refB), is(false));
+
+ // now show that Resource.equals() does work
+ try (Resource a = newResource(resourceClass, refA);
+ Resource b = newResource(resourceClass, refB);)
+ {
+ assertThat("A.equals(B)", a.equals(b), is(true));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testExistBadURINull(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path path = dir.resolve("a.jsp");
+ Files.createFile(path);
+
+ try
+ {
+ // request with null at end
+ URI uri = workDir.getPath().toUri().resolve("a.jsp%00");
+ assertThat("Null URI", uri, notNullValue());
+
+ Resource r = newResource(resourceClass, uri);
+
+ // if we have r, then it better not exist
+ assertFalse(r.exists());
+ }
+ catch (InvalidPathException e)
+ {
+ // Exception is acceptable
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testExistBadURINullX(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path path = dir.resolve("a.jsp");
+ Files.createFile(path);
+
+ try
+ {
+ // request with null and x at end
+ URI uri = workDir.getPath().toUri().resolve("a.jsp%00x");
+ assertThat("NullX URI", uri, notNullValue());
+
+ Resource r = newResource(resourceClass, uri);
+
+ // if we have r, then it better not exist
+ assertFalse(r.exists());
+ }
+ catch (InvalidPathException e)
+ {
+ // Exception is acceptable
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testAddPathWindowsSlash(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path basePath = dir.resolve("base");
+ FS.ensureDirExists(basePath);
+ Path dirPath = basePath.resolve("aa");
+ FS.ensureDirExists(dirPath);
+ Path filePath = dirPath.resolve("foo.txt");
+ Files.createFile(filePath);
+
+ try (Resource base = newResource(resourceClass, basePath.toFile()))
+ {
+ assertThat("Exists: " + basePath, base.exists(), is(true));
+ assertThat("Alias: " + basePath, base, hasNoAlias());
+
+ Resource r = base.addPath("aa\\/foo.txt");
+ assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa%5C/foo.txt"));
+
+ if (org.junit.jupiter.api.condition.OS.WINDOWS.isCurrentOs())
+ {
+ assertThat("isAlias()", r.isAlias(), is(true));
+ assertThat("getAlias()", r.getAlias(), notNullValue());
+ assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("aa/foo.txt"));
+ assertThat("Exists: " + r, r.exists(), is(true));
+ }
+ else
+ {
+ assertThat("isAlias()", r.isAlias(), is(false));
+ assertThat("Exists: " + r, r.exists(), is(false));
+ }
+ }
+ catch (InvalidPathException e)
+ {
+ // Exception is acceptable
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testAddPathWindowsExtensionLess(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path basePath = dir.resolve("base");
+ FS.ensureDirExists(basePath);
+ Path dirPath = basePath.resolve("aa");
+ FS.ensureDirExists(dirPath);
+ Path filePath = dirPath.resolve("foo.txt");
+ Files.createFile(filePath);
+
+ try (Resource base = newResource(resourceClass, basePath.toFile()))
+ {
+ assertThat("Exists: " + basePath, base.exists(), is(true));
+ assertThat("Alias: " + basePath, base, hasNoAlias());
+
+ Resource r = base.addPath("aa./foo.txt");
+ assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa./foo.txt"));
+
+ if (OS.WINDOWS.isCurrentOs())
+ {
+ assertThat("isAlias()", r.isAlias(), is(true));
+ assertThat("getAlias()", r.getAlias(), notNullValue());
+ assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("aa/foo.txt"));
+ assertThat("Exists: " + r, r.exists(), is(true));
+ }
+ else
+ {
+ assertThat("isAlias()", r.isAlias(), is(false));
+ assertThat("Exists: " + r, r.exists(), is(false));
+ }
+ }
+ catch (InvalidPathException e)
+ {
+ // Exception is acceptable
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testAddInitialSlash(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path basePath = dir.resolve("base");
+ FS.ensureDirExists(basePath);
+ Path filePath = basePath.resolve("foo.txt");
+ Files.createFile(filePath);
+
+ try (Resource base = newResource(resourceClass, basePath.toFile()))
+ {
+ assertThat("Exists: " + basePath, base.exists(), is(true));
+ assertThat("Alias: " + basePath, base, hasNoAlias());
+
+ Resource r = base.addPath("/foo.txt");
+ assertThat("getURI()", r.getURI().toASCIIString(), containsString("/foo.txt"));
+
+ assertThat("isAlias()", r.isAlias(), is(false));
+ assertThat("Exists: " + r, r.exists(), is(true));
+ }
+ catch (InvalidPathException e)
+ {
+ // Exception is acceptable
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testAddInitialDoubleSlash(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path basePath = dir.resolve("base");
+ FS.ensureDirExists(basePath);
+ Path filePath = basePath.resolve("foo.txt");
+ Files.createFile(filePath);
+
+ try (Resource base = newResource(resourceClass, basePath.toFile()))
+ {
+ assertThat("Exists: " + basePath, base.exists(), is(true));
+ assertThat("Alias: " + basePath, base, hasNoAlias());
+
+ Resource r = base.addPath("//foo.txt");
+ assertThat("getURI()", r.getURI().toASCIIString(), containsString("//foo.txt"));
+
+ assertThat("isAlias()", r.isAlias(), is(true));
+ assertThat("getAlias()", r.getAlias(), notNullValue());
+ assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("/foo.txt"));
+ assertThat("Exists: " + r, r.exists(), is(true));
+ }
+ catch (InvalidPathException e)
+ {
+ // Exception is acceptable
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testAddDoubleSlash(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path basePath = dir.resolve("base");
+ FS.ensureDirExists(basePath);
+ Path dirPath = basePath.resolve("aa");
+ FS.ensureDirExists(dirPath);
+ Path filePath = dirPath.resolve("foo.txt");
+ Files.createFile(filePath);
+
+ try (Resource base = newResource(resourceClass, basePath.toFile()))
+ {
+ assertThat("Exists: " + basePath, base.exists(), is(true));
+ assertThat("Alias: " + basePath, base, hasNoAlias());
+
+ Resource r = base.addPath("aa//foo.txt");
+ assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa//foo.txt"));
+
+ assertThat("isAlias()", r.isAlias(), is(true));
+ assertThat("getAlias()", r.getAlias(), notNullValue());
+ assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("aa/foo.txt"));
+ assertThat("Exists: " + r, r.exists(), is(true));
+ }
+ catch (InvalidPathException e)
+ {
+ // Exception is acceptable
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testEncoding(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Files.createDirectories(dir);
+
+ Path specials = dir.resolve("a file with,spe#ials");
+ Files.createFile(specials);
+
+ try (Resource res = newResource(resourceClass, specials.toFile()))
+ {
+ assertThat("Specials URL", res.getURI().toASCIIString(), containsString("a%20file%20with,spe%23ials"));
+ assertThat("Specials Filename", res.getFile().toString(), containsString("a file with,spe#ials"));
+
+ res.delete();
+ assertThat("File should have been deleted.", res.exists(), is(false));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("fsResourceProvider")
+ public void testUtf8Dir(Class resourceClass) throws Exception
+ {
+ Path dir = workDir.getEmptyPathDir();
+ Path utf8Dir;
+
+ try
+ {
+ utf8Dir = dir.resolve("bãm");
+ Files.createDirectories(utf8Dir);
+ }
+ catch (InvalidPathException e)
+ {
+ // if unable to create file, no point testing the rest.
+ // this is the path that occurs if you have a system that doesn't support UTF-8
+ // directory names (or you simply don't have a Locale set properly)
+ assumeTrue(false, "Not supported on this OS");
+ return;
+ }
+
+ Path file = utf8Dir.resolve("file.txt");
+ Files.createFile(file);
+
+ try (Resource base = newResource(resourceClass, utf8Dir.toFile()))
+ {
+ assertThat("Exists: " + utf8Dir, base.exists(), is(true));
+ assertThat("Alias: " + utf8Dir, base, hasNoAlias());
+
+ Resource r = base.addPath("file.txt");
+ assertThat("Exists: " + r, r.exists(), is(true));
+ assertThat("Alias: " + r, r, hasNoAlias());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(classes = PathResource.class) // FileResource does not support this
+ @EnabledOnOs(WINDOWS)
+ public void testUncPath(Class resourceClass) throws Exception
+ {
+ try (Resource base = newResource(resourceClass, URI.create("file:////127.0.0.1/path")))
+ {
+ Resource resource = base.addPath("WEB-INF/");
+ assertThat("getURI()", resource.getURI().toASCIIString(), containsString("path/WEB-INF/"));
+ assertThat("isAlias()", resource.isAlias(), is(false));
+ assertThat("getAlias()", resource.getAlias(), nullValue());
+ }
+ }
+
+ private String toString(Resource resource) throws IOException
+ {
+ try (InputStream inputStream = resource.getInputStream();
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream())
+ {
+ IO.copy(inputStream, outputStream);
+ return outputStream.toString("utf-8");
+ }
+ }
+
+ private void touchFile(Path outputFile, String content) throws IOException
+ {
+ try (StringReader reader = new StringReader(content);
+ BufferedWriter writer = Files.newBufferedWriter(outputFile))
+ {
+ IO.copy(reader, writer);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java
new file mode 100644
index 0000000..da53c9b
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java
@@ -0,0 +1,286 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.zip.ZipFile;
+
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(WorkDirExtension.class)
+public class JarResourceTest
+{
+ public WorkDir workDir;
+
+ @Test
+ public void testJarFile()
+ throws Exception
+ {
+ Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/test.zip");
+ String s = "jar:" + testZip.toUri().toASCIIString() + "!/subdir/";
+ Resource r = Resource.newResource(s);
+
+ Set<String> entries = new HashSet<>(Arrays.asList(r.list()));
+ assertThat(entries, containsInAnyOrder("alphabet", "numbers", "subsubdir/"));
+
+ Path extract = workDir.getPathFile("extract");
+ FS.ensureEmpty(extract);
+
+ r.copyTo(extract.toFile());
+
+ Resource e = Resource.newResource(extract.toString());
+
+ entries = new HashSet<>(Arrays.asList(e.list()));
+ assertThat(entries, containsInAnyOrder("alphabet", "numbers", "subsubdir/"));
+
+ s = "jar:" + testZip.toUri().toASCIIString() + "!/subdir/subsubdir/";
+ r = Resource.newResource(s);
+
+ entries = new HashSet<>(Arrays.asList(r.list()));
+ assertThat(entries, containsInAnyOrder("alphabet", "numbers"));
+
+ Path extract2 = workDir.getPathFile("extract2");
+ FS.ensureEmpty(extract2);
+
+ r.copyTo(extract2.toFile());
+
+ e = Resource.newResource(extract2.toString());
+
+ entries = new HashSet<>(Arrays.asList(e.list()));
+ assertThat(entries, containsInAnyOrder("alphabet", "numbers"));
+ }
+
+ @Test
+ public void testJarFileGetAllResoures()
+ throws Exception
+ {
+ Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/test.zip");
+ String s = "jar:" + testZip.toUri().toASCIIString() + "!/subdir/";
+ Resource r = Resource.newResource(s);
+ Collection<Resource> deep = r.getAllResources();
+
+ assertEquals(4, deep.size());
+ }
+
+ @Test
+ public void testJarFileIsContainedIn()
+ throws Exception
+ {
+ Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/test.zip");
+ String s = "jar:" + testZip.toUri().toASCIIString() + "!/subdir/";
+ Resource r = Resource.newResource(s);
+ Resource container = Resource.newResource(testZip);
+
+ assertThat(r, instanceOf(JarFileResource.class));
+ JarFileResource jarFileResource = (JarFileResource)r;
+
+ assertTrue(jarFileResource.isContainedIn(container));
+
+ container = Resource.newResource(testZip.getParent());
+ assertFalse(jarFileResource.isContainedIn(container));
+ }
+
+ @Test
+ public void testJarFileLastModified()
+ throws Exception
+ {
+ Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/test.zip");
+
+ String s = "jar:" + testZip.toUri().toASCIIString() + "!/subdir/numbers";
+
+ try (ZipFile zf = new ZipFile(testZip.toFile()))
+ {
+ long last = zf.getEntry("subdir/numbers").getTime();
+
+ Resource r = Resource.newResource(s);
+ assertEquals(last, r.lastModified());
+ }
+ }
+
+ @Test
+ public void testJarFileCopyToDirectoryTraversal() throws Exception
+ {
+ Path extractZip = MavenTestingUtils.getTestResourcePathFile("TestData/extract.zip");
+
+ String s = "jar:" + extractZip.toUri().toASCIIString() + "!/";
+ Resource r = Resource.newResource(s);
+
+ assertThat(r, instanceOf(JarResource.class));
+ JarResource jarResource = (JarResource)r;
+
+ Path destParent = workDir.getPathFile("copyjar");
+ FS.ensureEmpty(destParent);
+
+ Path dest = destParent.toRealPath().resolve("extract");
+ FS.ensureEmpty(dest);
+
+ jarResource.copyTo(dest.toFile());
+
+ // dest contains only the valid entry; dest.getParent() contains only the dest directory
+ assertEquals(1, listFiles(dest).size());
+ assertEquals(1, listFiles(dest.getParent()).size());
+
+ DirectoryStream.Filter<? super Path> dotdotFilenameFilter = (path) ->
+ path.getFileName().toString().equalsIgnoreCase("dotdot.dot");
+
+ assertEquals(0, listFiles(dest, dotdotFilenameFilter).size());
+ assertEquals(0, listFiles(dest.getParent(), dotdotFilenameFilter).size());
+
+ DirectoryStream.Filter<? super Path> extractfileFilenameFilter = (path) ->
+ path.getFileName().toString().equalsIgnoreCase("extract-filenotdir");
+
+ assertEquals(0, listFiles(dest, extractfileFilenameFilter).size());
+ assertEquals(0, listFiles(dest.getParent(), extractfileFilenameFilter).size());
+
+ DirectoryStream.Filter<? super Path> currentDirectoryFilenameFilter = (path) ->
+ path.getFileName().toString().equalsIgnoreCase("current.txt");
+
+ assertEquals(1, listFiles(dest, currentDirectoryFilenameFilter).size());
+ assertEquals(0, listFiles(dest.getParent(), currentDirectoryFilenameFilter).size());
+ }
+
+ @Test
+ public void testEncodedFileName()
+ throws Exception
+ {
+ Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/test.zip");
+
+ String s = "jar:" + testZip.toUri().toASCIIString() + "!/file%20name.txt";
+ Resource r = Resource.newResource(s);
+ assertTrue(r.exists());
+ }
+
+ @Test
+ public void testJarFileResourceList() throws Exception
+ {
+ Path testJar = MavenTestingUtils.getTestResourcePathFile("jar-file-resource.jar");
+ String uri = "jar:" + testJar.toUri().toASCIIString() + "!/";
+
+ Resource resource = new JarFileResource(URI.create(uri).toURL());
+ Resource rez = resource.addPath("rez/");
+
+ assertThat("path /rez/ is a dir", rez.isDirectory(), is(true));
+
+ List<String> actual = Arrays.asList(rez.list());
+ String[] expected = new String[]{
+ "one",
+ "aaa",
+ "bbb",
+ "oddities/",
+ "another dir/",
+ "ccc",
+ "deep/",
+ };
+ assertThat("Dir contents", actual, containsInAnyOrder(expected));
+ }
+
+ /**
+ * Test getting a file listing of a Directory in a JAR
+ * Where the JAR entries contain names that are URI encoded / escaped
+ */
+ @Test
+ public void testJarFileResourceListPreEncodedEntries() throws Exception
+ {
+ Path testJar = MavenTestingUtils.getTestResourcePathFile("jar-file-resource.jar");
+ String uri = "jar:" + testJar.toUri().toASCIIString() + "!/";
+
+ Resource resource = new JarFileResource(URI.create(uri).toURL());
+ Resource rez = resource.addPath("rez/oddities/");
+
+ assertThat("path /rez/oddities/ is a dir", rez.isDirectory(), is(true));
+
+ List<String> actual = Arrays.asList(rez.list());
+ String[] expected = new String[]{
+ ";",
+ "#hashcode",
+ "index.html#fragment",
+ "other%2fkind%2Fof%2fslash", // pre-encoded / escaped
+ "a file with a space",
+ ";\" onmousedown=\"alert(document.location)\"",
+ "some\\slash\\you\\got\\there" // not encoded, stored as backslash native
+ };
+ assertThat("Dir contents", actual, containsInAnyOrder(expected));
+ }
+
+ @Test
+ public void testJarFileResourceListDirWithSpace() throws Exception
+ {
+ Path testJar = MavenTestingUtils.getTestResourcePathFile("jar-file-resource.jar");
+ String uri = "jar:" + testJar.toUri().toASCIIString() + "!/";
+
+ Resource resource = new JarFileResource(URI.create(uri).toURL());
+ Resource anotherDir = resource.addPath("rez/another dir/");
+
+ assertThat("path /rez/another dir/ is a dir", anotherDir.isDirectory(), is(true));
+
+ List<String> actual = Arrays.asList(anotherDir.list());
+ String[] expected = new String[]{
+ "a file.txt",
+ "another file.txt",
+ "..\\a different file.txt",
+ };
+ assertThat("Dir contents", actual, containsInAnyOrder(expected));
+ }
+
+ private List<Path> listFiles(Path dir) throws IOException
+ {
+ try (Stream<Path> s = Files.list(dir))
+ {
+ return s.collect(Collectors.toList());
+ }
+ }
+
+ private List<Path> listFiles(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException
+ {
+ List<Path> results = new ArrayList<>();
+ try (DirectoryStream<Path> filteredDirStream = Files.newDirectoryStream(dir, filter))
+ {
+ for (Path path : filteredDirStream)
+ {
+ results.add(path);
+ }
+ return results;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JrtResourceTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JrtResourceTest.java
new file mode 100644
index 0000000..875fc16
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JrtResourceTest.java
@@ -0,0 +1,112 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.net.URI;
+
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.TypeUtil;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnJre;
+import org.junit.jupiter.api.condition.JRE;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.startsWith;
+
+public class JrtResourceTest
+{
+ @Test
+ @DisabledOnJre(JRE.JAVA_8)
+ @Disabled("Not supported on Java 9+ Module API")
+ public void testResourceFromUriForString()
+ throws Exception
+ {
+ URI stringLoc = TypeUtil.getLocationOfClass(String.class);
+ Resource resource = Resource.newResource(stringLoc);
+
+ assertThat(resource.exists(), is(true));
+ assertThat(resource.isDirectory(), is(false));
+ assertThat(IO.readBytes(resource.getInputStream()).length, Matchers.greaterThan(0));
+ assertThat(IO.readBytes(resource.getInputStream()).length, is((int)resource.length()));
+ assertThat(resource.getWeakETag("-xxx"), startsWith("W/\""));
+ assertThat(resource.getWeakETag("-xxx"), endsWith("-xxx\""));
+ }
+
+ @Test
+ @DisabledOnJre(JRE.JAVA_8)
+ @Disabled("Not supported on Java 9+ Module API")
+ public void testResourceFromStringForString()
+ throws Exception
+ {
+ URI stringLoc = TypeUtil.getLocationOfClass(String.class);
+ Resource resource = Resource.newResource(stringLoc.toASCIIString());
+
+ assertThat(resource.exists(), is(true));
+ assertThat(resource.isDirectory(), is(false));
+ assertThat(IO.readBytes(resource.getInputStream()).length, Matchers.greaterThan(0));
+ assertThat(IO.readBytes(resource.getInputStream()).length, is((int)resource.length()));
+ assertThat(resource.getWeakETag("-xxx"), startsWith("W/\""));
+ assertThat(resource.getWeakETag("-xxx"), endsWith("-xxx\""));
+ }
+
+ @Test
+ @DisabledOnJre(JRE.JAVA_8)
+ @Disabled("Not supported on Java 9+ Module API")
+ public void testResourceFromURLForString()
+ throws Exception
+ {
+ URI stringLoc = TypeUtil.getLocationOfClass(String.class);
+ Resource resource = Resource.newResource(stringLoc.toURL());
+
+ assertThat(resource.exists(), is(true));
+ assertThat(resource.isDirectory(), is(false));
+ assertThat(IO.readBytes(resource.getInputStream()).length, Matchers.greaterThan(0));
+ assertThat(IO.readBytes(resource.getInputStream()).length, is((int)resource.length()));
+ assertThat(resource.getWeakETag("-xxx"), startsWith("W/\""));
+ assertThat(resource.getWeakETag("-xxx"), endsWith("-xxx\""));
+ }
+
+ @Test
+ @DisabledOnJre(JRE.JAVA_8)
+ public void testResourceModule()
+ throws Exception
+ {
+ Resource resource = Resource.newResource("jrt:/java.base");
+
+ assertThat(resource.exists(), is(false));
+ assertThat(resource.isDirectory(), is(false));
+ assertThat(resource.length(), is(-1L));
+ }
+
+ @Test
+ @DisabledOnJre(JRE.JAVA_8)
+ public void testResourceAllModules()
+ throws Exception
+ {
+ Resource resource = Resource.newResource("jrt:/");
+
+ assertThat(resource.exists(), is(false));
+ assertThat(resource.isDirectory(), is(false));
+ assertThat(resource.length(), is(-1L));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java
new file mode 100644
index 0000000..e4bf10b
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java
@@ -0,0 +1,166 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
+
+public class PathResourceTest
+{
+ @Test
+ public void testNonDefaultFileSystemGetInputStream() throws URISyntaxException, IOException
+ {
+ Path exampleJar = MavenTestingUtils.getTestResourcePathFile("example.jar");
+
+ URI uri = new URI("jar", exampleJar.toUri().toASCIIString(), null);
+
+ Map<String, Object> env = new HashMap<>();
+ env.put("multi-release", "runtime");
+
+ try (FileSystem zipfs = FileSystems.newFileSystem(uri, env))
+ {
+ Path manifestPath = zipfs.getPath("/META-INF/MANIFEST.MF");
+ assertThat(manifestPath, is(not(nullValue())));
+
+ PathResource resource = new PathResource(manifestPath);
+
+ try (InputStream inputStream = resource.getInputStream())
+ {
+ assertThat("InputStream", inputStream, is(not(nullValue())));
+ }
+ }
+ }
+
+ @Test
+ public void testNonDefaultFileSystemGetReadableByteChannel() throws URISyntaxException, IOException
+ {
+ Path exampleJar = MavenTestingUtils.getTestResourcePathFile("example.jar");
+
+ URI uri = new URI("jar", exampleJar.toUri().toASCIIString(), null);
+
+ Map<String, Object> env = new HashMap<>();
+ env.put("multi-release", "runtime");
+
+ try (FileSystem zipfs = FileSystems.newFileSystem(uri, env))
+ {
+ Path manifestPath = zipfs.getPath("/META-INF/MANIFEST.MF");
+ assertThat(manifestPath, is(not(nullValue())));
+
+ PathResource resource = new PathResource(manifestPath);
+
+ try (ReadableByteChannel channel = resource.getReadableByteChannel())
+ {
+ assertThat("ReadableByteChannel", channel, is(not(nullValue())));
+ }
+ }
+ }
+
+ @Test
+ public void testNonDefaultFileSystemGetFile() throws URISyntaxException, IOException
+ {
+ Path exampleJar = MavenTestingUtils.getTestResourcePathFile("example.jar");
+
+ URI uri = new URI("jar", exampleJar.toUri().toASCIIString(), null);
+
+ Map<String, Object> env = new HashMap<>();
+ env.put("multi-release", "runtime");
+
+ try (FileSystem zipfs = FileSystems.newFileSystem(uri, env))
+ {
+ Path manifestPath = zipfs.getPath("/META-INF/MANIFEST.MF");
+ assertThat(manifestPath, is(not(nullValue())));
+
+ PathResource resource = new PathResource(manifestPath);
+ File file = resource.getFile();
+ assertThat("File should be null for non-default FileSystem", file, is(nullValue()));
+ }
+ }
+
+ @Test
+ public void testNonDefaultFileSystemWriteTo() throws URISyntaxException, IOException
+ {
+ Path exampleJar = MavenTestingUtils.getTestResourcePathFile("example.jar");
+
+ URI uri = new URI("jar", exampleJar.toUri().toASCIIString(), null);
+
+ Map<String, Object> env = new HashMap<>();
+ env.put("multi-release", "runtime");
+
+ try (FileSystem zipfs = FileSystems.newFileSystem(uri, env))
+ {
+ Path manifestPath = zipfs.getPath("/META-INF/MANIFEST.MF");
+ assertThat(manifestPath, is(not(nullValue())));
+
+ PathResource resource = new PathResource(manifestPath);
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream())
+ {
+ resource.writeTo(out, 2, 10);
+ String actual = new String(out.toByteArray(), UTF_8);
+ String expected = "nifest-Ver";
+ assertThat("writeTo(out, 2, 10)", actual, is(expected));
+ }
+ }
+ }
+
+ @Test
+ public void testDefaultFileSystemGetFile() throws Exception
+ {
+ Path exampleJar = MavenTestingUtils.getTestResourcePathFile("example.jar");
+ PathResource resource = new PathResource(exampleJar);
+
+ File file = resource.getFile();
+ assertThat("File for default FileSystem", file, is(exampleJar.toFile()));
+ }
+
+ @Test
+ public void testSame() throws Exception
+ {
+ Path rpath = MavenTestingUtils.getTestResourcePathFile("resource.txt");
+ Path epath = MavenTestingUtils.getTestResourcePathFile("example.jar");
+ PathResource rPathResource = new PathResource(rpath);
+ FileResource rFileResource = new FileResource(rpath.toFile());
+ PathResource ePathResource = new PathResource(epath);
+ FileResource eFileResource = new FileResource(epath.toFile());
+
+ assertThat(rPathResource.isSame(rPathResource), Matchers.is(true));
+ assertThat(rPathResource.isSame(rFileResource), Matchers.is(true));
+ assertThat(rPathResource.isSame(ePathResource), Matchers.is(false));
+ assertThat(rPathResource.isSame(eFileResource), Matchers.is(false));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceAliasTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceAliasTest.java
new file mode 100644
index 0000000..64876d3
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceAliasTest.java
@@ -0,0 +1,172 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.log.Log;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(WorkDirExtension.class)
+public class ResourceAliasTest
+{
+ public WorkDir workDir;
+
+ public static Stream<Function<Path, Resource>> resourceTypes()
+ {
+ List<Function<Path, Resource>> types = new ArrayList<>();
+
+ types.add((path) -> new PathResource(path));
+ types.add((path) -> new FileResource(path.toFile()));
+
+ return types.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("resourceTypes")
+ public void testPercentPaths(Function<Path, Resource> resourceType) throws IOException
+ {
+ Path baseDir = workDir.getEmptyPathDir();
+
+ Path foo = baseDir.resolve("%foo");
+ Files.createDirectories(foo);
+
+ Path bar = foo.resolve("bar%");
+ Files.createDirectories(bar);
+
+ Path text = bar.resolve("test.txt");
+ FS.touch(text);
+
+ // At this point we have a path .../%foo/bar%/test.txt present on the filesystem.
+ // This would also apply for paths found in JAR files (like META-INF/resources/%foo/bar%/test.txt)
+
+ assertTrue(Files.exists(text));
+
+ Resource baseResource = resourceType.apply(baseDir);
+ assertTrue(baseResource.exists(), "baseResource exists");
+
+ Resource fooResource = baseResource.addPath("%foo");
+ assertTrue(fooResource.exists(), "fooResource exists");
+ assertTrue(fooResource.isDirectory(), "fooResource isDir");
+ if (fooResource instanceof FileResource)
+ assertTrue(fooResource.isAlias(), "fooResource isAlias");
+ else
+ assertFalse(fooResource.isAlias(), "fooResource isAlias");
+
+ Resource barResource = fooResource.addPath("bar%");
+ assertTrue(barResource.exists(), "barResource exists");
+ assertTrue(barResource.isDirectory(), "barResource isDir");
+ if (fooResource instanceof FileResource)
+ assertTrue(barResource.isAlias(), "barResource isAlias");
+ else
+ assertFalse(barResource.isAlias(), "barResource isAlias");
+
+ Resource textResource = barResource.addPath("test.txt");
+ assertTrue(textResource.exists(), "textResource exists");
+ assertFalse(textResource.isDirectory(), "textResource isDir");
+ }
+
+ @Test
+ public void testNullCharEndingFilename() throws Exception
+ {
+ Path baseDir = workDir.getEmptyPathDir();
+
+ Path file = baseDir.resolve("test.txt");
+ FS.touch(file);
+
+ try
+ {
+ Path file0 = baseDir.resolve("test.txt\0");
+ if (!Files.exists(file0))
+ return; // this file system does get tricked by ending filenames
+
+ assertThat(file0 + " exists", Files.exists(file0), is(true)); // This is an alias!
+
+ Resource dir = Resource.newResource(baseDir);
+
+ // Test not alias paths
+ Resource resource = Resource.newResource(file);
+ assertTrue(resource.exists());
+ assertNull(resource.getAlias());
+ resource = Resource.newResource(file.toAbsolutePath());
+ assertTrue(resource.exists());
+ assertNull(resource.getAlias());
+ resource = Resource.newResource(file.toUri());
+ assertTrue(resource.exists());
+ assertNull(resource.getAlias());
+ resource = Resource.newResource(file.toUri().toString());
+ assertTrue(resource.exists());
+ assertNull(resource.getAlias());
+ resource = dir.addPath("test.txt");
+ assertTrue(resource.exists());
+ assertNull(resource.getAlias());
+
+ // Test alias paths
+ resource = Resource.newResource(file0);
+ assertTrue(resource.exists());
+ assertNotNull(resource.getAlias());
+ resource = Resource.newResource(file0.toAbsolutePath());
+ assertTrue(resource.exists());
+ assertNotNull(resource.getAlias());
+ resource = Resource.newResource(file0.toUri());
+ assertTrue(resource.exists());
+ assertNotNull(resource.getAlias());
+ resource = Resource.newResource(file0.toUri().toString());
+ assertTrue(resource.exists());
+ assertNotNull(resource.getAlias());
+
+ try
+ {
+ resource = dir.addPath("test.txt\0");
+ assertTrue(resource.exists());
+ assertNotNull(resource.getAlias());
+ }
+ catch (MalformedURLException e)
+ {
+ assertTrue(true);
+ }
+ }
+ catch (InvalidPathException e)
+ {
+ // this file system does allow null char ending filenames
+ Log.getRootLogger().ignore(e);
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java
new file mode 100644
index 0000000..4013cdb
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java
@@ -0,0 +1,271 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStreamReader;
+import java.nio.file.Path;
+import java.util.Arrays;
+
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.IO;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(WorkDirExtension.class)
+public class ResourceCollectionTest
+{
+ public WorkDir workdir;
+
+ @Test
+ public void testUnsetCollectionThrowsISE()
+ {
+ ResourceCollection coll = new ResourceCollection();
+
+ assertThrowIllegalStateException(coll);
+ }
+
+ @Test
+ public void testEmptyResourceArrayThrowsISE()
+ {
+ ResourceCollection coll = new ResourceCollection(new Resource[0]);
+
+ assertThrowIllegalStateException(coll);
+ }
+
+ @Test
+ public void testResourceArrayWithNullThrowsISE()
+ {
+ ResourceCollection coll = new ResourceCollection(new Resource[]{null});
+
+ assertThrowIllegalStateException(coll);
+ }
+
+ @Test
+ public void testEmptyStringArrayThrowsISE()
+ {
+ ResourceCollection coll = new ResourceCollection(new String[0]);
+
+ assertThrowIllegalStateException(coll);
+ }
+
+ @Test
+ public void testStringArrayWithNullThrowsIAE()
+ {
+ assertThrows(IllegalArgumentException.class,
+ () -> new ResourceCollection(new String[]{null}));
+ }
+
+ @Test
+ public void testNullCsvThrowsIAE()
+ {
+ assertThrows(IllegalArgumentException.class, () ->
+ {
+ String csv = null;
+ new ResourceCollection(csv); // throws IAE
+ });
+ }
+
+ @Test
+ public void testEmptyCsvThrowsIAE()
+ {
+ assertThrows(IllegalArgumentException.class, () ->
+ {
+ String csv = "";
+ new ResourceCollection(csv); // throws IAE
+ });
+ }
+
+ @Test
+ public void testBlankCsvThrowsIAE()
+ {
+ assertThrows(IllegalArgumentException.class, () ->
+ {
+ String csv = ",,,,";
+ new ResourceCollection(csv); // throws IAE
+ });
+ }
+
+ @Test
+ public void testSetResourceNullThrowsISE()
+ {
+ // Create a ResourceCollection with one valid entry
+ Path path = MavenTestingUtils.getTargetPath();
+ PathResource resource = new PathResource(path);
+ ResourceCollection coll = new ResourceCollection(resource);
+
+ // Reset collection to invalid state
+ coll.setResources(null);
+
+ assertThrowIllegalStateException(coll);
+ }
+
+ @Test
+ public void testSetResourceEmptyThrowsISE()
+ {
+ // Create a ResourceCollection with one valid entry
+ Path path = MavenTestingUtils.getTargetPath();
+ PathResource resource = new PathResource(path);
+ ResourceCollection coll = new ResourceCollection(resource);
+
+ // Reset collection to invalid state
+ coll.setResources(new Resource[0]);
+
+ assertThrowIllegalStateException(coll);
+ }
+
+ @Test
+ public void testSetResourceAllNullsThrowsISE()
+ {
+ // Create a ResourceCollection with one valid entry
+ Path path = MavenTestingUtils.getTargetPath();
+ PathResource resource = new PathResource(path);
+ ResourceCollection coll = new ResourceCollection(resource);
+
+ // Reset collection to invalid state
+ assertThrows(IllegalStateException.class, () -> coll.setResources(new Resource[]{null, null, null}));
+
+ // Ensure not modified.
+ assertThat(coll.getResources().length, is(1));
+ }
+
+ private void assertThrowIllegalStateException(ResourceCollection coll)
+ {
+ assertThrows(IllegalStateException.class, () -> coll.addPath("foo"));
+ assertThrows(IllegalStateException.class, coll::exists);
+ assertThrows(IllegalStateException.class, coll::getFile);
+ assertThrows(IllegalStateException.class, coll::getInputStream);
+ assertThrows(IllegalStateException.class, coll::getReadableByteChannel);
+ assertThrows(IllegalStateException.class, coll::getURL);
+ assertThrows(IllegalStateException.class, coll::getName);
+ assertThrows(IllegalStateException.class, coll::isDirectory);
+ assertThrows(IllegalStateException.class, coll::lastModified);
+ assertThrows(IllegalStateException.class, coll::list);
+ assertThrows(IllegalStateException.class, coll::close);
+ assertThrows(IllegalStateException.class, () ->
+ {
+ Path destPath = workdir.getPathFile("bar");
+ coll.copyTo(destPath.toFile());
+ });
+ }
+
+ @Test
+ public void testList() throws Exception
+ {
+ ResourceCollection rc1 = new ResourceCollection(
+ Resource.newResource("src/test/resources/org/eclipse/jetty/util/resource/one/"),
+ Resource.newResource("src/test/resources/org/eclipse/jetty/util/resource/two/"),
+ Resource.newResource("src/test/resources/org/eclipse/jetty/util/resource/three/"));
+
+ assertThat(Arrays.asList(rc1.list()), contains("1.txt", "2.txt", "3.txt", "dir/"));
+ assertThat(Arrays.asList(rc1.addPath("dir").list()), contains("1.txt", "2.txt", "3.txt"));
+ assertThat(rc1.addPath("unknown").list(), nullValue());
+ // TODO for jetty-10 assertThat(rc1.addPath("unknown").list(), nullValue());
+ }
+
+ @Test
+ public void testMultipleSources1() throws Exception
+ {
+ ResourceCollection rc1 = new ResourceCollection(new String[]{
+ "src/test/resources/org/eclipse/jetty/util/resource/one/",
+ "src/test/resources/org/eclipse/jetty/util/resource/two/",
+ "src/test/resources/org/eclipse/jetty/util/resource/three/"
+ });
+ assertEquals(getContent(rc1, "1.txt"), "1 - one");
+ assertEquals(getContent(rc1, "2.txt"), "2 - two");
+ assertEquals(getContent(rc1, "3.txt"), "3 - three");
+
+ ResourceCollection rc2 = new ResourceCollection(
+ "src/test/resources/org/eclipse/jetty/util/resource/one/," +
+ "src/test/resources/org/eclipse/jetty/util/resource/two/," +
+ "src/test/resources/org/eclipse/jetty/util/resource/three/"
+ );
+ assertEquals(getContent(rc2, "1.txt"), "1 - one");
+ assertEquals(getContent(rc2, "2.txt"), "2 - two");
+ assertEquals(getContent(rc2, "3.txt"), "3 - three");
+ }
+
+ @Test
+ public void testMergedDir() throws Exception
+ {
+ ResourceCollection rc = new ResourceCollection(new String[]{
+ "src/test/resources/org/eclipse/jetty/util/resource/one/",
+ "src/test/resources/org/eclipse/jetty/util/resource/two/",
+ "src/test/resources/org/eclipse/jetty/util/resource/three/"
+ });
+
+ Resource r = rc.addPath("dir");
+ assertTrue(r instanceof ResourceCollection);
+ rc = (ResourceCollection)r;
+ assertEquals(getContent(rc, "1.txt"), "1 - one");
+ assertEquals(getContent(rc, "2.txt"), "2 - two");
+ assertEquals(getContent(rc, "3.txt"), "3 - three");
+ }
+
+ @Test
+ public void testCopyTo() throws Exception
+ {
+ ResourceCollection rc = new ResourceCollection(new String[]{
+ "src/test/resources/org/eclipse/jetty/util/resource/one/",
+ "src/test/resources/org/eclipse/jetty/util/resource/two/",
+ "src/test/resources/org/eclipse/jetty/util/resource/three/"
+ });
+
+ File dest = MavenTestingUtils.getTargetTestingDir("copyto");
+ FS.ensureDirExists(dest);
+ rc.copyTo(dest);
+
+ Resource r = Resource.newResource(dest.toURI());
+ assertEquals(getContent(r, "1.txt"), "1 - one");
+ assertEquals(getContent(r, "2.txt"), "2 - two");
+ assertEquals(getContent(r, "3.txt"), "3 - three");
+ r = r.addPath("dir");
+ assertEquals(getContent(r, "1.txt"), "1 - one");
+ assertEquals(getContent(r, "2.txt"), "2 - two");
+ assertEquals(getContent(r, "3.txt"), "3 - three");
+
+ IO.delete(dest);
+ }
+
+ static String getContent(Resource r, String path) throws Exception
+ {
+ StringBuilder buffer = new StringBuilder();
+ String line;
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(r.addPath(path).getURL().openStream())))
+ {
+ while ((line = br.readLine()) != null)
+ {
+ buffer.append(line);
+ }
+ }
+ return buffer.toString();
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java
new file mode 100644
index 0000000..cf161c1
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java
@@ -0,0 +1,355 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.resource;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.IO;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+public class ResourceTest
+{
+ private static final boolean DIR = true;
+ private static final boolean EXISTS = true;
+
+ static class Scenario
+ {
+ Resource resource;
+ String test;
+ boolean exists;
+ boolean dir;
+ String content;
+
+ Scenario(Scenario data, String path, boolean exists, boolean dir)
+ throws Exception
+ {
+ this.test = data.resource + "+" + path;
+ resource = data.resource.addPath(path);
+ this.exists = exists;
+ this.dir = dir;
+ }
+
+ Scenario(Scenario data, String path, boolean exists, boolean dir, String content)
+ throws Exception
+ {
+ this.test = data.resource + "+" + path;
+ resource = data.resource.addPath(path);
+ this.exists = exists;
+ this.dir = dir;
+ this.content = content;
+ }
+
+ Scenario(URL url, boolean exists, boolean dir)
+ throws Exception
+ {
+ this.test = url.toString();
+ this.exists = exists;
+ this.dir = dir;
+ resource = Resource.newResource(url);
+ }
+
+ Scenario(String url, boolean exists, boolean dir)
+ throws Exception
+ {
+ this.test = url;
+ this.exists = exists;
+ this.dir = dir;
+ resource = Resource.newResource(url);
+ }
+
+ Scenario(URI uri, boolean exists, boolean dir)
+ throws Exception
+ {
+ this.test = uri.toASCIIString();
+ this.exists = exists;
+ this.dir = dir;
+ resource = Resource.newResource(uri);
+ }
+
+ Scenario(File file, boolean exists, boolean dir)
+ {
+ this.test = file.toString();
+ this.exists = exists;
+ this.dir = dir;
+ resource = Resource.newResource(file);
+ }
+
+ Scenario(String url, boolean exists, boolean dir, String content)
+ throws Exception
+ {
+ this.test = url;
+ this.exists = exists;
+ this.dir = dir;
+ this.content = content;
+ resource = Resource.newResource(url);
+ }
+
+ @Override
+ public String toString()
+ {
+ return this.test;
+ }
+ }
+
+ static class Scenarios extends ArrayList<Arguments>
+ {
+ final File fileRef;
+ final URI uriRef;
+ final String relRef;
+
+ final Scenario[] baseCases;
+
+ public Scenarios(String ref) throws Exception
+ {
+ // relative directory reference
+ this.relRef = FS.separators(ref);
+ // File object reference
+ this.fileRef = MavenTestingUtils.getProjectDir(relRef);
+ // URI reference
+ this.uriRef = fileRef.toURI();
+
+ // create baseline cases
+ baseCases = new Scenario[]{
+ new Scenario(relRef, EXISTS, DIR),
+ new Scenario(uriRef, EXISTS, DIR),
+ new Scenario(fileRef, EXISTS, DIR)
+ };
+
+ // add all baseline cases
+ for (Scenario bcase : baseCases)
+ {
+ addCase(bcase);
+ }
+ }
+
+ public void addCase(Scenario ucase)
+ {
+ add(Arguments.of(ucase));
+ }
+
+ public void addAllSimpleCases(String subpath, boolean exists, boolean dir)
+ throws Exception
+ {
+ addCase(new Scenario(FS.separators(relRef + subpath), exists, dir));
+ addCase(new Scenario(uriRef.resolve(subpath).toURL(), exists, dir));
+ addCase(new Scenario(new File(fileRef, subpath), exists, dir));
+ }
+
+ public Scenario addAllAddPathCases(String subpath, boolean exists, boolean dir) throws Exception
+ {
+ Scenario bdata = null;
+
+ for (Scenario bcase : baseCases)
+ {
+ bdata = new Scenario(bcase, subpath, exists, dir);
+ addCase(bdata);
+ }
+
+ return bdata;
+ }
+ }
+
+ public static Stream<Arguments> scenarios() throws Exception
+ {
+ Scenarios cases = new Scenarios("src/test/resources/");
+
+ File testDir = MavenTestingUtils.getTargetTestingDir(ResourceTest.class.getName());
+ FS.ensureEmpty(testDir);
+ File tmpFile = new File(testDir, "test.tmp");
+ FS.touch(tmpFile);
+
+ cases.addCase(new Scenario(tmpFile.toString(), EXISTS, !DIR));
+
+ // Some resource references.
+ cases.addAllSimpleCases("resource.txt", EXISTS, !DIR);
+ cases.addAllSimpleCases("NoName.txt", !EXISTS, !DIR);
+
+ // Some addPath() forms
+ cases.addAllAddPathCases("resource.txt", EXISTS, !DIR);
+ cases.addAllAddPathCases("/resource.txt", EXISTS, !DIR);
+ cases.addAllAddPathCases("//resource.txt", EXISTS, !DIR);
+ cases.addAllAddPathCases("NoName.txt", !EXISTS, !DIR);
+ cases.addAllAddPathCases("/NoName.txt", !EXISTS, !DIR);
+ cases.addAllAddPathCases("//NoName.txt", !EXISTS, !DIR);
+
+ Scenario tdata1 = cases.addAllAddPathCases("TestData", EXISTS, DIR);
+ Scenario tdata2 = cases.addAllAddPathCases("TestData/", EXISTS, DIR);
+
+ cases.addCase(new Scenario(tdata1, "alphabet.txt", EXISTS, !DIR, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ cases.addCase(new Scenario(tdata2, "alphabet.txt", EXISTS, !DIR, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+
+ cases.addCase(new Scenario("jar:file:/somejar.jar!/content/", !EXISTS, DIR));
+ cases.addCase(new Scenario("jar:file:/somejar.jar!/", !EXISTS, DIR));
+
+ String urlRef = cases.uriRef.toASCIIString();
+ Scenario zdata = new Scenario("jar:" + urlRef + "TestData/test.zip!/", EXISTS, DIR);
+ cases.addCase(zdata);
+
+ cases.addCase(new Scenario(zdata, "Unknown", !EXISTS, !DIR));
+ cases.addCase(new Scenario(zdata, "/Unknown/", !EXISTS, DIR));
+
+ cases.addCase(new Scenario(zdata, "subdir", EXISTS, DIR));
+ cases.addCase(new Scenario(zdata, "/subdir/", EXISTS, DIR));
+ cases.addCase(new Scenario(zdata, "alphabet", EXISTS, !DIR,
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ cases.addCase(new Scenario(zdata, "/subdir/alphabet", EXISTS, !DIR,
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+
+ cases.addAllAddPathCases("/TestData/test/subdir/subsubdir/", EXISTS, DIR);
+ cases.addAllAddPathCases("//TestData/test/subdir/subsubdir/", EXISTS, DIR);
+ cases.addAllAddPathCases("/TestData//test/subdir/subsubdir/", EXISTS, DIR);
+ cases.addAllAddPathCases("/TestData/test//subdir/subsubdir/", EXISTS, DIR);
+ cases.addAllAddPathCases("/TestData/test/subdir//subsubdir/", EXISTS, DIR);
+
+ cases.addAllAddPathCases("TestData/test/subdir/subsubdir/", EXISTS, DIR);
+ cases.addAllAddPathCases("TestData/test/subdir/subsubdir//", EXISTS, DIR);
+ cases.addAllAddPathCases("TestData/test/subdir//subsubdir/", EXISTS, DIR);
+ cases.addAllAddPathCases("TestData/test//subdir/subsubdir/", EXISTS, DIR);
+
+ cases.addAllAddPathCases("/TestData/../TestData/test/subdir/subsubdir/", EXISTS, DIR);
+
+ return cases.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("scenarios")
+ public void testResourceExists(Scenario data)
+ {
+ assertThat("Exists: " + data.resource.getName(), data.resource.exists(), equalTo(data.exists));
+ }
+
+ @ParameterizedTest
+ @MethodSource("scenarios")
+ public void testResourceDir(Scenario data)
+ {
+ assertThat("Is Directory: " + data.test, data.resource.isDirectory(), equalTo(data.dir));
+ }
+
+ @ParameterizedTest
+ @MethodSource("scenarios")
+ public void testEncodeAddPath(Scenario data)
+ throws Exception
+ {
+ if (data.dir)
+ {
+ Resource r = data.resource.addPath("foo%/b r");
+ assertThat(r.getURI().toString(), Matchers.endsWith("/foo%25/b%20r"));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("scenarios")
+ public void testResourceContent(Scenario data)
+ throws Exception
+ {
+ Assumptions.assumeTrue(data.content != null);
+
+ InputStream in = data.resource.getInputStream();
+ String c = IO.toString(in);
+ assertThat("Content: " + data.test, c, startsWith(data.content));
+ }
+
+ @Test
+ public void testGlobPath() throws IOException
+ {
+ Path testDir = MavenTestingUtils.getTargetTestingPath("testGlobPath");
+ FS.ensureEmpty(testDir);
+
+ try
+ {
+ String globReference = testDir.toAbsolutePath() + File.separator + '*';
+ Resource globResource = Resource.newResource(globReference);
+ assertNotNull(globResource, "Should have produced a Resource");
+ }
+ catch (InvalidPathException e)
+ {
+ // if unable to reference the glob file, no point testing the rest.
+ // this is the path that Microsoft Windows takes.
+ assumeTrue(true, "Not supported on this OS");
+ }
+ }
+
+ @Test
+ @EnabledOnOs(OS.WINDOWS)
+ public void testEqualsWindowsAltUriSyntax() throws Exception
+ {
+ URI a = new URI("file:/C:/foo/bar");
+ URI b = new URI("file:///C:/foo/bar");
+
+ Resource ra = Resource.newResource(a);
+ Resource rb = Resource.newResource(b);
+
+ assertEquals(rb, ra);
+ }
+
+ @Test
+ @EnabledOnOs(OS.WINDOWS)
+ public void testEqualsWindowsCaseInsensitiveDrive() throws Exception
+ {
+ URI a = new URI("file:///c:/foo/bar");
+ URI b = new URI("file:///C:/foo/bar");
+
+ Resource ra = Resource.newResource(a);
+ Resource rb = Resource.newResource(b);
+
+ assertEquals(rb, ra);
+ }
+
+ @Test
+ public void testClimbAboveBase() throws Exception
+ {
+ Resource resource = Resource.newResource("/foo/bar");
+ assertThrows(MalformedURLException.class, () -> resource.addPath(".."));
+
+ Resource same = resource.addPath(".");
+ assertNotNull(same);
+ assertTrue(same.isAlias());
+
+ assertThrows(MalformedURLException.class, () -> resource.addPath("./.."));
+
+ assertThrows(MalformedURLException.class, () -> resource.addPath("./../bar"));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/security/CredentialTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/security/CredentialTest.java
new file mode 100644
index 0000000..6b881a9
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/security/CredentialTest.java
@@ -0,0 +1,107 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.security;
+
+import org.eclipse.jetty.util.security.Credential.Crypt;
+import org.eclipse.jetty.util.security.Credential.MD5;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CredentialTest
+ */
+public class CredentialTest
+{
+
+ @Test
+ public void testCrypt() throws Exception
+ {
+ Crypt c1 = (Crypt)Credential.getCredential(Crypt.crypt("fred", "abc123"));
+ Crypt c2 = (Crypt)Credential.getCredential(Crypt.crypt("fred", "abc123"));
+
+ Crypt c3 = (Crypt)Credential.getCredential(Crypt.crypt("fred", "xyz123"));
+
+ Credential c4 = Credential.getCredential(Crypt.crypt("fred", "xyz123"));
+
+ assertTrue(c1.equals(c2));
+ assertTrue(c2.equals(c1));
+ assertFalse(c1.equals(c3));
+ assertFalse(c3.equals(c1));
+ assertFalse(c3.equals(c2));
+ assertTrue(c4.equals(c3));
+ assertFalse(c4.equals(c1));
+ }
+
+ @Test
+ public void testMD5() throws Exception
+ {
+ MD5 m1 = (MD5)Credential.getCredential(MD5.digest("123foo"));
+ MD5 m2 = (MD5)Credential.getCredential(MD5.digest("123foo"));
+ MD5 m3 = (MD5)Credential.getCredential(MD5.digest("123boo"));
+
+ assertTrue(m1.equals(m2));
+ assertTrue(m2.equals(m1));
+ assertFalse(m3.equals(m1));
+ }
+
+ @Test
+ public void testPassword() throws Exception
+ {
+ Password p1 = new Password(Password.obfuscate("abc123"));
+ Credential p2 = Credential.getCredential(Password.obfuscate("abc123"));
+
+ assertTrue(p1.equals(p2));
+ }
+
+ @Test
+ public void testStringEquals()
+ {
+ assertTrue(Credential.stringEquals("foo", "foo"));
+ assertFalse(Credential.stringEquals("foo", "fooo"));
+ assertFalse(Credential.stringEquals("foo", "fo"));
+ assertFalse(Credential.stringEquals("foo", "bar"));
+ }
+
+ @Test
+ public void testBytesEquals()
+ {
+ assertTrue(Credential.byteEquals("foo".getBytes(), "foo".getBytes()));
+ assertFalse(Credential.byteEquals("foo".getBytes(), "fooo".getBytes()));
+ assertFalse(Credential.byteEquals("foo".getBytes(), "fo".getBytes()));
+ assertFalse(Credential.byteEquals("foo".getBytes(), "bar".getBytes()));
+ }
+
+ @Test
+ public void testEmptyString()
+ {
+ assertFalse(Credential.stringEquals("fooo", ""));
+ assertFalse(Credential.stringEquals("", "fooo"));
+ assertTrue(Credential.stringEquals("", ""));
+ }
+
+ @Test
+ public void testEmptyBytes()
+ {
+ assertFalse(Credential.byteEquals("fooo".getBytes(), "".getBytes()));
+ assertFalse(Credential.byteEquals("".getBytes(), "fooo".getBytes()));
+ assertTrue(Credential.byteEquals("".getBytes(), "".getBytes()));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/security/PasswordTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/security/PasswordTest.java
new file mode 100644
index 0000000..72933d9
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/security/PasswordTest.java
@@ -0,0 +1,52 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.security;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class PasswordTest
+{
+ @Test
+ public void testDeobfuscate()
+ {
+ // check any changes do not break already encoded strings
+ String password = "secret password !# ";
+ String obfuscate = "OBF:1iaa1g3l1fb51i351sw01ym91hdc1yt41v1p1ym71v2p1yti1hhq1ym51svy1hyl1f7h1fzx1i5o";
+ assertEquals(password, Password.deobfuscate(obfuscate));
+ }
+
+ @Test
+ public void testObfuscate()
+ {
+ String password = "secret password !# ";
+ String obfuscate = Password.obfuscate(password);
+ assertEquals(password, Password.deobfuscate(obfuscate));
+ }
+
+ @Test
+ public void testObfuscateUnicode()
+ {
+ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
+ String password = "secret password !#\u20ac ";
+ String obfuscate = Password.obfuscate(password);
+ assertEquals(password, Password.deobfuscate(obfuscate));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java
new file mode 100644
index 0000000..011d12e
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java
@@ -0,0 +1,493 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.ssl;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.X509ExtendedKeyManager;
+
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.eclipse.jetty.util.resource.Resource;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.hasItemInArray;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.matchesRegex;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SslContextFactoryTest
+{
+ @Test
+ public void testSLOTH() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ cf.setKeyStorePassword("storepwd");
+ cf.setKeyManagerPassword("keypwd");
+
+ cf.start();
+
+ // cf.dump(System.out, "");
+ List<SslSelectionDump> dumps = cf.selectionDump();
+
+ Optional<SslSelectionDump> cipherSuiteDumpOpt = dumps.stream()
+ .filter((dump) -> dump.type.contains("Cipher Suite"))
+ .findFirst();
+
+ assertTrue(cipherSuiteDumpOpt.isPresent(), "Cipher Suite dump section should exist");
+
+ SslSelectionDump cipherDump = cipherSuiteDumpOpt.get();
+
+ for (String enabledCipher : cipherDump.enabled)
+ {
+ assertThat("Enabled Cipher Suite", enabledCipher, not(matchesRegex(".*_RSA_.*(SHA1|MD5|SHA)")));
+ }
+ }
+
+ @Test
+ public void testDumpExcludedProtocols() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ cf.setExcludeProtocols("TLSv1\\.?[01]?");
+ cf.start();
+
+ // Confirm behavior in engine
+ assertThat(cf.newSSLEngine().getEnabledProtocols(), not(hasItemInArray("TLSv1.1")));
+ assertThat(cf.newSSLEngine().getEnabledProtocols(), not(hasItemInArray("TLSv1")));
+
+ // Confirm output in dump
+ List<SslSelectionDump> dumps = cf.selectionDump();
+
+ Optional<SslSelectionDump> protocolDumpOpt = dumps.stream()
+ .filter((dump) -> dump.type.contains("Protocol"))
+ .findFirst();
+
+ assertTrue(protocolDumpOpt.isPresent(), "Protocol dump section should exist");
+
+ SslSelectionDump protocolDump = protocolDumpOpt.get();
+
+ long countTls11Enabled = protocolDump.enabled.stream().filter((t) -> t.contains("TLSv1.1")).count();
+ long countTls11Disabled = protocolDump.disabled.stream().filter((t) -> t.contains("TLSv1.1")).count();
+
+ assertThat("Enabled Protocols TLSv1.1 count", countTls11Enabled, is(0L));
+ assertThat("Disabled Protocols TLSv1.1 count", countTls11Disabled, is(1L));
+
+ // Uncomment to show dump in console.
+ // cf.dump(System.out, "");
+ }
+
+ @Test
+ public void testDumpIncludeTlsRsa() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ cf.setKeyStorePassword("storepwd");
+ cf.setKeyManagerPassword("keypwd");
+ cf.setIncludeCipherSuites("TLS_RSA_.*");
+ cf.setExcludeCipherSuites("BOGUS"); // just to not exclude anything
+
+ cf.start();
+
+ // cf.dump(System.out, "");
+ List<SslSelectionDump> dumps = cf.selectionDump();
+
+ SSLEngine ssl = SSLContext.getDefault().createSSLEngine();
+
+ List<String> tlsRsaSuites = Stream.of(ssl.getSupportedCipherSuites())
+ .filter((suite) -> suite.startsWith("TLS_RSA_"))
+ .collect(Collectors.toList());
+
+ List<String> selectedSuites = Arrays.asList(cf.getSelectedCipherSuites());
+
+ Optional<SslSelectionDump> cipherSuiteDumpOpt = dumps.stream()
+ .filter((dump) -> dump.type.contains("Cipher Suite"))
+ .findFirst();
+
+ assertTrue(cipherSuiteDumpOpt.isPresent(), "Cipher Suite dump section should exist");
+
+ SslSelectionDump cipherDump = cipherSuiteDumpOpt.get();
+
+ assertThat("Dump Enabled List size is equal to selected list size", cipherDump.enabled.size(), is(selectedSuites.size()));
+
+ for (String expectedCipherSuite : tlsRsaSuites)
+ {
+ assertThat("Selected Cipher Suites", selectedSuites, hasItem(expectedCipherSuite));
+ assertThat("Dump Enabled Cipher Suites", cipherDump.enabled, hasItem(expectedCipherSuite));
+ }
+ }
+
+ @Test
+ public void testNoTsFileKs() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ cf.setKeyStorePassword("storepwd");
+ cf.setKeyManagerPassword("keypwd");
+
+ cf.start();
+
+ assertNotNull(cf.getSslContext());
+ }
+
+ @Test
+ public void testNoTsSetKs() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ KeyStore ks = KeyStore.getInstance("JKS");
+ try (InputStream keystoreInputStream = this.getClass().getResourceAsStream("keystore"))
+ {
+ ks.load(keystoreInputStream, "storepwd".toCharArray());
+ }
+ cf.setKeyStore(ks);
+ cf.setKeyManagerPassword("keypwd");
+
+ cf.start();
+
+ assertNotNull(cf.getSslContext());
+ }
+
+ @Test
+ public void testNoTsNoKs() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ cf.start();
+ assertNotNull(cf.getSslContext());
+ }
+
+ @Test
+ public void testNoTsResourceKs() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ Resource keystoreResource = Resource.newSystemResource("keystore");
+
+ cf.setKeyStoreResource(keystoreResource);
+ cf.setKeyStorePassword("storepwd");
+ cf.setKeyManagerPassword("keypwd");
+ cf.setTrustStoreResource(keystoreResource);
+ cf.setTrustStorePassword(null);
+
+ cf.start();
+
+ assertNotNull(cf.getSslContext());
+ }
+
+ @Test
+ public void testResourceTsResourceKs() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ Resource keystoreResource = Resource.newSystemResource("keystore");
+ Resource truststoreResource = Resource.newSystemResource("keystore");
+
+ cf.setKeyStoreResource(keystoreResource);
+ cf.setTrustStoreResource(truststoreResource);
+ cf.setKeyStorePassword("storepwd");
+ cf.setKeyManagerPassword("keypwd");
+ cf.setTrustStorePassword("storepwd");
+
+ cf.start();
+
+ assertNotNull(cf.getSslContext());
+ }
+
+ @Test
+ public void testResourceTsResourceKsWrongPW() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ Resource keystoreResource = Resource.newSystemResource("keystore");
+ Resource truststoreResource = Resource.newSystemResource("keystore");
+
+ cf.setKeyStoreResource(keystoreResource);
+ cf.setTrustStoreResource(truststoreResource);
+ cf.setKeyStorePassword("storepwd");
+ cf.setKeyManagerPassword("wrong_keypwd");
+ cf.setTrustStorePassword("storepwd");
+
+ try (StacklessLogging ignore = new StacklessLogging(AbstractLifeCycle.class))
+ {
+ java.security.UnrecoverableKeyException x = assertThrows(
+ java.security.UnrecoverableKeyException.class, cf::start);
+ assertThat(x.getMessage(), containsString("Cannot recover key"));
+ }
+ }
+
+ @Test
+ public void testResourceTsWrongPWResourceKs() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ Resource keystoreResource = Resource.newSystemResource("keystore");
+ Resource truststoreResource = Resource.newSystemResource("keystore");
+
+ cf.setKeyStoreResource(keystoreResource);
+ cf.setTrustStoreResource(truststoreResource);
+ cf.setKeyStorePassword("storepwd");
+ cf.setKeyManagerPassword("keypwd");
+ cf.setTrustStorePassword("wrong_storepwd");
+
+ try (StacklessLogging ignore = new StacklessLogging(AbstractLifeCycle.class))
+ {
+ IOException x = assertThrows(IOException.class, cf::start);
+ assertThat(x.getMessage(), containsString("Keystore was tampered with, or password was incorrect"));
+ }
+ }
+
+ @Test
+ public void testNoKeyConfig()
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ try (StacklessLogging ignore = new StacklessLogging(AbstractLifeCycle.class))
+ {
+ IllegalStateException x = assertThrows(IllegalStateException.class, () ->
+ {
+ cf.setTrustStorePath("/foo");
+ cf.start();
+ });
+ assertThat(x.getMessage(), containsString(File.separator + "foo is not a valid keystore"));
+ }
+ }
+
+ @Test
+ public void testSetExcludeCipherSuitesRegex() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ cf.setExcludeCipherSuites(".*RC4.*");
+ cf.start();
+ SSLEngine sslEngine = cf.newSSLEngine();
+ String[] enabledCipherSuites = sslEngine.getEnabledCipherSuites();
+ assertThat("At least 1 cipherSuite is enabled", enabledCipherSuites.length, greaterThan(0));
+ for (String enabledCipherSuite : enabledCipherSuites)
+ {
+ assertThat("CipherSuite does not contain RC4", enabledCipherSuite.contains("RC4"), equalTo(false));
+ }
+ }
+
+ @Test
+ public void testSetIncludeCipherSuitesRegex() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ cf.setIncludeCipherSuites(".*ECDHE.*", ".*WIBBLE.*");
+
+ cf.start();
+ SSLEngine sslEngine = cf.newSSLEngine();
+ String[] enabledCipherSuites = sslEngine.getEnabledCipherSuites();
+ assertThat("At least 1 cipherSuite is enabled", enabledCipherSuites.length, greaterThan(1));
+ for (String enabledCipherSuite : enabledCipherSuites)
+ {
+ assertThat("CipherSuite contains ECDHE", enabledCipherSuite.contains("ECDHE"), equalTo(true));
+ }
+ }
+
+ @Test
+ public void testProtocolAndCipherSettingsAreNPESafe()
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ assertNotNull(cf.getExcludeProtocols());
+ assertNotNull(cf.getIncludeProtocols());
+ assertNotNull(cf.getExcludeCipherSuites());
+ assertNotNull(cf.getIncludeCipherSuites());
+ }
+
+ @Test
+ public void testSNICertificates() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ Resource keystoreResource = Resource.newSystemResource("snikeystore");
+
+ cf.setKeyStoreResource(keystoreResource);
+ cf.setKeyStorePassword("storepwd");
+ cf.setKeyManagerPassword("keypwd");
+
+ cf.start();
+
+ assertThat(cf.getAliases(), containsInAnyOrder("jetty", "other", "san", "wild"));
+
+ assertThat(cf.getX509("jetty").getHosts(), containsInAnyOrder("jetty.eclipse.org"));
+ assertTrue(cf.getX509("jetty").getWilds().isEmpty());
+ assertTrue(cf.getX509("jetty").matches("JETTY.Eclipse.Org"));
+ assertFalse(cf.getX509("jetty").matches("m.jetty.eclipse.org"));
+ assertFalse(cf.getX509("jetty").matches("eclipse.org"));
+
+ assertThat(cf.getX509("other").getHosts(), containsInAnyOrder("www.example.com"));
+ assertTrue(cf.getX509("other").getWilds().isEmpty());
+ assertTrue(cf.getX509("other").matches("www.example.com"));
+ assertFalse(cf.getX509("other").matches("eclipse.org"));
+
+ assertThat(cf.getX509("san").getHosts(), containsInAnyOrder("san example", "www.san.com", "m.san.com"));
+ assertTrue(cf.getX509("san").getWilds().isEmpty());
+ assertTrue(cf.getX509("san").matches("www.san.com"));
+ assertTrue(cf.getX509("san").matches("m.san.com"));
+ assertFalse(cf.getX509("san").matches("other.san.com"));
+ assertFalse(cf.getX509("san").matches("san.com"));
+ assertFalse(cf.getX509("san").matches("eclipse.org"));
+
+ assertTrue(cf.getX509("wild").getHosts().isEmpty());
+ assertThat(cf.getX509("wild").getWilds(), containsInAnyOrder("domain.com"));
+ assertTrue(cf.getX509("wild").matches("domain.com"));
+ assertTrue(cf.getX509("wild").matches("www.domain.com"));
+ assertTrue(cf.getX509("wild").matches("other.domain.com"));
+ assertFalse(cf.getX509("wild").matches("foo.bar.domain.com"));
+ assertFalse(cf.getX509("wild").matches("other.com"));
+ }
+
+ @Test
+ public void testNonDefaultKeyStoreTypeUsedForTrustStore() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ cf.setKeyStoreResource(Resource.newSystemResource("keystore.p12"));
+ cf.setKeyStoreType("pkcs12");
+ cf.setKeyStorePassword("storepwd");
+ cf.start();
+ cf.stop();
+
+ cf = new SslContextFactory.Server();
+ cf.setKeyStoreResource(Resource.newSystemResource("keystore.jce"));
+ cf.setKeyStoreType("jceks");
+ cf.setKeyStorePassword("storepwd");
+ cf.start();
+ cf.stop();
+ }
+
+ @Test
+ public void testClientSslContextFactory() throws Exception
+ {
+ SslContextFactory.Client cf = new SslContextFactory.Client();
+ cf.start();
+
+ assertEquals("HTTPS", cf.getEndpointIdentificationAlgorithm());
+ }
+
+ @Test
+ public void testServerSslContextFactory() throws Exception
+ {
+ SslContextFactory.Server cf = new SslContextFactory.Server();
+ cf.start();
+
+ assertNull(cf.getEndpointIdentificationAlgorithm());
+ }
+
+ @Test
+ public void testSNIWithPKIX() throws Exception
+ {
+ SslContextFactory.Server serverTLS = new SslContextFactory.Server()
+ {
+ @Override
+ protected X509ExtendedKeyManager newSniX509ExtendedKeyManager(X509ExtendedKeyManager keyManager)
+ {
+ SniX509ExtendedKeyManager result = new SniX509ExtendedKeyManager(keyManager, this);
+ result.setAliasMapper(alias ->
+ {
+ // Workaround for https://bugs.openjdk.java.net/browse/JDK-8246262.
+ Matcher matcher = Pattern.compile(".*\\..*\\.(.*)").matcher(alias);
+ if (matcher.matches())
+ return matcher.group(1);
+ return alias;
+ });
+ return result;
+ }
+ };
+ // This test requires a SNI keystore so that the X509ExtendedKeyManager is wrapped.
+ serverTLS.setKeyStoreResource(Resource.newSystemResource("keystore_sni.p12"));
+ serverTLS.setKeyStorePassword("storepwd");
+ serverTLS.setKeyManagerPassword("keypwd");
+ serverTLS.setKeyManagerFactoryAlgorithm("PKIX");
+ // Don't pick a default certificate if SNI does not match.
+ serverTLS.setSniRequired(true);
+ serverTLS.start();
+
+ SslContextFactory.Client clientTLS = new SslContextFactory.Client(true);
+ clientTLS.start();
+
+ try (SSLServerSocket serverSocket = serverTLS.newSslServerSocket(null, 0, 128);
+ SSLSocket clientSocket = clientTLS.newSslSocket())
+ {
+ SSLParameters sslParameters = clientSocket.getSSLParameters();
+ String hostName = "jetty.eclipse.org";
+ sslParameters.setServerNames(Collections.singletonList(new SNIHostName(hostName)));
+ clientSocket.setSSLParameters(sslParameters);
+ clientSocket.connect(new InetSocketAddress("localhost", serverSocket.getLocalPort()), 5000);
+ try (SSLSocket sslSocket = (SSLSocket)serverSocket.accept())
+ {
+ byte[] data = "HELLO".getBytes(StandardCharsets.UTF_8);
+ new Thread(() ->
+ {
+ try
+ {
+ // Start the TLS handshake and verify that
+ // the client got the right server certificate.
+ clientSocket.startHandshake();
+ Certificate[] certificates = clientSocket.getSession().getPeerCertificates();
+ assertThat(certificates.length, greaterThan(0));
+ X509Certificate certificate = (X509Certificate)certificates[0];
+ assertThat(certificate.getSubjectX500Principal().getName(), startsWith("CN=" + hostName));
+ // Send some data to verify communication is ok.
+ OutputStream output = clientSocket.getOutputStream();
+ output.write(data);
+ output.flush();
+ clientSocket.close();
+ }
+ catch (Throwable x)
+ {
+ x.printStackTrace();
+ }
+ }).start();
+ // Verify that we received the data the client sent.
+ sslSocket.setSoTimeout(5000);
+ InputStream input = sslSocket.getInputStream();
+ byte[] bytes = IO.readBytes(input);
+ assertArrayEquals(data, bytes);
+ }
+ }
+
+ clientTLS.stop();
+ serverTLS.stop();
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/X509CertificateAdapter.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/X509CertificateAdapter.java
new file mode 100644
index 0000000..973dd1a
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/X509CertificateAdapter.java
@@ -0,0 +1,192 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.ssl;
+
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Principal;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.Set;
+
+/**
+ * Bogus X509Certificate to aide in testing
+ */
+public class X509CertificateAdapter extends X509Certificate
+{
+ @Override
+ public void checkValidity() throws CertificateExpiredException, CertificateNotYetValidException
+ {
+ }
+
+ @Override
+ public void checkValidity(Date date) throws CertificateExpiredException, CertificateNotYetValidException
+ {
+ }
+
+ @Override
+ public byte[] getEncoded() throws CertificateEncodingException
+ {
+ return new byte[0];
+ }
+
+ @Override
+ public boolean hasUnsupportedCriticalExtension()
+ {
+ return false;
+ }
+
+ @Override
+ public Set<String> getCriticalExtensionOIDs()
+ {
+ return null;
+ }
+
+ @Override
+ public Set<String> getNonCriticalExtensionOIDs()
+ {
+ return null;
+ }
+
+ @Override
+ public byte[] getExtensionValue(String oid)
+ {
+ return new byte[0];
+ }
+
+ @Override
+ public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException
+ {
+ }
+
+ @Override
+ public void verify(PublicKey key, String sigProvider) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException
+ {
+ }
+
+ @Override
+ public String toString()
+ {
+ return null;
+ }
+
+ @Override
+ public PublicKey getPublicKey()
+ {
+ return null;
+ }
+
+ @Override
+ public int getVersion()
+ {
+ return 0;
+ }
+
+ @Override
+ public BigInteger getSerialNumber()
+ {
+ return null;
+ }
+
+ @Override
+ public Principal getIssuerDN()
+ {
+ return null;
+ }
+
+ @Override
+ public Principal getSubjectDN()
+ {
+ return null;
+ }
+
+ @Override
+ public Date getNotBefore()
+ {
+ return null;
+ }
+
+ @Override
+ public Date getNotAfter()
+ {
+ return null;
+ }
+
+ @Override
+ public byte[] getTBSCertificate() throws CertificateEncodingException
+ {
+ return new byte[0];
+ }
+
+ @Override
+ public byte[] getSignature()
+ {
+ return new byte[0];
+ }
+
+ @Override
+ public String getSigAlgName()
+ {
+ return null;
+ }
+
+ @Override
+ public String getSigAlgOID()
+ {
+ return null;
+ }
+
+ @Override
+ public byte[] getSigAlgParams()
+ {
+ return new byte[0];
+ }
+
+ @Override
+ public boolean[] getIssuerUniqueID()
+ {
+ return new boolean[0];
+ }
+
+ @Override
+ public boolean[] getSubjectUniqueID()
+ {
+ return new boolean[0];
+ }
+
+ @Override
+ public boolean[] getKeyUsage()
+ {
+ return new boolean[0];
+ }
+
+ @Override
+ public int getBasicConstraints()
+ {
+ return 0;
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/X509Test.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/X509Test.java
new file mode 100644
index 0000000..16364cb
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/X509Test.java
@@ -0,0 +1,199 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.ssl;
+
+import java.nio.file.Path;
+import java.security.cert.X509Certificate;
+
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.resource.PathResource;
+import org.eclipse.jetty.util.resource.Resource;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class X509Test
+{
+ @Test
+ public void testIsCertSignNormal()
+ {
+ X509Certificate bogusX509 = new X509CertificateAdapter()
+ {
+ @Override
+ public boolean[] getKeyUsage()
+ {
+ boolean[] keyUsage = new boolean[8];
+ keyUsage[5] = true;
+ return keyUsage;
+ }
+ };
+
+ assertThat("Normal X509", X509.isCertSign(bogusX509), is(true));
+ }
+
+ @Test
+ public void testIsCertSignNormalNoSupported()
+ {
+ X509Certificate bogusX509 = new X509CertificateAdapter()
+ {
+ @Override
+ public boolean[] getKeyUsage()
+ {
+ boolean[] keyUsage = new boolean[8];
+ keyUsage[5] = false;
+ return keyUsage;
+ }
+ };
+
+ assertThat("Normal X509", X509.isCertSign(bogusX509), is(false));
+ }
+
+ @Test
+ public void testIsCertSignNonStandardShort()
+ {
+ X509Certificate bogusX509 = new X509CertificateAdapter()
+ {
+ @Override
+ public boolean[] getKeyUsage()
+ {
+ boolean[] keyUsage = new boolean[6]; // at threshold
+ keyUsage[5] = true;
+ return keyUsage;
+ }
+ };
+
+ assertThat("NonStandard X509", X509.isCertSign(bogusX509), is(true));
+ }
+
+ @Test
+ public void testIsCertSignNonStandardShorter()
+ {
+ X509Certificate bogusX509 = new X509CertificateAdapter()
+ {
+ @Override
+ public boolean[] getKeyUsage()
+ {
+ boolean[] keyUsage = new boolean[5]; // just below threshold
+ return keyUsage;
+ }
+ };
+
+ assertThat("NonStandard X509", X509.isCertSign(bogusX509), is(false));
+ }
+
+ @Test
+ public void testIsCertSignNormalNull()
+ {
+ X509Certificate bogusX509 = new X509CertificateAdapter()
+ {
+ @Override
+ public boolean[] getKeyUsage()
+ {
+ return null;
+ }
+ };
+
+ assertThat("Normal X509", X509.isCertSign(bogusX509), is(false));
+ }
+
+ @Test
+ public void testIsCertSignNormalEmpty()
+ {
+ X509Certificate bogusX509 = new X509CertificateAdapter()
+ {
+ @Override
+ public boolean[] getKeyUsage()
+ {
+ return new boolean[0];
+ }
+ };
+
+ assertThat("Normal X509", X509.isCertSign(bogusX509), is(false));
+ }
+
+ @Test
+ public void testBaseClassWithSni()
+ {
+ SslContextFactory baseSsl = new SslContextFactory();
+ Path keystorePath = MavenTestingUtils.getTestResourcePathFile("keystore_sni.p12");
+ baseSsl.setKeyStoreResource(new PathResource(keystorePath));
+ baseSsl.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4");
+ baseSsl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
+ IllegalStateException ex = assertThrows(IllegalStateException.class, baseSsl::start);
+ assertThat("IllegalStateException.message", ex.getMessage(), containsString("KeyStores with multiple certificates are not supported on the base class"));
+ }
+
+ @Test
+ public void testServerClassWithSni() throws Exception
+ {
+ SslContextFactory serverSsl = new SslContextFactory.Server();
+ Path keystorePath = MavenTestingUtils.getTestResourcePathFile("keystore_sni.p12");
+ serverSsl.setKeyStoreResource(new PathResource(keystorePath));
+ serverSsl.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4");
+ serverSsl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
+ serverSsl.start();
+ }
+
+ @Test
+ public void testClientClassWithSni() throws Exception
+ {
+ SslContextFactory clientSsl = new SslContextFactory.Client();
+ Path keystorePath = MavenTestingUtils.getTestResourcePathFile("keystore_sni.p12");
+ clientSsl.setKeyStoreResource(new PathResource(keystorePath));
+ clientSsl.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4");
+ clientSsl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
+ clientSsl.start();
+ }
+
+ @Test
+ public void testBaseClassWithoutSni() throws Exception
+ {
+ SslContextFactory baseSsl = new SslContextFactory();
+ Resource keystoreResource = Resource.newSystemResource("keystore");
+ baseSsl.setKeyStoreResource(keystoreResource);
+ baseSsl.setKeyStorePassword("storepwd");
+ baseSsl.setKeyManagerPassword("keypwd");
+ baseSsl.start();
+ }
+
+ @Test
+ public void testServerClassWithoutSni() throws Exception
+ {
+ SslContextFactory serverSsl = new SslContextFactory.Server();
+ Resource keystoreResource = Resource.newSystemResource("keystore");
+ serverSsl.setKeyStoreResource(keystoreResource);
+ serverSsl.setKeyStorePassword("storepwd");
+ serverSsl.setKeyManagerPassword("keypwd");
+ serverSsl.start();
+ }
+
+ @Test
+ public void testClientClassWithoutSni() throws Exception
+ {
+ SslContextFactory clientSsl = new SslContextFactory.Client();
+ Resource keystoreResource = Resource.newSystemResource("keystore");
+ clientSsl.setKeyStoreResource(keystoreResource);
+ clientSsl.setKeyStorePassword("storepwd");
+ clientSsl.setKeyManagerPassword("keypwd");
+ clientSsl.start();
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/statistic/CounterStatisticTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/statistic/CounterStatisticTest.java
new file mode 100644
index 0000000..6731e16
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/statistic/CounterStatisticTest.java
@@ -0,0 +1,144 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.statistic;
+
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+
+public class CounterStatisticTest
+{
+
+ @Test
+ public void testCounter()
+ throws Exception
+ {
+ CounterStatistic count = new CounterStatistic();
+
+ assertThat(count.getCurrent(), equalTo(0L));
+ assertThat(count.getMax(), equalTo(0L));
+ assertThat(count.getTotal(), equalTo(0L));
+
+ count.increment();
+ count.increment();
+ count.decrement();
+ count.add(4);
+ count.add(-2);
+
+ assertThat(count.getCurrent(), equalTo(3L));
+ assertThat(count.getMax(), equalTo(5L));
+ assertThat(count.getTotal(), equalTo(6L));
+
+ count.reset();
+ assertThat(count.getCurrent(), equalTo(3L));
+ assertThat(count.getMax(), equalTo(3L));
+ assertThat(count.getTotal(), equalTo(3L));
+
+ count.increment();
+ count.decrement();
+ count.add(-2);
+ count.decrement();
+ assertThat(count.getCurrent(), equalTo(0L));
+ assertThat(count.getMax(), equalTo(4L));
+ assertThat(count.getTotal(), equalTo(4L));
+
+ count.decrement();
+ assertThat(count.getCurrent(), equalTo(-1L));
+ assertThat(count.getMax(), equalTo(4L));
+ assertThat(count.getTotal(), equalTo(4L));
+
+ count.increment();
+ assertThat(count.getCurrent(), equalTo(0L));
+ assertThat(count.getMax(), equalTo(4L));
+ assertThat(count.getTotal(), equalTo(5L));
+ }
+
+ @Test
+ public void testCounterContended()
+ throws Exception
+ {
+ final CounterStatistic counter = new CounterStatistic();
+ final int N = 100;
+ final int L = 1000;
+ final Thread[] threads = new Thread[N];
+ final CyclicBarrier incBarrier = new CyclicBarrier(N);
+ final CountDownLatch decBarrier = new CountDownLatch(N / 2);
+
+ for (int i = N; i-- > 0; )
+ {
+ threads[i] = (i >= N / 2)
+ ? new Thread(() ->
+ {
+ try
+ {
+ incBarrier.await();
+ decBarrier.await();
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ Random random = new Random();
+ for (int l = L; l-- > 0; )
+ {
+ counter.decrement();
+ if (random.nextInt(5) == 0)
+ Thread.yield();
+ }
+ })
+
+ : new Thread(() ->
+ {
+ try
+ {
+ incBarrier.await();
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ Random random = new Random();
+ for (int l = L; l-- > 0; )
+ {
+ counter.increment();
+ if (l == L / 2)
+ decBarrier.countDown();
+ if (random.nextInt(5) == 0)
+ Thread.yield();
+ }
+ });
+ threads[i].start();
+ }
+
+ for (int i = N; i-- > 0; )
+ {
+ threads[i].join();
+ }
+
+ assertThat(counter.getCurrent(), equalTo(0L));
+ assertThat(counter.getTotal(), equalTo(N * L / 2L));
+ assertThat(counter.getMax(), greaterThanOrEqualTo((N / 2) * (L / 2L)));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/statistic/RateStatisticTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/statistic/RateStatisticTest.java
new file mode 100644
index 0000000..8b28507
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/statistic/RateStatisticTest.java
@@ -0,0 +1,71 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.statistic;
+
+import java.util.concurrent.TimeUnit;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+public class RateStatisticTest
+{
+ @Test
+ public void testRate()
+ throws Exception
+ {
+ RateStatistic rs = new RateStatistic(1, TimeUnit.HOURS);
+ assertThat(rs.getCount(), equalTo(0L));
+ assertThat(rs.getRate(), equalTo(0));
+ assertThat(rs.getMax(), equalTo(0L));
+
+ rs.record();
+ assertThat(rs.getCount(), equalTo(1L));
+ assertThat(rs.getRate(), equalTo(1));
+ assertThat(rs.getMax(), equalTo(1L));
+
+ rs.age(35, TimeUnit.MINUTES);
+ assertThat(rs.getCount(), equalTo(1L));
+ assertThat(rs.getRate(), equalTo(1));
+ assertThat(rs.getMax(), equalTo(1L));
+ assertThat(rs.getOldest(TimeUnit.MINUTES), Matchers.is(35L));
+
+ rs.record();
+ assertThat(rs.getCount(), equalTo(2L));
+ assertThat(rs.getRate(), equalTo(2));
+ assertThat(rs.getMax(), equalTo(2L));
+
+ rs.age(35, TimeUnit.MINUTES);
+ assertThat(rs.getCount(), equalTo(2L));
+ assertThat(rs.getRate(), equalTo(1));
+ assertThat(rs.getMax(), equalTo(2L));
+
+ rs.record();
+ assertThat(rs.getCount(), equalTo(3L));
+ assertThat(rs.getRate(), equalTo(2));
+ assertThat(rs.getMax(), equalTo(2L));
+
+ rs.age(35, TimeUnit.MINUTES);
+ assertThat(rs.getCount(), equalTo(3L));
+ assertThat(rs.getRate(), equalTo(1));
+ assertThat(rs.getMax(), equalTo(2L));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/statistic/SampleStatisticTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/statistic/SampleStatisticTest.java
new file mode 100644
index 0000000..0d6f5b8
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/statistic/SampleStatisticTest.java
@@ -0,0 +1,74 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.statistic;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class SampleStatisticTest
+{
+ private static long[][] data =
+ {
+ {100, 100, 100, 100, 100, 100, 100, 100, 100, 100},
+ {100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 90, 110},
+ {100, 100, 100, 100, 100, 100, 100, 100, 90, 110, 95, 105, 97, 103},
+ {
+ 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 90, 110, 95, 105, 97,
+ 103
+ },
+ };
+
+ private static double[][] results =
+ /* {mean,stddev}*/
+ {{100.0, 0.0},
+ {100.0, Math.sqrt((10 * 10 + 10 * 10) / 12.0)},
+ {100.0, Math.sqrt((10 * 10 + 10 * 10 + 5 * 5 + 5 * 5 + 3 * 3 + 3 * 3) / 14.0)},
+ {100.0, Math.sqrt((10 * 10 + 10 * 10 + 5 * 5 + 5 * 5 + 3 * 3 + 3 * 3) / 24.0)},
+ {100.0, Math.sqrt((10 * 10 + 10 * 10 + 5 * 5 + 5 * 5 + 3 * 3 + 3 * 3) / 104.0)}
+ };
+
+ @Test
+ public void testData()
+ throws Exception
+ {
+ SampleStatistic stats = new SampleStatistic();
+ for (int d = 0; d < data.length; d++)
+ {
+ stats.reset();
+ for (long x : data[d])
+ {
+ stats.record(x);
+ }
+
+ assertEquals(data[d].length, (int)stats.getCount(), "count" + d);
+ assertNearEnough("mean" + d, results[d][0], stats.getMean());
+ assertNearEnough("stddev" + d, results[d][1], stats.getStdDev());
+ }
+ System.err.println(stats);
+ }
+
+ private void assertNearEnough(String test, double expected, double actual)
+ {
+ assertThat(actual, Matchers.greaterThan(expected - 0.1D));
+ assertThat(actual, Matchers.lessThan(expected + 0.1D));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/AbstractThreadPoolTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/AbstractThreadPoolTest.java
new file mode 100644
index 0000000..b2f03f5
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/AbstractThreadPoolTest.java
@@ -0,0 +1,118 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.time.Duration;
+
+import org.eclipse.jetty.util.ProcessorUtils;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.thread.ThreadPool.SizedThreadPool;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public abstract class AbstractThreadPoolTest
+{
+ private static int originalCoreCount;
+
+ @BeforeAll
+ public static void saveState()
+ {
+ originalCoreCount = ProcessorUtils.availableProcessors();
+ }
+
+ @AfterAll
+ public static void restoreState()
+ {
+ ProcessorUtils.setAvailableProcessors(originalCoreCount);
+ }
+
+ protected abstract SizedThreadPool newPool(int max);
+
+ @Test
+ public void testBudgetConstructMaxThenLease()
+ {
+ SizedThreadPool pool = newPool(4);
+
+ pool.getThreadPoolBudget().leaseTo(this, 2);
+
+ try
+ {
+ pool.getThreadPoolBudget().leaseTo(this, 3);
+ fail();
+ }
+ catch (IllegalStateException e)
+ {
+ assertThat(e.getMessage(), Matchers.containsString("Insufficient configured threads"));
+ }
+
+ pool.getThreadPoolBudget().leaseTo(this, 1);
+ }
+
+ @Test
+ public void testBudgetLeaseThenSetMax()
+ {
+ SizedThreadPool pool = newPool(4);
+
+ pool.getThreadPoolBudget().leaseTo(this, 2);
+
+ pool.setMaxThreads(3);
+
+ try
+ {
+ pool.setMaxThreads(1);
+ fail();
+ }
+ catch (IllegalStateException e)
+ {
+ assertThat(e.getMessage(), Matchers.containsString("Insufficient configured threads"));
+ }
+
+ assertThat(pool.getMaxThreads(), Matchers.is(3));
+ }
+
+ @Test
+ public void testJoinWithStopTimeout()
+ {
+ // ThreadPool must be an implement ContainerLifeCycle for this test to be valid.
+ SizedThreadPool threadPool = newPool(3);
+ if (!(threadPool instanceof ContainerLifeCycle))
+ return;
+
+ final long stopTimeout = 100;
+ ((ContainerLifeCycle)threadPool).setStopTimeout(100);
+ LifeCycle.start(threadPool);
+
+ // Verify that join does not timeout after waiting twice the stopTimeout.
+ assertThrows(Throwable.class, () ->
+ assertTimeoutPreemptively(Duration.ofMillis(stopTimeout * 2), threadPool::join)
+ );
+
+ // After stopping the ThreadPool join should unblock.
+ LifeCycle.stop(threadPool);
+ assertTimeoutPreemptively(Duration.ofMillis(stopTimeout), threadPool::join);
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/EatWhatYouKillTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/EatWhatYouKillTest.java
new file mode 100644
index 0000000..c417043
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/EatWhatYouKillTest.java
@@ -0,0 +1,149 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.eclipse.jetty.util.thread.strategy.EatWhatYouKill;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class EatWhatYouKillTest
+{
+ private EatWhatYouKill ewyk;
+
+ private void startEWYK(ExecutionStrategy.Producer producer) throws Exception
+ {
+ QueuedThreadPool executor = new QueuedThreadPool();
+ ewyk = new EatWhatYouKill(producer, executor);
+ ewyk.start();
+ ReservedThreadExecutor tryExecutor = executor.getBean(ReservedThreadExecutor.class);
+ // Prime the executor so that there is a reserved thread.
+ executor.tryExecute(() ->
+ {
+ });
+ while (tryExecutor.getAvailable() == 0)
+ {
+ Thread.sleep(10);
+ }
+ }
+
+ @AfterEach
+ public void dispose() throws Exception
+ {
+ if (ewyk != null)
+ ewyk.stop();
+ }
+
+ @Test
+ public void testExceptionThrownByTask() throws Exception
+ {
+ try (StacklessLogging stackLess = new StacklessLogging(EatWhatYouKill.class))
+ {
+ AtomicReference<Throwable> detector = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(2);
+ BlockingQueue<Task> tasks = new LinkedBlockingQueue<>();
+ startEWYK(() ->
+ {
+ boolean proceed = detector.compareAndSet(null, new Throwable());
+ if (proceed)
+ {
+ try
+ {
+ latch.countDown();
+ return tasks.poll(1, TimeUnit.SECONDS);
+ }
+ catch (InterruptedException x)
+ {
+ x.printStackTrace();
+ return null;
+ }
+ finally
+ {
+ detector.set(null);
+ }
+ }
+ else
+ {
+ return null;
+ }
+ });
+
+ // Start production in another thread.
+ ewyk.dispatch();
+
+ tasks.offer(new Task(() ->
+ {
+ try
+ {
+ // While thread1 runs this task, simulate
+ // that thread2 starts producing.
+ ewyk.dispatch();
+ // Wait for thread2 to block in produce().
+ latch.await();
+ // Throw to verify that exceptions are handled correctly.
+ throw new RuntimeException();
+ }
+ catch (InterruptedException x)
+ {
+ x.printStackTrace();
+ }
+ }, Invocable.InvocationType.BLOCKING));
+
+ // Wait until EWYK is idle.
+ while (!ewyk.isIdle())
+ {
+ Thread.sleep(10);
+ }
+
+ assertNull(detector.get());
+ }
+ }
+
+ private static class Task implements Runnable, Invocable
+ {
+ private final Runnable task;
+ private final InvocationType invocationType;
+
+ private Task(Runnable task, InvocationType invocationType)
+ {
+ this.task = task;
+ this.invocationType = invocationType;
+ }
+
+ @Override
+ public void run()
+ {
+ task.run();
+ }
+
+ @Override
+ public InvocationType getInvocationType()
+ {
+ return invocationType;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ExecutorThreadPoolTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ExecutorThreadPoolTest.java
new file mode 100644
index 0000000..e61b6d3
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ExecutorThreadPoolTest.java
@@ -0,0 +1,30 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import org.eclipse.jetty.util.thread.ThreadPool.SizedThreadPool;
+
+public class ExecutorThreadPoolTest extends AbstractThreadPoolTest
+{
+ @Override
+ protected SizedThreadPool newPool(int max)
+ {
+ return new ExecutorThreadPool(max);
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/LockerTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/LockerTest.java
new file mode 100644
index 0000000..91ffa6a
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/LockerTest.java
@@ -0,0 +1,140 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class LockerTest
+{
+ public LockerTest()
+ {
+ }
+
+ @Test
+ public void testLocked()
+ {
+ Locker lock = new Locker();
+ assertFalse(lock.isLocked());
+
+ try (Locker.Lock l = lock.lock())
+ {
+ assertTrue(lock.isLocked());
+ }
+ finally
+ {
+ assertFalse(lock.isLocked());
+ }
+
+ assertFalse(lock.isLocked());
+ }
+
+ @Test
+ public void testLockedException()
+ {
+ Locker lock = new Locker();
+ assertFalse(lock.isLocked());
+
+ try (Locker.Lock l = lock.lock())
+ {
+ assertTrue(lock.isLocked());
+ throw new Exception();
+ }
+ catch (Exception e)
+ {
+ assertFalse(lock.isLocked());
+ }
+ finally
+ {
+ assertFalse(lock.isLocked());
+ }
+
+ assertFalse(lock.isLocked());
+ }
+
+ @Test
+ public void testContend() throws Exception
+ {
+ final Locker lock = new Locker();
+
+ final CountDownLatch held0 = new CountDownLatch(1);
+ final CountDownLatch hold0 = new CountDownLatch(1);
+
+ Thread thread0 = new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try (Locker.Lock l = lock.lock())
+ {
+ held0.countDown();
+ hold0.await();
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ };
+ thread0.start();
+ held0.await();
+
+ assertTrue(lock.isLocked());
+
+ final CountDownLatch held1 = new CountDownLatch(1);
+ final CountDownLatch hold1 = new CountDownLatch(1);
+ Thread thread1 = new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try (Locker.Lock l = lock.lock())
+ {
+ held1.countDown();
+ hold1.await();
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ };
+ thread1.start();
+ // thread1 will be spinning here
+ assertFalse(held1.await(100, TimeUnit.MILLISECONDS));
+
+ // Let thread0 complete
+ hold0.countDown();
+ thread0.join();
+
+ // thread1 can progress
+ held1.await();
+
+ // let thread1 complete
+ hold1.countDown();
+ thread1.join();
+
+ assertFalse(lock.isLocked());
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java
new file mode 100644
index 0000000..33d5232
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java
@@ -0,0 +1,986 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.io.Closeable;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.eclipse.jetty.util.thread.ThreadPool.SizedThreadPool;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThan;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class QueuedThreadPoolTest extends AbstractThreadPoolTest
+{
+ private static final Logger LOG = Log.getLogger(QueuedThreadPoolTest.class);
+ private final AtomicInteger _jobs = new AtomicInteger();
+
+ private static class TestQueuedThreadPool extends QueuedThreadPool
+ {
+ private final AtomicInteger _started;
+ private final CountDownLatch _enteredRemoveThread;
+ private final CountDownLatch _exitRemoveThread;
+
+ public TestQueuedThreadPool(AtomicInteger started, CountDownLatch enteredRemoveThread, CountDownLatch exitRemoveThread)
+ {
+ _started = started;
+ _enteredRemoveThread = enteredRemoveThread;
+ _exitRemoveThread = exitRemoveThread;
+ }
+
+ public void superStartThread()
+ {
+ super.startThread();
+ }
+
+ @Override
+ protected void startThread()
+ {
+ switch (_started.incrementAndGet())
+ {
+ case 1:
+ case 2:
+ case 3:
+ super.startThread();
+ break;
+
+ case 4:
+ // deliberately not start thread
+ break;
+
+ default:
+ throw new IllegalStateException("too many threads started");
+ }
+ }
+
+ @Override
+ protected void removeThread(Thread thread)
+ {
+ try
+ {
+ _enteredRemoveThread.countDown();
+ _exitRemoveThread.await();
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+
+ super.removeThread(thread);
+ }
+ }
+
+ private class RunningJob implements Runnable
+ {
+ final CountDownLatch _run = new CountDownLatch(1);
+ final CountDownLatch _stopping = new CountDownLatch(1);
+ final CountDownLatch _stopped = new CountDownLatch(1);
+ final String _name;
+ final boolean _fail;
+ final int _waitMs;
+ final AtomicBoolean _interrupted = new AtomicBoolean();
+
+ RunningJob()
+ {
+ this(null, false, -1);
+ }
+
+ public RunningJob(String name)
+ {
+ this(name, false, -1);
+ }
+
+ public RunningJob(int waitMs)
+ {
+ this(null, false, waitMs);
+ }
+
+ public RunningJob(String name, boolean fail)
+ {
+ this(name, fail, -1);
+ }
+
+ public RunningJob(String name, boolean fail, int waitMs)
+ {
+ _name = name;
+ _fail = fail;
+ _waitMs = waitMs;
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ _run.countDown();
+ if (_waitMs > 0)
+ _stopping.await(_waitMs, TimeUnit.MILLISECONDS);
+ else
+ _stopping.await();
+ if (_fail)
+ throw new IllegalStateException("Testing!");
+ }
+ catch (InterruptedException e)
+ {
+ _interrupted.set(true);
+ }
+ catch (IllegalStateException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ LOG.debug(e);
+ }
+ finally
+ {
+ _jobs.incrementAndGet();
+ _stopped.countDown();
+ }
+ }
+
+ public void stop() throws InterruptedException
+ {
+ if (_run.await(10, TimeUnit.SECONDS))
+ _stopping.countDown();
+ if (!_stopped.await(10, TimeUnit.SECONDS))
+ throw new IllegalStateException();
+ }
+
+ public boolean wasInterrupted()
+ {
+ return _interrupted.get();
+ }
+
+ @Override
+ public String toString()
+ {
+ if (_name == null)
+ return super.toString();
+ return String.format("%s@%x", _name, hashCode());
+ }
+ }
+
+ private class CloseableJob extends RunningJob implements Closeable
+ {
+ final CountDownLatch _closed = new CountDownLatch(1);
+
+ @Override
+ public void close()
+ {
+ _closed.countDown();
+ }
+ }
+
+ @Test
+ public void testThreadPool() throws Exception
+ {
+ QueuedThreadPool tp = new QueuedThreadPool();
+ tp.setMinThreads(2);
+ tp.setMaxThreads(4);
+ tp.setIdleTimeout(1000);
+ tp.setThreadsPriority(Thread.NORM_PRIORITY - 1);
+
+ tp.start();
+
+ // min threads started
+ waitForThreads(tp, 2);
+ waitForIdle(tp, 2);
+
+ // Doesn't shrink to less than min threads
+ Thread.sleep(3L * tp.getIdleTimeout() / 2);
+ assertThat(tp.getThreads(), is(2));
+ assertThat(tp.getIdleThreads(), is(2));
+
+ // Run job0
+ RunningJob job0 = new RunningJob("JOB0");
+ tp.execute(job0);
+ assertTrue(job0._run.await(10, TimeUnit.SECONDS));
+ assertThat(tp.getThreads(), is(2));
+ assertThat(tp.getIdleThreads(), is(1));
+
+ // Run job1
+ RunningJob job1 = new RunningJob("JOB1");
+ tp.execute(job1);
+ assertTrue(job1._run.await(10, TimeUnit.SECONDS));
+ assertThat(tp.getThreads(), is(2));
+ assertThat(tp.getIdleThreads(), is(0));
+
+ // Run job2
+ RunningJob job2 = new RunningJob("JOB2");
+ tp.execute(job2);
+ assertTrue(job2._run.await(10, TimeUnit.SECONDS));
+ assertThat(tp.getThreads(), is(3));
+ assertThat(tp.getIdleThreads(), is(0));
+
+ // Run job3
+ RunningJob job3 = new RunningJob("JOB3");
+ tp.execute(job3);
+ assertTrue(job3._run.await(10, TimeUnit.SECONDS));
+ assertThat(tp.getThreads(), is(4));
+ assertThat(tp.getIdleThreads(), is(0));
+
+ // Check no short term change
+ Thread.sleep(100);
+ assertThat(tp.getThreads(), is(4));
+ assertThat(tp.getIdleThreads(), is(0));
+
+ // Run job4. will be queued
+ RunningJob job4 = new RunningJob("JOB4");
+ tp.execute(job4);
+ assertFalse(job4._run.await(250, TimeUnit.MILLISECONDS));
+ assertThat(tp.getThreads(), is(4));
+ assertThat(tp.getIdleThreads(), is(0));
+ assertThat(tp.getQueueSize(), is(1));
+
+ // finish job 0
+ job0._stopping.countDown();
+ assertTrue(job0._stopped.await(10, TimeUnit.SECONDS));
+
+ // job4 should now run
+ assertTrue(job4._run.await(10, TimeUnit.SECONDS));
+ assertThat(tp.getThreads(), is(4));
+ assertThat(tp.getIdleThreads(), is(0));
+ assertThat(tp.getQueueSize(), is(0));
+
+ // finish job 1, and its thread will become idle
+ job1._stopping.countDown();
+ assertTrue(job1._stopped.await(10, TimeUnit.SECONDS));
+ waitForIdle(tp, 1);
+ waitForThreads(tp, 4);
+
+ // finish job 2,3,4
+ job2._stopping.countDown();
+ job3._stopping.countDown();
+ job4._stopping.countDown();
+ assertTrue(job2._stopped.await(10, TimeUnit.SECONDS));
+ assertTrue(job3._stopped.await(10, TimeUnit.SECONDS));
+ assertTrue(job4._stopped.await(10, TimeUnit.SECONDS));
+
+ // At beginning of the test we waited 1.5*idleTimeout, but
+ // never actually shrunk the pool because it was at minThreads.
+ // Now that all jobs are finished, one thread will figure out
+ // that it will go idle and will shrink itself out of the pool.
+ // Give it some time to detect that, but not too much to shrink
+ // two threads.
+ Thread.sleep(tp.getIdleTimeout() / 4);
+
+ // Now we have 3 idle threads.
+ waitForIdle(tp, 3);
+ assertThat(tp.getThreads(), is(3));
+
+ tp.stop();
+ }
+
+ @Test
+ public void testThreadPoolFailingJobs() throws Exception
+ {
+ QueuedThreadPool tp = new QueuedThreadPool();
+ tp.setMinThreads(2);
+ tp.setMaxThreads(4);
+ tp.setIdleTimeout(900);
+ tp.setThreadsPriority(Thread.NORM_PRIORITY - 1);
+
+ try (StacklessLogging stackless = new StacklessLogging(QueuedThreadPool.class))
+ {
+ tp.start();
+
+ // min threads started
+ waitForThreads(tp, 2);
+ waitForIdle(tp, 2);
+
+ // Doesn't shrink to less than min threads
+ Thread.sleep(3L * tp.getIdleTimeout() / 2);
+ waitForThreads(tp, 2);
+ waitForIdle(tp, 2);
+
+ // Run job0
+ RunningJob job0 = new RunningJob("JOB0", true);
+ tp.execute(job0);
+ assertTrue(job0._run.await(10, TimeUnit.SECONDS));
+ waitForIdle(tp, 1);
+
+ // Run job1
+ RunningJob job1 = new RunningJob("JOB1", true);
+ tp.execute(job1);
+ assertTrue(job1._run.await(10, TimeUnit.SECONDS));
+ waitForThreads(tp, 2);
+ waitForIdle(tp, 0);
+
+ // Run job2
+ RunningJob job2 = new RunningJob("JOB2", true);
+ tp.execute(job2);
+ assertTrue(job2._run.await(10, TimeUnit.SECONDS));
+ waitForThreads(tp, 3);
+ waitForIdle(tp, 0);
+
+ // Run job3
+ RunningJob job3 = new RunningJob("JOB3", true);
+ tp.execute(job3);
+ assertTrue(job3._run.await(10, TimeUnit.SECONDS));
+ waitForThreads(tp, 4);
+ waitForIdle(tp, 0);
+ assertThat(tp.getIdleThreads(), is(0));
+ Thread.sleep(100);
+ assertThat(tp.getIdleThreads(), is(0));
+
+ // Run job4. will be queued
+ RunningJob job4 = new RunningJob("JOB4", true);
+ tp.execute(job4);
+ assertFalse(job4._run.await(1, TimeUnit.SECONDS));
+
+ // finish job 0
+ job0._stopping.countDown();
+ assertTrue(job0._stopped.await(10, TimeUnit.SECONDS));
+
+ // job4 should now run
+ assertTrue(job4._run.await(10, TimeUnit.SECONDS));
+ assertThat(tp.getThreads(), is(4));
+ assertThat(tp.getIdleThreads(), is(0));
+
+ // finish job 1
+ job1._stopping.countDown();
+ assertTrue(job1._stopped.await(10, TimeUnit.SECONDS));
+ waitForThreads(tp, 3);
+ assertThat(tp.getIdleThreads(), is(0));
+
+ // finish job 2,3,4
+ job2._stopping.countDown();
+ job3._stopping.countDown();
+ job4._stopping.countDown();
+ assertTrue(job2._stopped.await(10, TimeUnit.SECONDS));
+ assertTrue(job3._stopped.await(10, TimeUnit.SECONDS));
+ assertTrue(job4._stopped.await(10, TimeUnit.SECONDS));
+
+ waitForIdle(tp, 2);
+ waitForThreads(tp, 2);
+ }
+
+ tp.stop();
+ }
+
+ @Test
+ public void testExecuteNoIdleThreads() throws Exception
+ {
+ QueuedThreadPool tp = new QueuedThreadPool();
+ tp.setDetailedDump(true);
+ tp.setMinThreads(1);
+ tp.setMaxThreads(10);
+ tp.setIdleTimeout(500);
+
+ tp.start();
+
+ RunningJob job1 = new RunningJob();
+ tp.execute(job1);
+
+ RunningJob job2 = new RunningJob();
+ tp.execute(job2);
+
+ RunningJob job3 = new RunningJob();
+ tp.execute(job3);
+
+ // make sure these jobs have started running
+ assertTrue(job1._run.await(5, TimeUnit.SECONDS));
+ assertTrue(job2._run.await(5, TimeUnit.SECONDS));
+ assertTrue(job3._run.await(5, TimeUnit.SECONDS));
+
+ waitForThreads(tp, 3);
+ assertThat(tp.getIdleThreads(), is(0));
+
+ job1._stopping.countDown();
+ assertTrue(job1._stopped.await(10, TimeUnit.SECONDS));
+ waitForIdle(tp, 1);
+ assertThat(tp.getThreads(), is(3));
+
+ waitForIdle(tp, 0);
+ assertThat(tp.getThreads(), is(2));
+
+ RunningJob job4 = new RunningJob();
+ tp.execute(job4);
+ assertTrue(job4._run.await(5, TimeUnit.SECONDS));
+
+ tp.stop();
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {800, 0})
+ public void testLifeCycleStop(int stopTimeout) throws Exception
+ {
+ QueuedThreadPool tp = new QueuedThreadPool();
+ tp.setName("TestPool");
+ tp.setMinThreads(1);
+ tp.setMaxThreads(2);
+ tp.setIdleTimeout(900);
+ tp.setStopTimeout(stopTimeout);
+ tp.setThreadsPriority(Thread.NORM_PRIORITY - 1);
+ tp.start();
+
+ // min threads started
+ waitForThreads(tp, 1);
+ waitForIdle(tp, 1);
+
+ // Run job0 and job1
+ RunningJob job0 = new RunningJob();
+ RunningJob job1 = new RunningJob(200);
+ tp.execute(job0);
+ tp.execute(job1);
+
+ // Add more jobs (which should not be run)
+ RunningJob job2 = new RunningJob();
+ CloseableJob job3 = new CloseableJob();
+ RunningJob job4 = new RunningJob();
+ tp.execute(job2);
+ tp.execute(job3);
+ tp.execute(job4);
+
+ // Wait until the first 2 start running
+ waitForThreads(tp, 2);
+ waitForIdle(tp, 0);
+ assertTrue(job0._run.await(200, TimeUnit.MILLISECONDS));
+ assertTrue(job1._run.await(200, TimeUnit.MILLISECONDS));
+
+ // Queue should be empty after thread pool is stopped
+ tp.stop();
+ assertThat(tp.getQueue().size(), is(0));
+
+ // First 2 jobs are stopped
+ assertTrue(job0._stopped.await(200, TimeUnit.MILLISECONDS));
+ assertTrue(job1._stopped.await(200, TimeUnit.MILLISECONDS));
+
+ // first job stopped by interrupt
+ assertTrue(job0.wasInterrupted());
+ // second job stops naturally if there was a timeout, else it is interrupted
+ assertEquals(stopTimeout == 0, job1.wasInterrupted());
+
+ // Verify RunningJobs in the queue have not been run
+ assertFalse(job2._run.await(200, TimeUnit.MILLISECONDS));
+ assertFalse(job4._run.await(200, TimeUnit.MILLISECONDS));
+
+ // Verify ClosableJobs have not been run but have been closed
+ assertFalse(job3._run.await(200, TimeUnit.MILLISECONDS));
+ assertTrue(job3._closed.await(200, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testShrink() throws Exception
+ {
+ final AtomicInteger sleep = new AtomicInteger(100);
+ Runnable job = () ->
+ {
+ try
+ {
+ Thread.sleep(sleep.get());
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ };
+
+ QueuedThreadPool tp = new QueuedThreadPool();
+ tp.setMinThreads(2);
+ tp.setMaxThreads(10);
+ tp.setIdleTimeout(400);
+ tp.setThreadsPriority(Thread.NORM_PRIORITY - 1);
+
+ tp.start();
+ waitForIdle(tp, 2);
+ waitForThreads(tp, 2);
+
+ sleep.set(200);
+ tp.execute(job);
+ tp.execute(job);
+ for (int i = 0; i < 20; i++)
+ {
+ tp.execute(job);
+ }
+
+ waitForThreads(tp, 10);
+ waitForIdle(tp, 0);
+
+ sleep.set(5);
+ for (int i = 0; i < 500; i++)
+ {
+ tp.execute(job);
+ Thread.sleep(10);
+ }
+ waitForThreads(tp, 2);
+ waitForIdle(tp, 2);
+ tp.stop();
+ }
+
+ @Test
+ public void testSteadyShrink() throws Exception
+ {
+ CountDownLatch latch = new CountDownLatch(1);
+ Runnable job = () ->
+ {
+ try
+ {
+ latch.await();
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ };
+
+ QueuedThreadPool tp = new QueuedThreadPool();
+ tp.setMinThreads(2);
+ tp.setMaxThreads(10);
+ int timeout = 500;
+ tp.setIdleTimeout(timeout);
+ tp.setThreadsPriority(Thread.NORM_PRIORITY - 1);
+
+ tp.start();
+ waitForIdle(tp, 2);
+ waitForThreads(tp, 2);
+
+ for (int i = 0; i < 10; i++)
+ {
+ tp.execute(job);
+ }
+
+ waitForThreads(tp, 10);
+ int threads = tp.getThreads();
+ // let the jobs run
+ latch.countDown();
+
+ for (int i = 5; i-- > 0; )
+ {
+ Thread.sleep(timeout / 2);
+ tp.execute(job);
+ }
+
+ // Assert that steady rate of jobs doesn't prevent some idling out
+ assertThat(tp.getThreads(), lessThan(threads));
+ threads = tp.getThreads();
+ for (int i = 5; i-- > 0; )
+ {
+ Thread.sleep(timeout / 2);
+ tp.execute(job);
+ }
+ assertThat(tp.getThreads(), lessThan(threads));
+ }
+
+ @Test
+ public void testEnsureThreads() throws Exception
+ {
+ AtomicInteger started = new AtomicInteger(0);
+
+ CountDownLatch enteredRemoveThread = new CountDownLatch(1);
+ CountDownLatch exitRemoveThread = new CountDownLatch(1);
+ TestQueuedThreadPool tp = new TestQueuedThreadPool(started, enteredRemoveThread, exitRemoveThread);
+
+ tp.setMinThreads(2);
+ tp.setMaxThreads(10);
+ tp.setIdleTimeout(400);
+ tp.setThreadsPriority(Thread.NORM_PRIORITY - 1);
+
+ tp.start();
+ waitForIdle(tp, 2);
+ waitForThreads(tp, 2);
+
+ RunningJob job1 = new RunningJob();
+ RunningJob job2 = new RunningJob();
+ RunningJob job3 = new RunningJob();
+ tp.execute(job1);
+ tp.execute(job2);
+ tp.execute(job3);
+
+ waitForThreads(tp, 3);
+ waitForIdle(tp, 0);
+
+ // We stop job3, the thread becomes idle, thread decides to shrink, and then blocks in removeThread().
+ job3.stop();
+ assertTrue(enteredRemoveThread.await(5, TimeUnit.SECONDS));
+ waitForThreads(tp, 3);
+ waitForIdle(tp, 1);
+
+ // Executing job4 will not start a new thread because we already have 1 idle thread.
+ RunningJob job4 = new RunningJob();
+ tp.execute(job4);
+
+ // Allow thread to exit from removeThread().
+ // The 4th thread is not actually started in our startThread() until tp.superStartThread() is called.
+ // Delay by 1000ms to check that ensureThreads is only starting one thread even though it is slow to start.
+ assertThat(started.get(), is(3));
+ exitRemoveThread.countDown();
+ Thread.sleep(1000);
+
+ // Now startThreads() should have been called 4 times.
+ // Actually start the thread, and job4 should be run.
+ assertThat(started.get(), is(4));
+ tp.superStartThread();
+ assertTrue(job4._run.await(5, TimeUnit.SECONDS));
+
+ job1.stop();
+ job2.stop();
+ job4.stop();
+ tp.stop();
+ }
+
+ @Test
+ public void testMaxStopTime() throws Exception
+ {
+ QueuedThreadPool tp = new QueuedThreadPool();
+ long stopTimeout = 500;
+ tp.setStopTimeout(stopTimeout);
+ tp.start();
+ CountDownLatch interruptedLatch = new CountDownLatch(1);
+ tp.execute(() ->
+ {
+ try
+ {
+ Thread.sleep(10 * stopTimeout);
+ }
+ catch (InterruptedException expected)
+ {
+ interruptedLatch.countDown();
+ }
+ });
+
+ long beforeStop = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ tp.stop();
+ long afterStop = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ assertTrue(tp.isStopped());
+ assertTrue(afterStop - beforeStop < 1000);
+ assertTrue(interruptedLatch.await(5, TimeUnit.SECONDS));
+ }
+
+ private void waitForIdle(QueuedThreadPool tp, int idle)
+ {
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ long start = now;
+ while (tp.getIdleThreads() != idle && (now - start) < 10000)
+ {
+ try
+ {
+ Thread.sleep(50);
+ }
+ catch (InterruptedException ignored)
+ {
+ }
+ now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ }
+ assertThat(tp.getIdleThreads(), is(idle));
+ }
+
+ private void waitForReserved(QueuedThreadPool tp, int reserved)
+ {
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ long start = now;
+ ReservedThreadExecutor reservedThreadExecutor = tp.getBean(ReservedThreadExecutor.class);
+ while (reservedThreadExecutor.getAvailable() != reserved && (now - start) < 10000)
+ {
+ try
+ {
+ Thread.sleep(50);
+ }
+ catch (InterruptedException ignored)
+ {
+ }
+ now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ }
+ assertThat(reservedThreadExecutor.getAvailable(), is(reserved));
+ }
+
+ private void waitForThreads(QueuedThreadPool tp, int threads)
+ {
+ long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ long start = now;
+ while (tp.getThreads() != threads && (now - start) < 10000)
+ {
+ try
+ {
+ Thread.sleep(50);
+ }
+ catch (InterruptedException ignored)
+ {
+ }
+ now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ }
+ assertThat(tp.getThreads(), is(threads));
+ }
+
+ @Test
+ public void testException() throws Exception
+ {
+ QueuedThreadPool tp = new QueuedThreadPool();
+ tp.setMinThreads(5);
+ tp.setMaxThreads(10);
+ tp.setIdleTimeout(1000);
+ tp.start();
+ try (StacklessLogging stackless = new StacklessLogging(QueuedThreadPool.class))
+ {
+ tp.execute(() ->
+ {
+ throw new IllegalStateException();
+ });
+ tp.execute(() ->
+ {
+ throw new Error();
+ });
+ tp.execute(() ->
+ {
+ throw new RuntimeException();
+ });
+ tp.execute(() ->
+ {
+ throw new ThreadDeath();
+ });
+
+ Thread.sleep(100);
+ assertThat(tp.getThreads(), greaterThanOrEqualTo(5));
+ }
+ tp.stop();
+ }
+
+ @Test
+ public void testZeroMinThreads() throws Exception
+ {
+ int maxThreads = 10;
+ int minThreads = 0;
+ QueuedThreadPool pool = new QueuedThreadPool(maxThreads, minThreads);
+ pool.start();
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ pool.execute(latch::countDown);
+
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void testConstructorMinMaxThreadsValidation()
+ {
+ assertThrows(IllegalArgumentException.class, () -> new QueuedThreadPool(4, 8));
+ }
+
+ @Test
+ public void testDump() throws Exception
+ {
+ QueuedThreadPool pool = new QueuedThreadPool(4, 3);
+ pool.setIdleTimeout(10000);
+
+ String dump = pool.dump();
+ // TODO use hamcrest 2.0 regex matcher
+ assertThat(dump, containsString("STOPPED"));
+ assertThat(dump, containsString(",3<=0<=4,i=0,r=-1,q=0"));
+ assertThat(dump, containsString("[NO_TRY]"));
+
+ pool.setReservedThreads(2);
+ dump = pool.dump();
+ assertThat(dump, containsString("STOPPED"));
+ assertThat(dump, containsString(",3<=0<=4,i=0,r=2,q=0"));
+ assertThat(dump, containsString("[NO_TRY]"));
+
+ pool.start();
+ waitForIdle(pool, 3);
+ Thread.sleep(250); // TODO need to give time for threads to read idle poll after setting idle
+ dump = pool.dump();
+ assertThat(count(dump, " - STARTED"), is(2));
+ assertThat(dump, containsString(",3<=3<=4,i=3,r=2,q=0"));
+ assertThat(dump, containsString("[ReservedThreadExecutor@"));
+ assertThat(count(dump, " IDLE"), is(3));
+ assertThat(count(dump, " RESERVED"), is(0));
+
+ CountDownLatch started = new CountDownLatch(1);
+ CountDownLatch waiting = new CountDownLatch(1);
+ pool.execute(() ->
+ {
+ try
+ {
+ started.countDown();
+ waiting.await();
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ });
+ started.await();
+ Thread.sleep(250); // TODO need to give time for threads to read idle poll after setting idle
+ dump = pool.dump();
+ assertThat(count(dump, " - STARTED"), is(2));
+ assertThat(dump, containsString(",3<=3<=4,i=2,r=2,q=0"));
+ assertThat(dump, containsString("[ReservedThreadExecutor@"));
+ assertThat(count(dump, " IDLE"), is(2));
+ assertThat(count(dump, " WAITING"), is(1));
+ assertThat(count(dump, " RESERVED"), is(0));
+ assertThat(count(dump, "QueuedThreadPoolTest.lambda$testDump$"), is(0));
+
+ pool.setDetailedDump(true);
+ dump = pool.dump();
+ assertThat(count(dump, " - STARTED"), is(2));
+ assertThat(dump, containsString(",3<=3<=4,i=2,r=2,q=0"));
+ assertThat(dump, containsString("reserved=0/2"));
+ assertThat(dump, containsString("[ReservedThreadExecutor@"));
+ assertThat(count(dump, " IDLE"), is(2));
+ assertThat(count(dump, " WAITING"), is(1));
+ assertThat(count(dump, " RESERVED"), is(0));
+ assertThat(count(dump, "QueuedThreadPoolTest.lambda$testDump$"), is(1));
+
+ assertFalse(pool.tryExecute(() ->
+ {
+ }));
+ waitForReserved(pool, 1);
+ Thread.sleep(250); // TODO need to give time for threads to read idle poll after setting idle
+ dump = pool.dump();
+ assertThat(count(dump, " - STARTED"), is(2));
+ assertThat(dump, containsString(",3<=3<=4,i=1,r=2,q=0"));
+ assertThat(dump, containsString("reserved=1/2"));
+ assertThat(dump, containsString("[ReservedThreadExecutor@"));
+ assertThat(count(dump, " IDLE"), is(1));
+ assertThat(count(dump, " WAITING"), is(1));
+ assertThat(count(dump, " RESERVED"), is(1));
+ assertThat(count(dump, "QueuedThreadPoolTest.lambda$testDump$"), is(1));
+ }
+
+ @Test
+ public void testContextClassLoader() throws Exception
+ {
+ QueuedThreadPool tp = new QueuedThreadPool();
+ try (StacklessLogging stackless = new StacklessLogging(QueuedThreadPool.class))
+ {
+ //change the current thread's classloader to something else
+ Thread.currentThread().setContextClassLoader(new URLClassLoader(new URL[] {}));
+
+ //create a new thread
+ Thread t = tp.newThread(() ->
+ {
+ //the executing thread should be still set to the classloader of the QueuedThreadPool,
+ //not that of the thread that created this thread.
+ assertThat(Thread.currentThread().getContextClassLoader(), Matchers.equalTo(QueuedThreadPool.class.getClassLoader()));
+ });
+
+ //new thread should be set to the classloader of the QueuedThreadPool
+ assertThat(t.getContextClassLoader(), Matchers.equalTo(QueuedThreadPool.class.getClassLoader()));
+ }
+ }
+
+ @Test
+ public void testThreadCounts() throws Exception
+ {
+ int maxThreads = 100;
+ QueuedThreadPool tp = new QueuedThreadPool(maxThreads, 0);
+ // Long timeout so it does not expire threads during the test.
+ tp.setIdleTimeout(60000);
+ int reservedThreads = 7;
+ tp.setReservedThreads(reservedThreads);
+ tp.start();
+ int leasedThreads = 5;
+ tp.getThreadPoolBudget().leaseTo(new Object(), leasedThreads);
+ List<RunningJob> leasedJobs = new ArrayList<>();
+ for (int i = 0; i < leasedThreads; ++i)
+ {
+ RunningJob job = new RunningJob("JOB" + i);
+ leasedJobs.add(job);
+ tp.execute(job);
+ assertTrue(job._run.await(5, TimeUnit.SECONDS));
+ }
+
+ // Run some job to spawn threads.
+ for (int i = 0; i < 3; ++i)
+ {
+ tp.tryExecute(() -> {});
+ }
+ int spawned = 13;
+ List<RunningJob> jobs = new ArrayList<>();
+ for (int i = 0; i < spawned; ++i)
+ {
+ RunningJob job = new RunningJob("JOB" + i);
+ jobs.add(job);
+ tp.execute(job);
+ assertTrue(job._run.await(5, TimeUnit.SECONDS));
+ }
+ for (RunningJob job : jobs)
+ {
+ job._stopping.countDown();
+ }
+
+ // Wait for the threads to become idle again.
+ Thread.sleep(1000);
+
+ // Submit less jobs to the queue so we have active and idle threads.
+ jobs.clear();
+ int transientJobs = spawned / 2;
+ for (int i = 0; i < transientJobs; ++i)
+ {
+ RunningJob job = new RunningJob("JOB" + i);
+ jobs.add(job);
+ tp.execute(job);
+ assertTrue(job._run.await(5, TimeUnit.SECONDS));
+ }
+
+ try
+ {
+ assertThat(tp.getMaxReservedThreads(), Matchers.equalTo(reservedThreads));
+ assertThat(tp.getLeasedThreads(), Matchers.equalTo(leasedThreads));
+ assertThat(tp.getReadyThreads(), Matchers.equalTo(tp.getIdleThreads() + tp.getAvailableReservedThreads()));
+ assertThat(tp.getUtilizedThreads(), Matchers.equalTo(transientJobs));
+ assertThat(tp.getThreads(), Matchers.equalTo(tp.getReadyThreads() + tp.getLeasedThreads() + tp.getUtilizedThreads()));
+ assertThat(tp.getBusyThreads(), Matchers.equalTo(tp.getUtilizedThreads() + tp.getLeasedThreads()));
+ }
+ finally
+ {
+ jobs.forEach(job -> job._stopping.countDown());
+ leasedJobs.forEach(job -> job._stopping.countDown());
+ tp.stop();
+ }
+ }
+
+ private int count(String s, String p)
+ {
+ int c = 0;
+ int i = s.indexOf(p);
+ while (i >= 0)
+ {
+ c++;
+ i = s.indexOf(p, i + 1);
+ }
+ return c;
+ }
+
+ @Override
+ protected SizedThreadPool newPool(int max)
+ {
+ return new QueuedThreadPool(max);
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java
new file mode 100644
index 0000000..bead74b
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java
@@ -0,0 +1,369 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class ReservedThreadExecutorTest
+{
+ private static final int SIZE = 2;
+ private static final Runnable NOOP = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ }
+
+ @Override
+ public String toString()
+ {
+ return "NOOP!";
+ }
+ };
+
+ private TestExecutor _executor;
+ private ReservedThreadExecutor _reservedExecutor;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ System.gc();
+ _executor = new TestExecutor();
+ _reservedExecutor = new ReservedThreadExecutor(_executor, SIZE);
+ _reservedExecutor.start();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _reservedExecutor.stop();
+ }
+
+ @Test
+ public void testStarted()
+ {
+ // Reserved threads are lazily started.
+ assertThat(_executor._queue.size(), is(0));
+ }
+
+ @Test
+ public void testPending() throws Exception
+ {
+ assertThat(_executor._queue.size(), is(0));
+
+ for (int i = 0; i < SIZE; i++)
+ {
+ _reservedExecutor.tryExecute(NOOP);
+ }
+ assertThat(_executor._queue.size(), is(SIZE));
+
+ for (int i = 0; i < SIZE; i++)
+ {
+ _executor.startThread();
+ }
+ assertThat(_executor._queue.size(), is(0));
+
+ waitForAllAvailable();
+
+ for (int i = 0; i < SIZE; i++)
+ {
+ assertThat(_reservedExecutor.tryExecute(new Task()), is(true));
+ }
+ assertThat(_executor._queue.size(), is(1));
+ assertThat(_reservedExecutor.getAvailable(), is(0));
+
+ for (int i = 0; i < SIZE; i++)
+ {
+ assertThat(_reservedExecutor.tryExecute(NOOP), is(false));
+ }
+ assertThat(_executor._queue.size(), is(SIZE));
+ assertThat(_reservedExecutor.getAvailable(), is(0));
+ }
+
+ @Test
+ public void testExecuted() throws Exception
+ {
+ assertThat(_executor._queue.size(), is(0));
+
+ for (int i = 0; i < SIZE; i++)
+ {
+ _reservedExecutor.tryExecute(NOOP);
+ }
+ assertThat(_executor._queue.size(), is(SIZE));
+
+ for (int i = 0; i < SIZE; i++)
+ {
+ _executor.startThread();
+ }
+ assertThat(_executor._queue.size(), is(0));
+
+ waitForAllAvailable();
+
+ Task[] tasks = new Task[SIZE];
+ for (int i = 0; i < SIZE; i++)
+ {
+ tasks[i] = new Task();
+ assertThat(_reservedExecutor.tryExecute(tasks[i]), is(true));
+ }
+
+ for (int i = 0; i < SIZE; i++)
+ {
+ tasks[i]._ran.await(10, TimeUnit.SECONDS);
+ }
+
+ assertThat(_executor._queue.size(), is(1));
+
+ Task extra = new Task();
+ assertThat(_reservedExecutor.tryExecute(extra), is(false));
+ assertThat(_executor._queue.size(), is(2));
+
+ Thread.sleep(500);
+ assertThat(extra._ran.getCount(), is(1L));
+
+ for (int i = 0; i < SIZE; i++)
+ {
+ tasks[i]._complete.countDown();
+ }
+
+ waitForAllAvailable();
+ }
+
+ @Test
+ public void testShrink() throws Exception
+ {
+ final long IDLE = 1000;
+
+ _reservedExecutor.stop();
+ _reservedExecutor.setIdleTimeout(IDLE, TimeUnit.MILLISECONDS);
+ _reservedExecutor.start();
+ assertThat(_reservedExecutor.getAvailable(), is(0));
+
+ assertThat(_reservedExecutor.tryExecute(NOOP), is(false));
+ assertThat(_reservedExecutor.tryExecute(NOOP), is(false));
+
+ _executor.startThread();
+ _executor.startThread();
+
+ waitForAvailable(2);
+
+ int available = _reservedExecutor.getAvailable();
+ assertThat(available, is(2));
+ Thread.sleep((5 * IDLE) / 2);
+ assertThat(_reservedExecutor.getAvailable(), is(0));
+ }
+
+ @Test
+ public void testBusyShrink() throws Exception
+ {
+ final long IDLE = 1000;
+
+ _reservedExecutor.stop();
+ _reservedExecutor.setIdleTimeout(IDLE, TimeUnit.MILLISECONDS);
+ _reservedExecutor.start();
+ assertThat(_reservedExecutor.getAvailable(), is(0));
+
+ assertThat(_reservedExecutor.tryExecute(NOOP), is(false));
+ assertThat(_reservedExecutor.tryExecute(NOOP), is(false));
+
+ _executor.startThread();
+ _executor.startThread();
+
+ waitForAvailable(2);
+
+ int available = _reservedExecutor.getAvailable();
+ assertThat(available, is(2));
+
+ for (int i = 10; i-- > 0;)
+ {
+ assertThat(_reservedExecutor.tryExecute(NOOP), is(true));
+ Thread.sleep(200);
+ }
+ assertThat(_reservedExecutor.getAvailable(), is(1));
+ }
+
+ @Test
+ public void testReservedIdleTimeoutWithOneReservedThread() throws Exception
+ {
+ long idleTimeout = 500;
+ _reservedExecutor.stop();
+ _reservedExecutor.setIdleTimeout(idleTimeout, TimeUnit.MILLISECONDS);
+ _reservedExecutor.start();
+
+ assertThat(_reservedExecutor.tryExecute(NOOP), is(false));
+ Thread thread = _executor.startThread();
+ assertNotNull(thread);
+ waitForAvailable(1);
+
+ Thread.sleep(2 * idleTimeout);
+
+ waitForAvailable(0);
+ thread.join(2 * idleTimeout);
+ assertFalse(thread.isAlive());
+ }
+
+ protected void waitForAvailable(int size) throws InterruptedException
+ {
+ long started = System.nanoTime();
+ while (_reservedExecutor.getAvailable() < size)
+ {
+ long elapsed = System.nanoTime() - started;
+ if (elapsed > TimeUnit.SECONDS.toNanos(10))
+ fail("Took too long");
+ Thread.sleep(10);
+ }
+ assertThat(_reservedExecutor.getAvailable(), is(size));
+ }
+
+ protected void waitForAllAvailable() throws InterruptedException
+ {
+ waitForAvailable(SIZE);
+ }
+
+ private static class TestExecutor implements Executor
+ {
+ private final Deque<Runnable> _queue = new ArrayDeque<>();
+
+ @Override
+ public void execute(Runnable task)
+ {
+ _queue.addLast(task);
+ }
+
+ public Thread startThread()
+ {
+ Runnable task = _queue.pollFirst();
+ if (task != null)
+ {
+ Thread thread = new Thread(task);
+ thread.start();
+ return thread;
+ }
+ return null;
+ }
+ }
+
+ private static class Task implements Runnable
+ {
+ private CountDownLatch _ran = new CountDownLatch(1);
+ private CountDownLatch _complete = new CountDownLatch(1);
+
+ @Override
+ public void run()
+ {
+ _ran.countDown();
+ try
+ {
+ _complete.await();
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Test
+ public void stressTest() throws Exception
+ {
+ QueuedThreadPool pool = new QueuedThreadPool(20);
+ pool.setStopTimeout(10000);
+ pool.start();
+ ReservedThreadExecutor reserved = new ReservedThreadExecutor(pool, 10);
+ reserved.setIdleTimeout(0, null);
+ reserved.start();
+
+ final int LOOPS = 200000;
+ final AtomicInteger executions = new AtomicInteger(LOOPS);
+ final CountDownLatch executed = new CountDownLatch(LOOPS);
+ final AtomicInteger usedReserved = new AtomicInteger(0);
+ final AtomicInteger usedPool = new AtomicInteger(0);
+
+ Runnable task = new Runnable()
+ {
+ public void run()
+ {
+ try
+ {
+ while (true)
+ {
+ int loops = executions.get();
+ if (loops <= 0)
+ return;
+
+ if (executions.compareAndSet(loops, loops - 1))
+ {
+ if (reserved.tryExecute(this))
+ {
+ usedReserved.incrementAndGet();
+ }
+ else
+ {
+ usedPool.incrementAndGet();
+ pool.execute(this);
+ }
+ return;
+ }
+ }
+ }
+ finally
+ {
+ executed.countDown();
+ }
+ }
+ };
+
+ task.run();
+ task.run();
+ task.run();
+ task.run();
+ task.run();
+ task.run();
+ task.run();
+ task.run();
+
+ assertTrue(executed.await(60, TimeUnit.SECONDS));
+
+ // ensure tryExecute is still working
+ while (!reserved.tryExecute(() -> {}))
+ Thread.yield();
+
+ reserved.stop();
+ pool.stop();
+
+ assertThat(usedReserved.get(), greaterThan(0));
+ assertThat(usedReserved.get() + usedPool.get(), is(LOOPS));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/SchedulerTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/SchedulerTest.java
new file mode 100644
index 0000000..290468f
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/SchedulerTest.java
@@ -0,0 +1,214 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SchedulerTest
+{
+ public static Stream<Class<? extends Scheduler>> schedulerProvider()
+ {
+ return Stream.of(
+ TimerScheduler.class,
+ ScheduledExecutorScheduler.class
+ );
+ }
+
+ private List<Scheduler> schedulers = new ArrayList<>();
+
+ public Scheduler start(Class<? extends Scheduler> impl) throws Exception
+ {
+ System.gc();
+ Scheduler scheduler = impl.getDeclaredConstructor().newInstance();
+ scheduler.start();
+ schedulers.add(scheduler);
+ assertThat("Scheduler is started", scheduler.isStarted(), is(true));
+ return scheduler;
+ }
+
+ @AfterEach
+ public void after()
+ {
+ schedulers.forEach((scheduler) ->
+ {
+ try
+ {
+ scheduler.stop();
+ }
+ catch (Exception ignore)
+ {
+ // no op
+ }
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("schedulerProvider")
+ public void testExecution(Class<? extends Scheduler> impl) throws Exception
+ {
+ Scheduler scheduler = start(impl);
+ final AtomicLong executed = new AtomicLong();
+ long expected = System.currentTimeMillis() + 1000;
+ Scheduler.Task task = scheduler.schedule(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ executed.set(System.currentTimeMillis());
+ }
+ }, 1000, TimeUnit.MILLISECONDS);
+
+ Thread.sleep(1500);
+ assertFalse(task.cancel());
+ assertThat(executed.get(), Matchers.greaterThanOrEqualTo(expected));
+ assertThat(expected - executed.get(), Matchers.lessThan(1000L));
+ }
+
+ @ParameterizedTest
+ @MethodSource("schedulerProvider")
+ public void testTwoExecution(Class<? extends Scheduler> impl) throws Exception
+ {
+ Scheduler scheduler = start(impl);
+ final AtomicLong executed = new AtomicLong();
+ long expected = System.currentTimeMillis() + 1000;
+ Scheduler.Task task = scheduler.schedule(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ executed.set(System.currentTimeMillis());
+ }
+ }, 1000, TimeUnit.MILLISECONDS);
+
+ Thread.sleep(1500);
+ assertFalse(task.cancel());
+ assertThat(executed.get(), Matchers.greaterThanOrEqualTo(expected));
+ assertThat(expected - executed.get(), Matchers.lessThan(1000L));
+
+ final AtomicLong executed1 = new AtomicLong();
+ long expected1 = System.currentTimeMillis() + 1000;
+ Scheduler.Task task1 = scheduler.schedule(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ executed1.set(System.currentTimeMillis());
+ }
+ }, 1000, TimeUnit.MILLISECONDS);
+
+ Thread.sleep(1500);
+ assertFalse(task1.cancel());
+ assertThat(executed1.get(), Matchers.greaterThanOrEqualTo(expected1));
+ assertThat(expected1 - executed1.get(), Matchers.lessThan(1000L));
+ }
+
+ @ParameterizedTest
+ @MethodSource("schedulerProvider")
+ public void testQuickCancel(Class<? extends Scheduler> impl) throws Exception
+ {
+ Scheduler scheduler = start(impl);
+ final AtomicLong executed = new AtomicLong();
+ Scheduler.Task task = scheduler.schedule(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ executed.set(System.currentTimeMillis());
+ }
+ }, 2000, TimeUnit.MILLISECONDS);
+
+ Thread.sleep(100);
+ assertTrue(task.cancel());
+ Thread.sleep(2500);
+ assertEquals(0, executed.get());
+ }
+
+ @ParameterizedTest
+ @MethodSource("schedulerProvider")
+ public void testLongCancel(Class<? extends Scheduler> impl) throws Exception
+ {
+ Scheduler scheduler = start(impl);
+ final AtomicLong executed = new AtomicLong();
+ Scheduler.Task task = scheduler.schedule(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ executed.set(System.currentTimeMillis());
+ }
+ }, 2000, TimeUnit.MILLISECONDS);
+
+ Thread.sleep(100);
+ assertTrue(task.cancel());
+ Thread.sleep(2500);
+ assertEquals(0, executed.get());
+ }
+
+ @ParameterizedTest
+ @MethodSource("schedulerProvider")
+ public void testTaskThrowsException(Class<? extends Scheduler> impl) throws Exception
+ {
+ Scheduler scheduler = start(impl);
+ try (StacklessLogging ignore = new StacklessLogging(TimerScheduler.class))
+ {
+ long delay = 500;
+ scheduler.schedule(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ throw new RuntimeException("Thrown by testTaskThrowsException");
+ }
+ }, delay, TimeUnit.MILLISECONDS);
+
+ TimeUnit.MILLISECONDS.sleep(2 * delay);
+
+ // Check whether after a task throwing an exception, the scheduler is still working
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ scheduler.schedule(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ latch.countDown();
+ }
+ }, delay, TimeUnit.MILLISECONDS);
+
+ assertTrue(latch.await(2 * delay, TimeUnit.MILLISECONDS));
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/SerializedExecutorTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/SerializedExecutorTest.java
new file mode 100644
index 0000000..49c4f34
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/SerializedExecutorTest.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SerializedExecutorTest
+{
+ @Test
+ public void test() throws Exception
+ {
+ int threads = 64;
+ int loops = 1000;
+ int depth = 100;
+
+ AtomicInteger ran = new AtomicInteger();
+ AtomicBoolean running = new AtomicBoolean();
+ SerializedExecutor executor = new SerializedExecutor();
+ CountDownLatch start = new CountDownLatch(1);
+ CountDownLatch stop = new CountDownLatch(threads);
+ Random random = new Random();
+
+ for (int t = threads; t-- > 0; )
+ {
+ new Thread(() ->
+ {
+ try
+ {
+ start.await();
+
+ for (int l = loops; l-- > 0; )
+ {
+ final AtomicInteger d = new AtomicInteger(depth);
+ executor.execute(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ ran.incrementAndGet();
+ if (!running.compareAndSet(false, true))
+ throw new IllegalStateException();
+ if (d.decrementAndGet() > 0)
+ executor.execute(this);
+ if (!running.compareAndSet(true, false))
+ throw new IllegalStateException();
+ }
+ });
+ Thread.sleep(random.nextInt(5));
+ }
+ }
+ catch (Throwable th)
+ {
+ th.printStackTrace();
+ }
+ finally
+ {
+ stop.countDown();
+ }
+ }).start();
+ }
+
+ start.countDown();
+ assertTrue(stop.await(30, TimeUnit.SECONDS));
+ assertThat(ran.get(), Matchers.is(threads * loops * depth));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/SweeperTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/SweeperTest.java
new file mode 100644
index 0000000..98a9544
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/SweeperTest.java
@@ -0,0 +1,130 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SweeperTest
+{
+ private Scheduler scheduler;
+
+ @BeforeEach
+ public void prepare() throws Exception
+ {
+ scheduler = new ScheduledExecutorScheduler();
+ scheduler.start();
+ }
+
+ @AfterEach
+ public void dispose() throws Exception
+ {
+ scheduler.stop();
+ }
+
+ @Test
+ public void testResourceNotSweptIsNotRemoved() throws Exception
+ {
+ testResourceSweepRemove(false);
+ }
+
+ @Test
+ public void testResourceSweptIsRemoved() throws Exception
+ {
+ testResourceSweepRemove(true);
+ }
+
+ private void testResourceSweepRemove(final boolean sweep) throws Exception
+ {
+ long period = 1000;
+ final CountDownLatch taskLatch = new CountDownLatch(1);
+ Sweeper sweeper = new Sweeper(scheduler, period)
+ {
+ @Override
+ public void run()
+ {
+ super.run();
+ taskLatch.countDown();
+ }
+ };
+ sweeper.start();
+
+ final CountDownLatch sweepLatch = new CountDownLatch(1);
+ sweeper.offer(new Sweeper.Sweepable()
+ {
+ @Override
+ public boolean sweep()
+ {
+ sweepLatch.countDown();
+ return sweep;
+ }
+ });
+
+ assertTrue(sweepLatch.await(2 * period, TimeUnit.MILLISECONDS));
+ assertTrue(taskLatch.await(2 * period, TimeUnit.MILLISECONDS));
+ assertEquals(sweep ? 0 : 1, sweeper.getSize());
+
+ sweeper.stop();
+ }
+
+ @Test
+ public void testSweepThrows() throws Exception
+ {
+ try (StacklessLogging scope = new StacklessLogging(Sweeper.class))
+ {
+ long period = 500;
+ final CountDownLatch taskLatch = new CountDownLatch(2);
+ Sweeper sweeper = new Sweeper(scheduler, period)
+ {
+ @Override
+ public void run()
+ {
+ super.run();
+ taskLatch.countDown();
+ }
+ };
+ sweeper.start();
+
+ final CountDownLatch sweepLatch = new CountDownLatch(2);
+ sweeper.offer(new Sweeper.Sweepable()
+ {
+ @Override
+ public boolean sweep()
+ {
+ sweepLatch.countDown();
+ throw new NullPointerException("Test exception!");
+ }
+ });
+
+ assertTrue(sweepLatch.await(4 * period, TimeUnit.MILLISECONDS));
+ assertTrue(taskLatch.await(4 * period, TimeUnit.MILLISECONDS));
+ assertEquals(1, sweeper.getSize());
+
+ sweeper.stop();
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ThreadClassLoaderScopeTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ThreadClassLoaderScopeTest.java
new file mode 100644
index 0000000..b883e93
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ThreadClassLoaderScopeTest.java
@@ -0,0 +1,79 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.not;
+
+public class ThreadClassLoaderScopeTest
+{
+ private static class ClassLoaderFoo extends URLClassLoader
+ {
+ public ClassLoaderFoo()
+ {
+ super(new URL[0]);
+ }
+ }
+
+ private static class ClassLoaderBar extends URLClassLoader
+ {
+ public ClassLoaderBar()
+ {
+ super(new URL[0]);
+ }
+ }
+
+ @Test
+ public void testNormal()
+ {
+ try (ThreadClassLoaderScope scope = new ThreadClassLoaderScope(new ClassLoaderFoo()))
+ {
+ assertThat("ClassLoader in scope", Thread.currentThread().getContextClassLoader(), instanceOf(ClassLoaderFoo.class));
+ assertThat("Scoped ClassLoader", scope.getScopedClassLoader(), instanceOf(ClassLoaderFoo.class));
+ }
+ assertThat("ClassLoader after scope", Thread.currentThread().getContextClassLoader(), not(instanceOf(ClassLoaderFoo.class)));
+ }
+
+ @Test
+ public void testWithException()
+ {
+ try (ThreadClassLoaderScope scope = new ThreadClassLoaderScope(new ClassLoaderBar()))
+ {
+ assertThat("ClassLoader in 'scope'", Thread.currentThread().getContextClassLoader(), instanceOf(ClassLoaderBar.class));
+ assertThat("Scoped ClassLoader", scope.getScopedClassLoader(), instanceOf(ClassLoaderBar.class));
+ try (ThreadClassLoaderScope inner = new ThreadClassLoaderScope(new ClassLoaderFoo()))
+ {
+ assertThat("ClassLoader in 'inner'", Thread.currentThread().getContextClassLoader(), instanceOf(ClassLoaderFoo.class));
+ assertThat("Scoped ClassLoader", scope.getScopedClassLoader(), instanceOf(ClassLoaderFoo.class));
+ throw new RuntimeException("Intention exception");
+ }
+ }
+ catch (Throwable ignore)
+ {
+ /* ignore */
+ }
+ assertThat("ClassLoader after 'scope'", Thread.currentThread().getContextClassLoader(), not(instanceOf(ClassLoaderBar.class)));
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ThreadFactoryTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ThreadFactoryTest.java
new file mode 100644
index 0000000..7604b4c
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ThreadFactoryTest.java
@@ -0,0 +1,104 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.util.MultiException;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ThreadFactoryTest
+{
+ @Test
+ public void testThreadFactory() throws Exception
+ {
+ ThreadGroup threadGroup = new ThreadGroup("my-group");
+ MyThreadFactory threadFactory = new MyThreadFactory(threadGroup);
+
+ QueuedThreadPool qtp = new QueuedThreadPool(200, 10, 2000, 0, null, threadGroup, threadFactory);
+ try
+ {
+ qtp.start();
+
+ int testThreads = 2000;
+ CountDownLatch threadsLatch = new CountDownLatch(testThreads);
+ MultiException mex = new MultiException();
+
+ for (int i = 0; i < testThreads; i++)
+ {
+ qtp.execute(() ->
+ {
+ try
+ {
+ TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextInt(20, 500));
+ Thread thread = Thread.currentThread();
+
+ if (!thread.getName().startsWith("My-"))
+ {
+ mex.add(new AssertionError("Thread " + thread.getName() + " does not start with 'My-'"));
+ }
+
+ if (!thread.getThreadGroup().getName().equalsIgnoreCase("my-group"))
+ {
+ mex.add(new AssertionError("Thread Group " + thread.getThreadGroup().getName() + " is not 'my-group'"));
+ }
+
+ threadsLatch.countDown();
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ assertTrue(threadsLatch.await(5, TimeUnit.SECONDS), "Did not see all tasks finish");
+ mex.ifExceptionThrow();
+ }
+ finally
+ {
+ qtp.stop();
+ }
+ }
+
+ public static class MyThreadFactory implements ThreadFactory
+ {
+ private final ThreadGroup threadGroup;
+
+ public MyThreadFactory(ThreadGroup threadGroup)
+ {
+ this.threadGroup = threadGroup;
+ }
+
+ @Override
+ public Thread newThread(Runnable runnable)
+ {
+ Thread thread = new Thread(threadGroup, runnable);
+ thread.setDaemon(false);
+ thread.setPriority(Thread.MIN_PRIORITY);
+ thread.setName("My-" + thread.getId());
+ return thread;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/strategy/ExecuteProduceConsumeTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/strategy/ExecuteProduceConsumeTest.java
new file mode 100644
index 0000000..1f5db63
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/strategy/ExecuteProduceConsumeTest.java
@@ -0,0 +1,377 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread.strategy;
+
+import java.util.Queue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import org.eclipse.jetty.util.thread.ExecutionStrategy.Producer;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ExecuteProduceConsumeTest
+{
+ private static final Runnable NULLTASK = () ->
+ {
+ };
+
+ private final BlockingQueue<Runnable> _produce = new LinkedBlockingQueue<>();
+ private final Queue<Runnable> _executions = new LinkedBlockingQueue<>();
+ private ExecuteProduceConsume _ewyk;
+ private volatile Thread _producer;
+
+ @BeforeEach
+ public void before()
+ {
+ _executions.clear();
+
+ Producer producer = () ->
+ {
+ try
+ {
+ _producer = Thread.currentThread();
+ Runnable task = _produce.take();
+ if (task == NULLTASK)
+ return null;
+ return task;
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ return null;
+ }
+ finally
+ {
+ _producer = null;
+ }
+ };
+
+ Executor executor = _executions::add;
+
+ _ewyk = new ExecuteProduceConsume(producer, executor);
+ }
+
+ @AfterEach
+ public void after()
+ {
+ // All done and checked
+ assertThat(_produce.size(), Matchers.equalTo(0));
+ assertThat(_executions.size(), Matchers.equalTo(0));
+ }
+
+ @Test
+ public void testIdle()
+ {
+ _produce.add(NULLTASK);
+ _ewyk.produce();
+ }
+
+ @Test
+ public void testProduceOneNonBlockingTask()
+ {
+ Task t0 = new Task();
+ _produce.add(t0);
+ _produce.add(NULLTASK);
+ _ewyk.produce();
+ assertThat(t0.hasRun(), Matchers.equalTo(true));
+ assertEquals(_ewyk, _executions.poll());
+ }
+
+ @Test
+ public void testProduceManyNonBlockingTask()
+ {
+ Task[] tasks = new Task[10];
+ for (int i = 0; i < tasks.length; i++)
+ {
+ tasks[i] = new Task();
+ _produce.add(tasks[i]);
+ }
+ _produce.add(NULLTASK);
+ _ewyk.produce();
+
+ for (Task task : tasks)
+ {
+ assertThat(task.hasRun(), Matchers.equalTo(true));
+ }
+ assertEquals(_ewyk, _executions.poll());
+ }
+
+ @Test
+ public void testProduceOneBlockingTaskIdleByDispatch() throws Exception
+ {
+ final Task t0 = new Task(true);
+ Thread thread = new Thread()
+ {
+ @Override
+ public void run()
+ {
+ _produce.add(t0);
+ _produce.add(NULLTASK);
+ _ewyk.produce();
+ }
+ };
+ thread.start();
+
+ // wait for execute thread to block in
+ t0.awaitRun();
+ assertEquals(thread, t0.getThread());
+
+ // Should have dispatched only one helper
+ assertEquals(_ewyk, _executions.poll());
+ // which is make us idle
+ _ewyk.run();
+ assertThat(_ewyk.isIdle(), Matchers.equalTo(true));
+
+ // unblock task
+ t0.unblock();
+ // will run to completion because are already idle
+ thread.join();
+ }
+
+ @Test
+ public void testProduceOneBlockingTaskIdleByTask() throws Exception
+ {
+ final Task t0 = new Task(true);
+ Thread thread = new Thread()
+ {
+ @Override
+ public void run()
+ {
+ _produce.add(t0);
+ _produce.add(NULLTASK);
+ _ewyk.produce();
+ }
+ };
+ thread.start();
+
+ // wait for execute thread to block in
+ t0.awaitRun();
+
+ // Should have dispatched only one helper
+ assertEquals(_ewyk, _executions.poll());
+
+ // unblock task
+ t0.unblock();
+ // will run to completion because are become idle
+ thread.join();
+ assertThat(_ewyk.isIdle(), Matchers.equalTo(true));
+
+ // because we are idle, dispatched thread is noop
+ _ewyk.run();
+ }
+
+ @Test
+ public void testBlockedInProduce() throws Exception
+ {
+ final Task t0 = new Task(true);
+ Thread thread0 = new Thread()
+ {
+ @Override
+ public void run()
+ {
+ _produce.add(t0);
+ _ewyk.produce();
+ }
+ };
+ thread0.start();
+
+ // wait for execute thread to block in task
+ t0.awaitRun();
+ assertEquals(thread0, t0.getThread());
+
+ // Should have dispatched another helper
+ assertEquals(_ewyk, _executions.poll());
+
+ // dispatched thread will block in produce
+ Thread thread1 = new Thread(_ewyk);
+ thread1.start();
+
+ // Spin
+ while (_producer == null)
+ {
+ Thread.yield();
+ }
+
+ // thread1 is blocked in producing
+ assertEquals(thread1, _producer);
+
+ // because we are producing, any other dispatched threads are noops
+ _ewyk.run();
+
+ // ditto with execute
+ _ewyk.produce();
+
+ // Now if unblock the production by the dispatched thread
+ final Task t1 = new Task(true);
+ _produce.add(t1);
+
+ // task will be run by thread1
+ t1.awaitRun();
+ assertEquals(thread1, t1.getThread());
+
+ // and another thread will have been requested
+ assertEquals(_ewyk, _executions.poll());
+
+ // If we unblock t1, it will overtake t0 and try to produce again!
+ t1.unblock();
+
+ // Now thread1 is producing again
+ while (_producer == null)
+ {
+ Thread.yield();
+ }
+ assertEquals(thread1, _producer);
+
+ // If we unblock t0, it will decide it is not needed
+ t0.unblock();
+ thread0.join();
+
+ // If the requested extra thread turns up, it is also noop because we are producing
+ _ewyk.run();
+
+ // Give the idle job
+ _produce.add(NULLTASK);
+
+ // Which will eventually idle the producer
+ thread1.join();
+ assertEquals(null, _producer);
+ }
+
+ @Test
+ public void testExecuteWhileIdling() throws Exception
+ {
+ final Task t0 = new Task(true);
+ Thread thread0 = new Thread()
+ {
+ @Override
+ public void run()
+ {
+ _produce.add(t0);
+ _ewyk.produce();
+ }
+ };
+ thread0.start();
+
+ // wait for execute thread to block in task
+ t0.awaitRun();
+ assertEquals(thread0, t0.getThread());
+
+ // Should have dispatched another helper
+ assertEquals(_ewyk, _executions.poll());
+
+ // We will go idle when we next produce
+ _produce.add(NULLTASK);
+
+ // execute will return immediately because it did not yet see the idle.
+ _ewyk.produce();
+
+ // When we unblock t0, thread1 will see the idle,
+ t0.unblock();
+
+ // and will see new tasks
+ final Task t1 = new Task(true);
+ _produce.add(t1);
+ t1.awaitRun();
+ assertThat(t1.getThread(), Matchers.equalTo(thread0));
+
+ // Should NOT have dispatched another helper, because the last is still pending
+ assertThat(_executions.size(), Matchers.equalTo(0));
+
+ // When the dispatched thread turns up, it will see the second idle
+ _produce.add(NULLTASK);
+ _ewyk.run();
+ assertThat(_ewyk.isIdle(), Matchers.equalTo(true));
+
+ // So that when t1 completes it does not produce again.
+ t1.unblock();
+ thread0.join();
+ }
+
+ private static class Task implements Runnable
+ {
+ private final CountDownLatch _block = new CountDownLatch(1);
+ private final CountDownLatch _run = new CountDownLatch(1);
+ private volatile Thread _thread;
+
+ public Task()
+ {
+ this(false);
+ }
+
+ public Task(boolean block)
+ {
+ if (!block)
+ _block.countDown();
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ _thread = Thread.currentThread();
+ _run.countDown();
+ _block.await();
+ }
+ catch (InterruptedException e)
+ {
+ throw new IllegalStateException(e);
+ }
+ finally
+ {
+ _thread = null;
+ }
+ }
+
+ public boolean hasRun()
+ {
+ return _run.getCount() <= 0;
+ }
+
+ public void awaitRun()
+ {
+ try
+ {
+ _run.await();
+ }
+ catch (InterruptedException e)
+ {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public void unblock()
+ {
+ _block.countDown();
+ }
+
+ public Thread getThread()
+ {
+ return _thread;
+ }
+ }
+}
diff --git a/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/strategy/ExecutionStrategyTest.java b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/strategy/ExecutionStrategyTest.java
new file mode 100644
index 0000000..fbe37e9
--- /dev/null
+++ b/third_party/jetty-util/src/test/java/org/eclipse/jetty/util/thread/strategy/ExecutionStrategyTest.java
@@ -0,0 +1,212 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread.strategy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.thread.ExecutionStrategy;
+import org.eclipse.jetty.util.thread.ExecutionStrategy.Producer;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ExecutionStrategyTest
+{
+ public static Stream<Arguments> strategies()
+ {
+ return Stream.of(
+ ProduceExecuteConsume.class,
+ ExecuteProduceConsume.class,
+ EatWhatYouKill.class
+ ).map(Arguments::of);
+ }
+
+ QueuedThreadPool _threads = new QueuedThreadPool(20);
+ List<ExecutionStrategy> strategies = new ArrayList<>();
+
+ protected ExecutionStrategy newExecutionStrategy(Class<? extends ExecutionStrategy> strategyClass, Producer producer, Executor executor) throws Exception
+ {
+ ExecutionStrategy strategy = strategyClass.getDeclaredConstructor(Producer.class, Executor.class).newInstance(producer, executor);
+ strategies.add(strategy);
+ LifeCycle.start(strategy);
+ return strategy;
+ }
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ _threads.setDetailedDump(true);
+ _threads.start();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ strategies.forEach((strategy) -> LifeCycle.stop(strategy));
+ _threads.stop();
+ }
+
+ public abstract static class TestProducer implements Producer
+ {
+ @Override
+ public String toString()
+ {
+ return "TestProducer";
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("strategies")
+ public void idleTest(Class<? extends ExecutionStrategy> strategyClass) throws Exception
+ {
+ AtomicInteger count = new AtomicInteger(0);
+ Producer producer = new TestProducer()
+ {
+ @Override
+ public Runnable produce()
+ {
+ count.incrementAndGet();
+ return null;
+ }
+ };
+
+ ExecutionStrategy strategy = newExecutionStrategy(strategyClass, producer, _threads);
+ strategy.produce();
+ assertThat(count.get(), greaterThan(0));
+ }
+
+ @ParameterizedTest
+ @MethodSource("strategies")
+ public void simpleTest(Class<? extends ExecutionStrategy> strategyClass) throws Exception
+ {
+ final int TASKS = 3 * _threads.getMaxThreads();
+ final CountDownLatch latch = new CountDownLatch(TASKS);
+ Producer producer = new TestProducer()
+ {
+ int tasks = TASKS;
+
+ @Override
+ public Runnable produce()
+ {
+ if (tasks-- > 0)
+ {
+ return () -> latch.countDown();
+ }
+
+ return null;
+ }
+ };
+
+ ExecutionStrategy strategy = newExecutionStrategy(strategyClass, producer, _threads);
+
+ for (int p = 0; latch.getCount() > 0 && p < TASKS; p++)
+ {
+ strategy.produce();
+ }
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS),
+ () ->
+ {
+ // Dump state on failure
+ return String.format("Timed out waiting for latch: %s%ntasks=%d latch=%d%n%s",
+ strategy, TASKS, latch.getCount(), _threads.dump());
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("strategies")
+ public void blockingProducerTest(Class<? extends ExecutionStrategy> strategyClass) throws Exception
+ {
+ final int TASKS = 3 * _threads.getMaxThreads();
+ final BlockingQueue<CountDownLatch> q = new ArrayBlockingQueue<>(_threads.getMaxThreads());
+
+ Producer producer = new TestProducer()
+ {
+ AtomicInteger tasks = new AtomicInteger(TASKS);
+
+ @Override
+ public Runnable produce()
+ {
+ final int id = tasks.decrementAndGet();
+
+ if (id >= 0)
+ {
+ while (_threads.isRunning())
+ {
+ try
+ {
+ final CountDownLatch latch = q.take();
+ return () -> latch.countDown();
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ return null;
+ }
+ };
+
+ ExecutionStrategy strategy = newExecutionStrategy(strategyClass, producer, _threads);
+ strategy.dispatch();
+
+ final CountDownLatch latch = new CountDownLatch(TASKS);
+ _threads.execute(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ for (int t = TASKS; t-- > 0; )
+ {
+ Thread.sleep(20);
+ q.offer(latch);
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ });
+
+ assertTrue(latch.await(30, TimeUnit.SECONDS),
+ String.format("Timed out waiting for latch: %s%ntasks=%d latch=%d q=%d%n%s",
+ strategy, TASKS, latch.getCount(), q.size(), _threads.dump()));
+ }
+}
diff --git a/third_party/jetty-util/src/test/resources/TestData/WindowsDir.zip b/third_party/jetty-util/src/test/resources/TestData/WindowsDir.zip
new file mode 100644
index 0000000..26f2357
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/TestData/WindowsDir.zip
Binary files differ
diff --git a/third_party/jetty-util/src/test/resources/TestData/alphabet.txt b/third_party/jetty-util/src/test/resources/TestData/alphabet.txt
new file mode 100644
index 0000000..a6860d9
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/TestData/alphabet.txt
@@ -0,0 +1 @@
+ABCDEFGHIJKLMNOPQRSTUVWXYZ
\ No newline at end of file
diff --git a/third_party/jetty-util/src/test/resources/TestData/alt.zip b/third_party/jetty-util/src/test/resources/TestData/alt.zip
new file mode 100644
index 0000000..d096769
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/TestData/alt.zip
Binary files differ
diff --git a/third_party/jetty-util/src/test/resources/TestData/extract.zip b/third_party/jetty-util/src/test/resources/TestData/extract.zip
new file mode 100644
index 0000000..c1ced9f
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/TestData/extract.zip
Binary files differ
diff --git a/third_party/jetty-util/src/test/resources/TestData/test.zip b/third_party/jetty-util/src/test/resources/TestData/test.zip
new file mode 100644
index 0000000..e620b0e
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/TestData/test.zip
Binary files differ
diff --git a/third_party/jetty-util/src/test/resources/TestData/test/META-INF/MANIFEST.MF b/third_party/jetty-util/src/test/resources/TestData/test/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..b1f9510
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/TestData/test/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Created-By: 1.2.2 (Sun Microsystems Inc.)
+
diff --git a/third_party/jetty-util/src/test/resources/TestData/test/alphabet b/third_party/jetty-util/src/test/resources/TestData/test/alphabet
new file mode 100644
index 0000000..72d007b
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/TestData/test/alphabet
@@ -0,0 +1 @@
+ABCDEFGHIJKLMNOPQRSTUVWXYZ
diff --git a/third_party/jetty-util/src/test/resources/TestData/test/numbers b/third_party/jetty-util/src/test/resources/TestData/test/numbers
new file mode 100644
index 0000000..a32a434
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/TestData/test/numbers
@@ -0,0 +1 @@
+1234567890
diff --git a/third_party/jetty-util/src/test/resources/TestData/test/subdir/alphabet b/third_party/jetty-util/src/test/resources/TestData/test/subdir/alphabet
new file mode 100644
index 0000000..72d007b
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/TestData/test/subdir/alphabet
@@ -0,0 +1 @@
+ABCDEFGHIJKLMNOPQRSTUVWXYZ
diff --git a/third_party/jetty-util/src/test/resources/TestData/test/subdir/numbers b/third_party/jetty-util/src/test/resources/TestData/test/subdir/numbers
new file mode 100644
index 0000000..a32a434
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/TestData/test/subdir/numbers
@@ -0,0 +1 @@
+1234567890
diff --git a/third_party/jetty-util/src/test/resources/TestData/test/subdir/subsubdir/alphabet b/third_party/jetty-util/src/test/resources/TestData/test/subdir/subsubdir/alphabet
new file mode 100644
index 0000000..72d007b
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/TestData/test/subdir/subsubdir/alphabet
@@ -0,0 +1 @@
+ABCDEFGHIJKLMNOPQRSTUVWXYZ
diff --git a/third_party/jetty-util/src/test/resources/TestData/test/subdir/subsubdir/numbers b/third_party/jetty-util/src/test/resources/TestData/test/subdir/subsubdir/numbers
new file mode 100644
index 0000000..a32a434
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/TestData/test/subdir/subsubdir/numbers
@@ -0,0 +1 @@
+1234567890
diff --git a/third_party/jetty-util/src/test/resources/example.jar b/third_party/jetty-util/src/test/resources/example.jar
new file mode 100644
index 0000000..ae65888
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/example.jar
Binary files differ
diff --git a/third_party/jetty-util/src/test/resources/jar-file-resource.jar b/third_party/jetty-util/src/test/resources/jar-file-resource.jar
new file mode 100644
index 0000000..de93283
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/jar-file-resource.jar
Binary files differ
diff --git a/third_party/jetty-util/src/test/resources/jetty-logging.properties b/third_party/jetty-util/src/test/resources/jetty-logging.properties
new file mode 100644
index 0000000..33c6ec7
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/jetty-logging.properties
@@ -0,0 +1,5 @@
+org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
+#org.eclipse.jetty.util.LEVEL=DEBUG
+#org.eclipse.jetty.util.PathWatcher.LEVEL=DEBUG
+#org.eclipse.jetty.util.thread.QueuedThreadPool.LEVEL=DEBUG
+#org.eclipse.jetty.util.thread.ReservedThreadExecutor.LEVEL=DEBUG
diff --git a/third_party/jetty-util/src/test/resources/keystore b/third_party/jetty-util/src/test/resources/keystore
new file mode 100644
index 0000000..b727bd0
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/keystore
Binary files differ
diff --git a/third_party/jetty-util/src/test/resources/keystore.jce b/third_party/jetty-util/src/test/resources/keystore.jce
new file mode 100644
index 0000000..e5d6144
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/keystore.jce
Binary files differ
diff --git a/third_party/jetty-util/src/test/resources/keystore.p12 b/third_party/jetty-util/src/test/resources/keystore.p12
new file mode 100644
index 0000000..b51c835
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/keystore.p12
Binary files differ
diff --git a/third_party/jetty-util/src/test/resources/keystore_sni.p12 b/third_party/jetty-util/src/test/resources/keystore_sni.p12
new file mode 100644
index 0000000..fab22bd
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/keystore_sni.p12
Binary files differ
diff --git a/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/four/four b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/four/four
new file mode 100644
index 0000000..02bf84b
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/four/four
@@ -0,0 +1 @@
+4 - four (no extension)
\ No newline at end of file
diff --git a/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/four/four.txt b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/four/four.txt
new file mode 100644
index 0000000..05a6c6f
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/four/four.txt
@@ -0,0 +1 @@
+4 - four
\ No newline at end of file
diff --git a/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/one/1.txt b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/one/1.txt
new file mode 100644
index 0000000..8676a0f
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/one/1.txt
@@ -0,0 +1 @@
+1 - one
\ No newline at end of file
diff --git a/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/one/dir/1.txt b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/one/dir/1.txt
new file mode 100644
index 0000000..8676a0f
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/one/dir/1.txt
@@ -0,0 +1 @@
+1 - one
\ No newline at end of file
diff --git a/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/resource.txt b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/resource.txt
new file mode 100644
index 0000000..016e97f
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/resource.txt
@@ -0,0 +1 @@
+this is test data
diff --git a/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/three/2.txt b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/three/2.txt
new file mode 100644
index 0000000..c2e3226
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/three/2.txt
@@ -0,0 +1 @@
+2 - three
\ No newline at end of file
diff --git a/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/three/3.txt b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/three/3.txt
new file mode 100644
index 0000000..b074b8a
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/three/3.txt
@@ -0,0 +1 @@
+3 - three
\ No newline at end of file
diff --git a/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/three/dir/3.txt b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/three/dir/3.txt
new file mode 100644
index 0000000..b074b8a
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/three/dir/3.txt
@@ -0,0 +1 @@
+3 - three
\ No newline at end of file
diff --git a/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/two/1.txt b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/two/1.txt
new file mode 100644
index 0000000..15bd7e5
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/two/1.txt
@@ -0,0 +1 @@
+1 - two
\ No newline at end of file
diff --git a/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/two/2.txt b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/two/2.txt
new file mode 100644
index 0000000..49a910e
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/two/2.txt
@@ -0,0 +1 @@
+2 - two
\ No newline at end of file
diff --git a/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/two/dir/2.txt b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/two/dir/2.txt
new file mode 100644
index 0000000..49a910e
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/two/dir/2.txt
@@ -0,0 +1 @@
+2 - two
\ No newline at end of file
diff --git a/third_party/jetty-util/src/test/resources/resource.txt b/third_party/jetty-util/src/test/resources/resource.txt
new file mode 100644
index 0000000..a496efe
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/resource.txt
@@ -0,0 +1 @@
+This is a text file
diff --git a/third_party/jetty-util/src/test/resources/snikeystore b/third_party/jetty-util/src/test/resources/snikeystore
new file mode 100644
index 0000000..3c69266
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/snikeystore
Binary files differ
diff --git a/third_party/jetty-util/src/test/resources/test-base-resource.jar b/third_party/jetty-util/src/test/resources/test-base-resource.jar
new file mode 100644
index 0000000..aa30669
--- /dev/null
+++ b/third_party/jetty-util/src/test/resources/test-base-resource.jar
Binary files differ